From 3644853d38fd3f95fae038b6f5441444306306ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 18:07:51 -0600 Subject: [PATCH 001/436] dashboard: fix subprocesses blocking the event loop (#5772) * dashboard: fix subprocesses blocking the event loop - break apart the util module - adds a new util to run subprocesses with asyncio * take a list --- esphome/dashboard/settings.py | 2 +- esphome/dashboard/status/ping.py | 15 +++----- esphome/dashboard/util.py | 52 ---------------------------- esphome/dashboard/util/__init__.py | 0 esphome/dashboard/util/itertools.py | 22 ++++++++++++ esphome/dashboard/util/password.py | 11 ++++++ esphome/dashboard/util/subprocess.py | 31 +++++++++++++++++ esphome/dashboard/util/text.py | 25 +++++++++++++ esphome/dashboard/web_server.py | 13 +++---- 9 files changed, 101 insertions(+), 70 deletions(-) delete mode 100644 esphome/dashboard/util.py create mode 100644 esphome/dashboard/util/__init__.py create mode 100644 esphome/dashboard/util/itertools.py create mode 100644 esphome/dashboard/util/password.py create mode 100644 esphome/dashboard/util/subprocess.py create mode 100644 esphome/dashboard/util/text.py diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 1ddb6f652d..888616f6f7 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -10,7 +10,7 @@ from esphome.helpers import get_bool_env from esphome.storage_json import ext_storage_path from .entries import DashboardEntry -from .util import password_hash +from .util.password import password_hash class DashboardSettings: diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 17c1254c9d..678d7844ae 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -7,22 +7,15 @@ from typing import cast from ..core import DASHBOARD from ..entries import DashboardEntry from ..core import list_dashboard_entries -from ..util import chunked +from ..util.itertools import chunked +from ..util.subprocess import async_system_command_status async def _async_ping_host(host: str) -> bool: """Ping a host.""" - ping_command = ["ping", "-n" if os.name == "nt" else "-c", "1"] - process = await asyncio.create_subprocess_exec( - *ping_command, - host, - stdin=asyncio.subprocess.DEVNULL, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL, - close_fds=False, + return await async_system_command_status( + ["ping", "-n" if os.name == "nt" else "-c", "1", host] ) - await process.wait() - return process.returncode == 0 class PingStatus: diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py deleted file mode 100644 index 7b6572b989..0000000000 --- a/esphome/dashboard/util.py +++ /dev/null @@ -1,52 +0,0 @@ -import hashlib -import unicodedata -from collections.abc import Iterable -from functools import partial -from itertools import islice -from typing import Any - -from esphome.const import ALLOWED_NAME_CHARS - - -def password_hash(password: str) -> bytes: - """Create a hash of a password to transform it to a fixed-length digest. - - Note this is not meant for secure storage, but for securely comparing passwords. - """ - return hashlib.sha256(password.encode()).digest() - - -def strip_accents(value): - return "".join( - c - for c in unicodedata.normalize("NFD", str(value)) - if unicodedata.category(c) != "Mn" - ) - - -def friendly_name_slugify(value): - value = ( - strip_accents(value) - .lower() - .replace(" ", "-") - .replace("_", "-") - .replace("--", "-") - .strip("-") - ) - return "".join(c for c in value if c in ALLOWED_NAME_CHARS) - - -def take(take_num: int, iterable: Iterable) -> list[Any]: - """Return first n items of the iterable as a list. - - From itertools recipes - """ - return list(islice(iterable, take_num)) - - -def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]: - """Break *iterable* into lists of length *n*. - - From more-itertools - """ - return iter(partial(take, chunked_num, iter(iterable)), []) diff --git a/esphome/dashboard/util/__init__.py b/esphome/dashboard/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/dashboard/util/itertools.py b/esphome/dashboard/util/itertools.py new file mode 100644 index 0000000000..54e95ef802 --- /dev/null +++ b/esphome/dashboard/util/itertools.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from collections.abc import Iterable +from functools import partial +from itertools import islice +from typing import Any + + +def take(take_num: int, iterable: Iterable) -> list[Any]: + """Return first n items of the iterable as a list. + + From itertools recipes + """ + return list(islice(iterable, take_num)) + + +def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]: + """Break *iterable* into lists of length *n*. + + From more-itertools + """ + return iter(partial(take, chunked_num, iter(iterable)), []) diff --git a/esphome/dashboard/util/password.py b/esphome/dashboard/util/password.py new file mode 100644 index 0000000000..e7ea28c25d --- /dev/null +++ b/esphome/dashboard/util/password.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import hashlib + + +def password_hash(password: str) -> bytes: + """Create a hash of a password to transform it to a fixed-length digest. + + Note this is not meant for secure storage, but for securely comparing passwords. + """ + return hashlib.sha256(password.encode()).digest() diff --git a/esphome/dashboard/util/subprocess.py b/esphome/dashboard/util/subprocess.py new file mode 100644 index 0000000000..583dd116e3 --- /dev/null +++ b/esphome/dashboard/util/subprocess.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Iterable + + +async def async_system_command_status(command: Iterable[str]) -> bool: + """Run a system command checking only the status.""" + process = await asyncio.create_subprocess_exec( + *command, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + close_fds=False, + ) + await process.wait() + return process.returncode == 0 + + +async def async_run_system_command(command: Iterable[str]) -> tuple[bool, bytes, bytes]: + """Run a system command and return a tuple of returncode, stdout, stderr.""" + process = await asyncio.create_subprocess_exec( + *command, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + close_fds=False, + ) + stdout, stderr = await process.communicate() + await process.wait() + return process.returncode, stdout, stderr diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py new file mode 100644 index 0000000000..08d2df6abf --- /dev/null +++ b/esphome/dashboard/util/text.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import unicodedata + +from esphome.const import ALLOWED_NAME_CHARS + + +def strip_accents(value): + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def friendly_name_slugify(value): + value = ( + strip_accents(value) + .lower() + .replace(" ", "-") + .replace("_", "-") + .replace("--", "-") + .strip("-") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 086a28cbb2..c0cc00b66c 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -31,13 +31,14 @@ import yaml from tornado.log import access_log from esphome import const, platformio_api, yaml_util -from esphome.helpers import get_bool_env, mkdir_p, run_system_command +from esphome.helpers import get_bool_env, mkdir_p from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_path from esphome.util import get_serial_ports, shlex_quote from .core import DASHBOARD, list_dashboard_entries from .entries import DashboardEntry -from .util import friendly_name_slugify +from .util.text import friendly_name_slugify +from .util.subprocess import async_run_system_command _LOGGER = logging.getLogger(__name__) @@ -522,7 +523,7 @@ class DownloadListRequestHandler(BaseHandler): class DownloadBinaryRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration=None): compressed = self.get_argument("compressed", "0") == "1" storage_path = ext_storage_path(configuration) @@ -548,7 +549,7 @@ class DownloadBinaryRequestHandler(BaseHandler): if not Path(path).is_file(): args = ["esphome", "idedata", settings.rel_path(configuration)] - rc, stdout, _ = run_system_command(*args) + rc, stdout, _ = await async_run_system_command(args) if rc != 0: self.send_error(404 if rc == 2 else 500) @@ -902,7 +903,7 @@ SafeLoaderIgnoreUnknown.add_constructor( class JsonConfigRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration=None): filename = settings.rel_path(configuration) if not os.path.isfile(filename): self.send_error(404) @@ -910,7 +911,7 @@ class JsonConfigRequestHandler(BaseHandler): args = ["esphome", "config", filename, "--show-secrets"] - rc, stdout, _ = run_system_command(*args) + rc, stdout, _ = await async_run_system_command(args) if rc != 0: self.send_error(422) From 5f1d8dfa5bcc9d1ecae593e2e05c8a5609bca767 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 18:08:17 -0600 Subject: [PATCH 002/436] dashboard: use fastest available yaml loader in the dashboard (#5771) * dashboard: use fastest available yaml loader in the dashboard * remove unrelated change --- esphome/dashboard/web_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index c0cc00b66c..76faa43015 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -34,6 +34,7 @@ from esphome import const, platformio_api, yaml_util from esphome.helpers import get_bool_env, mkdir_p from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_path from esphome.util import get_serial_ports, shlex_quote +from esphome.yaml_util import FastestAvailableSafeLoader from .core import DASHBOARD, list_dashboard_entries from .entries import DashboardEntry @@ -886,7 +887,7 @@ class SecretKeysRequestHandler(BaseHandler): self.write(json.dumps(secret_keys)) -class SafeLoaderIgnoreUnknown(yaml.SafeLoader): +class SafeLoaderIgnoreUnknown(FastestAvailableSafeLoader): def ignore_unknown(self, node): return f"{node.tag} {node.value}" From 149d814fab0980a4948806e014306d587805eadf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 20:49:56 -0600 Subject: [PATCH 003/436] dashboard: Centralize dashboard entries into DashboardEntries class (#5774) * Centralize dashboard entries into DashboardEntries class * preen * preen * preen * preen * preen --- esphome/dashboard/core.py | 11 +- esphome/dashboard/entries.py | 202 ++++++++++++++++++++++++++----- esphome/dashboard/settings.py | 86 ++----------- esphome/dashboard/status/mdns.py | 7 +- esphome/dashboard/status/mqtt.py | 7 +- esphome/dashboard/status/ping.py | 3 +- esphome/dashboard/web_server.py | 26 ++-- 7 files changed, 209 insertions(+), 133 deletions(-) diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index 4cc2938bb1..f18da92d80 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -6,7 +6,7 @@ import threading from typing import TYPE_CHECKING from ..zeroconf import DiscoveredImport -from .entries import DashboardEntry +from .entries import DashboardEntries from .settings import DashboardSettings if TYPE_CHECKING: @@ -15,15 +15,11 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -def list_dashboard_entries() -> list[DashboardEntry]: - """List all dashboard entries.""" - return DASHBOARD.settings.entries() - - class ESPHomeDashboard: """Class that represents the dashboard.""" __slots__ = ( + "entries", "loop", "ping_result", "import_result", @@ -36,6 +32,7 @@ class ESPHomeDashboard: def __init__(self) -> None: """Initialize the ESPHomeDashboard.""" + self.entries: DashboardEntries | None = None self.loop: asyncio.AbstractEventLoop | None = None self.ping_result: dict[str, bool | None] = {} self.import_result: dict[str, DiscoveredImport] = {} @@ -49,12 +46,14 @@ class ESPHomeDashboard: """Setup the dashboard.""" self.loop = asyncio.get_running_loop() self.ping_request = asyncio.Event() + self.entries = DashboardEntries(self.settings.config_dir) async def async_run(self) -> None: """Run the dashboard.""" settings = self.settings mdns_task: asyncio.Task | None = None ping_status_task: asyncio.Task | None = None + await self.entries.async_update_entries() if settings.status_use_ping: from .status.ping import PingStatus diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index 582073d655..ff539fc620 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -1,10 +1,150 @@ from __future__ import annotations +import asyncio +import logging import os -from esphome import const +from esphome import const, util from esphome.storage_json import StorageJSON, ext_storage_path +_LOGGER = logging.getLogger(__name__) + +DashboardCacheKeyType = tuple[int, int, float, int] + + +class DashboardEntries: + """Represents all dashboard entries.""" + + __slots__ = ("_loop", "_config_dir", "_entries", "_loaded_entries", "_update_lock") + + def __init__(self, config_dir: str) -> None: + """Initialize the DashboardEntries.""" + self._loop = asyncio.get_running_loop() + self._config_dir = config_dir + # Entries are stored as + # { + # "path/to/file.yaml": DashboardEntry, + # ... + # } + self._entries: dict[str, DashboardEntry] = {} + self._loaded_entries = False + self._update_lock = asyncio.Lock() + + def get(self, path: str) -> DashboardEntry | None: + """Get an entry by path.""" + return self._entries.get(path) + + async def _async_all(self) -> list[DashboardEntry]: + """Return all entries.""" + return list(self._entries.values()) + + def all(self) -> list[DashboardEntry]: + """Return all entries.""" + return asyncio.run_coroutine_threadsafe(self._async_all, self._loop).result() + + def async_all(self) -> list[DashboardEntry]: + """Return all entries.""" + return list(self._entries.values()) + + async def async_request_update_entries(self) -> None: + """Request an update of the dashboard entries from disk. + + If an update is already in progress, this will do nothing. + """ + if self._update_lock.locked(): + _LOGGER.debug("Dashboard entries are already being updated") + return + await self.async_update_entries() + + async def async_update_entries(self) -> None: + """Update the dashboard entries from disk.""" + async with self._update_lock: + await self._async_update_entries() + + def _load_entries( + self, entries: dict[DashboardEntry, DashboardCacheKeyType] + ) -> None: + """Load all entries from disk.""" + for entry, cache_key in entries.items(): + _LOGGER.debug( + "Loading dashboard entry %s because cache key changed: %s", + entry.path, + cache_key, + ) + entry.load_from_disk(cache_key) + + async def _async_update_entries(self) -> list[DashboardEntry]: + """Sync the dashboard entries from disk.""" + _LOGGER.debug("Updating dashboard entries") + # At some point it would be nice to use watchdog to avoid polling + + path_to_cache_key = await self._loop.run_in_executor( + None, self._get_path_to_cache_key + ) + added: dict[DashboardEntry, DashboardCacheKeyType] = {} + updated: dict[DashboardEntry, DashboardCacheKeyType] = {} + removed: set[DashboardEntry] = { + entry + for filename, entry in self._entries.items() + if filename not in path_to_cache_key + } + entries = self._entries + for path, cache_key in path_to_cache_key.items(): + if entry := self._entries.get(path): + if entry.cache_key != cache_key: + updated[entry] = cache_key + else: + entry = DashboardEntry(path, cache_key) + added[entry] = cache_key + + if added or updated: + await self._loop.run_in_executor( + None, self._load_entries, {**added, **updated} + ) + + for entry in added: + _LOGGER.debug("Added dashboard entry %s", entry.path) + entries[entry.path] = entry + + if entry in removed: + _LOGGER.debug("Removed dashboard entry %s", entry.path) + entries.pop(entry.path) + + for entry in updated: + _LOGGER.debug("Updated dashboard entry %s", entry.path) + # In the future we can fire events when entries are added/removed/updated + + def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: + """Return a dict of path to cache key.""" + path_to_cache_key: dict[str, DashboardCacheKeyType] = {} + # + # The cache key is (inode, device, mtime, size) + # which allows us to avoid locking since it ensures + # every iteration of this call will always return the newest + # items from disk at the cost of a stat() call on each + # file which is much faster than reading the file + # for the cache hit case which is the common case. + # + for file in util.list_yaml_files([self._config_dir]): + try: + # Prefer the json storage path if it exists + stat = os.stat(ext_storage_path(os.path.basename(file))) + except OSError: + try: + # Fallback to the yaml file if the storage + # file does not exist or could not be generated + stat = os.stat(file) + except OSError: + # File was deleted, ignore + continue + path_to_cache_key[file] = ( + stat.st_ino, + stat.st_dev, + stat.st_mtime, + stat.st_size, + ) + return path_to_cache_key + class DashboardEntry: """Represents a single dashboard entry. @@ -12,13 +152,15 @@ class DashboardEntry: This class is thread-safe and read-only. """ - __slots__ = ("path", "_storage", "_loaded_storage") + __slots__ = ("path", "filename", "_storage_path", "cache_key", "storage") - def __init__(self, path: str) -> None: + def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None: """Initialize the DashboardEntry.""" self.path = path - self._storage = None - self._loaded_storage = False + self.filename = os.path.basename(path) + self._storage_path = ext_storage_path(self.filename) + self.cache_key = cache_key + self.storage: StorageJSON | None = None def __repr__(self): """Return the representation of this entry.""" @@ -30,87 +172,91 @@ class DashboardEntry: f"no_mdns={self.no_mdns})" ) - @property - def filename(self): - """Return the filename of this entry.""" - return os.path.basename(self.path) + def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None: + """Load this entry from disk.""" + self.storage = StorageJSON.load(self._storage_path) + # + # Currently StorageJSON.load() will return None if the file does not exist + # + # StorageJSON currently does not provide an updated cache key so we use the + # one that is passed in. + # + # The cache key was read from the disk moments ago and may be stale but + # it does not matter since we are polling anyways, and the next call to + # async_update_entries() will load it again in the extremely rare case that + # it changed between the two calls. + # + if cache_key: + self.cache_key = cache_key @property - def storage(self) -> StorageJSON | None: - """Return the StorageJSON object for this entry.""" - if not self._loaded_storage: - self._storage = StorageJSON.load(ext_storage_path(self.filename)) - self._loaded_storage = True - return self._storage - - @property - def address(self): + def address(self) -> str | None: """Return the address of this entry.""" if self.storage is None: return None return self.storage.address @property - def no_mdns(self): + def no_mdns(self) -> bool | None: """Return the no_mdns of this entry.""" if self.storage is None: return None return self.storage.no_mdns @property - def web_port(self): + def web_port(self) -> int | None: """Return the web port of this entry.""" if self.storage is None: return None return self.storage.web_port @property - def name(self): + def name(self) -> str: """Return the name of this entry.""" if self.storage is None: return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name @property - def friendly_name(self): + def friendly_name(self) -> str: """Return the friendly name of this entry.""" if self.storage is None: return self.name return self.storage.friendly_name @property - def comment(self): + def comment(self) -> str | None: """Return the comment of this entry.""" if self.storage is None: return None return self.storage.comment @property - def target_platform(self): + def target_platform(self) -> str | None: """Return the target platform of this entry.""" if self.storage is None: return None return self.storage.target_platform @property - def update_available(self): + def update_available(self) -> bool: """Return if an update is available for this entry.""" if self.storage is None: return True return self.update_old != self.update_new @property - def update_old(self): + def update_old(self) -> str: if self.storage is None: return "" return self.storage.esphome_version or "" @property - def update_new(self): + def update_new(self) -> str: return const.__version__ @property - def loaded_integrations(self): + def loaded_integrations(self) -> list[str]: if self.storage is None: return [] return self.storage.loaded_integrations diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 888616f6f7..76633e1bf2 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -4,29 +4,23 @@ import hmac import os from pathlib import Path -from esphome import util from esphome.core import CORE from esphome.helpers import get_bool_env -from esphome.storage_json import ext_storage_path -from .entries import DashboardEntry from .util.password import password_hash class DashboardSettings: """Settings for the dashboard.""" - def __init__(self): - self.config_dir = "" - self.password_hash = "" - self.username = "" - self.using_password = False - self.on_ha_addon = False - self.cookie_secret = None - self.absolute_config_dir = None - self._entry_cache: dict[ - str, tuple[tuple[int, int, float, int], DashboardEntry] - ] = {} + def __init__(self) -> None: + self.config_dir: str = "" + self.password_hash: str = "" + self.username: str = "" + self.using_password: bool = False + self.on_ha_addon: bool = False + self.cookie_secret: str | None = None + self.absolute_config_dir: Path | None = None def parse_args(self, args): self.on_ha_addon: bool = args.ha_addon @@ -80,67 +74,3 @@ class DashboardSettings: # Raises ValueError if not relative to ESPHome config folder Path(joined_path).resolve().relative_to(self.absolute_config_dir) return joined_path - - def list_yaml_files(self) -> list[str]: - return util.list_yaml_files([self.config_dir]) - - def entries(self) -> list[DashboardEntry]: - """Fetch all dashboard entries, thread-safe.""" - path_to_cache_key: dict[str, tuple[int, int, float, int]] = {} - # - # The cache key is (inode, device, mtime, size) - # which allows us to avoid locking since it ensures - # every iteration of this call will always return the newest - # items from disk at the cost of a stat() call on each - # file which is much faster than reading the file - # for the cache hit case which is the common case. - # - # Because there is no lock the cache may - # get built more than once but that's fine as its still - # thread-safe and results in orders of magnitude less - # reads from disk than if we did not cache at all and - # does not have a lock contention issue. - # - for file in self.list_yaml_files(): - try: - # Prefer the json storage path if it exists - stat = os.stat(ext_storage_path(os.path.basename(file))) - except OSError: - try: - # Fallback to the yaml file if the storage - # file does not exist or could not be generated - stat = os.stat(file) - except OSError: - # File was deleted, ignore - continue - path_to_cache_key[file] = ( - stat.st_ino, - stat.st_dev, - stat.st_mtime, - stat.st_size, - ) - - entry_cache = self._entry_cache - - # Remove entries that no longer exist - removed: list[str] = [] - for file in entry_cache: - if file not in path_to_cache_key: - removed.append(file) - - for file in removed: - entry_cache.pop(file) - - dashboard_entries: list[DashboardEntry] = [] - for file, cache_key in path_to_cache_key.items(): - if cached_entry := entry_cache.get(file): - entry_key, dashboard_entry = cached_entry - if entry_key == cache_key: - dashboard_entries.append(dashboard_entry) - continue - - dashboard_entry = DashboardEntry(file) - dashboard_entries.append(dashboard_entry) - entry_cache[file] = (cache_key, dashboard_entry) - - return dashboard_entries diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index 454bba9cb5..51d11390b7 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -10,7 +10,7 @@ from esphome.zeroconf import ( DashboardStatus, ) -from ..core import DASHBOARD, list_dashboard_entries +from ..core import DASHBOARD class MDNSStatus: @@ -41,12 +41,13 @@ class MDNSStatus: async def async_refresh_hosts(self): """Refresh the hosts to track.""" - entries = await self._loop.run_in_executor(None, list_dashboard_entries) + dashboard = DASHBOARD + entries = dashboard.entries.async_all() host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_mdns_state = self.host_mdns_state host_name_to_filename = self.host_name_to_filename filename_to_host_name = self.filename_to_host_name - ping_result = DASHBOARD.ping_result + ping_result = dashboard.ping_result for entry in entries: name = entry.name diff --git a/esphome/dashboard/status/mqtt.py b/esphome/dashboard/status/mqtt.py index 109b60e133..2fd3a332a7 100644 --- a/esphome/dashboard/status/mqtt.py +++ b/esphome/dashboard/status/mqtt.py @@ -7,7 +7,7 @@ import threading from esphome import mqtt -from ..core import DASHBOARD, list_dashboard_entries +from ..core import DASHBOARD class MqttStatusThread(threading.Thread): @@ -16,7 +16,7 @@ class MqttStatusThread(threading.Thread): def run(self) -> None: """Run the status thread.""" dashboard = DASHBOARD - entries = list_dashboard_entries() + entries = dashboard.entries.all() config = mqtt.config_from_env() topic = "esphome/discover/#" @@ -51,8 +51,7 @@ class MqttStatusThread(threading.Thread): client.loop_start() while not dashboard.stop_event.wait(2): - # update entries - entries = list_dashboard_entries() + entries = dashboard.entries.all() # will be set to true on on_message for entry in entries: diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 678d7844ae..35fb2259f0 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -6,7 +6,6 @@ from typing import cast from ..core import DASHBOARD from ..entries import DashboardEntry -from ..core import list_dashboard_entries from ..util.itertools import chunked from ..util.subprocess import async_system_command_status @@ -32,7 +31,7 @@ class PingStatus: # Only ping if the dashboard is open await dashboard.ping_request.wait() dashboard.ping_result.clear() - entries = await self._loop.run_in_executor(None, list_dashboard_entries) + entries = dashboard.entries.async_all() to_ping: list[DashboardEntry] = [ entry for entry in entries if entry.address is not None ] diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 76faa43015..9a5de0a933 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -36,10 +36,9 @@ from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_pa from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader -from .core import DASHBOARD, list_dashboard_entries -from .entries import DashboardEntry -from .util.text import friendly_name_slugify +from .core import DASHBOARD from .util.subprocess import async_run_system_command +from .util.text import friendly_name_slugify _LOGGER = logging.getLogger(__name__) @@ -601,11 +600,11 @@ class EsphomeVersionHandler(BaseHandler): class ListDevicesHandler(BaseHandler): @authenticated async def get(self): - loop = asyncio.get_running_loop() - entries = await loop.run_in_executor(None, list_dashboard_entries) + dashboard = DASHBOARD + await dashboard.entries.async_request_update_entries() + entries = dashboard.entries.async_all() self.set_header("content-type", "application/json") configured = {entry.name for entry in entries} - dashboard = DASHBOARD self.write( json.dumps( @@ -658,8 +657,10 @@ class MainRequestHandler(BaseHandler): class PrometheusServiceDiscoveryHandler(BaseHandler): @authenticated - def get(self): - entries = list_dashboard_entries() + async def get(self): + dashboard = DASHBOARD + await dashboard.entries.async_request_update_entries() + entries = dashboard.entries.async_all() self.set_header("content-type", "application/json") sd = [] for entry in entries: @@ -733,16 +734,17 @@ class PingRequestHandler(BaseHandler): class InfoRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration=None): yaml_path = settings.rel_path(configuration) - all_yaml_files = settings.list_yaml_files() + dashboard = DASHBOARD + entry = dashboard.entries.get(yaml_path) - if yaml_path not in all_yaml_files: + if not entry: self.set_status(404) return self.set_header("content-type", "application/json") - self.write(DashboardEntry(yaml_path).storage.to_json()) + self.write(entry.storage.to_json()) class EditRequestHandler(BaseHandler): From ef945d298c9d171535c16e5b1719bd1cbdf0d152 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 15 Nov 2023 21:29:50 -0600 Subject: [PATCH 004/436] Add more VA triggers (#5762) --- .../components/voice_assistant/__init__.py | 61 ++++++++++++++++--- .../voice_assistant/voice_assistant.cpp | 39 ++++++++---- .../voice_assistant/voice_assistant.h | 16 +++-- 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 3270b9f370..5715604605 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -18,20 +18,25 @@ DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] -CONF_SILENCE_DETECTION = "silence_detection" -CONF_ON_LISTENING = "on_listening" -CONF_ON_START = "on_start" -CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" -CONF_ON_STT_END = "on_stt_end" -CONF_ON_TTS_START = "on_tts_start" -CONF_ON_TTS_END = "on_tts_end" 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" +CONF_ON_START = "on_start" +CONF_ON_STT_END = "on_stt_end" +CONF_ON_STT_VAD_END = "on_stt_vad_end" +CONF_ON_STT_VAD_START = "on_stt_vad_start" +CONF_ON_TTS_END = "on_tts_end" +CONF_ON_TTS_START = "on_tts_start" +CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" + +CONF_SILENCE_DETECTION = "silence_detection" CONF_USE_WAKE_WORD = "use_wake_word" CONF_VAD_THRESHOLD = "vad_threshold" -CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_AUTO_GAIN = "auto_gain" +CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_VOLUME_MULTIPLIER = "volume_multiplier" @@ -88,6 +93,18 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), ) @@ -177,6 +194,34 @@ async def to_code(config): config[CONF_ON_CLIENT_DISCONNECTED], ) + if CONF_ON_INTENT_START in config: + await automation.build_automation( + var.get_intent_start_trigger(), + [], + config[CONF_ON_INTENT_START], + ) + + if CONF_ON_INTENT_END in config: + await automation.build_automation( + var.get_intent_end_trigger(), + [], + config[CONF_ON_INTENT_END], + ) + + if CONF_ON_STT_VAD_START in config: + await automation.build_automation( + var.get_stt_vad_start_trigger(), + [], + config[CONF_ON_STT_VAD_START], + ) + + if CONF_ON_STT_VAD_END in config: + await automation.build_automation( + var.get_stt_vad_end_trigger(), + [], + config[CONF_ON_STT_VAD_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fc5dd6e4e4..7ebbe762b3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -31,7 +31,7 @@ void VoiceAssistant::setup() { this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); + ESP_LOGW(TAG, "Could not create socket"); this->mark_failed(); return; } @@ -69,7 +69,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); if (this->speaker_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate speaker buffer."); + ESP_LOGW(TAG, "Could not allocate speaker buffer"); this->mark_failed(); return; } @@ -79,7 +79,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); if (this->input_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate input buffer."); + ESP_LOGW(TAG, "Could not allocate input buffer"); this->mark_failed(); return; } @@ -89,7 +89,7 @@ void VoiceAssistant::setup() { this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); if (this->ring_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate ring buffer."); + ESP_LOGW(TAG, "Could not allocate ring buffer"); this->mark_failed(); return; } @@ -98,7 +98,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); if (send_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate send buffer."); + ESP_LOGW(TAG, "Could not allocate send buffer"); this->mark_failed(); return; } @@ -221,8 +221,8 @@ void VoiceAssistant::loop() { msg.audio_settings = audio_settings; if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { - ESP_LOGW(TAG, "Could not request start."); - this->error_trigger_->trigger("not-connected", "Could not request start."); + ESP_LOGW(TAG, "Could not request start"); + this->error_trigger_->trigger("not-connected", "Could not request start"); this->continuous_ = false; this->set_state_(State::IDLE, State::IDLE); break; @@ -280,7 +280,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_size_ += len; } } else { - ESP_LOGW(TAG, "Receive buffer full."); + ESP_LOGW(TAG, "Receive buffer full"); } if (this->speaker_buffer_size_ > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); @@ -290,7 +290,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_index_ -= written; this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); } else { - ESP_LOGW(TAG, "Speaker buffer full."); + ESP_LOGW(TAG, "Speaker buffer full"); } } if (this->wait_for_stream_end_) { @@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_STT_START: - ESP_LOGD(TAG, "STT Started"); + ESP_LOGD(TAG, "STT started"); this->listening_trigger_->trigger(); break; case api::enums::VOICE_ASSISTANT_STT_END: { @@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in STT_END event."); + ESP_LOGW(TAG, "No text in STT_END event"); return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->stt_end_trigger_->trigger(text); break; } + case api::enums::VOICE_ASSISTANT_INTENT_START: + ESP_LOGD(TAG, "Intent started"); + this->intent_start_trigger_->trigger(); + break; case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { this->conversation_id_ = std::move(arg.value); } } + this->intent_end_trigger_->trigger(); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in TTS_START event."); + ESP_LOGW(TAG, "No text in TTS_START event"); return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); @@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (url.empty()) { - ESP_LOGW(TAG, "No url in TTS_END event."); + ESP_LOGW(TAG, "No url in TTS_END event"); return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); @@ -634,6 +639,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); break; } + case api::enums::VOICE_ASSISTANT_STT_VAD_START: + ESP_LOGD(TAG, "Starting STT by VAD"); + this->stt_vad_start_trigger_->trigger(); + break; + case api::enums::VOICE_ASSISTANT_STT_VAD_END: + ESP_LOGD(TAG, "STT by VAD end"); + this->stt_vad_end_trigger_->trigger(); + break; default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a265522bca..a985bc4678 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -100,13 +100,17 @@ class VoiceAssistant : public Component { void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; } void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; } + Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } + Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } + Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } + Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } + Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } - Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } @@ -124,13 +128,17 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; + Trigger<> *intent_end_trigger_ = new Trigger<>(); + Trigger<> *intent_start_trigger_ = new Trigger<>(); Trigger<> *listening_trigger_ = new Trigger<>(); + Trigger<> *end_trigger_ = new Trigger<>(); Trigger<> *start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); - Trigger<> *end_trigger_ = new Trigger<>(); + Trigger *tts_start_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); Trigger<> *client_connected_trigger_ = new Trigger<>(); From 10a9129b7b1cfa05e13dc55452a7dec087e566ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Nov 2023 01:41:49 -0600 Subject: [PATCH 005/436] Pass the name to the log runner when available (#5759) --- esphome/components/api/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 2c43eca70c..701848b1f1 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -18,9 +18,10 @@ from . import CONF_ENCRYPTION _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config, address): +async def async_run_logs(config: dict[str, Any], address: str) -> None: """Run the logs command in the event loop.""" conf = config["api"] + name = config["esphome"]["name"] port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: str | None = None @@ -28,7 +29,6 @@ async def async_run_logs(config, address): noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) aiozc = AsyncZeroconf() - cli = APIClient( address, port, @@ -48,7 +48,7 @@ async def async_run_logs(config, address): text = text.replace("\033", "\\033") print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") - stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc) + stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc, name=name) try: while True: await asyncio.sleep(60) From 754bd5b7bedca31190edcb0b19638daceae180a9 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Thu, 16 Nov 2023 07:45:08 +0000 Subject: [PATCH 006/436] Fix MY9231 flicker (#5765) --- esphome/components/my9231/my9231.cpp | 26 +++++++++++++++++++++----- esphome/components/my9231/my9231.h | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index a97587b7be..c511591856 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -1,5 +1,6 @@ #include "my9231.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace my9231 { @@ -51,7 +52,11 @@ void MY9231OutputComponent::setup() { MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE; ESP_LOGV(TAG, " Command: 0x%02X", command); - this->init_chips_(command); + { + InterruptLock lock; + this->send_dcki_pulses_(32 * this->num_chips_); + this->init_chips_(command); + } ESP_LOGV(TAG, " Chips initialized."); } void MY9231OutputComponent::dump_config() { @@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() { if (!this->update_) return; - for (auto pwm_amount : this->pwm_amounts_) { - this->write_word_(pwm_amount, this->bit_depth_); + { + InterruptLock lock; + for (auto pwm_amount : this->pwm_amounts_) { + this->write_word_(pwm_amount, this->bit_depth_); + } + // Send 8 DI pulses. After 8 falling edges, the duty data are store. + this->send_di_pulses_(8); } - // Send 8 DI pulses. After 8 falling edges, the duty data are store. - this->send_di_pulses_(8); this->update_ = false; } void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) { @@ -92,6 +100,7 @@ void MY9231OutputComponent::init_chips_(uint8_t command) { // Send 16 DI pulse. After 14 falling edges, the command data are // stored and after 16 falling edges the duty mode is activated. this->send_di_pulses_(16); + delayMicroseconds(12); } void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) { for (uint8_t i = bits; i > 0; i--) { @@ -106,6 +115,13 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) { this->pin_di_->digital_write(false); } } +void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) { + delayMicroseconds(12); + for (uint8_t i = 0; i < count; i++) { + this->pin_dcki_->digital_write(true); + this->pin_dcki_->digital_write(false); + } +} } // namespace my9231 } // namespace esphome diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index a777dcc960..77c1259853 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component { void init_chips_(uint8_t command); void write_word_(uint16_t value, uint8_t bits); void send_di_pulses_(uint8_t count); + void send_dcki_pulses_(uint8_t count); GPIOPin *pin_di_; GPIOPin *pin_dcki_; From 4ac49907ca339345f934467ff73f56654b7afc49 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 15 Nov 2023 21:29:50 -0600 Subject: [PATCH 007/436] Add more VA triggers (#5762) --- .../components/voice_assistant/__init__.py | 61 ++++++++++++++++--- .../voice_assistant/voice_assistant.cpp | 39 ++++++++---- .../voice_assistant/voice_assistant.h | 16 +++-- 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 3270b9f370..5715604605 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -18,20 +18,25 @@ DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] -CONF_SILENCE_DETECTION = "silence_detection" -CONF_ON_LISTENING = "on_listening" -CONF_ON_START = "on_start" -CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" -CONF_ON_STT_END = "on_stt_end" -CONF_ON_TTS_START = "on_tts_start" -CONF_ON_TTS_END = "on_tts_end" 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" +CONF_ON_START = "on_start" +CONF_ON_STT_END = "on_stt_end" +CONF_ON_STT_VAD_END = "on_stt_vad_end" +CONF_ON_STT_VAD_START = "on_stt_vad_start" +CONF_ON_TTS_END = "on_tts_end" +CONF_ON_TTS_START = "on_tts_start" +CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" + +CONF_SILENCE_DETECTION = "silence_detection" CONF_USE_WAKE_WORD = "use_wake_word" CONF_VAD_THRESHOLD = "vad_threshold" -CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_AUTO_GAIN = "auto_gain" +CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_VOLUME_MULTIPLIER = "volume_multiplier" @@ -88,6 +93,18 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), ) @@ -177,6 +194,34 @@ async def to_code(config): config[CONF_ON_CLIENT_DISCONNECTED], ) + if CONF_ON_INTENT_START in config: + await automation.build_automation( + var.get_intent_start_trigger(), + [], + config[CONF_ON_INTENT_START], + ) + + if CONF_ON_INTENT_END in config: + await automation.build_automation( + var.get_intent_end_trigger(), + [], + config[CONF_ON_INTENT_END], + ) + + if CONF_ON_STT_VAD_START in config: + await automation.build_automation( + var.get_stt_vad_start_trigger(), + [], + config[CONF_ON_STT_VAD_START], + ) + + if CONF_ON_STT_VAD_END in config: + await automation.build_automation( + var.get_stt_vad_end_trigger(), + [], + config[CONF_ON_STT_VAD_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fc5dd6e4e4..7ebbe762b3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -31,7 +31,7 @@ void VoiceAssistant::setup() { this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); + ESP_LOGW(TAG, "Could not create socket"); this->mark_failed(); return; } @@ -69,7 +69,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); if (this->speaker_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate speaker buffer."); + ESP_LOGW(TAG, "Could not allocate speaker buffer"); this->mark_failed(); return; } @@ -79,7 +79,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); if (this->input_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate input buffer."); + ESP_LOGW(TAG, "Could not allocate input buffer"); this->mark_failed(); return; } @@ -89,7 +89,7 @@ void VoiceAssistant::setup() { this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); if (this->ring_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate ring buffer."); + ESP_LOGW(TAG, "Could not allocate ring buffer"); this->mark_failed(); return; } @@ -98,7 +98,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); if (send_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate send buffer."); + ESP_LOGW(TAG, "Could not allocate send buffer"); this->mark_failed(); return; } @@ -221,8 +221,8 @@ void VoiceAssistant::loop() { msg.audio_settings = audio_settings; if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { - ESP_LOGW(TAG, "Could not request start."); - this->error_trigger_->trigger("not-connected", "Could not request start."); + ESP_LOGW(TAG, "Could not request start"); + this->error_trigger_->trigger("not-connected", "Could not request start"); this->continuous_ = false; this->set_state_(State::IDLE, State::IDLE); break; @@ -280,7 +280,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_size_ += len; } } else { - ESP_LOGW(TAG, "Receive buffer full."); + ESP_LOGW(TAG, "Receive buffer full"); } if (this->speaker_buffer_size_ > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); @@ -290,7 +290,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_index_ -= written; this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); } else { - ESP_LOGW(TAG, "Speaker buffer full."); + ESP_LOGW(TAG, "Speaker buffer full"); } } if (this->wait_for_stream_end_) { @@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_STT_START: - ESP_LOGD(TAG, "STT Started"); + ESP_LOGD(TAG, "STT started"); this->listening_trigger_->trigger(); break; case api::enums::VOICE_ASSISTANT_STT_END: { @@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in STT_END event."); + ESP_LOGW(TAG, "No text in STT_END event"); return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->stt_end_trigger_->trigger(text); break; } + case api::enums::VOICE_ASSISTANT_INTENT_START: + ESP_LOGD(TAG, "Intent started"); + this->intent_start_trigger_->trigger(); + break; case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { this->conversation_id_ = std::move(arg.value); } } + this->intent_end_trigger_->trigger(); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in TTS_START event."); + ESP_LOGW(TAG, "No text in TTS_START event"); return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); @@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (url.empty()) { - ESP_LOGW(TAG, "No url in TTS_END event."); + ESP_LOGW(TAG, "No url in TTS_END event"); return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); @@ -634,6 +639,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); break; } + case api::enums::VOICE_ASSISTANT_STT_VAD_START: + ESP_LOGD(TAG, "Starting STT by VAD"); + this->stt_vad_start_trigger_->trigger(); + break; + case api::enums::VOICE_ASSISTANT_STT_VAD_END: + ESP_LOGD(TAG, "STT by VAD end"); + this->stt_vad_end_trigger_->trigger(); + break; default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a265522bca..a985bc4678 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -100,13 +100,17 @@ class VoiceAssistant : public Component { void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; } void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; } + Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } + Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } + Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } + Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } + Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } - Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } @@ -124,13 +128,17 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; + Trigger<> *intent_end_trigger_ = new Trigger<>(); + Trigger<> *intent_start_trigger_ = new Trigger<>(); Trigger<> *listening_trigger_ = new Trigger<>(); + Trigger<> *end_trigger_ = new Trigger<>(); Trigger<> *start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); - Trigger<> *end_trigger_ = new Trigger<>(); + Trigger *tts_start_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); Trigger<> *client_connected_trigger_ = new Trigger<>(); From 255483de63f9c1fd846cbbdaae98be96df41d6cb Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Thu, 16 Nov 2023 07:45:08 +0000 Subject: [PATCH 008/436] Fix MY9231 flicker (#5765) --- esphome/components/my9231/my9231.cpp | 26 +++++++++++++++++++++----- esphome/components/my9231/my9231.h | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index a97587b7be..c511591856 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -1,5 +1,6 @@ #include "my9231.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace my9231 { @@ -51,7 +52,11 @@ void MY9231OutputComponent::setup() { MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE; ESP_LOGV(TAG, " Command: 0x%02X", command); - this->init_chips_(command); + { + InterruptLock lock; + this->send_dcki_pulses_(32 * this->num_chips_); + this->init_chips_(command); + } ESP_LOGV(TAG, " Chips initialized."); } void MY9231OutputComponent::dump_config() { @@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() { if (!this->update_) return; - for (auto pwm_amount : this->pwm_amounts_) { - this->write_word_(pwm_amount, this->bit_depth_); + { + InterruptLock lock; + for (auto pwm_amount : this->pwm_amounts_) { + this->write_word_(pwm_amount, this->bit_depth_); + } + // Send 8 DI pulses. After 8 falling edges, the duty data are store. + this->send_di_pulses_(8); } - // Send 8 DI pulses. After 8 falling edges, the duty data are store. - this->send_di_pulses_(8); this->update_ = false; } void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) { @@ -92,6 +100,7 @@ void MY9231OutputComponent::init_chips_(uint8_t command) { // Send 16 DI pulse. After 14 falling edges, the command data are // stored and after 16 falling edges the duty mode is activated. this->send_di_pulses_(16); + delayMicroseconds(12); } void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) { for (uint8_t i = bits; i > 0; i--) { @@ -106,6 +115,13 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) { this->pin_di_->digital_write(false); } } +void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) { + delayMicroseconds(12); + for (uint8_t i = 0; i < count; i++) { + this->pin_dcki_->digital_write(true); + this->pin_dcki_->digital_write(false); + } +} } // namespace my9231 } // namespace esphome diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index a777dcc960..77c1259853 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component { void init_chips_(uint8_t command); void write_word_(uint16_t value, uint8_t bits); void send_di_pulses_(uint8_t count); + void send_dcki_pulses_(uint8_t count); GPIOPin *pin_di_; GPIOPin *pin_dcki_; From 445b13dbc6330df2361e06fb9499ae31c6a1153b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:55:28 +1300 Subject: [PATCH 009/436] Bump version to 2023.11.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f937ecf068..7ceaffbe57 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0" +__version__ = "2023.11.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fefdb80fdc941d583aba45b947bd79ae72ee6fa7 Mon Sep 17 00:00:00 2001 From: Nikita Kuklev Date: Thu, 16 Nov 2023 02:06:03 -0600 Subject: [PATCH 010/436] Add proper support for SH1107 to SSD1306 component (#5166) --- esphome/components/ssd1306_base/__init__.py | 7 +- .../components/ssd1306_base/ssd1306_base.cpp | 103 ++++++++++++------ .../components/ssd1306_base/ssd1306_base.h | 2 + .../components/ssd1306_i2c/ssd1306_i2c.cpp | 14 ++- .../components/ssd1306_spi/ssd1306_spi.cpp | 8 +- 5 files changed, 95 insertions(+), 39 deletions(-) diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index f4abd845c8..55239dfcb8 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -33,6 +33,7 @@ MODELS = { "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, "SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64, + "SH1107_128X128": SSD1306Model.SH1107_MODEL_128_128, "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, } @@ -63,8 +64,10 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, cv.Optional(CONF_FLIP_X, default=True): cv.boolean, cv.Optional(CONF_FLIP_Y, default=True): cv.boolean, - cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=-32, max=32), - cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32), + # Offsets determine shifts of memory location to LCD rows/columns, + # and this family of controllers supports up to 128x128 screens + cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=128), + cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=128), cv.Optional(CONF_INVERT, default=False): cv.boolean, } ).extend(cv.polling_component_schema("1s")) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 3cacd473d1..00b5c2d5a2 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -35,16 +35,31 @@ static const uint8_t SSD1306_COMMAND_INVERSE_DISPLAY = 0xA7; static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82; static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; +static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC; +static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD; + void SSD1306::setup() { this->init_internal_(this->get_buffer_length_()); + // SH1107 resources + // + // Datasheet v2.3: + // www.displayfuture.com/Display/datasheet/controller/SH1107.pdf + // Adafruit C++ driver: + // github.com/adafruit/Adafruit_SH110x + // Adafruit CircuitPython driver: + // github.com/adafruit/Adafruit_CircuitPython_DisplayIO_SH1107 + // Turn off display during initialization (0xAE) this->command(SSD1306_COMMAND_DISPLAY_OFF); - // Set oscillator frequency to 4'b1000 with no clock division (0xD5) - this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); - // Oscillator frequency <= 4'b1000, no clock division - this->command(0x80); + // If SH1107, use POR defaults (0x50) = divider 1, frequency +0% + if (!this->is_sh1107_()) { + // Set oscillator frequency to 4'b1000 with no clock division (0xD5) + this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); + // Oscillator frequency <= 4'b1000, no clock division + this->command(0x80); + } // Enable low power display mode for SSD1305 (0xD8) if (this->is_ssd1305_()) { @@ -60,11 +75,26 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y); this->command(0x00 + this->offset_y_); - // Set start line at line 0 (0x40) - this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + if (this->is_sh1107_()) { + // Set start line at line 0 (0xDC) + this->command(SH1107_COMMAND_SET_START_LINE); + this->command(0x00); + } else { + // Set start line at line 0 (0x40) + this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + } - // SSD1305 does not have charge pump - if (!this->is_ssd1305_()) { + if (this->is_ssd1305_()) { + // SSD1305 does not have charge pump + } else if (this->is_sh1107_()) { + // Enable charge pump (0xAD) + this->command(SH1107_COMMAND_CHARGE_PUMP); + if (this->external_vcc_) { + this->command(0x8A); + } else { + this->command(0x8B); + } + } else { // Enable charge pump (0x8D) this->command(SSD1306_COMMAND_CHARGE_PUMP); if (this->external_vcc_) { @@ -76,34 +106,41 @@ void SSD1306::setup() { // Set addressing mode to horizontal (0x20) this->command(SSD1306_COMMAND_MEMORY_MODE); - this->command(0x00); - + if (!this->is_sh1107_()) { + // SH1107 memory mode is a 1 byte command + this->command(0x00); + } // X flip mode (0xA0, 0xA1) this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_); // Y flip mode (0xC0, 0xC8) this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3)); - // Set pin configuration (0xDA) - this->command(SSD1306_COMMAND_SET_COM_PINS); - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - this->command(0x02); - break; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1306_MODEL_64_48: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_64_48: - case SH1107_MODEL_128_64: - case SSD1305_MODEL_128_32: - case SSD1305_MODEL_128_64: - case SSD1306_MODEL_72_40: - this->command(0x12); - break; + if (!this->is_sh1107_()) { + // Set pin configuration (0xDA) + this->command(SSD1306_COMMAND_SET_COM_PINS); + switch (this->model_) { + case SSD1306_MODEL_128_32: + case SH1106_MODEL_128_32: + case SSD1306_MODEL_96_16: + case SH1106_MODEL_96_16: + this->command(0x02); + break; + case SSD1306_MODEL_128_64: + case SH1106_MODEL_128_64: + case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: + case SH1106_MODEL_64_48: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: + case SSD1306_MODEL_72_40: + this->command(0x12); + break; + case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: + // Not used, but prevents build warning + break; + } } // Pre-charge period (0xD9) @@ -118,6 +155,7 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_SET_VCOM_DETECT); switch (this->model_) { case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: this->command(0x35); break; case SSD1306_MODEL_72_40: @@ -149,7 +187,7 @@ void SSD1306::setup() { this->turn_on(); } void SSD1306::display() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { this->write_display_data(); return; } @@ -183,6 +221,7 @@ bool SSD1306::is_sh1106_() const { return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 || this->model_ == SH1106_MODEL_128_64; } +bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; } bool SSD1306::is_ssd1305_() const { return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; } @@ -224,6 +263,7 @@ void SSD1306::turn_off() { int SSD1306::get_height_internal() { switch (this->model_) { case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_128_32: case SSD1306_MODEL_64_32: @@ -254,6 +294,7 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_128_64: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 4b0e9bb80e..34b76d284d 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -19,6 +19,7 @@ enum SSD1306Model { SH1106_MODEL_96_16, SH1106_MODEL_64_48, SH1107_MODEL_128_64, + SH1107_MODEL_128_128, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, }; @@ -58,6 +59,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void init_reset_(); bool is_sh1106_() const; + bool is_sh1107_() const; bool is_ssd1305_() const; void draw_absolute_pixel_internal(int x, int y, Color color) override; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 96734eb618..ed7cf102ee 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -38,13 +38,19 @@ void I2CSSD1306::dump_config() { } void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); } void HOT I2CSSD1306::write_display_data() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { uint32_t i = 0; for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) { this->command(0xB0 + page); // row - this->command(0x02); // lower column - this->command(0x10); // higher column - + if (this->is_sh1106_()) { + this->command(0x02); // lower column - 0x02 is historical SH1106 value + } else { + // Other SH1107 drivers use 0x00 + // Column values dont change and it seems they can be set only once, + // but we follow SH1106 implementation and resend them + this->command(0x00); + } + this->command(0x10); // higher column for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) { uint8_t data[16]; for (uint8_t &j : data) diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 7f025d77cd..0a0debfd65 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -36,10 +36,14 @@ void SPISSD1306::command(uint8_t value) { this->disable(); } void HOT SPISSD1306::write_display_data() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { this->command(0xB0 + y); - this->command(0x02); + if (this->is_sh1106_()) { + this->command(0x02); + } else { + this->command(0x00); + } this->command(0x10); this->dc_pin_->digital_write(true); for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) { From 208edf89dc1dd458eb55ca6ca3f976bc231b7d4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Nov 2023 21:06:16 +1300 Subject: [PATCH 011/436] Split release workflow jobs per system arch (#5723) --- .github/actions/build-image/action.yaml | 97 ++++++++++++++++++ .github/workflows/release.yml | 130 +++++++++++++++++------- docker/generate_tags.py | 54 +++++++--- 3 files changed, 232 insertions(+), 49 deletions(-) create mode 100644 .github/actions/build-image/action.yaml diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml new file mode 100644 index 0000000000..4c98a47ecd --- /dev/null +++ b/.github/actions/build-image/action.yaml @@ -0,0 +1,97 @@ +name: Build Image +inputs: + platform: + description: "Platform to build for" + required: true + example: "linux/amd64" + target: + description: "Target to build" + required: true + example: "docker" + baseimg: + description: "Base image type" + required: true + example: "docker" + suffix: + description: "Suffix to add to tags" + required: true + version: + description: "Version to build" + required: true + example: "2023.12.0" +runs: + using: "composite" + steps: + - name: Generate short tags + id: tags + shell: bash + run: | + output=$(docker/generate_tags.py \ + --tag "${{ inputs.version }}" \ + --suffix "${{ inputs.suffix }}") + echo $output + for l in $output; do + echo $l >> $GITHUB_OUTPUT + done + + - name: Build and push to ghcr by digest + id: build-ghcr + uses: docker/build-push-action@v5.0.0 + with: + context: . + file: ./docker/Dockerfile + platforms: ${{ inputs.platform }} + target: ${{ inputs.target }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BASEIMGTYPE=${{ inputs.baseimg }} + BUILD_VERSION=${{ inputs.version }} + outputs: | + type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true + + - name: Export ghcr digests + shell: bash + run: | + mkdir -p /tmp/digests/${{ inputs.target }}/ghcr + digest="${{ steps.build-ghcr.outputs.digest }}" + touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" + + - name: Upload ghcr digest + uses: actions/upload-artifact@v3.1.3 + with: + name: digests-${{ inputs.target }}-ghcr + path: /tmp/digests/${{ inputs.target }}/ghcr/* + if-no-files-found: error + retention-days: 1 + + - name: Build and push to dockerhub by digest + id: build-dockerhub + uses: docker/build-push-action@v5.0.0 + with: + context: . + file: ./docker/Dockerfile + platforms: ${{ inputs.platform }} + target: ${{ inputs.target }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BASEIMGTYPE=${{ inputs.baseimg }} + BUILD_VERSION=${{ inputs.version }} + outputs: | + type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true + + - name: Export dockerhub digests + shell: bash + run: | + mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub + digest="${{ steps.build-dockerhub.outputs.digest }}" + touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" + + - name: Upload dockerhub digest + uses: actions/upload-artifact@v3.1.3 + with: + name: digests-${{ inputs.target }}-dockerhub + path: /tmp/digests/${{ inputs.target }}/dockerhub/* + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14dbeee7b7..0e23db521a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,30 +63,20 @@ jobs: run: twine upload dist/* deploy-docker: - name: Build and publish ESPHome ${{ matrix.image.title}} + name: Build ESPHome ${{ matrix.platform }} if: github.repository == 'esphome/esphome' permissions: contents: read packages: write runs-on: ubuntu-latest - continue-on-error: ${{ matrix.image.title == 'lint' }} needs: [init] strategy: fail-fast: false matrix: - image: - - title: "ha-addon" - suffix: "hassio" - target: "hassio" - baseimg: "hassio" - - title: "docker" - suffix: "" - target: "docker" - baseimg: "docker" - - title: "lint" - suffix: "lint" - target: "lint" - baseimg: "docker" + platform: + - linux/amd64 + - linux/arm/v7 + - linux/arm64 steps: - uses: actions/checkout@v4.1.1 - name: Set up Python @@ -97,6 +87,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.0.0 - name: Set up QEMU + if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v3.0.0 - name: Log in to docker hub @@ -111,34 +102,105 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build docker + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: docker + baseimg: docker + suffix: "" + version: ${{ needs.init.outputs.tag }} + + - name: Build ha-addon + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: hassio + baseimg: hassio + suffix: "hassio" + version: ${{ needs.init.outputs.tag }} + + - name: Build lint + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: lint + baseimg: docker + suffix: lint + version: ${{ needs.init.outputs.tag }} + + deploy-manifest: + name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }} + runs-on: ubuntu-latest + needs: + - init + - deploy-docker + if: github.repository == 'esphome/esphome' + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + image: + - title: "ha-addon" + target: "hassio" + suffix: "hassio" + - title: "docker" + target: "docker" + suffix: "" + - title: "lint" + target: "lint" + suffix: "lint" + registry: + - ghcr + - dockerhub + steps: + - uses: actions/checkout@v4.1.1 + - name: Download digests + uses: actions/download-artifact@v3.0.2 + with: + name: digests-${{ matrix.image.target }}-${{ matrix.registry }} + path: /tmp/digests + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Log in to docker hub + if: matrix.registry == 'dockerhub' + uses: docker/login-action@v3.0.0 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + if: matrix.registry == 'ghcr' + uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Generate short tags id: tags run: | - docker/generate_tags.py \ + output=$(docker/generate_tags.py \ --tag "${{ needs.init.outputs.tag }}" \ - --suffix "${{ matrix.image.suffix }}" + --suffix "${{ matrix.image.suffix }}" \ + --registry "${{ matrix.registry }}") + echo $output + for l in $output; do + echo $l >> $GITHUB_OUTPUT + done - - name: Build and push - uses: docker/build-push-action@v5.0.0 - with: - context: . - file: ./docker/Dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64 - target: ${{ matrix.image.target }} - push: true - # yamllint disable rule:line-length - cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }} - cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max - # yamllint enable rule:line-length - tags: ${{ steps.tags.outputs.tags }} - build-args: | - BASEIMGTYPE=${{ matrix.image.baseimg }} - BUILD_VERSION=${{ needs.init.outputs.tag }} + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ + $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) deploy-ha-addon-repo: if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest - needs: [deploy-docker] + needs: [deploy-manifest] steps: - name: Trigger Workflow uses: actions/github-script@v6.4.1 diff --git a/docker/generate_tags.py b/docker/generate_tags.py index 71d0735526..3fc787d485 100755 --- a/docker/generate_tags.py +++ b/docker/generate_tags.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 import re -import os import argparse -import json CHANNEL_DEV = "dev" CHANNEL_BETA = "beta" CHANNEL_RELEASE = "release" +GHCR = "ghcr" +DOCKERHUB = "dockerhub" + parser = argparse.ArgumentParser() parser.add_argument( "--tag", @@ -21,21 +22,31 @@ parser.add_argument( required=True, help="The suffix of the tag.", ) +parser.add_argument( + "--registry", + type=str, + choices=[GHCR, DOCKERHUB], + required=False, + action="append", + help="The registry to build tags for.", +) def main(): args = parser.parse_args() # detect channel from tag - match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) + match = re.match(r"^(\d+\.\d+)(?:\.\d+)(?:(b\d+)|(-dev\d+))?$", args.tag) major_minor_version = None - if match is None: + if match is None: # eg 2023.12.0-dev20231109-testbranch + channel = None # Ran with custom tag for a branch etc + elif match.group(3) is not None: # eg 2023.12.0-dev20231109 channel = CHANNEL_DEV - elif match.group(2) is None: + elif match.group(2) is not None: # eg 2023.12.0b1 + channel = CHANNEL_BETA + else: # eg 2023.12.0 major_minor_version = match.group(1) channel = CHANNEL_RELEASE - else: - channel = CHANNEL_BETA tags_to_push = [args.tag] if channel == CHANNEL_DEV: @@ -53,15 +64,28 @@ def main(): suffix = f"-{args.suffix}" if args.suffix else "" - with open(os.environ["GITHUB_OUTPUT"], "w") as f: - print(f"channel={channel}", file=f) - print(f"image=esphome/esphome{suffix}", file=f) - full_tags = [] + image_name = f"esphome/esphome{suffix}" - for tag in tags_to_push: - full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"] - full_tags += [f"esphome/esphome{suffix}:{tag}"] - print(f"tags={','.join(full_tags)}", file=f) + print(f"channel={channel}") + + if args.registry is None: + args.registry = [GHCR, DOCKERHUB] + elif len(args.registry) == 1: + if GHCR in args.registry: + print(f"image=ghcr.io/{image_name}") + if DOCKERHUB in args.registry: + print(f"image=docker.io/{image_name}") + + print(f"image_name={image_name}") + + full_tags = [] + + for tag in tags_to_push: + if GHCR in args.registry: + full_tags += [f"ghcr.io/{image_name}:{tag}"] + if DOCKERHUB in args.registry: + full_tags += [f"docker.io/{image_name}:{tag}"] + print(f"tags={','.join(full_tags)}") if __name__ == "__main__": From 5464368c081fbc369d7db67548ca8e7faf053931 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 23:35:42 -0600 Subject: [PATCH 012/436] Bump aioesphomeapi from 18.4.1 to 18.5.2 (#5780) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9afe7064c2..3ac0a1f937 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.1 +aioesphomeapi==18.5.2 zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default From 32e3f2623973f45bd37b6b0dc956adcd82a45e77 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 17 Nov 2023 01:16:03 -0800 Subject: [PATCH 013/436] fix 32-bit arm (#5781) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7ca633a982..a892e1df38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,7 @@ ENV \ # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian RUN \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ - ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ + ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \ fi RUN \ From 6f8d7c6acde30483e9c9508d72697a9f22400c7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Nov 2023 17:48:53 -0600 Subject: [PATCH 014/436] Bump aioesphomeapi to 18.5.3 (#5785) - Avoids creating a zeroconf instance when we do not need one supports https://github.com/esphome/esphome/pull/5783 changelog: https://github.com/esphome/aioesphomeapi/compare/v18.5.2...v18.5.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3ac0a1f937..1866d33ab2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.2 +aioesphomeapi==18.5.3 zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default From 288af1f4d2f6bf57d1701ef483b502691758a73a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Nov 2023 17:50:40 -0600 Subject: [PATCH 015/436] Refactor log api client to let aioesphomeapi manage zeroconf (#5783) aioesphomeapi is now smart enough to avoid creating a zeroconf instance until its needed after https://github.com/esphome/aioesphomeapi/pull/643 This avoids the needs to have a background zeroconf instance running that is processing incoming records but will never do anything --- esphome/components/api/client.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 701848b1f1..dd013c8c34 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -8,7 +8,6 @@ from typing import Any from aioesphomeapi import APIClient from aioesphomeapi.api_pb2 import SubscribeLogsResponse from aioesphomeapi.log_runner import async_run -from zeroconf.asyncio import AsyncZeroconf from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE @@ -28,14 +27,12 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: if CONF_ENCRYPTION in conf: noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) - aiozc = AsyncZeroconf() cli = APIClient( address, port, password, client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, - zeroconf_instance=aiozc.zeroconf, ) dashboard = CORE.dashboard @@ -48,12 +45,10 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: text = text.replace("\033", "\\033") print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") - stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc, name=name) + stop = await async_run(cli, on_log, name=name) try: - while True: - await asyncio.sleep(60) + await asyncio.Event().wait() finally: - await aiozc.async_close() await stop() From 3c243e663f18723570cf313d502da85fec2a063e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Nov 2023 18:33:10 -0600 Subject: [PATCH 016/436] dashboard: Add support for firing events (#5775) * dashboard: fire events when entry is updated or state changes * dashboard: fire events when entry is updated or state changes * dashboard: fire events when entry is updated or state changes * tweaks * fixes * remove typing_extensions * rename for asyncio * rename for asyncio * rename for asyncio * preen * lint * lint * move dict converter * lint --- esphome/dashboard/const.py | 8 ++ esphome/dashboard/core.py | 49 ++++++++++- esphome/dashboard/entries.py | 141 +++++++++++++++++++++++++++---- esphome/dashboard/enum.py | 19 +++++ esphome/dashboard/status/mdns.py | 48 ++++++----- esphome/dashboard/status/mqtt.py | 17 ++-- esphome/dashboard/status/ping.py | 10 +-- esphome/dashboard/web_server.py | 37 ++++---- 8 files changed, 251 insertions(+), 78 deletions(-) create mode 100644 esphome/dashboard/const.py create mode 100644 esphome/dashboard/enum.py diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py new file mode 100644 index 0000000000..ed2b81d3e8 --- /dev/null +++ b/esphome/dashboard/const.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +EVENT_ENTRY_ADDED = "entry_added" +EVENT_ENTRY_REMOVED = "entry_removed" +EVENT_ENTRY_UPDATED = "entry_updated" +EVENT_ENTRY_STATE_CHANGED = "entry_state_changed" + +SENTINEL = object() diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index f18da92d80..ffec9784e8 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -3,7 +3,9 @@ from __future__ import annotations import asyncio import logging import threading -from typing import TYPE_CHECKING +from dataclasses import dataclass +from functools import partial +from typing import TYPE_CHECKING, Any, Callable from ..zeroconf import DiscoveredImport from .entries import DashboardEntries @@ -12,16 +14,55 @@ from .settings import DashboardSettings if TYPE_CHECKING: from .status.mdns import MDNSStatus + _LOGGER = logging.getLogger(__name__) +@dataclass +class Event: + """Dashboard Event.""" + + event_type: str + data: dict[str, Any] + + +class EventBus: + """Dashboard event bus.""" + + def __init__(self) -> None: + """Initialize the Dashboard event bus.""" + self._listeners: dict[str, set[Callable[[Event], None]]] = {} + + def async_add_listener( + self, event_type: str, listener: Callable[[Event], None] + ) -> Callable[[], None]: + """Add a listener to the event bus.""" + self._listeners.setdefault(event_type, set()).add(listener) + return partial(self._async_remove_listener, event_type, listener) + + def _async_remove_listener( + self, event_type: str, listener: Callable[[Event], None] + ) -> None: + """Remove a listener from the event bus.""" + self._listeners[event_type].discard(listener) + + def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None: + """Fire an event.""" + event = Event(event_type, event_data) + + _LOGGER.debug("Firing event: %s", event) + + for listener in self._listeners.get(event_type, set()): + listener(event) + + class ESPHomeDashboard: """Class that represents the dashboard.""" __slots__ = ( + "bus", "entries", "loop", - "ping_result", "import_result", "stop_event", "ping_request", @@ -32,9 +73,9 @@ class ESPHomeDashboard: def __init__(self) -> None: """Initialize the ESPHomeDashboard.""" + self.bus = EventBus() self.entries: DashboardEntries | None = None self.loop: asyncio.AbstractEventLoop | None = None - self.ping_result: dict[str, bool | None] = {} self.import_result: dict[str, DiscoveredImport] = {} self.stop_event = threading.Event() self.ping_request: asyncio.Event | None = None @@ -46,7 +87,7 @@ class ESPHomeDashboard: """Setup the dashboard.""" self.loop = asyncio.get_running_loop() self.ping_request = asyncio.Event() - self.entries = DashboardEntries(self.settings.config_dir) + self.entries = DashboardEntries(self) async def async_run(self) -> None: """Run the dashboard.""" diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index ff539fc620..42b3a2e743 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -3,24 +3,78 @@ from __future__ import annotations import asyncio import logging import os +from typing import TYPE_CHECKING, Any from esphome import const, util from esphome.storage_json import StorageJSON, ext_storage_path +from .const import ( + EVENT_ENTRY_ADDED, + EVENT_ENTRY_REMOVED, + EVENT_ENTRY_STATE_CHANGED, + EVENT_ENTRY_UPDATED, +) +from .enum import StrEnum + +if TYPE_CHECKING: + from .core import ESPHomeDashboard + _LOGGER = logging.getLogger(__name__) + DashboardCacheKeyType = tuple[int, int, float, int] +# Currently EntryState is a simple +# online/offline/unknown enum, but in the future +# it may be expanded to include more states + + +class EntryState(StrEnum): + ONLINE = "online" + OFFLINE = "offline" + UNKNOWN = "unknown" + + +_BOOL_TO_ENTRY_STATE = { + True: EntryState.ONLINE, + False: EntryState.OFFLINE, + None: EntryState.UNKNOWN, +} +_ENTRY_STATE_TO_BOOL = { + EntryState.ONLINE: True, + EntryState.OFFLINE: False, + EntryState.UNKNOWN: None, +} + + +def bool_to_entry_state(value: bool) -> EntryState: + """Convert a bool to an entry state.""" + return _BOOL_TO_ENTRY_STATE[value] + + +def entry_state_to_bool(value: EntryState) -> bool | None: + """Convert an entry state to a bool.""" + return _ENTRY_STATE_TO_BOOL[value] + class DashboardEntries: """Represents all dashboard entries.""" - __slots__ = ("_loop", "_config_dir", "_entries", "_loaded_entries", "_update_lock") + __slots__ = ( + "_dashboard", + "_loop", + "_config_dir", + "_entries", + "_entry_states", + "_loaded_entries", + "_update_lock", + ) - def __init__(self, config_dir: str) -> None: + def __init__(self, dashboard: ESPHomeDashboard) -> None: """Initialize the DashboardEntries.""" + self._dashboard = dashboard self._loop = asyncio.get_running_loop() - self._config_dir = config_dir + self._config_dir = dashboard.settings.config_dir # Entries are stored as # { # "path/to/file.yaml": DashboardEntry, @@ -46,6 +100,25 @@ class DashboardEntries: """Return all entries.""" return list(self._entries.values()) + def set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + asyncio.run_coroutine_threadsafe( + self._async_set_state(entry, state), self._loop + ).result() + + async def _async_set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + self.async_set_state(entry, state) + + def async_set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + if entry.state == state: + return + entry.state = state + self._dashboard.bus.async_fire( + EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state} + ) + async def async_request_update_entries(self) -> None: """Request an update of the dashboard entries from disk. @@ -81,16 +154,17 @@ class DashboardEntries: path_to_cache_key = await self._loop.run_in_executor( None, self._get_path_to_cache_key ) + entries = self._entries added: dict[DashboardEntry, DashboardCacheKeyType] = {} updated: dict[DashboardEntry, DashboardCacheKeyType] = {} removed: set[DashboardEntry] = { entry - for filename, entry in self._entries.items() + for filename, entry in entries.items() if filename not in path_to_cache_key } - entries = self._entries + for path, cache_key in path_to_cache_key.items(): - if entry := self._entries.get(path): + if entry := entries.get(path): if entry.cache_key != cache_key: updated[entry] = cache_key else: @@ -102,17 +176,17 @@ class DashboardEntries: None, self._load_entries, {**added, **updated} ) + bus = self._dashboard.bus for entry in added: - _LOGGER.debug("Added dashboard entry %s", entry.path) entries[entry.path] = entry + bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry}) - if entry in removed: - _LOGGER.debug("Removed dashboard entry %s", entry.path) - entries.pop(entry.path) + for entry in removed: + del entries[entry.path] + bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry}) for entry in updated: - _LOGGER.debug("Updated dashboard entry %s", entry.path) - # In the future we can fire events when entries are added/removed/updated + bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry}) def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: """Return a dict of path to cache key.""" @@ -152,29 +226,64 @@ class DashboardEntry: This class is thread-safe and read-only. """ - __slots__ = ("path", "filename", "_storage_path", "cache_key", "storage") + __slots__ = ( + "path", + "filename", + "_storage_path", + "cache_key", + "storage", + "state", + "_to_dict", + ) def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None: """Initialize the DashboardEntry.""" self.path = path - self.filename = os.path.basename(path) + self.filename: str = os.path.basename(path) self._storage_path = ext_storage_path(self.filename) self.cache_key = cache_key self.storage: StorageJSON | None = None + self.state = EntryState.UNKNOWN + self._to_dict: dict[str, Any] | None = None def __repr__(self): """Return the representation of this entry.""" return ( - f"DashboardEntry({self.path} " + f"DashboardEntry(path={self.path} " f"address={self.address} " f"web_port={self.web_port} " f"name={self.name} " - f"no_mdns={self.no_mdns})" + f"no_mdns={self.no_mdns} " + f"state={self.state} " + ")" ) + def to_dict(self) -> dict[str, Any]: + """Return a dict representation of this entry. + + The dict includes the loaded configuration but not + the current state of the entry. + """ + if self._to_dict is None: + self._to_dict = { + "name": self.name, + "friendly_name": self.friendly_name, + "configuration": self.filename, + "loaded_integrations": self.loaded_integrations, + "deployed_version": self.update_old, + "current_version": self.update_new, + "path": self.path, + "comment": self.comment, + "address": self.address, + "web_port": self.web_port, + "target_platform": self.target_platform, + } + return self._to_dict + def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None: """Load this entry from disk.""" self.storage = StorageJSON.load(self._storage_path) + self._to_dict = None # # Currently StorageJSON.load() will return None if the file does not exist # diff --git a/esphome/dashboard/enum.py b/esphome/dashboard/enum.py new file mode 100644 index 0000000000..6aff21620e --- /dev/null +++ b/esphome/dashboard/enum.py @@ -0,0 +1,19 @@ +"""Enum backports from standard lib.""" +from __future__ import annotations + +from enum import Enum +from typing import Any + + +class StrEnum(str, Enum): + """Partial backport of Python 3.11's StrEnum for our basic use cases.""" + + def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: + """Create a new StrEnum instance.""" + if not isinstance(value, str): + raise TypeError(f"{value!r} is not a string") + return super().__new__(cls, value, *args, **kwargs) + + def __str__(self) -> str: + """Return self.value.""" + return str(self.value) diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index 51d11390b7..cbe3b3309e 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -10,7 +10,9 @@ from esphome.zeroconf import ( DashboardStatus, ) +from ..const import SENTINEL from ..core import DASHBOARD +from ..entries import bool_to_entry_state class MDNSStatus: @@ -22,16 +24,16 @@ class MDNSStatus: self.aiozc: AsyncEsphomeZeroconf | None = None # This is the current mdns state for each host (True, False, None) self.host_mdns_state: dict[str, bool | None] = {} - # This is the hostnames to filenames mapping - self.host_name_to_filename: dict[str, str] = {} - self.filename_to_host_name: dict[str, str] = {} + # This is the hostnames to path mapping + self.host_name_to_path: dict[str, str] = {} + self.path_to_host_name: dict[str, str] = {} # This is a set of host names to track (i.e no_mdns = false) self.host_name_with_mdns_enabled: set[set] = set() self._loop = asyncio.get_running_loop() - def filename_to_host_name_thread_safe(self, filename: str) -> str | None: - """Resolve a filename to an address in a thread-safe manner.""" - return self.filename_to_host_name.get(filename) + def get_path_to_host_name(self, path: str) -> str | None: + """Resolve a path to an address in a thread-safe manner.""" + return self.path_to_host_name.get(path) async def async_resolve_host(self, host_name: str) -> str | None: """Resolve a host name to an address in a thread-safe manner.""" @@ -42,14 +44,14 @@ class MDNSStatus: async def async_refresh_hosts(self): """Refresh the hosts to track.""" dashboard = DASHBOARD - entries = dashboard.entries.async_all() + current_entries = dashboard.entries.async_all() host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_mdns_state = self.host_mdns_state - host_name_to_filename = self.host_name_to_filename - filename_to_host_name = self.filename_to_host_name - ping_result = dashboard.ping_result + host_name_to_path = self.host_name_to_path + path_to_host_name = self.path_to_host_name + entries = dashboard.entries - for entry in entries: + for entry in current_entries: name = entry.name # If no_mdns is set, remove it from the set if entry.no_mdns: @@ -58,37 +60,37 @@ class MDNSStatus: # We are tracking this host host_name_with_mdns_enabled.add(name) - filename = entry.filename + path = entry.path # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if name in host_mdns_state: - ping_result[filename] = host_mdns_state[name] + if (online := host_mdns_state.get(name, SENTINEL)) != SENTINEL: + entries.async_set_state(entry, bool_to_entry_state(online)) # Make sure the mapping is up to date # so when we get an mdns update we can map it back # to the filename - host_name_to_filename[name] = filename - filename_to_host_name[filename] = name + host_name_to_path[name] = path + path_to_host_name[path] = name async def async_run(self) -> None: dashboard = DASHBOARD - + entries = dashboard.entries aiozc = AsyncEsphomeZeroconf() self.aiozc = aiozc host_mdns_state = self.host_mdns_state - host_name_to_filename = self.host_name_to_filename + host_name_to_path = self.host_name_to_path host_name_with_mdns_enabled = self.host_name_with_mdns_enabled - ping_result = dashboard.ping_result def on_update(dat: dict[str, bool | None]) -> None: - """Update the global PING_RESULT dict.""" + """Update the entry state.""" for name, result in dat.items(): host_mdns_state[name] = result - if name in host_name_with_mdns_enabled: - filename = host_name_to_filename[name] - ping_result[filename] = result + if name not in host_name_with_mdns_enabled: + continue + if entry := entries.get(host_name_to_path[name]): + entries.async_set_state(entry, bool_to_entry_state(result)) stat = DashboardStatus(on_update) imports = DashboardImportDiscovery() diff --git a/esphome/dashboard/status/mqtt.py b/esphome/dashboard/status/mqtt.py index 2fd3a332a7..8c35dd2535 100644 --- a/esphome/dashboard/status/mqtt.py +++ b/esphome/dashboard/status/mqtt.py @@ -8,6 +8,7 @@ import threading from esphome import mqtt from ..core import DASHBOARD +from ..entries import EntryState class MqttStatusThread(threading.Thread): @@ -16,22 +17,23 @@ class MqttStatusThread(threading.Thread): def run(self) -> None: """Run the status thread.""" dashboard = DASHBOARD - entries = dashboard.entries.all() + entries = dashboard.entries + current_entries = entries.all() config = mqtt.config_from_env() topic = "esphome/discover/#" def on_message(client, userdata, msg): - nonlocal entries + nonlocal current_entries payload = msg.payload.decode(errors="backslashreplace") if len(payload) > 0: data = json.loads(payload) if "name" not in data: return - for entry in entries: + for entry in current_entries: if entry.name == data["name"]: - dashboard.ping_result[entry.filename] = True + entries.set_state(entry, EntryState.ONLINE) return def on_connect(client, userdata, flags, return_code): @@ -51,12 +53,11 @@ class MqttStatusThread(threading.Thread): client.loop_start() while not dashboard.stop_event.wait(2): - entries = dashboard.entries.all() - + current_entries = entries.all() # will be set to true on on_message - for entry in entries: + for entry in current_entries: if entry.no_mdns: - dashboard.ping_result[entry.filename] = False + entries.set_state(entry, EntryState.OFFLINE) client.publish("esphome/discover", None, retain=False) dashboard.mqtt_ping_request.wait() diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 35fb2259f0..d8281d9de1 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -5,7 +5,7 @@ import os from typing import cast from ..core import DASHBOARD -from ..entries import DashboardEntry +from ..entries import DashboardEntry, bool_to_entry_state from ..util.itertools import chunked from ..util.subprocess import async_system_command_status @@ -26,14 +26,14 @@ class PingStatus: async def async_run(self) -> None: """Run the ping status.""" dashboard = DASHBOARD + entries = dashboard.entries while not dashboard.stop_event.is_set(): # Only ping if the dashboard is open await dashboard.ping_request.wait() - dashboard.ping_result.clear() - entries = dashboard.entries.async_all() + current_entries = dashboard.entries.async_all() to_ping: list[DashboardEntry] = [ - entry for entry in entries if entry.address is not None + entry for entry in current_entries if entry.address is not None ] for ping_group in chunked(to_ping, 16): ping_group = cast(list[DashboardEntry], ping_group) @@ -46,4 +46,4 @@ class PingStatus: result = False elif isinstance(result, BaseException): raise result - dashboard.ping_result[entry.filename] = result + entries.async_set_state(entry, bool_to_entry_state(result)) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9a5de0a933..9972808948 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -37,6 +37,7 @@ from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader from .core import DASHBOARD +from .entries import EntryState, entry_state_to_bool from .util.subprocess import async_run_system_command from .util.text import friendly_name_slugify @@ -275,7 +276,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): if ( port == "OTA" and (mdns := dashboard.mdns_status) - and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) + and (host_name := mdns.get_path_to_host_name(config_file)) and (address := await mdns.async_resolve_host(host_name)) ): port = address @@ -315,7 +316,9 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket): return # Remove the old ping result from the cache - DASHBOARD.ping_result.pop(self.old_name, None) + entries = DASHBOARD.entries + if entry := entries.get(self.old_name): + entries.async_set_state(entry, EntryState.UNKNOWN) class EsphomeUploadHandler(EsphomePortCommandWebSocket): @@ -609,22 +612,7 @@ class ListDevicesHandler(BaseHandler): self.write( json.dumps( { - "configured": [ - { - "name": entry.name, - "friendly_name": entry.friendly_name, - "configuration": entry.filename, - "loaded_integrations": entry.loaded_integrations, - "deployed_version": entry.update_old, - "current_version": entry.update_new, - "path": entry.path, - "comment": entry.comment, - "address": entry.address, - "web_port": entry.web_port, - "target_platform": entry.target_platform, - } - for entry in entries - ], + "configured": [entry.to_dict() for entry in entries], "importable": [ { "name": res.device_name, @@ -728,7 +716,15 @@ class PingRequestHandler(BaseHandler): if settings.status_use_mqtt: dashboard.mqtt_ping_request.set() self.set_header("content-type", "application/json") - self.write(json.dumps(dashboard.ping_result)) + + self.write( + json.dumps( + { + entry.filename: entry_state_to_bool(entry.state) + for entry in dashboard.entries.async_all() + } + ) + ) class InfoRequestHandler(BaseHandler): @@ -785,9 +781,6 @@ class DeleteRequestHandler(BaseHandler): if build_folder is not None: shutil.rmtree(build_folder, os.path.join(trash_path, name)) - # Remove the old ping result from the cache - DASHBOARD.ping_result.pop(configuration, None) - class UndoDeleteRequestHandler(BaseHandler): @authenticated From 8fbb4e27d1e09e3e7336baf5f2ef5113ecbdb734 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 18 Nov 2023 02:00:59 -0600 Subject: [PATCH 017/436] Add 2MB option for partitions.csv generation and restore use of user-defined partitions (#5779) --- esphome/components/esp32/__init__.py | 58 ++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9b83d144f8..fd5e9377dd 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -3,23 +3,26 @@ from typing import Union, Optional from pathlib import Path import logging import os +import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, CONF_COMPONENTS, + CONF_ESPHOME, CONF_FRAMEWORK, + CONF_IGNORE_EFUSE_MAC_CRC, CONF_NAME, + CONF_PATH, + CONF_PLATFORMIO_OPTIONS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_TYPE, + CONF_URL, CONF_VARIANT, CONF_VERSION, - CONF_ADVANCED, - CONF_REFRESH, - CONF_PATH, - CONF_URL, - CONF_REF, - CONF_IGNORE_EFUSE_MAC_CRC, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_NAME, @@ -327,6 +330,32 @@ def _detect_variant(value): return value +def final_validate(config): + if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]: + return config + + pio_flash_size_key = "board_upload.flash_size" + pio_partitions_key = "board_build.partitions" + if ( + CONF_PARTITIONS in config + and pio_partitions_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" + ) + + if ( + pio_flash_size_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" + ) + + return config + + CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( @@ -387,6 +416,7 @@ FRAMEWORK_SCHEMA = cv.typed_schema( FLASH_SIZES = [ + "2MB", "4MB", "8MB", "16MB", @@ -394,6 +424,7 @@ FLASH_SIZES = [ ] CONF_FLASH_SIZE = "flash_size" +CONF_PARTITIONS = "partitions" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -401,6 +432,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), + cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } @@ -410,6 +442,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) + + async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) @@ -462,7 +497,10 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -507,7 +545,10 @@ async def to_code(config): [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], ) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_define( "USE_ARDUINO_VERSION_CODE", @@ -518,6 +559,7 @@ async def to_code(config): APP_PARTITION_SIZES = { + "2MB": 0x0C0000, # 768 KB "4MB": 0x1C0000, # 1792 KB "8MB": 0x3C0000, # 3840 KB "16MB": 0x7C0000, # 7936 KB From c36944326368ca3d323eeb41ba3146d052c30629 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:28:04 -0600 Subject: [PATCH 018/436] Bump aioesphomeapi from 18.4.0 to 18.4.1 (#5767) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1d53c2761c..b1ca8e5058 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.0 +aioesphomeapi==18.4.1 zeroconf==0.123.0 # esp-idf requires this, but doesn't bundle it by default From e5e3b253bca349ee354ee76565b11ea571bbad4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 23:35:42 -0600 Subject: [PATCH 019/436] Bump aioesphomeapi from 18.4.1 to 18.5.2 (#5780) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b1ca8e5058..7a3bb421ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.1 +aioesphomeapi==18.5.2 zeroconf==0.123.0 # esp-idf requires this, but doesn't bundle it by default From 625ce2b8ebb88367174a1a73b684fa690261471d Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 17 Nov 2023 01:16:03 -0800 Subject: [PATCH 020/436] fix 32-bit arm (#5781) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7ca633a982..a892e1df38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,7 @@ ENV \ # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian RUN \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ - ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ + ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \ fi RUN \ From 22eef036c7a74072534e00b7071775fc8b20d1a8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 18 Nov 2023 02:00:59 -0600 Subject: [PATCH 021/436] Add 2MB option for partitions.csv generation and restore use of user-defined partitions (#5779) --- esphome/components/esp32/__init__.py | 58 ++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9b83d144f8..fd5e9377dd 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -3,23 +3,26 @@ from typing import Union, Optional from pathlib import Path import logging import os +import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, CONF_COMPONENTS, + CONF_ESPHOME, CONF_FRAMEWORK, + CONF_IGNORE_EFUSE_MAC_CRC, CONF_NAME, + CONF_PATH, + CONF_PLATFORMIO_OPTIONS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_TYPE, + CONF_URL, CONF_VARIANT, CONF_VERSION, - CONF_ADVANCED, - CONF_REFRESH, - CONF_PATH, - CONF_URL, - CONF_REF, - CONF_IGNORE_EFUSE_MAC_CRC, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_NAME, @@ -327,6 +330,32 @@ def _detect_variant(value): return value +def final_validate(config): + if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]: + return config + + pio_flash_size_key = "board_upload.flash_size" + pio_partitions_key = "board_build.partitions" + if ( + CONF_PARTITIONS in config + and pio_partitions_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" + ) + + if ( + pio_flash_size_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" + ) + + return config + + CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( @@ -387,6 +416,7 @@ FRAMEWORK_SCHEMA = cv.typed_schema( FLASH_SIZES = [ + "2MB", "4MB", "8MB", "16MB", @@ -394,6 +424,7 @@ FLASH_SIZES = [ ] CONF_FLASH_SIZE = "flash_size" +CONF_PARTITIONS = "partitions" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -401,6 +432,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), + cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } @@ -410,6 +442,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) + + async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) @@ -462,7 +497,10 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -507,7 +545,10 @@ async def to_code(config): [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], ) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_define( "USE_ARDUINO_VERSION_CODE", @@ -518,6 +559,7 @@ async def to_code(config): APP_PARTITION_SIZES = { + "2MB": 0x0C0000, # 768 KB "4MB": 0x1C0000, # 1792 KB "8MB": 0x3C0000, # 3840 KB "16MB": 0x7C0000, # 7936 KB From 8fb6b8f1a2a113cf3a47e4f294fb6b3b20ef3c6c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 18 Nov 2023 21:15:56 +1300 Subject: [PATCH 022/436] Bump version to 2023.11.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 7ceaffbe57..0abb6a8e2d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.1" +__version__ = "2023.11.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4e4fe3c26db3760db997aa27fb732c4a6d56864f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 21:28:35 -0600 Subject: [PATCH 023/436] dashboard: Ensure disk I/O happens in the executor (#5789) * Ensure I/O executor * safe file writer * fixes * more io * more io --- esphome/dashboard/settings.py | 4 ++- esphome/dashboard/util/file.py | 55 +++++++++++++++++++++++++++++++ esphome/dashboard/web_server.py | 52 +++++++++++++++++++++-------- tests/dashboard/__init__.py | 0 tests/dashboard/util/__init__.py | 0 tests/dashboard/util/test_file.py | 53 +++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 esphome/dashboard/util/file.py create mode 100644 tests/dashboard/__init__.py create mode 100644 tests/dashboard/util/__init__.py create mode 100644 tests/dashboard/util/test_file.py diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 76633e1bf2..61718298d2 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -3,6 +3,7 @@ from __future__ import annotations import hmac import os from pathlib import Path +from typing import Any from esphome.core import CORE from esphome.helpers import get_bool_env @@ -69,7 +70,8 @@ class DashboardSettings: # Compare password in constant running time (to prevent timing attacks) return hmac.compare_digest(self.password_hash, password_hash(password)) - def rel_path(self, *args): + def rel_path(self, *args: Any) -> str: + """Return a path relative to the ESPHome config folder.""" joined_path = os.path.join(self.config_dir, *args) # Raises ValueError if not relative to ESPHome config folder Path(joined_path).resolve().relative_to(self.absolute_config_dir) diff --git a/esphome/dashboard/util/file.py b/esphome/dashboard/util/file.py new file mode 100644 index 0000000000..5f3c5f5f1b --- /dev/null +++ b/esphome/dashboard/util/file.py @@ -0,0 +1,55 @@ +import logging +import os +import tempfile +from pathlib import Path + +_LOGGER = logging.getLogger(__name__) + + +def write_utf8_file( + filename: Path, + utf8_str: str, + private: bool = False, +) -> None: + """Write a file and rename it into place. + + Writes all or nothing. + """ + write_file(filename, utf8_str.encode("utf-8"), private) + + +# from https://github.com/home-assistant/core/blob/dev/homeassistant/util/file.py +def write_file( + filename: Path, + utf8_data: bytes, + private: bool = False, +) -> None: + """Write a file and rename it into place. + + Writes all or nothing. + """ + + tmp_filename = "" + try: + # Modern versions of Python tempfile create this file with mode 0o600 + with tempfile.NamedTemporaryFile( + mode="wb", dir=os.path.dirname(filename), delete=False + ) as fdesc: + fdesc.write(utf8_data) + tmp_filename = fdesc.name + if not private: + os.fchmod(fdesc.fileno(), 0o644) + os.replace(tmp_filename, filename) + finally: + if os.path.exists(tmp_filename): + try: + os.remove(tmp_filename) + except OSError as err: + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error( + "File replacement cleanup failed for %s while saving %s: %s", + tmp_filename, + filename, + err, + ) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9972808948..8901da095f 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -38,6 +38,7 @@ from esphome.yaml_util import FastestAvailableSafeLoader from .core import DASHBOARD from .entries import EntryState, entry_state_to_bool +from .util.file import write_file from .util.subprocess import async_run_system_command from .util.text import friendly_name_slugify @@ -524,9 +525,19 @@ class DownloadListRequestHandler(BaseHandler): class DownloadBinaryRequestHandler(BaseHandler): + def _load_file(self, path: str, compressed: bool) -> bytes: + """Load a file from disk and compress it if requested.""" + with open(path, "rb") as f: + data = f.read() + if compressed: + return gzip.compress(data, 9) + return data + @authenticated @bind_config - async def get(self, configuration=None): + async def get(self, configuration: str | None = None): + """Download a binary file.""" + loop = asyncio.get_running_loop() compressed = self.get_argument("compressed", "0") == "1" storage_path = ext_storage_path(configuration) @@ -583,11 +594,8 @@ class DownloadBinaryRequestHandler(BaseHandler): self.send_error(404) return - with open(path, "rb") as f: - data = f.read() - if compressed: - data = gzip.compress(data, 9) - self.write(data) + data = await loop.run_in_executor(None, self._load_file, path, compressed) + self.write(data) self.finish() @@ -746,19 +754,35 @@ class InfoRequestHandler(BaseHandler): class EditRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration: str | None = None): + """Get the content of a file.""" + loop = asyncio.get_running_loop() filename = settings.rel_path(configuration) - content = "" - if os.path.isfile(filename): - with open(file=filename, encoding="utf-8") as f: - content = f.read() + content = await loop.run_in_executor(None, self._read_file, filename) self.write(content) + def _read_file(self, filename: str) -> bytes: + """Read a file and return the content as bytes.""" + with open(file=filename, encoding="utf-8") as f: + return f.read() + + def _write_file(self, filename: str, content: bytes) -> None: + """Write a file with the given content.""" + write_file(filename, content) + @authenticated @bind_config - def post(self, configuration=None): - with open(file=settings.rel_path(configuration), mode="wb") as f: - f.write(self.request.body) + async def post(self, configuration: str | None = None): + """Write the content of a file.""" + loop = asyncio.get_running_loop() + config_file = settings.rel_path(configuration) + await loop.run_in_executor( + None, self._write_file, config_file, self.request.body + ) + # Ensure the StorageJSON is updated as well + await async_run_system_command( + [*DASHBOARD_COMMAND, "compile", "--only-generate", config_file] + ) self.set_status(200) diff --git a/tests/dashboard/__init__.py b/tests/dashboard/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dashboard/util/__init__.py b/tests/dashboard/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dashboard/util/test_file.py b/tests/dashboard/util/test_file.py new file mode 100644 index 0000000000..89e6b97086 --- /dev/null +++ b/tests/dashboard/util/test_file.py @@ -0,0 +1,53 @@ +import os +from pathlib import Path +from unittest.mock import patch + +import py +import pytest + +from esphome.dashboard.util.file import write_file, write_utf8_file + + +def test_write_utf8_file(tmp_path: Path) -> None: + write_utf8_file(tmp_path.joinpath("foo.txt"), "foo") + assert tmp_path.joinpath("foo.txt").read_text() == "foo" + + with pytest.raises(OSError): + write_utf8_file(Path("/not-writable"), "bar") + + +def test_write_file(tmp_path: Path) -> None: + write_file(tmp_path.joinpath("foo.txt"), b"foo") + assert tmp_path.joinpath("foo.txt").read_text() == "foo" + + +def test_write_utf8_file_fails_at_rename( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test that if rename fails not not remove, we do not log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(OSError), patch( + "esphome.dashboard.util.file.os.replace", side_effect=OSError + ): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert not os.path.exists(test_file) + + assert "File replacement cleanup failed" not in caplog.text + + +def test_write_utf8_file_fails_at_rename_and_remove( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test that if rename and remove both fail, we log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(OSError), patch( + "esphome.dashboard.util.file.os.remove", side_effect=OSError + ), patch("esphome.dashboard.util.file.os.replace", side_effect=OSError): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert "File replacement cleanup failed" in caplog.text From cd9bf29df112506387cb32f4fadaa19df21484a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 21:29:40 -0600 Subject: [PATCH 024/436] dashboard: Add lookup by name to entries (#5790) * Add lookup by name to entries * adj * tweak * tweak * tweak * tweak * tweak * tweak * preen --- esphome/dashboard/entries.py | 24 +++++++++++++--- esphome/dashboard/status/mdns.py | 47 ++++++-------------------------- esphome/dashboard/web_server.py | 5 ++-- 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index 42b3a2e743..c5d7f3a245 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import logging import os +from collections import defaultdict from typing import TYPE_CHECKING, Any from esphome import const, util @@ -68,6 +69,7 @@ class DashboardEntries: "_entry_states", "_loaded_entries", "_update_lock", + "_name_to_entry", ) def __init__(self, dashboard: ESPHomeDashboard) -> None: @@ -83,11 +85,16 @@ class DashboardEntries: self._entries: dict[str, DashboardEntry] = {} self._loaded_entries = False self._update_lock = asyncio.Lock() + self._name_to_entry: dict[str, set[DashboardEntry]] = defaultdict(set) def get(self, path: str) -> DashboardEntry | None: """Get an entry by path.""" return self._entries.get(path) + def get_by_name(self, name: str) -> set[DashboardEntry] | None: + """Get an entry by name.""" + return self._name_to_entry.get(name) + async def _async_all(self) -> list[DashboardEntry]: """Return all entries.""" return list(self._entries.values()) @@ -155,6 +162,7 @@ class DashboardEntries: None, self._get_path_to_cache_key ) entries = self._entries + name_to_entry = self._name_to_entry added: dict[DashboardEntry, DashboardCacheKeyType] = {} updated: dict[DashboardEntry, DashboardCacheKeyType] = {} removed: set[DashboardEntry] = { @@ -162,14 +170,17 @@ class DashboardEntries: for filename, entry in entries.items() if filename not in path_to_cache_key } + original_names: dict[DashboardEntry, str] = {} for path, cache_key in path_to_cache_key.items(): - if entry := entries.get(path): - if entry.cache_key != cache_key: - updated[entry] = cache_key - else: + if not (entry := entries.get(path)): entry = DashboardEntry(path, cache_key) added[entry] = cache_key + continue + + if entry.cache_key != cache_key: + updated[entry] = cache_key + original_names[entry] = entry.name if added or updated: await self._loop.run_in_executor( @@ -179,13 +190,18 @@ class DashboardEntries: bus = self._dashboard.bus for entry in added: entries[entry.path] = entry + name_to_entry[entry.name].add(entry) bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry}) for entry in removed: del entries[entry.path] + name_to_entry[entry.name].discard(entry) bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry}) for entry in updated: + if (original_name := original_names[entry]) != (current_name := entry.name): + name_to_entry[original_name].discard(entry) + name_to_entry[current_name].add(entry) bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry}) def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index cbe3b3309e..4f4fa560d0 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -24,17 +24,8 @@ class MDNSStatus: self.aiozc: AsyncEsphomeZeroconf | None = None # This is the current mdns state for each host (True, False, None) self.host_mdns_state: dict[str, bool | None] = {} - # This is the hostnames to path mapping - self.host_name_to_path: dict[str, str] = {} - self.path_to_host_name: dict[str, str] = {} - # This is a set of host names to track (i.e no_mdns = false) - self.host_name_with_mdns_enabled: set[set] = set() self._loop = asyncio.get_running_loop() - def get_path_to_host_name(self, path: str) -> str | None: - """Resolve a path to an address in a thread-safe manner.""" - return self.path_to_host_name.get(path) - async def async_resolve_host(self, host_name: str) -> str | None: """Resolve a host name to an address in a thread-safe manner.""" if aiozc := self.aiozc: @@ -44,53 +35,32 @@ class MDNSStatus: async def async_refresh_hosts(self): """Refresh the hosts to track.""" dashboard = DASHBOARD - current_entries = dashboard.entries.async_all() - host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_mdns_state = self.host_mdns_state - host_name_to_path = self.host_name_to_path - path_to_host_name = self.path_to_host_name entries = dashboard.entries - - for entry in current_entries: - name = entry.name - # If no_mdns is set, remove it from the set + for entry in entries.async_all(): if entry.no_mdns: - host_name_with_mdns_enabled.discard(name) continue - - # We are tracking this host - host_name_with_mdns_enabled.add(name) - path = entry.path - # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if (online := host_mdns_state.get(name, SENTINEL)) != SENTINEL: + if (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: entries.async_set_state(entry, bool_to_entry_state(online)) - # Make sure the mapping is up to date - # so when we get an mdns update we can map it back - # to the filename - host_name_to_path[name] = path - path_to_host_name[path] = name - async def async_run(self) -> None: dashboard = DASHBOARD entries = dashboard.entries aiozc = AsyncEsphomeZeroconf() self.aiozc = aiozc host_mdns_state = self.host_mdns_state - host_name_to_path = self.host_name_to_path - host_name_with_mdns_enabled = self.host_name_with_mdns_enabled def on_update(dat: dict[str, bool | None]) -> None: """Update the entry state.""" for name, result in dat.items(): host_mdns_state[name] = result - if name not in host_name_with_mdns_enabled: - continue - if entry := entries.get(host_name_to_path[name]): - entries.async_set_state(entry, bool_to_entry_state(result)) + if matching_entries := entries.get_by_name(name): + for entry in matching_entries: + if not entry.no_mdns: + entries.async_set_state(entry, bool_to_entry_state(result)) stat = DashboardStatus(on_update) imports = DashboardImportDiscovery() @@ -102,10 +72,11 @@ class MDNSStatus: [stat.browser_callback, imports.browser_callback], ) + ping_request = dashboard.ping_request while not dashboard.stop_event.is_set(): await self.async_refresh_hosts() - await dashboard.ping_request.wait() - dashboard.ping_request.clear() + await ping_request.wait() + ping_request.clear() await browser.async_cancel() await aiozc.async_close() diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 8901da095f..7c5f653b5b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -271,14 +271,15 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): ) -> list[str]: """Build the command to run.""" dashboard = DASHBOARD + entries = dashboard.entries configuration = json_message["configuration"] config_file = settings.rel_path(configuration) port = json_message["port"] if ( port == "OTA" and (mdns := dashboard.mdns_status) - and (host_name := mdns.get_path_to_host_name(config_file)) - and (address := await mdns.async_resolve_host(host_name)) + and (entry := entries.get(config_file)) + and (address := await mdns.async_resolve_host(entry.name)) ): port = address From 2aaee813136eb95812910fbcd17ef568befd2a34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 21:31:00 -0600 Subject: [PATCH 025/436] Refactor StorageJSON to keep loaded_integrations a set until its converted to JSON (#5793) * Refactor StorageJSON to keep loaded_integrations a set until its converted to a dict after #5792 we will be checking loaded_integrations often. ESPHome core keep uses a set, but it would get converted to a list when passed through StorageJSON. Keep it a set until its needed to be read/write to JSON so we do not have to linear searches on it since they have a time complexity of O(n) vs O(1) * legacy --- esphome/dashboard/entries.py | 4 +- esphome/storage_json.py | 95 ++++++++++++++++-------------------- 2 files changed, 45 insertions(+), 54 deletions(-) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index c5d7f3a245..8ccfa795d5 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -285,7 +285,7 @@ class DashboardEntry: "name": self.name, "friendly_name": self.friendly_name, "configuration": self.filename, - "loaded_integrations": self.loaded_integrations, + "loaded_integrations": sorted(self.loaded_integrations), "deployed_version": self.update_old, "current_version": self.update_new, "path": self.path, @@ -381,7 +381,7 @@ class DashboardEntry: return const.__version__ @property - def loaded_integrations(self) -> list[str]: + def loaded_integrations(self) -> set[str]: if self.storage is None: return [] return self.storage.loaded_integrations diff --git a/esphome/storage_json.py b/esphome/storage_json.py index a2619cb536..0a41a4f738 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -1,21 +1,15 @@ +from __future__ import annotations import binascii import codecs -from datetime import datetime import json import logging import os -from typing import Optional +from datetime import datetime from esphome import const +from esphome.const import CONF_DISABLED, CONF_MDNS from esphome.core import CORE from esphome.helpers import write_file_if_changed - - -from esphome.const import ( - CONF_MDNS, - CONF_DISABLED, -) - from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) @@ -40,48 +34,47 @@ def trash_storage_path() -> str: class StorageJSON: def __init__( self, - storage_version, - name, - friendly_name, - comment, - esphome_version, - src_version, - address, - web_port, - target_platform, - build_path, - firmware_bin_path, - loaded_integrations, - no_mdns, - ): + storage_version: int, + name: str, + friendly_name: str, + comment: str, + esphome_version: str, + src_version: int | None, + address: str, + web_port: int | None, + target_platform: str, + build_path: str, + firmware_bin_path: str, + loaded_integrations: set[str], + no_mdns: bool, + ) -> None: # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) - self.storage_version: int = storage_version + self.storage_version = storage_version # The name of the node - self.name: str = name + self.name = name # The friendly name of the node - self.friendly_name: str = friendly_name + self.friendly_name = friendly_name # The comment of the node - self.comment: str = comment + self.comment = comment # The esphome version this was compiled with - self.esphome_version: str = esphome_version + self.esphome_version = esphome_version # The version of the file in src/main.cpp - Used to migrate the file assert src_version is None or isinstance(src_version, int) - self.src_version: int = src_version + self.src_version = src_version # Address of the ESP, for example livingroom.local or a static IP - self.address: str = address + self.address = address # Web server port of the ESP, for example 80 assert web_port is None or isinstance(web_port, int) - self.web_port: int = web_port + self.web_port = web_port # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. - self.target_platform: str = target_platform + self.target_platform = target_platform # The absolute path to the platformio project - self.build_path: str = build_path + self.build_path = build_path # The absolute path to the firmware binary - self.firmware_bin_path: str = firmware_bin_path - # A list of strings of names of loaded integrations - self.loaded_integrations: list[str] = loaded_integrations - self.loaded_integrations.sort() + self.firmware_bin_path = firmware_bin_path + # A set of strings of names of loaded integrations + self.loaded_integrations = loaded_integrations # Is mDNS disabled self.no_mdns = no_mdns @@ -98,7 +91,7 @@ class StorageJSON: "esp_platform": self.target_platform, "build_path": self.build_path, "firmware_bin_path": self.firmware_bin_path, - "loaded_integrations": self.loaded_integrations, + "loaded_integrations": sorted(self.loaded_integrations), "no_mdns": self.no_mdns, } @@ -109,9 +102,7 @@ class StorageJSON: write_file_if_changed(path, self.to_json()) @staticmethod - def from_esphome_core( - esph: CoreType, old: Optional["StorageJSON"] - ) -> "StorageJSON": + def from_esphome_core(esph: CoreType, old: StorageJSON | None) -> StorageJSON: hardware = esph.target_platform.upper() if esph.is_esp32: from esphome.components import esp32 @@ -129,7 +120,7 @@ class StorageJSON: target_platform=hardware, build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, - loaded_integrations=list(esph.loaded_integrations), + loaded_integrations=esph.loaded_integrations, no_mdns=( CONF_MDNS in esph.config and CONF_DISABLED in esph.config[CONF_MDNS] @@ -140,7 +131,7 @@ class StorageJSON: @staticmethod def from_wizard( name: str, friendly_name: str, address: str, platform: str - ) -> "StorageJSON": + ) -> StorageJSON: return StorageJSON( storage_version=1, name=name, @@ -153,12 +144,12 @@ class StorageJSON: target_platform=platform, build_path=None, firmware_bin_path=None, - loaded_integrations=[], + loaded_integrations=set(), no_mdns=False, ) @staticmethod - def _load_impl(path: str) -> Optional["StorageJSON"]: + def _load_impl(path: str) -> StorageJSON | None: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -174,7 +165,7 @@ class StorageJSON: esp_platform = storage.get("esp_platform") build_path = storage.get("build_path") firmware_bin_path = storage.get("firmware_bin_path") - loaded_integrations = storage.get("loaded_integrations", []) + loaded_integrations = set(storage.get("loaded_integrations", [])) no_mdns = storage.get("no_mdns", False) return StorageJSON( storage_version, @@ -193,7 +184,7 @@ class StorageJSON: ) @staticmethod - def load(path: str) -> Optional["StorageJSON"]: + def load(path: str) -> StorageJSON | None: try: return StorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except @@ -215,7 +206,7 @@ class EsphomeStorageJSON: # The last time ESPHome checked for an update as an isoformat encoded str self.last_update_check_str: str = last_update_check # Cache of the version gotten in the last version check - self.remote_version: Optional[str] = remote_version + self.remote_version: str | None = remote_version def as_dict(self) -> dict: return { @@ -226,7 +217,7 @@ class EsphomeStorageJSON: } @property - def last_update_check(self) -> Optional[datetime]: + def last_update_check(self) -> datetime | None: try: return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") except Exception: # pylint: disable=broad-except @@ -243,7 +234,7 @@ class EsphomeStorageJSON: write_file_if_changed(path, self.to_json()) @staticmethod - def _load_impl(path: str) -> Optional["EsphomeStorageJSON"]: + def _load_impl(path: str) -> EsphomeStorageJSON | None: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -255,14 +246,14 @@ class EsphomeStorageJSON: ) @staticmethod - def load(path: str) -> Optional["EsphomeStorageJSON"]: + def load(path: str) -> EsphomeStorageJSON | None: try: return EsphomeStorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except return None @staticmethod - def get_default() -> "EsphomeStorageJSON": + def get_default() -> EsphomeStorageJSON: return EsphomeStorageJSON( storage_version=1, cookie_secret=binascii.hexlify(os.urandom(64)).decode(), From e367ab26e157f31317e817edba5f0dd88200cd95 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Sun, 19 Nov 2023 22:32:46 -0500 Subject: [PATCH 026/436] wifi: Don't build SoftAP/DHCPS support unless 'ap' is in config. (#5649) --- esphome/components/wifi/__init__.py | 4 +++ esphome/components/wifi/wifi_component.cpp | 20 +++++++++-- esphome/components/wifi/wifi_component.h | 10 ++++++ .../wifi/wifi_component_esp32_arduino.cpp | 6 ++++ .../wifi/wifi_component_esp8266.cpp | 6 ++++ .../wifi/wifi_component_esp_idf.cpp | 35 ++++++++++++++----- .../wifi/wifi_component_libretiny.cpp | 6 ++++ .../components/wifi/wifi_component_pico_w.cpp | 3 ++ esphome/core/defines.h | 1 + 9 files changed, 79 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c42835f169..32c9d07046 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -403,6 +403,10 @@ async def to_code(config): lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) + cg.add_define("USE_WIFI_AP") + elif CORE.is_esp32 and CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) + add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c1d5138f7b..d023405728 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -82,6 +82,7 @@ void WiFiComponent::start() { } else { this->start_scanning(); } +#ifdef USE_WIFI_AP } else if (this->has_ap()) { this->setup_ap_config_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { @@ -94,6 +95,7 @@ void WiFiComponent::start() { captive_portal::global_captive_portal->start(); } #endif +#endif // USE_WIFI_AP } #ifdef USE_IMPROV if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) { @@ -160,6 +162,7 @@ void WiFiComponent::loop() { return; } +#ifdef USE_WIFI_AP if (this->has_ap() && !this->ap_setup_) { if (now - this->last_connected_ > this->ap_timeout_) { ESP_LOGI(TAG, "Starting fallback AP!"); @@ -170,6 +173,7 @@ void WiFiComponent::loop() { #endif } } +#endif // USE_WIFI_AP #ifdef USE_IMPROV if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { @@ -199,11 +203,16 @@ void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } #endif + network::IPAddress WiFiComponent::get_ip_address() { if (this->has_sta()) return this->wifi_sta_ip(); + +#ifdef USE_WIFI_AP if (this->has_ap()) return this->wifi_soft_ap_ip(); +#endif // USE_WIFI_AP + return {}; } network::IPAddress WiFiComponent::get_dns_address(int num) { @@ -218,6 +227,8 @@ std::string WiFiComponent::get_use_address() const { return this->use_address_; } void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } + +#ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { this->wifi_mode_({}, true); @@ -255,13 +266,16 @@ void WiFiComponent::setup_ap_config_() { } } -float WiFiComponent::get_loop_priority() const { - return 10.0f; // before other loop components -} void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; this->has_ap_ = true; } +#endif // USE_WIFI_AP + +float WiFiComponent::get_loop_priority() const { + return 10.0f; // before other loop components +} + void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 3ee69bb5de..6cbdc51caf 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -194,6 +194,7 @@ class WiFiComponent : public Component { void add_sta(const WiFiAP &ap); void clear_sta(); +#ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. * * This can also be used without set_sta(). Then the AP will always be active. @@ -203,6 +204,7 @@ class WiFiComponent : public Component { */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } +#endif // USE_WIFI_AP void enable(); void disable(); @@ -299,7 +301,11 @@ class WiFiComponent : public Component { protected: static std::string format_mac_addr(const uint8_t mac[6]); + +#ifdef USE_WIFI_AP void setup_ap_config_(); +#endif // USE_WIFI_AP + void print_connect_params_(); void wifi_loop_(); @@ -313,8 +319,12 @@ class WiFiComponent : public Component { void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_(); bool wifi_scan_start_(bool passive); + +#ifdef USE_WIFI_AP bool wifi_ap_ip_config_(optional manual_ip); bool wifi_start_ap_(const WiFiAP &ap); +#endif // USE_WIFI_AP + bool wifi_disconnect_(); int32_t wifi_channel_(); network::IPAddress wifi_subnet_mask_(); diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 17b15757ef..5d8aa7f749 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -597,6 +597,8 @@ void WiFiComponent::wifi_scan_done_callback_() { WiFi.scanDelete(); this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { esp_err_t err; @@ -654,6 +656,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -692,11 +695,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); return network::IPAddress(&ip.ip); } +#endif // USE_WIFI_AP + bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } bssid_t WiFiComponent::wifi_bssid() { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a48c6c711d..15b0c65641 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -688,6 +688,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) @@ -753,6 +755,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -790,11 +793,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { struct ip_info ip {}; wifi_get_ip_info(SOFTAP_IF, &ip); return network::IPAddress(&ip.ip); } +#endif // USE_WIFI_AP + bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; uint8_t *raw_bssid = WiFi.BSSID(); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 34ecaf887d..8fcafc5c12 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -17,7 +17,11 @@ #ifdef USE_WIFI_WPA2_EAP #include #endif + +#ifdef USE_WIFI_AP #include "dhcpserver/dhcpserver.h" +#endif // USE_WIFI_AP + #include "lwip/err.h" #include "lwip/dns.h" @@ -35,15 +39,19 @@ static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#ifdef USE_WIFI_AP +static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif // USE_WIFI_AP + +static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) struct IDFWiFiEvent { esp_event_base_t event_base; @@ -159,7 +167,11 @@ void WiFiComponent::wifi_pre_setup_() { } s_sta_netif = esp_netif_create_default_wifi_sta(); + +#ifdef USE_WIFI_AP s_ap_netif = esp_netif_create_default_wifi_ap(); +#endif // USE_WIFI_AP + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // cfg.nvs_enable = false; err = esp_wifi_init(&cfg); @@ -761,6 +773,8 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { scan_done_ = false; return true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { esp_err_t err; @@ -816,6 +830,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -853,6 +868,8 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } +#endif // USE_WIFI_AP + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(s_sta_netif, &ip); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index d7f4406540..29c6ce64d0 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -412,6 +412,8 @@ void WiFiComponent::wifi_scan_done_callback_() { WiFi.scanDelete(); this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) @@ -423,6 +425,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); } } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -438,7 +441,10 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), ap.get_channel().value_or(1), ap.get_hidden()); } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } +#endif // USE_WIFI_AP + bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); } bssid_t WiFiComponent::wifi_bssid() { diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index d67b466d6c..c71203a877 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -138,6 +138,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { return true; } +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { // TODO: return false; @@ -151,7 +152,9 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } +#endif // USE_WIFI_AP bool WiFiComponent::wifi_disconnect_() { int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d4187d4c08..b93b8c9270 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -50,6 +50,7 @@ #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER #define USE_WIFI +#define USE_WIFI_AP // Arduino-specific feature flags #ifdef USE_ARDUINO From d462beea6e4676362a38274f3941dc4cbe6e43cf Mon Sep 17 00:00:00 2001 From: Christian Schmitt Date: Mon, 20 Nov 2023 04:34:26 +0100 Subject: [PATCH 027/436] ssd1306: handle V_COM differently for SH1106 (#5796) --- esphome/components/ssd1306_base/ssd1306_base.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 00b5c2d5a2..749c3511c1 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -154,6 +154,7 @@ void SSD1306::setup() { // Set V_COM (0xDB) this->command(SSD1306_COMMAND_SET_VCOM_DETECT); switch (this->model_) { + case SH1106_MODEL_128_64: case SH1107_MODEL_128_64: case SH1107_MODEL_128_128: this->command(0x35); From 5744490f2fee57a83e349f0d951f7cc1b18954aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:18:00 +0100 Subject: [PATCH 028/436] Bump aioesphomeapi from 18.5.3 to 18.5.5 (#5804) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1866d33ab2..4203ce6304 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.3 +aioesphomeapi==18.5.5 zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default From d5d97c455831e5ee0830bfd15cfcdcc2e79c1e36 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 20 Nov 2023 16:59:38 -0700 Subject: [PATCH 029/436] include payload_open when a lock supports OPEN (#5809) --- esphome/components/mqtt/mqtt_lock.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 197d0c32d4..f4a5126d0c 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->lock_->traits.get_assumed_state()) root[MQTT_OPTIMISTIC] = true; + if (this->lock_->traits.get_supports_open()) + root[MQTT_PAYLOAD_OPEN] = "OPEN"; } bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } From 47d42afda342b6b5f8dc45d0bdcd5742c5f9a2cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Nov 2023 01:15:32 +0100 Subject: [PATCH 030/436] dashboard: Fix online status when api is disabled (#5791) --- esphome/dashboard/dashboard.py | 13 +++++++++++-- esphome/zeroconf.py | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 050564d21e..dd51382db8 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -973,6 +973,7 @@ class MDNSStatusThread(threading.Thread): self.host_name_to_filename: dict[str, str] = {} # This is a set of host names to track (i.e no_mdns = false) self.host_name_with_mdns_enabled: set[set] = set() + self.zc: EsphomeZeroconf | None = None self._refresh_hosts() def _refresh_hosts(self): @@ -996,7 +997,14 @@ class MDNSStatusThread(threading.Thread): # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if name in host_mdns_state: + if self.zc and ( + entry.loaded_integrations and "api" not in entry.loaded_integrations + ): + # No api available so we have to poll since + # the device won't respond to a request to ._esphomelib._tcp.local. + PING_RESULT[filename] = bool(self.zc.resolve_host(entry.name)) + elif name in host_mdns_state: + # We already have a state for this host PING_RESULT[filename] = host_mdns_state[name] # Make sure the mapping is up to date @@ -1007,7 +1015,8 @@ class MDNSStatusThread(threading.Thread): def run(self): global IMPORT_RESULT - zc = EsphomeZeroconf() + self.zc = EsphomeZeroconf() + zc = self.zc host_mdns_state = self.host_mdns_state host_name_to_filename = self.host_name_to_filename host_name_with_mdns_enabled = self.host_name_with_mdns_enabled diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index f4cb7f080b..2585d5adc0 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -150,7 +150,11 @@ class EsphomeZeroconf(Zeroconf): def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: """Resolve a host name to an IP address.""" name = host.partition(".")[0] - info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") + info = HostResolver( + ESPHOME_SERVICE_TYPE, + f"{name}.{ESPHOME_SERVICE_TYPE}", + server=f"{name}.local.", + ) if ( info.load_from_cache(self) or (timeout and info.request(self, timeout * 1000)) From 7d5ebeda524e36c17d1123118a6c8c453ab31856 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Nov 2023 01:16:06 +0100 Subject: [PATCH 031/436] dashboard: Fix online status when api is disabled (#5792) --- esphome/dashboard/status/mdns.py | 19 +++++++++++++++++-- esphome/zeroconf.py | 4 +++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index 4f4fa560d0..bd212bc563 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -12,7 +12,7 @@ from esphome.zeroconf import ( from ..const import SENTINEL from ..core import DASHBOARD -from ..entries import bool_to_entry_state +from ..entries import DashboardEntry, bool_to_entry_state class MDNSStatus: @@ -37,15 +37,30 @@ class MDNSStatus: dashboard = DASHBOARD host_mdns_state = self.host_mdns_state entries = dashboard.entries + poll_names: dict[str, set[DashboardEntry]] = {} for entry in entries.async_all(): if entry.no_mdns: continue # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: + if entry.loaded_integrations and "api" not in entry.loaded_integrations: + # No api available so we have to poll since + # the device won't respond to a request to ._esphomelib._tcp.local. + poll_names.setdefault(entry.name, set()).add(entry) + elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: entries.async_set_state(entry, bool_to_entry_state(online)) + if poll_names and self.aiozc: + results = await asyncio.gather( + *(self.aiozc.async_resolve_host(name) for name in poll_names) + ) + for name, address in zip(poll_names, results): + result = bool(address) + host_mdns_state[name] = result + for entry in poll_names[name]: + entries.async_set_state(entry, bool_to_entry_state(result)) + async def async_run(self) -> None: dashboard = DASHBOARD entries = dashboard.entries diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 956e348e07..72cc4c00c6 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -169,7 +169,9 @@ class DashboardImportDiscovery: def _make_host_resolver(host: str) -> HostResolver: """Create a new HostResolver for the given host name.""" name = host.partition(".")[0] - info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") + info = HostResolver( + ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}", server=f"{name}.local." + ) return info From 55f13dc3479aa683ecbf24fa8d95d4e3b4e163de Mon Sep 17 00:00:00 2001 From: CVan Date: Mon, 20 Nov 2023 19:19:36 -0500 Subject: [PATCH 032/436] fix: compile errors with fonts (#5808) --- docker/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index a892e1df38..1bf754464d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -48,6 +48,8 @@ RUN \ libfreetype-dev=2.12.1+dfsg-5 \ libssl-dev=3.0.11-1~deb12u2 \ libffi-dev=3.4.4-1 \ + libopenjp2-7=2.5.0-2 \ + libtiff6=4.5.0-6 \ cargo=0.66.0+ds1-1 \ pkg-config=1.8.1-1 \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \ From cf6b56c1ac7a6204d04a8a2f6d705b1fb597a8a2 Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Tue, 21 Nov 2023 02:12:36 +0100 Subject: [PATCH 033/436] Haier component updated to support new protocol variations (#5713) Co-authored-by: Pavlo Dudnytskyi --- esphome/components/haier/climate.py | 40 +- esphome/components/haier/haier_base.cpp | 264 ++++---- esphome/components/haier/haier_base.h | 86 ++- esphome/components/haier/hon_climate.cpp | 640 +++++++++++------- esphome/components/haier/hon_climate.h | 49 +- esphome/components/haier/hon_packet.h | 90 +-- .../components/haier/smartair2_climate.cpp | 376 +++++----- esphome/components/haier/smartair2_climate.h | 22 +- esphome/components/haier/smartair2_packet.h | 18 +- platformio.ini | 2 +- 10 files changed, 870 insertions(+), 717 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index d796f13581..49d42a231f 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -38,16 +38,20 @@ PROTOCOL_MIN_TEMPERATURE = 16.0 PROTOCOL_MAX_TEMPERATURE = 30.0 PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 +PROTOCOL_CONTROL_PACKET_SIZE = 10 CODEOWNERS = ["@paveldn"] AUTO_LOAD = ["sensor"] DEPENDENCIES = ["climate", "uart"] -CONF_WIFI_SIGNAL = "wifi_signal" +CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control" CONF_ANSWER_TIMEOUT = "answer_timeout" +CONF_CONTROL_METHOD = "control_method" +CONF_CONTROL_PACKET_SIZE = "control_packet_size" CONF_DISPLAY = "display" +CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_VERTICAL_AIRFLOW = "vertical_airflow" -CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" +CONF_WIFI_SIGNAL = "wifi_signal" PROTOCOL_HON = "HON" PROTOCOL_SMARTAIR2 = "SMARTAIR2" @@ -107,6 +111,13 @@ SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, } +HonControlMethod = haier_ns.enum("HonControlMethod", True) +SUPPORTED_HON_CONTROL_METHODS = { + "MONITOR_ONLY": HonControlMethod.MONITOR_ONLY, + "SET_GROUP_PARAMETERS": HonControlMethod.SET_GROUP_PARAMETERS, + "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, +} + def validate_visual(config): if CONF_VISUAL in config: @@ -184,6 +195,9 @@ CONFIG_SCHEMA = cv.All( PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Smartair2Climate), + cv.Optional( + CONF_ALTERNATIVE_SWING_CONTROL, default=False + ): cv.boolean, cv.Optional( CONF_SUPPORTED_PRESETS, default=list( @@ -197,7 +211,15 @@ CONFIG_SCHEMA = cv.All( PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(HonClimate), + cv.Optional( + CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" + ): cv.ensure_list( + cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True) + ), cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional( + CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE + ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50), cv.Optional( CONF_SUPPORTED_PRESETS, default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), @@ -408,6 +430,8 @@ async def to_code(config): await climate.register_climate(var, config) cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) + if CONF_CONTROL_METHOD in config: + cg.add(var.set_control_method(config[CONF_CONTROL_METHOD])) if CONF_BEEPER in config: cg.add(var.set_beeper_state(config[CONF_BEEPER])) if CONF_DISPLAY in config: @@ -423,5 +447,15 @@ async def to_code(config): cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) if CONF_ANSWER_TIMEOUT in config: cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) + if CONF_ALTERNATIVE_SWING_CONTROL in config: + cg.add( + var.set_alternative_swing_control(config[CONF_ALTERNATIVE_SWING_CONTROL]) + ) + if CONF_CONTROL_PACKET_SIZE in config: + cg.add( + var.set_extra_control_packet_bytes_size( + config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE + ) + ) # https://github.com/paveldn/HaierProtocol - cg.add_library("pavlodn/HaierProtocol", "0.9.20") + cg.add_library("pavlodn/HaierProtocol", "0.9.24") diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 22899b1a70..6943fc7d9c 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -19,56 +19,45 @@ constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000; constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; -constexpr size_t CONTROL_TIMEOUT_MS = 7000; -constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied -#if (HAIER_LOG_LEVEL > 4) -// To reduce size of binary this function only available when log level is Verbose const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { static const char *phase_names[] = { "SENDING_INIT_1", - "WAITING_INIT_1_ANSWER", "SENDING_INIT_2", - "WAITING_INIT_2_ANSWER", "SENDING_FIRST_STATUS_REQUEST", - "WAITING_FIRST_STATUS_ANSWER", "SENDING_ALARM_STATUS_REQUEST", - "WAITING_ALARM_STATUS_ANSWER", "IDLE", - "UNKNOWN", "SENDING_STATUS_REQUEST", - "WAITING_STATUS_ANSWER", "SENDING_UPDATE_SIGNAL_REQUEST", - "WAITING_UPDATE_SIGNAL_ANSWER", "SENDING_SIGNAL_LEVEL", - "WAITING_SIGNAL_LEVEL_ANSWER", "SENDING_CONTROL", - "WAITING_CONTROL_ANSWER", - "SENDING_POWER_ON_COMMAND", - "WAITING_POWER_ON_ANSWER", - "SENDING_POWER_OFF_COMMAND", - "WAITING_POWER_OFF_ANSWER", + "SENDING_ACTION_COMMAND", "UNKNOWN" // Should be the last! }; + static_assert( + (sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1), + "Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases"); int phase_index = (int) phase; if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; return phase_names[phase_index]; } -#endif + +bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, + size_t timeout) { + return std::chrono::duration_cast(now - tpoint).count() > timeout; +} HaierClimateBase::HaierClimateBase() : haier_protocol_(*this), protocol_phase_(ProtocolPhases::SENDING_INIT_1), - action_request_(ActionRequest::NO_ACTION), display_status_(true), health_mode_(false), force_send_control_(false), - forced_publish_(false), forced_request_status_(false), - first_control_attempt_(false), reset_protocol_request_(false), - send_wifi_signal_(true) { + send_wifi_signal_(true), + use_crc_(false) { this->traits_ = climate::ClimateTraits(); this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, @@ -84,42 +73,43 @@ HaierClimateBase::~HaierClimateBase() {} void HaierClimateBase::set_phase(ProtocolPhases phase) { if (this->protocol_phase_ != phase) { -#if (HAIER_LOG_LEVEL > 4) ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); -#else - ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase); -#endif this->protocol_phase_ = phase; } } -bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, - std::chrono::steady_clock::time_point tpoint, size_t timeout) { - return std::chrono::duration_cast(now - tpoint).count() > timeout; +void HaierClimateBase::reset_phase_() { + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); +} + +void HaierClimateBase::reset_to_idle_() { + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + this->forced_request_status_ = true; + this->set_phase(ProtocolPhases::IDLE); + this->action_request_.reset(); } bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); + return check_timeout(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); } bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); -} - -bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS); + return check_timeout(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); } bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); + return check_timeout(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); } bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); + return check_timeout(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); } #ifdef USE_WIFI -haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) { +haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() { static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; if (wifi::global_wifi_component->is_connected()) { wifi_status_data[1] = 0; @@ -131,7 +121,8 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t wifi_status_data[1] = 1; wifi_status_data[3] = 0; } - return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data)); + return haier_protocol::HaierMessage(haier_protocol::FrameType::REPORT_NETWORK_STATUS, wifi_status_data, + sizeof(wifi_status_data)); } #endif @@ -140,7 +131,7 @@ bool HaierClimateBase::get_display_state() const { return this->display_status_; void HaierClimateBase::set_display_state(bool state) { if (this->display_status_ != state) { this->display_status_ = state; - this->set_force_send_control_(true); + this->force_send_control_ = true; } } @@ -149,15 +140,24 @@ bool HaierClimateBase::get_health_mode() const { return this->health_mode_; } void HaierClimateBase::set_health_mode(bool state) { if (this->health_mode_ != state) { this->health_mode_ = state; - this->set_force_send_control_(true); + this->force_send_control_ = true; } } -void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } +void HaierClimateBase::send_power_on_command() { + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_ON, esphome::optional()}); +} -void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } +void HaierClimateBase::send_power_off_command() { + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional()}); +} -void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } +void HaierClimateBase::toggle_power() { + this->action_request_ = + PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional()}); +} void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { this->traits_.set_supported_swing_modes(modes); @@ -165,9 +165,7 @@ void HaierClimateBase::set_supported_swing_modes(const std::settraits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); } -void HaierClimateBase::set_answer_timeout(uint32_t timeout) { - this->answer_timeout_ = std::chrono::milliseconds(timeout); -} +void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } void HaierClimateBase::set_supported_modes(const std::set &modes) { this->traits_.set_supported_modes(modes); @@ -183,29 +181,42 @@ void HaierClimateBase::set_supported_presets(const std::setsend_wifi_signal_ = send_wifi; } -haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, - uint8_t expected_request_message_type, - uint8_t answer_message_type, - uint8_t expected_answer_message_type, - ProtocolPhases expected_phase) { +void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { + this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message}); +} + +haier_protocol::HandlerError HaierClimateBase::answer_preprocess_( + haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type, + haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type, + ProtocolPhases expected_phase) { haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; - if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) + if ((expected_request_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && + (request_message_type != expected_request_message_type)) result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; - if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) + if ((expected_answer_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && + (answer_message_type != expected_answer_message_type)) result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; - if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) + if (!this->haier_protocol_.is_waiting_for_answer() || + ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))) result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; - if (is_message_invalid(answer_message_type)) + if (answer_message_type == haier_protocol::FrameType::INVALID) result = haier_protocol::HandlerError::INVALID_ANSWER; return result; } -haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { -#if (HAIER_LOG_LEVEL > 4) - ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); -#else - ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); -#endif +haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + haier_protocol::FrameType::CONFIRM, ProtocolPhases::SENDING_SIGNAL_LEVEL); + this->set_phase(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) { + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type, + phase_to_string_(this->protocol_phase_)); if (this->protocol_phase_ > ProtocolPhases::IDLE) { this->set_phase(ProtocolPhases::IDLE); } else { @@ -219,79 +230,95 @@ void HaierClimateBase::setup() { // Set timestamp here to give AC time to boot this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->set_phase(ProtocolPhases::SENDING_INIT_1); - this->set_handlers(); this->haier_protocol_.set_default_timeout_handler( std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); + this->set_handlers(); } void HaierClimateBase::dump_config() { LOG_CLIMATE("", "Haier Climate", this); - ESP_LOGCONFIG(TAG, " Device communication status: %s", - (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none"); + ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none"); } void HaierClimateBase::loop() { std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); if ((std::chrono::duration_cast(now - this->last_valid_status_timestamp_).count() > COMMUNICATION_TIMEOUT_MS) || - (this->reset_protocol_request_)) { + (this->reset_protocol_request_ && (!this->haier_protocol_.is_waiting_for_answer()))) { + this->last_valid_status_timestamp_ = now; if (this->protocol_phase_ >= ProtocolPhases::IDLE) { // No status too long, reseting protocol + // No need to reset protocol if we didn't pass initialization phase if (this->reset_protocol_request_) { this->reset_protocol_request_ = false; ESP_LOGW(TAG, "Protocol reset requested"); } else { ESP_LOGW(TAG, "Communication timeout, reseting protocol"); } - this->last_valid_status_timestamp_ = now; - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); - this->set_phase(ProtocolPhases::SENDING_INIT_1); + this->process_protocol_reset(); return; - } else { - // No need to reset protocol if we didn't pass initialization phase - this->last_valid_status_timestamp_ = now; } }; - if ((this->protocol_phase_ == ProtocolPhases::IDLE) || - (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || - (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || - (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { + if ((!this->haier_protocol_.is_waiting_for_answer()) && + ((this->protocol_phase_ == ProtocolPhases::IDLE) || + (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL))) { // If control message or action is pending we should send it ASAP unless we are in initialisation // procedure or waiting for an answer - if (this->action_request_ != ActionRequest::NO_ACTION) { - this->process_pending_action(); - } else if (this->hvac_settings_.valid || this->force_send_control_) { + if (this->action_request_.has_value() && this->prepare_pending_action()) { + this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND); + } else if (this->next_hvac_settings_.valid || this->force_send_control_) { ESP_LOGV(TAG, "Control packet is pending..."); this->set_phase(ProtocolPhases::SENDING_CONTROL); + if (this->next_hvac_settings_.valid) { + this->current_hvac_settings_ = this->next_hvac_settings_; + this->next_hvac_settings_.reset(); + } else { + this->current_hvac_settings_.reset(); + } } } this->process_phase(now); this->haier_protocol_.loop(); } -void HaierClimateBase::process_pending_action() { - ActionRequest request = this->action_request_; - if (this->action_request_ == ActionRequest::TOGGLE_POWER) { - request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; - } - switch (request) { - case ActionRequest::TURN_POWER_ON: - this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND); - break; - case ActionRequest::TURN_POWER_OFF: - this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND); - break; - case ActionRequest::TOGGLE_POWER: - case ActionRequest::NO_ACTION: - // shouldn't get here, do nothing - break; - default: - ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); - break; - } - this->action_request_ = ActionRequest::NO_ACTION; +void HaierClimateBase::process_protocol_reset() { + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + if (this->next_hvac_settings_.valid) + this->next_hvac_settings_.reset(); + this->mode = CLIMATE_MODE_OFF; + this->current_temperature = NAN; + this->target_temperature = NAN; + this->fan_mode.reset(); + this->preset.reset(); + this->publish_state(); + this->set_phase(ProtocolPhases::SENDING_INIT_1); +} + +bool HaierClimateBase::prepare_pending_action() { + if (this->action_request_.has_value()) { + switch (this->action_request_.value().action) { + case ActionRequest::SEND_CUSTOM_COMMAND: + return true; + case ActionRequest::TURN_POWER_ON: + this->action_request_.value().message = this->get_power_message(true); + return true; + case ActionRequest::TURN_POWER_OFF: + this->action_request_.value().message = this->get_power_message(false); + return true; + case ActionRequest::TOGGLE_POWER: + this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF); + return true; + default: + ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action); + this->action_request_.reset(); + return false; + } + } else + return false; } ClimateTraits HaierClimateBase::traits() { return traits_; } @@ -302,23 +329,22 @@ void HaierClimateBase::control(const ClimateCall &call) { ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); return; // cancel the control, we cant do it without a poll answer. } - if (this->hvac_settings_.valid) { - ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); + if (this->current_hvac_settings_.valid) { + ESP_LOGW(TAG, "New settings come faster then processed!"); } { if (call.get_mode().has_value()) - this->hvac_settings_.mode = call.get_mode(); + this->next_hvac_settings_.mode = call.get_mode(); if (call.get_fan_mode().has_value()) - this->hvac_settings_.fan_mode = call.get_fan_mode(); + this->next_hvac_settings_.fan_mode = call.get_fan_mode(); if (call.get_swing_mode().has_value()) - this->hvac_settings_.swing_mode = call.get_swing_mode(); + this->next_hvac_settings_.swing_mode = call.get_swing_mode(); if (call.get_target_temperature().has_value()) - this->hvac_settings_.target_temperature = call.get_target_temperature(); + this->next_hvac_settings_.target_temperature = call.get_target_temperature(); if (call.get_preset().has_value()) - this->hvac_settings_.preset = call.get_preset(); - this->hvac_settings_.valid = true; + this->next_hvac_settings_.preset = call.get_preset(); + this->next_hvac_settings_.valid = true; } - this->first_control_attempt_ = true; } void HaierClimateBase::HvacSettings::reset() { @@ -330,19 +356,9 @@ void HaierClimateBase::HvacSettings::reset() { this->preset.reset(); } -void HaierClimateBase::set_force_send_control_(bool status) { - this->force_send_control_ = status; - if (status) { - this->first_control_attempt_ = true; - } -} - -void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) { - if (this->answer_timeout_.has_value()) { - this->haier_protocol_.send_message(command, use_crc, this->answer_timeout_.value()); - } else { - this->haier_protocol_.send_message(command, use_crc); - } +void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats, + std::chrono::milliseconds interval) { + this->haier_protocol_.send_message(command, use_crc, num_repeats, interval); this->last_request_timestamp_ = std::chrono::steady_clock::now(); } diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index b2446d6fb5..75abbc20fb 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -11,7 +11,7 @@ namespace esphome { namespace haier { enum class ActionRequest : uint8_t { - NO_ACTION = 0, + SEND_CUSTOM_COMMAND = 0, TURN_POWER_ON = 1, TURN_POWER_OFF = 2, TOGGLE_POWER = 3, @@ -33,7 +33,6 @@ class HaierClimateBase : public esphome::Component, void control(const esphome::climate::ClimateCall &call) override; void dump_config() override; float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } - void set_fahrenheit(bool fahrenheit); void set_display_state(bool state); bool get_display_state() const; void set_health_mode(bool state); @@ -45,6 +44,7 @@ class HaierClimateBase : public esphome::Component, void set_supported_modes(const std::set &modes); void set_supported_swing_modes(const std::set &modes); void set_supported_presets(const std::set &presets); + bool valid_connection() { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; @@ -55,63 +55,56 @@ class HaierClimateBase : public esphome::Component, bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; void set_answer_timeout(uint32_t timeout); void set_send_wifi(bool send_wifi); + void send_custom_command(const haier_protocol::HaierMessage &message); protected: enum class ProtocolPhases { UNKNOWN = -1, // INITIALIZATION SENDING_INIT_1 = 0, - WAITING_INIT_1_ANSWER = 1, - SENDING_INIT_2 = 2, - WAITING_INIT_2_ANSWER = 3, - SENDING_FIRST_STATUS_REQUEST = 4, - WAITING_FIRST_STATUS_ANSWER = 5, - SENDING_ALARM_STATUS_REQUEST = 6, - WAITING_ALARM_STATUS_ANSWER = 7, + SENDING_INIT_2, + SENDING_FIRST_STATUS_REQUEST, + SENDING_ALARM_STATUS_REQUEST, // FUNCTIONAL STATE - IDLE = 8, - SENDING_STATUS_REQUEST = 10, - WAITING_STATUS_ANSWER = 11, - SENDING_UPDATE_SIGNAL_REQUEST = 12, - WAITING_UPDATE_SIGNAL_ANSWER = 13, - SENDING_SIGNAL_LEVEL = 14, - WAITING_SIGNAL_LEVEL_ANSWER = 15, - SENDING_CONTROL = 16, - WAITING_CONTROL_ANSWER = 17, - SENDING_POWER_ON_COMMAND = 18, - WAITING_POWER_ON_ANSWER = 19, - SENDING_POWER_OFF_COMMAND = 20, - WAITING_POWER_OFF_ANSWER = 21, + IDLE, + SENDING_STATUS_REQUEST, + SENDING_UPDATE_SIGNAL_REQUEST, + SENDING_SIGNAL_LEVEL, + SENDING_CONTROL, + SENDING_ACTION_COMMAND, NUM_PROTOCOL_PHASES }; -#if (HAIER_LOG_LEVEL > 4) const char *phase_to_string_(ProtocolPhases phase); -#endif 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 bool is_message_invalid(uint8_t message_type) = 0; - virtual void process_pending_action(); + virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; + virtual bool prepare_pending_action(); + virtual void process_protocol_reset(); esphome::climate::ClimateTraits traits() override; - // Answers handlers - haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, - uint8_t answer_message_type, uint8_t expected_answer_message_type, + // Answer handlers + haier_protocol::HandlerError answer_preprocess_(haier_protocol::FrameType request_message_type, + haier_protocol::FrameType expected_request_message_type, + haier_protocol::FrameType answer_message_type, + haier_protocol::FrameType expected_answer_message_type, ProtocolPhases expected_phase); + haier_protocol::HandlerError report_network_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); // Timeout handler - haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); + haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type); // Helper functions - void set_force_send_control_(bool status); - void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); + void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0, + std::chrono::milliseconds interval = std::chrono::milliseconds::zero()); virtual void set_phase(ProtocolPhases phase); - bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, - size_t timeout); + void reset_phase_(); + void reset_to_idle_(); bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); - bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now); bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); #ifdef USE_WIFI - haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type); + haier_protocol::HaierMessage get_wifi_signal_message_(); #endif struct HvacSettings { @@ -122,29 +115,34 @@ class HaierClimateBase : public esphome::Component, esphome::optional preset; bool valid; HvacSettings() : valid(false){}; + HvacSettings(const HvacSettings &) = default; + HvacSettings &operator=(const HvacSettings &) = default; void reset(); }; + struct PendingAction { + ActionRequest action; + esphome::optional message; + }; haier_protocol::ProtocolHandler haier_protocol_; ProtocolPhases protocol_phase_; - ActionRequest action_request_; + esphome::optional action_request_; uint8_t fan_mode_speed_; uint8_t other_modes_fan_speed_; bool display_status_; bool health_mode_; bool force_send_control_; - bool forced_publish_; bool forced_request_status_; - bool first_control_attempt_; bool reset_protocol_request_; + bool send_wifi_signal_; + bool use_crc_; esphome::climate::ClimateTraits traits_; - HvacSettings hvac_settings_; + HvacSettings current_hvac_settings_; + HvacSettings next_hvac_settings_; + std::unique_ptr last_status_message_; std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout std::chrono::steady_clock::time_point last_status_request_; // To request AC status - std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message - optional answer_timeout_; // Message answer timeout - bool send_wifi_signal_; - std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level + std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level }; } // namespace haier diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index d4944410f7..09f90fffa8 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -14,6 +14,8 @@ namespace haier { static const char *const TAG = "haier.climate"; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; +constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; +constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { switch (direction) { @@ -48,14 +50,11 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir } HonClimate::HonClimate() - : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), - cleaning_status_(CleaningState::NO_CLEANING), + : cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), - hvac_hardware_info_available_(false), - hvac_functions_{false, false, false, false, false}, - use_crc_(hvac_functions_[2]), active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, outdoor_sensor_(nullptr) { + last_status_message_ = std::unique_ptr(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]); this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; } @@ -72,14 +71,14 @@ AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this- void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { this->vertical_direction_ = direction; - this->set_force_send_control_(true); + this->force_send_control_ = true; } AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { this->horizontal_direction_ = direction; - this->set_force_send_control_(true); + this->force_send_control_ = true; } std::string HonClimate::get_cleaning_status_text() const { @@ -98,35 +97,35 @@ CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_st void HonClimate::start_self_cleaning() { if (this->cleaning_status_ == CleaningState::NO_CLEANING) { ESP_LOGI(TAG, "Sending self cleaning start request"); - this->action_request_ = ActionRequest::START_SELF_CLEAN; - this->set_force_send_control_(true); + this->action_request_ = + PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional()}); } } void HonClimate::start_steri_cleaning() { if (this->cleaning_status_ == CleaningState::NO_CLEANING) { ESP_LOGI(TAG, "Sending steri cleaning start request"); - this->action_request_ = ActionRequest::START_STERI_CLEAN; - this->set_force_send_control_(true); + this->action_request_ = + PendingAction({ActionRequest::START_STERI_CLEAN, esphome::optional()}); } } -haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { // Should check this before preprocess - if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) { + if (message_type == haier_protocol::FrameType::INVALID) { ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " "protocol instead of hOn"); this->set_phase(ProtocolPhases::SENDING_INIT_1); return haier_protocol::HandlerError::INVALID_ANSWER; } - haier_protocol::HandlerError result = this->answer_preprocess_( - request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, - (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER); + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type, + haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1); if (result == haier_protocol::HandlerError::HANDLER_OK) { if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { // Wrong structure - this->set_phase(ProtocolPhases::SENDING_INIT_1); return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; } // All OK @@ -134,54 +133,57 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint char tmp[9]; tmp[8] = 0; strncpy(tmp, answr->protocol_version, 8); - this->hvac_protocol_version_ = std::string(tmp); + this->hvac_hardware_info_ = HardwareInfo(); + this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp); strncpy(tmp, answr->software_version, 8); - this->hvac_software_version_ = std::string(tmp); + this->hvac_hardware_info_.value().software_version_ = std::string(tmp); strncpy(tmp, answr->hardware_version, 8); - this->hvac_hardware_version_ = std::string(tmp); + this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp); strncpy(tmp, answr->device_name, 8); - this->hvac_device_name_ = std::string(tmp); - this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support - this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support - this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support - this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support - this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support - this->hvac_hardware_info_available_ = true; + this->hvac_hardware_info_.value().device_name_ = std::string(tmp); + this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + this->hvac_hardware_info_.value().functions_[1] = + (answr->functions[1] & 0x02) != 0; // controller-device mode support + this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->use_crc_ = this->hvac_hardware_info_.value().functions_[2]; this->set_phase(ProtocolPhases::SENDING_INIT_2); return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->reset_phase_(); return result; } } -haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { - haier_protocol::HandlerError result = this->answer_preprocess_( - request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, - (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER); + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type, + haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2); if (result == haier_protocol::HandlerError::HANDLER_OK) { this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->reset_phase_(); return result; } } -haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, - const uint8_t *data, size_t data_size) { +haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, - (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, + haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); if (result == haier_protocol::HandlerError::HANDLER_OK) { result = this->process_status_message_(data, data_size); if (result != haier_protocol::HandlerError::HANDLER_OK) { ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->reset_phase_(); + this->action_request_.reset(); + this->force_send_control_ = false; } else { if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); @@ -189,36 +191,48 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, sizeof(hon_protocol::HaierPacketControl)); } - if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { - ESP_LOGI(TAG, "First HVAC status received"); - this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); - } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || - (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || - (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { - this->set_phase(ProtocolPhases::IDLE); - } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { - this->set_phase(ProtocolPhases::IDLE); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + // Do nothing, phase will be changed in process_phase + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_CONTROL: + if (!this->control_messages_queue_.empty()) + this->control_messages_queue_.pop(); + if (this->control_messages_queue_.empty()) { + this->set_phase(ProtocolPhases::IDLE); + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + } else { + this->set_phase(ProtocolPhases::SENDING_CONTROL); + } + break; + default: + break; } } return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->action_request_.reset(); + this->force_send_control_ = false; + this->reset_phase_(); return result; } } -haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, - size_t data_size) { - haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, - message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, - ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); +haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type, + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); if (result == haier_protocol::HandlerError::HANDLER_OK) { this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); return result; @@ -228,25 +242,16 @@ haier_protocol::HandlerError HonClimate::get_management_information_answer_handl } } -haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, size_t data_size) { - haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, - (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); - this->set_phase(ProtocolPhases::IDLE); - return result; -} - -haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { - if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { - if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { + if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) { + if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { // Unexpected answer to request this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; } - if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { + if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) { // Don't expect this answer now this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; @@ -263,27 +268,27 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_ void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), + haier_protocol::FrameType::GET_DEVICE_VERSION, std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), + haier_protocol::FrameType::GET_DEVICE_ID, std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::CONTROL), + haier_protocol::FrameType::CONTROL, std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), + haier_protocol::FrameType::GET_ALARM_STATUS, std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), + haier_protocol::FrameType::REPORT_NETWORK_STATUS, std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); } @@ -291,14 +296,18 @@ void HonClimate::set_handlers() { void HonClimate::dump_config() { HaierClimateBase::dump_config(); ESP_LOGCONFIG(TAG, " Protocol version: hOn"); - if (this->hvac_hardware_info_available_) { - ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str()); - ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str()); - ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str()); - ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str()); - ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), - (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), - (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); + ESP_LOGCONFIG(TAG, " Control method: %d", (uint8_t) this->control_method_); + if (this->hvac_hardware_info_.has_value()) { + ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str()); + ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", + (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""), + (this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""), + (this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""), + (this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""), + (this->hvac_hardware_info_.value().functions_[4] ? " role" : "")); ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); } } @@ -307,7 +316,6 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { switch (this->protocol_phase_) { case ProtocolPhases::SENDING_INIT_1: if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { - this->hvac_hardware_info_available_ = false; // Indicate device capabilities: // bit 0 - if 1 module support interactive mode // bit 1 - if 1 module support controller-device mode @@ -316,109 +324,95 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { // bit 4..bit 15 - not used uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( - (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); } break; case ProtocolPhases::SENDING_INIT_2: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); + static const haier_protocol::HaierMessage DEVICEID_REQUEST(haier_protocol::FrameType::GET_DEVICE_ID); this->send_message_(DEVICEID_REQUEST, this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER); } break; case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage STATUS_REQUEST( - (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); this->send_message_(STATUS_REQUEST, this->use_crc_); this->last_status_request_ = now; - this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); } break; #ifdef USE_WIFI case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( - (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION); this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); this->last_signal_request_ = now; - this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); } break; case ProtocolPhases::SENDING_SIGNAL_LEVEL: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS), - this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); } break; - case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: - break; #else case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_SIGNAL_LEVEL: - case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: this->set_phase(ProtocolPhases::IDLE); break; #endif case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( - (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS); + static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); } break; case ProtocolPhases::SENDING_CONTROL: - if (this->first_control_attempt_) { - this->control_request_timestamp_ = now; - this->first_control_attempt_ = false; + if (this->control_messages_queue_.empty()) { + switch (this->control_method_) { + case HonControlMethod::SET_GROUP_PARAMETERS: { + haier_protocol::HaierMessage control_message = this->get_control_message(); + this->control_messages_queue_.push(control_message); + } break; + case HonControlMethod::SET_SINGLE_PARAMETER: + this->fill_control_messages_queue_(); + break; + case HonControlMethod::MONITOR_ONLY: + ESP_LOGI(TAG, "AC control is disabled, monitor only"); + this->reset_to_idle_(); + return; + default: + ESP_LOGW(TAG, "Unsupported control method for hOn protocol!"); + this->reset_to_idle_(); + return; + } } - if (this->is_control_message_timeout_exceeded_(now)) { - ESP_LOGW(TAG, "Sending control packet timeout!"); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); - this->forced_request_status_ = true; - this->forced_publish_ = true; - this->set_phase(ProtocolPhases::IDLE); + if (this->control_messages_queue_.empty()) { + ESP_LOGW(TAG, "Control message queue is empty!"); + this->reset_to_idle_(); } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { - haier_protocol::HaierMessage control_message = get_control_message(); - this->send_message_(control_message, this->use_crc_); - ESP_LOGI(TAG, "Control packet sent"); - this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); + ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size()); + this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES, + CONTROL_MESSAGE_RETRIES_INTERVAL); } break; - case ProtocolPhases::SENDING_POWER_ON_COMMAND: - case ProtocolPhases::SENDING_POWER_OFF_COMMAND: - if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; - if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) - pwr_cmd_buf[1] = 0x01; - haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, - ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, - pwr_cmd_buf, sizeof(pwr_cmd_buf)); - this->send_message_(power_cmd, this->use_crc_); - this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND - ? ProtocolPhases::WAITING_POWER_ON_ANSWER - : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + case ProtocolPhases::SENDING_ACTION_COMMAND: + if (this->action_request_.has_value()) { + if (this->action_request_.value().message.has_value()) { + this->send_message_(this->action_request_.value().message.value(), this->use_crc_); + this->action_request_.value().message.reset(); + } else { + // Message already sent, reseting request and return to idle + this->action_request_.reset(); + this->set_phase(ProtocolPhases::IDLE); + } + } else { + ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); + this->set_phase(ProtocolPhases::IDLE); } break; - - case ProtocolPhases::WAITING_INIT_1_ANSWER: - case ProtocolPhases::WAITING_INIT_2_ANSWER: - case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: - case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: - case ProtocolPhases::WAITING_STATUS_ANSWER: - case ProtocolPhases::WAITING_CONTROL_ANSWER: - case ProtocolPhases::WAITING_POWER_ON_ANSWER: - case ProtocolPhases::WAITING_POWER_OFF_ANSWER: - break; case ProtocolPhases::IDLE: { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); @@ -433,26 +427,35 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { } break; default: // Shouldn't get here -#if (HAIER_LOG_LEVEL > 4) ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); -#else - ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); -#endif this->set_phase(ProtocolPhases::SENDING_INIT_1); break; } } +haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { + if (state) { + static haier_protocol::HaierMessage power_on_message( + haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, + std::initializer_list({0x00, 0x01}).begin(), 2); + return power_on_message; + } else { + static haier_protocol::HaierMessage power_off_message( + haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, + std::initializer_list({0x00, 0x00}).begin(), 2); + return power_off_message; + } +} + haier_protocol::HaierMessage HonClimate::get_control_message() { uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; bool has_hvac_settings = false; - if (this->hvac_settings_.valid) { + if (this->current_hvac_settings_.valid) { has_hvac_settings = true; - HvacSettings climate_control; - climate_control = this->hvac_settings_; + HvacSettings &climate_control = this->current_hvac_settings_; if (climate_control.mode.has_value()) { switch (climate_control.mode.value()) { case CLIMATE_MODE_OFF: @@ -535,7 +538,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { } if (climate_control.target_temperature.has_value()) { float target_temp = climate_control.target_temperature.value(); - out_data->set_point = ((int) target_temp) - 16; // set the temperature at our offset, subtract 16. + out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; } if (out_data->ac_power == 0) { @@ -587,50 +590,28 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { control_out_buffer[4] = 0; // This byte should be cleared before setting values out_data->display_status = this->display_status_ ? 1 : 0; out_data->health_mode = this->health_mode_ ? 1 : 0; - switch (this->action_request_) { - case ActionRequest::START_SELF_CLEAN: - this->action_request_ = ActionRequest::NO_ACTION; - out_data->self_cleaning_status = 1; - out_data->steri_clean = 0; - out_data->set_point = 0x06; - out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; - out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; - out_data->ac_power = 1; - out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; - out_data->light_status = 0; - break; - case ActionRequest::START_STERI_CLEAN: - this->action_request_ = ActionRequest::NO_ACTION; - out_data->self_cleaning_status = 0; - out_data->steri_clean = 1; - out_data->set_point = 0x06; - out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; - out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; - out_data->ac_power = 1; - out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; - out_data->light_status = 0; - break; - default: - // No change - break; - } - return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL, + return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); } haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { - if (size < sizeof(hon_protocol::HaierStatus)) + if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_) return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; - hon_protocol::HaierStatus packet; - if (size < sizeof(hon_protocol::HaierStatus)) - size = sizeof(hon_protocol::HaierStatus); - memcpy(&packet, packet_buffer, size); + struct { + hon_protocol::HaierPacketControl control; + hon_protocol::HaierPacketSensors sensors; + } packet; + memcpy(&packet.control, packet_buffer + 2, sizeof(hon_protocol::HaierPacketControl)); + memcpy(&packet.sensors, + packet_buffer + 2 + sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_, + sizeof(hon_protocol::HaierPacketSensors)); if (packet.sensors.error_status != 0) { ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); } - if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { - got_valid_outdoor_temp_ = true; + if ((this->outdoor_sensor_ != nullptr) && + (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { + this->got_valid_outdoor_temp_ = true; float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) this->outdoor_sensor_->publish_state(otemp); @@ -703,7 +684,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // Do something only if display status changed if (this->mode == CLIMATE_MODE_OFF) { // AC just turned on from remote need to turn off display - this->set_force_send_control_(true); + this->force_send_control_ = true; } else { this->display_status_ = disp_status; } @@ -732,7 +713,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); if (new_cleaning == CleaningState::NO_CLEANING) { // Turning AC off after cleaning - this->action_request_ = ActionRequest::TURN_POWER_OFF; + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional()}); } this->cleaning_status_ = new_cleaning; } @@ -783,51 +765,257 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * should_publish = should_publish || (old_swing_mode != this->swing_mode); } this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); - if (this->forced_publish_ || should_publish) { -#if (HAIER_LOG_LEVEL > 4) - std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); -#endif + if (should_publish) { this->publish_state(); -#if (HAIER_LOG_LEVEL > 4) - ESP_LOGV(TAG, "Publish delay: %lld ms", - std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - - _publish_start) - .count()); -#endif - this->forced_publish_ = false; } if (should_publish) { ESP_LOGI(TAG, "HVAC values changed"); } - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "HVAC Mode = 0x%X", packet.control.ac_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Fan speed Status = 0x%X", packet.control.fan_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Set Point Status = 0x%X", packet.control.set_point); + int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; + esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); return haier_protocol::HandlerError::HANDLER_OK; } -bool HonClimate::is_message_invalid(uint8_t message_type) { - return message_type == (uint8_t) hon_protocol::FrameType::INVALID; +void HonClimate::fill_control_messages_queue_() { + static uint8_t one_buf[] = {0x00, 0x01}; + static uint8_t zero_buf[] = {0x00, 0x00}; + if (!this->current_hvac_settings_.valid && !this->force_send_control_) + return; + this->clear_control_messages_queue_(); + HvacSettings climate_control; + climate_control = this->current_hvac_settings_; + // Beeper command + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::BEEPER_STATUS, + this->beeper_status_ ? zero_buf : one_buf, 2)); + } + // Health mode + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::HEALTH_MODE, + this->health_mode_ ? one_buf : zero_buf, 2)); + } + // Climate mode + bool new_power = this->mode != CLIMATE_MODE_OFF; + uint8_t fan_mode_buf[] = {0x00, 0xFF}; + uint8_t quiet_mode_buf[] = {0x00, 0xFF}; + if (climate_control.mode.has_value()) { + uint8_t buffer[2] = {0x00, 0x00}; + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + new_power = false; + break; + case CLIMATE_MODE_HEAT_COOL: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::AUTO; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::HEAT; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::DRY; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::FAN; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode + // Disabling eco mode for Fan only + quiet_mode_buf[1] = 0; + break; + case CLIMATE_MODE_COOL: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::COOL; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Climate power + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_POWER, + new_power ? one_buf : zero_buf, 2)); + } + // CLimate preset + { + uint8_t fast_mode_buf[] = {0x00, 0xFF}; + if (!new_power) { + // If AC is off - no presets allowed + quiet_mode_buf[1] = 0x00; + fast_mode_buf[1] = 0x00; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + quiet_mode_buf[1] = 0x00; + fast_mode_buf[1] = 0x00; + break; + case CLIMATE_PRESET_ECO: + // Eco is not supported in Fan only mode + quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; + fast_mode_buf[1] = 0x00; + break; + case CLIMATE_PRESET_BOOST: + quiet_mode_buf[1] = 0x00; + // Boost is not supported in Fan only mode + fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + break; + } + } + if (quiet_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::QUIET_MODE, + quiet_mode_buf, 2)); + } + if (fast_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::FAST_MODE, + fast_mode_buf, 2)); + } + } + // Target temperature + if (climate_control.target_temperature.has_value()) { + uint8_t buffer[2] = {0x00, 0x00}; + buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::SET_POINT, + buffer, 2)); + } + // Fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + if (fan_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::FAN_MODE, + fan_mode_buf, 2)); + } + } } -void HonClimate::process_pending_action() { - switch (this->action_request_) { - case ActionRequest::START_SELF_CLEAN: - case ActionRequest::START_STERI_CLEAN: - // Will reset action with control message sending - this->set_phase(ProtocolPhases::SENDING_CONTROL); - break; +void HonClimate::clear_control_messages_queue_() { + while (!this->control_messages_queue_.empty()) + this->control_messages_queue_.pop(); +} + +bool HonClimate::prepare_pending_action() { + switch (this->action_request_.value().action) { + case ActionRequest::START_SELF_CLEAN: { + uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + out_data->self_cleaning_status = 1; + out_data->steri_clean = 0; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + this->action_request_.value().message = haier_protocol::HaierMessage( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); + } + return true; + case ActionRequest::START_STERI_CLEAN: { + uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + out_data->self_cleaning_status = 0; + out_data->steri_clean = 1; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + this->action_request_.value().message = haier_protocol::HaierMessage( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); + } + return true; default: - HaierClimateBase::process_pending_action(); - break; + return HaierClimateBase::prepare_pending_action(); } } +void HonClimate::process_protocol_reset() { + HaierClimateBase::process_protocol_reset(); + if (this->outdoor_sensor_ != nullptr) { + this->outdoor_sensor_->publish_state(NAN); + } + this->got_valid_outdoor_temp_ = false; + this->hvac_hardware_info_.reset(); +} + } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index cf566e3b8e..1ba6a8e041 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -30,6 +30,8 @@ enum class CleaningState : uint8_t { STERI_CLEAN = 2, }; +enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; + class HonClimate : public HaierClimateBase { public: HonClimate(); @@ -48,44 +50,57 @@ class HonClimate : public HaierClimateBase { CleaningState get_cleaning_status() const; void start_self_cleaning(); void start_steri_cleaning(); + void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; }; + void set_control_method(HonControlMethod method) { this->control_method_ = method; }; protected: void set_handlers() override; void process_phase(std::chrono::steady_clock::time_point now) override; haier_protocol::HaierMessage get_control_message() override; - bool is_message_invalid(uint8_t message_type) override; - void process_pending_action() override; + haier_protocol::HaierMessage get_power_message(bool state) override; + bool prepare_pending_action() override; + void process_protocol_reset() override; // Answers handlers - haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_management_information_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, - const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); // Helper functions haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); - std::unique_ptr last_status_message_; + void fill_control_messages_queue_(); + void clear_control_messages_queue_(); + + struct HardwareInfo { + std::string protocol_version_; + std::string software_version_; + std::string hardware_version_; + std::string device_name_; + bool functions_[5]; + }; + bool beeper_status_; CleaningState cleaning_status_; bool got_valid_outdoor_temp_; AirflowVerticalDirection vertical_direction_; AirflowHorizontalDirection horizontal_direction_; - bool hvac_hardware_info_available_; - std::string hvac_protocol_version_; - std::string hvac_software_version_; - std::string hvac_hardware_version_; - std::string hvac_device_name_; - bool hvac_functions_[5]; - bool &use_crc_; + esphome::optional hvac_hardware_info_; uint8_t active_alarms_[8]; + int extra_control_packet_bytes_; + HonControlMethod control_method_; esphome::sensor::Sensor *outdoor_sensor_; + std::queue control_messages_queue_; }; } // namespace haier diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h index c6b32df200..7724b43854 100644 --- a/esphome/components/haier/hon_packet.h +++ b/esphome/components/haier/hon_packet.h @@ -35,6 +35,20 @@ enum class ConditioningMode : uint8_t { FAN = 0x06 }; +enum class DataParameters : uint8_t { + AC_POWER = 0x01, + SET_POINT = 0x02, + AC_MODE = 0x04, + FAN_MODE = 0x05, + USE_FAHRENHEIT = 0x07, + TEN_DEGREE = 0x0A, + HEALTH_MODE = 0x0B, + BEEPER_STATUS = 0x16, + LOCK_REMOTE = 0x17, + QUIET_MODE = 0x19, + FAST_MODE = 0x1A, +}; + enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; @@ -124,11 +138,7 @@ struct HaierPacketSensors { uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) }; -struct HaierStatus { - uint16_t subcommand; - HaierPacketControl control; - HaierPacketSensors sensors; -}; +constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors); struct DeviceVersionAnswer { char protocol_version[8]; @@ -140,76 +150,6 @@ struct DeviceVersionAnswer { uint8_t functions[2]; }; -// In this section comments: -// - module is the ESP32 control module (communication module in Haier protocol document) -// - device is the conditioner control board (network appliances in Haier protocol document) -enum class FrameType : uint8_t { - CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required) - STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device, - // required) - INVALID = 0x03, // Communication error indication (module <-> device, required) - ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required) - CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module - // <-> device, required) - REPORT = 0x06, // Report frame (module <-> device, interactive, required) - STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required) - SYSTEM_DOWNLINK = 0x11, // System downlink frame (module -> device, optional) - DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional) - SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional) - SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional) - DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional) - DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional) - GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional) - GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required) - GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_ - GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional) - GET_ALL_ADDRESSES_RESPONSE = - 0x68, // Answer to request of all devices addresses (module <- device , interactive, optional) - HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional) - GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required) - GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required) - GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required) - GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required) - GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required) - GET_DEVICE_CONFIGURATION_RESPONSE = - 0x7D, // Response to device configuration request (module <- device, interactive, required) - DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device) - // (module -> device, interactive, optional) - UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module - // <- device, interactive, optional) - START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required) - START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required) - GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required) - GET_FIRMWARE_CONTENT_RESPONSE = - 0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?) - CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required) - CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required) - GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required) - GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required) - GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required) - GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required) - GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required) - GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required) - GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional) - GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional) - START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required) - START_WIFI_CONFIGURATION_RESPONSE = - 0xF3, // Response to start WiFi configuration request (module -> device, interactive, required) - STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required) - STOP_WIFI_CONFIGURATION_RESPONSE = - 0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required) - REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required) - CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional) - BIG_DATA_REPORT_CONFIGURATION = - 0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional) - BIG_DATA_REPORT_CONFIGURATION_RESPONSE = - 0xFB, // Response to set big data configuration (module <- device, interactive, optional) - GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required) - GET_MANAGEMENT_INFORMATION_RESPONSE = - 0xFD, // Response to management information request (module <- device, required) - WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional) -}; - enum class SubcommandsControl : uint16_t { GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index f29f840088..c2326883f7 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -12,21 +12,28 @@ namespace haier { static const char *const TAG = "haier.climate"; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; +constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; +constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); +constexpr uint8_t INIT_REQUESTS_RETRY = 2; +constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL = std::chrono::milliseconds(2000); -Smartair2Climate::Smartair2Climate() - : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {} +Smartair2Climate::Smartair2Climate() { + last_status_message_ = std::unique_ptr(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]); +} -haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, - (uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, + haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); if (result == haier_protocol::HandlerError::HANDLER_OK) { result = this->process_status_message_(data, data_size); if (result != haier_protocol::HandlerError::HANDLER_OK) { ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + this->reset_phase_(); + this->action_request_.reset(); + this->force_send_control_ = false; } else { if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); @@ -34,36 +41,45 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, sizeof(smartair2_protocol::HaierPacketControl)); } - if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { - ESP_LOGI(TAG, "First HVAC status received"); - this->set_phase(ProtocolPhases::IDLE); - } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { - this->set_phase(ProtocolPhases::IDLE); - } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { - this->set_phase(ProtocolPhases::IDLE); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + // Do nothing, phase will be changed in process_phase + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_CONTROL: + this->set_phase(ProtocolPhases::IDLE); + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + break; + default: + break; } } return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + this->action_request_.reset(); + this->force_send_control_ = false; + this->reset_phase_(); return result; } } -haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, - size_t data_size) { - if (request_type != (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION) +haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + if (request_type != haier_protocol::FrameType::GET_DEVICE_VERSION) return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; - if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_) + if (ProtocolPhases::SENDING_INIT_1 != this->protocol_phase_) return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; // Invalid packet is expected answer - if ((message_type == (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && + if ((message_type == haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && ((data[37] & 0x04) != 0)) { ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " "instead of smartAir2"); @@ -72,58 +88,35 @@ haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler return haier_protocol::HandlerError::HANDLER_OK; } -haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, - size_t data_size) { - haier_protocol::HandlerError result = this->answer_preprocess_( - request_type, (uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, - (uint8_t) smartair2_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); - this->set_phase(ProtocolPhases::IDLE); - return result; -} - -haier_protocol::HandlerError Smartair2Climate::initial_messages_timeout_handler_(uint8_t message_type) { +haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cycle_for_init_( + haier_protocol::FrameType message_type) { if (this->protocol_phase_ >= ProtocolPhases::IDLE) return HaierClimateBase::timeout_default_handler_(message_type); - this->timeouts_counter_++; - ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type, - (int) this->protocol_phase_, this->timeouts_counter_); - if (this->timeouts_counter_ >= 3) { - ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); - if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) - new_phase = ProtocolPhases::SENDING_INIT_1; - this->set_phase(new_phase); - } else { - // Returning to the previous state to try again - this->set_phase((ProtocolPhases) ((int) this->protocol_phase_ - 1)); - } + ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type, + phase_to_string_(this->protocol_phase_)); + ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); + if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) + new_phase = ProtocolPhases::SENDING_INIT_1; + this->set_phase(new_phase); return haier_protocol::HandlerError::HANDLER_OK; } void Smartair2Climate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( - (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), + haier_protocol::FrameType::GET_DEVICE_VERSION, std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (smartair2_protocol::FrameType::CONTROL), + haier_protocol::FrameType::CONTROL, std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), + haier_protocol::FrameType::REPORT_NETWORK_STATUS, std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); - this->haier_protocol_.set_timeout_handler( - (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID), - std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); - this->haier_protocol_.set_timeout_handler( - (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), - std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); - this->haier_protocol_.set_timeout_handler( - (uint8_t) (smartair2_protocol::FrameType::CONTROL), - std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); + this->haier_protocol_.set_default_timeout_handler( + std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); } void Smartair2Climate::dump_config() { @@ -134,9 +127,7 @@ void Smartair2Climate::dump_config() { void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { switch (this->protocol_phase_) { case ProtocolPhases::SENDING_INIT_1: - if (this->can_send_message() && - (((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) || - ((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) { + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { // Indicate device capabilities: // bit 0 - if 1 module support interactive mode // bit 1 - if 1 module support controller-device mode @@ -145,92 +136,65 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) // bit 4..bit 15 - not used uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( - (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, - sizeof(module_capabilities)); - this->send_message_(DEVICE_VERSION_REQUEST, false); - this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); + haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); } break; case ProtocolPhases::SENDING_INIT_2: - case ProtocolPhases::WAITING_INIT_2_ANSWER: this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); break; case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, - 0x4D01); - this->send_message_(STATUS_REQUEST, false); + static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01); + if (this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) { + this->send_message_(STATUS_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); + } else { + this->send_message_(STATUS_REQUEST, this->use_crc_); + } this->last_status_request_ = now; - this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); } break; #ifdef USE_WIFI case ProtocolPhases::SENDING_SIGNAL_LEVEL: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - this->send_message_( - this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false); + this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); this->last_signal_request_ = now; - this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); } break; - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: - break; #else case ProtocolPhases::SENDING_SIGNAL_LEVEL: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: this->set_phase(ProtocolPhases::IDLE); break; #endif case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: - case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); break; case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: - case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: this->set_phase(ProtocolPhases::SENDING_INIT_1); break; case ProtocolPhases::SENDING_CONTROL: - if (this->first_control_attempt_) { - this->control_request_timestamp_ = now; - this->first_control_attempt_ = false; + if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { + ESP_LOGI(TAG, "Sending control packet"); + this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES, + CONTROL_MESSAGE_RETRIES_INTERVAL); } - if (this->is_control_message_timeout_exceeded_(now)) { - ESP_LOGW(TAG, "Sending control packet timeout!"); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); - this->forced_request_status_ = true; - this->forced_publish_ = true; + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + if (this->action_request_.has_value()) { + if (this->action_request_.value().message.has_value()) { + this->send_message_(this->action_request_.value().message.value(), this->use_crc_); + this->action_request_.value().message.reset(); + } else { + // Message already sent, reseting request and return to idle + this->action_request_.reset(); + this->set_phase(ProtocolPhases::IDLE); + } + } else { + ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); this->set_phase(ProtocolPhases::IDLE); - } else if (this->can_send_message() && this->is_control_message_interval_exceeded_( - now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests - { - haier_protocol::HaierMessage control_message = get_control_message(); - this->send_message_(control_message, false); - ESP_LOGI(TAG, "Control packet sent"); - this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); } break; - case ProtocolPhases::SENDING_POWER_ON_COMMAND: - case ProtocolPhases::SENDING_POWER_OFF_COMMAND: - if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - haier_protocol::HaierMessage power_cmd( - (uint8_t) smartair2_protocol::FrameType::CONTROL, - this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03); - this->send_message_(power_cmd, false); - this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND - ? ProtocolPhases::WAITING_POWER_ON_ANSWER - : ProtocolPhases::WAITING_POWER_OFF_ANSWER); - } - break; - case ProtocolPhases::WAITING_INIT_1_ANSWER: - case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: - case ProtocolPhases::WAITING_STATUS_ANSWER: - case ProtocolPhases::WAITING_CONTROL_ANSWER: - case ProtocolPhases::WAITING_POWER_ON_ANSWER: - case ProtocolPhases::WAITING_POWER_OFF_ANSWER: - break; case ProtocolPhases::IDLE: { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); @@ -245,55 +209,55 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) } break; default: // Shouldn't get here -#if (HAIER_LOG_LEVEL > 4) ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); -#else - ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); -#endif this->set_phase(ProtocolPhases::SENDING_INIT_1); break; } } +haier_protocol::HaierMessage Smartair2Climate::get_power_message(bool state) { + if (state) { + static haier_protocol::HaierMessage power_on_message(haier_protocol::FrameType::CONTROL, 0x4D02); + return power_on_message; + } else { + static haier_protocol::HaierMessage power_off_message(haier_protocol::FrameType::CONTROL, 0x4D03); + return power_off_message; + } +} + haier_protocol::HaierMessage Smartair2Climate::get_control_message() { uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; out_data->cntrl = 0; - if (this->hvac_settings_.valid) { - HvacSettings climate_control; - climate_control = this->hvac_settings_; + if (this->current_hvac_settings_.valid) { + HvacSettings &climate_control = this->current_hvac_settings_; if (climate_control.mode.has_value()) { switch (climate_control.mode.value()) { case CLIMATE_MODE_OFF: out_data->ac_power = 0; break; - case CLIMATE_MODE_HEAT_COOL: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; out_data->fan_mode = this->other_modes_fan_speed_; break; - case CLIMATE_MODE_HEAT: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; out_data->fan_mode = this->other_modes_fan_speed_; break; - case CLIMATE_MODE_DRY: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; out_data->fan_mode = this->other_modes_fan_speed_; break; - case CLIMATE_MODE_FAN_ONLY: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode break; - case CLIMATE_MODE_COOL: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; @@ -327,32 +291,49 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } // Set swing mode if (climate_control.swing_mode.has_value()) { - switch (climate_control.swing_mode.value()) { - case CLIMATE_SWING_OFF: - out_data->use_swing_bits = 0; - out_data->swing_both = 0; - break; - case CLIMATE_SWING_VERTICAL: - out_data->swing_both = 0; - out_data->vertical_swing = 1; - out_data->horizontal_swing = 0; - break; - case CLIMATE_SWING_HORIZONTAL: - out_data->swing_both = 0; - out_data->vertical_swing = 0; - out_data->horizontal_swing = 1; - break; - case CLIMATE_SWING_BOTH: - out_data->swing_both = 1; - out_data->use_swing_bits = 0; - out_data->vertical_swing = 0; - out_data->horizontal_swing = 0; - break; + if (this->use_alternative_swing_control_) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->swing_mode = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_mode = 1; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_mode = 2; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_mode = 3; + break; + } + } else { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->use_swing_bits = 0; + out_data->swing_mode = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_mode = 0; + out_data->vertical_swing = 1; + out_data->horizontal_swing = 0; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_mode = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 1; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_mode = 1; + out_data->use_swing_bits = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 0; + break; + } } } if (climate_control.target_temperature.has_value()) { float target_temp = climate_control.target_temperature.value(); - out_data->set_point = target_temp - 16; // set the temperature with offset 16 + out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; } if (out_data->ac_power == 0) { @@ -383,7 +364,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } out_data->display_status = this->display_status_ ? 0 : 1; out_data->health_mode = this->health_mode_ ? 1 : 0; - return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, + return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, sizeof(smartair2_protocol::HaierPacketControl)); } @@ -459,13 +440,19 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin // Do something only if display status changed if (this->mode == CLIMATE_MODE_OFF) { // AC just turned on from remote need to turn off display - this->set_force_send_control_(true); + this->force_send_control_ = true; } else { this->display_status_ = disp_status; } } } } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } { // Climate mode ClimateMode old_mode = this->mode; @@ -493,70 +480,57 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin } should_publish = should_publish || (old_mode != this->mode); } - { - // Health mode - bool old_health_mode = this->health_mode_; - this->health_mode_ = packet.control.health_mode == 1; - should_publish = should_publish || (old_health_mode != this->health_mode_); - } { // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; - if (packet.control.swing_both == 0) { - if (packet.control.vertical_swing != 0) { - this->swing_mode = CLIMATE_SWING_VERTICAL; - } else if (packet.control.horizontal_swing != 0) { - this->swing_mode = CLIMATE_SWING_HORIZONTAL; - } else { - this->swing_mode = CLIMATE_SWING_OFF; + if (this->use_alternative_swing_control_) { + switch (packet.control.swing_mode) { + case 1: + this->swing_mode = CLIMATE_SWING_VERTICAL; + break; + case 2: + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + break; + case 3: + this->swing_mode = CLIMATE_SWING_BOTH; + break; + default: + this->swing_mode = CLIMATE_SWING_OFF; + break; } } else { - swing_mode = CLIMATE_SWING_BOTH; + if (packet.control.swing_mode == 0) { + if (packet.control.vertical_swing != 0) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else if (packet.control.horizontal_swing != 0) { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } else { + swing_mode = CLIMATE_SWING_BOTH; + } } should_publish = should_publish || (old_swing_mode != this->swing_mode); } this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); - if (this->forced_publish_ || should_publish) { -#if (HAIER_LOG_LEVEL > 4) - std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); -#endif + if (should_publish) { this->publish_state(); -#if (HAIER_LOG_LEVEL > 4) - ESP_LOGV(TAG, "Publish delay: %lld ms", - std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - - _publish_start) - .count()); -#endif - this->forced_publish_ = false; } if (should_publish) { ESP_LOGI(TAG, "HVAC values changed"); } - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "HVAC Mode = 0x%X", packet.control.ac_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Fan speed Status = 0x%X", packet.control.fan_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Vertical Swing Status = 0x%X", packet.control.vertical_swing); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Set Point Status = 0x%X", packet.control.set_point); + int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; + esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); + esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing); + esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); return haier_protocol::HandlerError::HANDLER_OK; } -bool Smartair2Climate::is_message_invalid(uint8_t message_type) { - return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; -} - -void Smartair2Climate::set_phase(HaierClimateBase::ProtocolPhases phase) { - int old_phase = (int) this->protocol_phase_; - int new_phase = (int) phase; - int min_p = std::min(old_phase, new_phase); - int max_p = std::max(old_phase, new_phase); - if ((min_p % 2 != 0) || (max_p - min_p > 1)) - this->timeouts_counter_ = 0; - HaierClimateBase::set_phase(phase); +void Smartair2Climate::set_alternative_swing_control(bool swing_control) { + this->use_alternative_swing_control_ = swing_control; } } // namespace haier diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h index f173b10749..6914d8a1fb 100644 --- a/esphome/components/haier/smartair2_climate.h +++ b/esphome/components/haier/smartair2_climate.h @@ -13,27 +13,27 @@ class Smartair2Climate : public HaierClimateBase { Smartair2Climate &operator=(const Smartair2Climate &) = delete; ~Smartair2Climate(); void dump_config() override; + void set_alternative_swing_control(bool swing_control); protected: void set_handlers() override; void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_power_message(bool state) override; haier_protocol::HaierMessage get_control_message() override; - bool is_message_invalid(uint8_t message_type) override; - void set_phase(HaierClimateBase::ProtocolPhases phase) override; - // Answer and timeout handlers - haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + // Answer handlers + haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, - const uint8_t *data, size_t data_size); - haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type); + haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type); // Helper functions haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); - std::unique_ptr last_status_message_; - unsigned int timeouts_counter_; + bool use_alternative_swing_control_; }; } // namespace haier diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h index f791c21af2..22570ff048 100644 --- a/esphome/components/haier/smartair2_packet.h +++ b/esphome/components/haier/smartair2_packet.h @@ -41,8 +41,9 @@ struct HaierPacketControl { // 24 uint8_t : 8; // 25 - uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define - // vertical/horizontal/off + uint8_t swing_mode; // In normal mode: If 1 - swing both direction, if 0 - horizontal_swing and + // vertical_swing define vertical/horizontal/off + // In alternative mode: 0 - off, 01 - vertical, 02 - horizontal, 03 - both // 26 uint8_t : 3; uint8_t use_fahrenheit : 1; @@ -82,19 +83,6 @@ struct HaierStatus { HaierPacketControl control; }; -enum class FrameType : uint8_t { - CONTROL = 0x01, - STATUS = 0x02, - INVALID = 0x03, - CONFIRM = 0x05, - GET_DEVICE_VERSION = 0x61, - GET_DEVICE_VERSION_RESPONSE = 0x62, - GET_DEVICE_ID = 0x70, - GET_DEVICE_ID_RESPONSE = 0x71, - REPORT_NETWORK_STATUS = 0xF7, - NO_COMMAND = 0xFF, -}; - } // namespace smartair2_protocol } // namespace haier } // namespace esphome diff --git a/platformio.ini b/platformio.ini index cbd87155be..68c4220aab 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,7 +39,7 @@ lib_deps = bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 - pavlodn/HaierProtocol@0.9.20 ; haier + pavlodn/HaierProtocol@0.9.24 ; haier ; 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 build_flags = From e7038d077a98f3fd8f6420bc81267a1aa0132de9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:24:47 +1300 Subject: [PATCH 034/436] Early return when there are no wifi scan results (#5797) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 8fcafc5c12..0035733553 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -686,6 +686,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } + if (it.number == 0) { + // no results + return; + } + uint16_t number = it.number; std::vector records(number); err = esp_wifi_scan_get_ap_records(&number, records.data()); From b809d0284662ca6798cb807e9d5dc47f8ea97f17 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 Nov 2023 15:09:14 -0600 Subject: [PATCH 035/436] Add some additional VA triggers, part 2 (#5811) --- .../components/voice_assistant/__init__.py | 34 +++++++++++++++++++ .../voice_assistant/voice_assistant.cpp | 6 ++++ .../voice_assistant/voice_assistant.h | 8 +++++ 3 files changed, 48 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 5715604605..d05f39072c 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -29,6 +29,8 @@ CONF_ON_STT_VAD_END = "on_stt_vad_end" CONF_ON_STT_VAD_START = "on_stt_vad_start" CONF_ON_TTS_END = "on_tts_end" CONF_ON_TTS_START = "on_tts_start" +CONF_ON_TTS_STREAM_START = "on_tts_stream_start" +CONF_ON_TTS_STREAM_END = "on_tts_stream_end" CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" CONF_SILENCE_DETECTION = "silence_detection" @@ -56,6 +58,17 @@ IsRunningCondition = voice_assistant_ns.class_( "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) ) + +def tts_stream_validate(config): + if CONF_SPEAKER not in config and ( + CONF_ON_TTS_STREAM_START in config or CONF_ON_TTS_STREAM_END in config + ): + raise cv.Invalid( + f"{CONF_SPEAKER} is required when using {CONF_ON_TTS_STREAM_START} and/or {CONF_ON_TTS_STREAM_END}" + ) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -105,8 +118,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_TTS_STREAM_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), + tts_stream_validate, ) @@ -222,6 +242,20 @@ async def to_code(config): config[CONF_ON_STT_VAD_END], ) + if CONF_ON_TTS_STREAM_START in config: + await automation.build_automation( + var.get_tts_stream_start_trigger(), + [], + config[CONF_ON_TTS_STREAM_START], + ) + + if CONF_ON_TTS_STREAM_END in config: + await automation.build_automation( + var.get_tts_stream_end_trigger(), + [], + config[CONF_ON_TTS_STREAM_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7ebbe762b3..9b13a71039 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -632,11 +632,17 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER this->wait_for_stream_end_ = true; + ESP_LOGD(TAG, "TTS stream start"); + this->tts_stream_start_trigger_->trigger(); #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); +#ifdef USE_SPEAKER + ESP_LOGD(TAG, "TTS stream end"); + this->tts_stream_end_trigger_->trigger(); +#endif break; } case api::enums::VOICE_ASSISTANT_STT_VAD_START: diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a985bc4678..f6dcd1c563 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -107,6 +107,10 @@ class VoiceAssistant : public Component { Trigger<> *get_start_trigger() const { return this->start_trigger_; } Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } +#ifdef USE_SPEAKER + Trigger<> *get_tts_stream_start_trigger() const { return this->tts_stream_start_trigger_; } + Trigger<> *get_tts_stream_end_trigger() const { return this->tts_stream_end_trigger_; } +#endif Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } @@ -135,6 +139,10 @@ class VoiceAssistant : public Component { Trigger<> *start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); +#ifdef USE_SPEAKER + Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); + Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); +#endif Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); From 1c4b06700fe3b89bb2bc1f4bcbb6f9af655c286e Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 20 Nov 2023 16:59:38 -0700 Subject: [PATCH 036/436] include payload_open when a lock supports OPEN (#5809) --- esphome/components/mqtt/mqtt_lock.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 197d0c32d4..f4a5126d0c 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->lock_->traits.get_assumed_state()) root[MQTT_OPTIMISTIC] = true; + if (this->lock_->traits.get_supports_open()) + root[MQTT_PAYLOAD_OPEN] = "OPEN"; } bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } From d0ac202a3f81822c16aff0cef3a53a5938cd018a Mon Sep 17 00:00:00 2001 From: CVan Date: Mon, 20 Nov 2023 19:19:36 -0500 Subject: [PATCH 037/436] fix: compile errors with fonts (#5808) --- docker/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index a892e1df38..1bf754464d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -48,6 +48,8 @@ RUN \ libfreetype-dev=2.12.1+dfsg-5 \ libssl-dev=3.0.11-1~deb12u2 \ libffi-dev=3.4.4-1 \ + libopenjp2-7=2.5.0-2 \ + libtiff6=4.5.0-6 \ cargo=0.66.0+ds1-1 \ pkg-config=1.8.1-1 \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \ From 10ca05b686cf0ffa5d1483bf38aa5e95e2fde77d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:24:47 +1300 Subject: [PATCH 038/436] Early return when there are no wifi scan results (#5797) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 34ecaf887d..56cae1b1cc 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -674,6 +674,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } + if (it.number == 0) { + // no results + return; + } + uint16_t number = it.number; std::vector records(number); err = esp_wifi_scan_get_ap_records(&number, records.data()); From b421fccc08c5824df5cc4fed9b3d8602344e4ffa Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 Nov 2023 15:09:14 -0600 Subject: [PATCH 039/436] Add some additional VA triggers, part 2 (#5811) --- .../components/voice_assistant/__init__.py | 34 +++++++++++++++++++ .../voice_assistant/voice_assistant.cpp | 6 ++++ .../voice_assistant/voice_assistant.h | 8 +++++ 3 files changed, 48 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 5715604605..d05f39072c 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -29,6 +29,8 @@ CONF_ON_STT_VAD_END = "on_stt_vad_end" CONF_ON_STT_VAD_START = "on_stt_vad_start" CONF_ON_TTS_END = "on_tts_end" CONF_ON_TTS_START = "on_tts_start" +CONF_ON_TTS_STREAM_START = "on_tts_stream_start" +CONF_ON_TTS_STREAM_END = "on_tts_stream_end" CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" CONF_SILENCE_DETECTION = "silence_detection" @@ -56,6 +58,17 @@ IsRunningCondition = voice_assistant_ns.class_( "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) ) + +def tts_stream_validate(config): + if CONF_SPEAKER not in config and ( + CONF_ON_TTS_STREAM_START in config or CONF_ON_TTS_STREAM_END in config + ): + raise cv.Invalid( + f"{CONF_SPEAKER} is required when using {CONF_ON_TTS_STREAM_START} and/or {CONF_ON_TTS_STREAM_END}" + ) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -105,8 +118,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_TTS_STREAM_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), + tts_stream_validate, ) @@ -222,6 +242,20 @@ async def to_code(config): config[CONF_ON_STT_VAD_END], ) + if CONF_ON_TTS_STREAM_START in config: + await automation.build_automation( + var.get_tts_stream_start_trigger(), + [], + config[CONF_ON_TTS_STREAM_START], + ) + + if CONF_ON_TTS_STREAM_END in config: + await automation.build_automation( + var.get_tts_stream_end_trigger(), + [], + config[CONF_ON_TTS_STREAM_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7ebbe762b3..9b13a71039 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -632,11 +632,17 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER this->wait_for_stream_end_ = true; + ESP_LOGD(TAG, "TTS stream start"); + this->tts_stream_start_trigger_->trigger(); #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); +#ifdef USE_SPEAKER + ESP_LOGD(TAG, "TTS stream end"); + this->tts_stream_end_trigger_->trigger(); +#endif break; } case api::enums::VOICE_ASSISTANT_STT_VAD_START: diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a985bc4678..f6dcd1c563 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -107,6 +107,10 @@ class VoiceAssistant : public Component { Trigger<> *get_start_trigger() const { return this->start_trigger_; } Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } +#ifdef USE_SPEAKER + Trigger<> *get_tts_stream_start_trigger() const { return this->tts_stream_start_trigger_; } + Trigger<> *get_tts_stream_end_trigger() const { return this->tts_stream_end_trigger_; } +#endif Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } @@ -135,6 +139,10 @@ class VoiceAssistant : public Component { Trigger<> *start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); +#ifdef USE_SPEAKER + Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); + Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); +#endif Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); From 3f40e32eba514e3561c092c49d16d3032fd84305 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:08:47 +1300 Subject: [PATCH 040/436] Bump version to 2023.11.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0abb6a8e2d..5f6e9f5eb1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.2" +__version__ = "2023.11.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 8738cef5a38a6212b20418e17200a6b9475cda98 Mon Sep 17 00:00:00 2001 From: matt7aylor Date: Wed, 22 Nov 2023 19:48:38 +0000 Subject: [PATCH 041/436] sen5x fix temperature compensation and gas tuning (#4901) --- esphome/components/sen5x/sen5x.cpp | 12 ++++++--- esphome/components/sen5x/sen5x.h | 40 +++++++++++++++++------------- esphome/components/sen5x/sensor.py | 13 +++++++++- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 42951d6089..c90880bc9f 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -201,13 +201,19 @@ void SEN5XComponent::setup() { ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); } } - if (this->voc_tuning_params_.has_value()) + if (this->voc_tuning_params_.has_value()) { this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); - if (this->nox_tuning_params_.has_value()) + delay(20); + } + if (this->nox_tuning_params_.has_value()) { this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); + delay(20); + } - if (this->temperature_compensation_.has_value()) + if (this->temperature_compensation_.has_value()) { this->write_temperature_compensation_(this->temperature_compensation_.value()); + delay(20); + } // Finally start sensor measurements auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index f306003a82..6d90636a89 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -41,8 +41,8 @@ struct GasTuning { }; struct TemperatureCompensation { - uint16_t offset; - uint16_t normalized_offset_slope; + int16_t offset; + int16_t normalized_offset_slope; uint16_t time_constant; }; @@ -70,27 +70,33 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t std_initial, uint16_t gain_factor) { - voc_tuning_params_.value().index_offset = index_offset; - voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - voc_tuning_params_.value().std_initial = std_initial; - voc_tuning_params_.value().gain_factor = gain_factor; + GasTuning tuning_params; + tuning_params.index_offset = index_offset; + tuning_params.learning_time_offset_hours = learning_time_offset_hours; + tuning_params.learning_time_gain_hours = learning_time_gain_hours; + tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; + tuning_params.std_initial = std_initial; + tuning_params.gain_factor = gain_factor; + voc_tuning_params_ = tuning_params; } void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t gain_factor) { - nox_tuning_params_.value().index_offset = index_offset; - nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - nox_tuning_params_.value().std_initial = 50; - nox_tuning_params_.value().gain_factor = gain_factor; + GasTuning tuning_params; + tuning_params.index_offset = index_offset; + tuning_params.learning_time_offset_hours = learning_time_offset_hours; + tuning_params.learning_time_gain_hours = learning_time_gain_hours; + tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; + tuning_params.std_initial = 50; + tuning_params.gain_factor = gain_factor; + nox_tuning_params_ = tuning_params; } void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { - temperature_compensation_.value().offset = offset * 200; - temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100; - temperature_compensation_.value().time_constant = time_constant; + TemperatureCompensation temp_comp; + temp_comp.offset = offset * 200; + temp_comp.normalized_offset_slope = normalized_offset_slope * 10000; + temp_comp.time_constant = time_constant; + temperature_compensation_ = temp_comp; } bool start_fan_cleaning(); diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 392510e417..4bc4a138a3 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -88,6 +88,15 @@ GAS_SENSOR = cv.Schema( } ) + +def float_previously_pct(value): + if isinstance(value, str) and "%" in value: + raise cv.Invalid( + f"The value '{value}' is a percentage. Suggested value: {float(value.strip('%')) / 100}" + ) + return value + + CONFIG_SCHEMA = ( cv.Schema( { @@ -151,7 +160,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema( { cv.Optional(CONF_OFFSET, default=0): cv.float_, - cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage, + cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.All( + float_previously_pct, cv.float_ + ), cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, } ), From 3ac59180ab5bb3e1730e44017297666e61e03ee3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:31:23 +1100 Subject: [PATCH 042/436] Add startup_delay to interval. (#5327) --- esphome/components/interval/__init__.py | 6 +++++- esphome/components/interval/interval.h | 20 +++++++++++++++++++- tests/test2.yaml | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index 4514f80ba3..db3232c4b0 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_INTERVAL +from esphome.const import CONF_ID, CONF_INTERVAL, CONF_STARTUP_DELAY CODEOWNERS = ["@esphome/core"] interval_ns = cg.esphome_ns.namespace("interval") @@ -13,6 +13,9 @@ CONFIG_SCHEMA = automation.validate_automation( cv.Schema( { cv.GenerateID(): cv.declare_id(IntervalTrigger), + cv.Optional( + CONF_STARTUP_DELAY, default="0s" + ): cv.positive_time_period_milliseconds, cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) @@ -26,3 +29,4 @@ async def to_code(config): await automation.build_automation(var, [], conf) cg.add(var.set_update_interval(conf[CONF_INTERVAL])) + cg.add(var.set_startup_delay(conf[CONF_STARTUP_DELAY])) diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 605ac868f3..5b8bc3081f 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -8,8 +8,26 @@ namespace interval { class IntervalTrigger : public Trigger<>, public PollingComponent { public: - void update() override { this->trigger(); } + void update() override { + if (this->started_) + this->trigger(); + } + + void setup() override { + if (this->startup_delay_ == 0) { + this->started_ = true; + } else { + this->set_timeout(this->startup_delay_, [this] { this->started_ = true; }); + } + } + + void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; } + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + uint32_t startup_delay_{0}; + bool started_{false}; }; } // namespace interval diff --git a/tests/test2.yaml b/tests/test2.yaml index bfc886eaa4..aaf14eaa57 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -718,6 +718,7 @@ stepper: interval: interval: 5s + startup_delay: 10s then: - logger.log: Interval Run From 49c09afb8755f62c96f11368027f9ba46017ffda Mon Sep 17 00:00:00 2001 From: Landon Rohatensky Date: Thu, 23 Nov 2023 11:10:33 -0800 Subject: [PATCH 043/436] Allow images to be downloaded from URLs (#5214) Co-authored-by: guillempages Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 1 + esphome/components/image/__init__.py | 121 +++++++++++++++++++++------ esphome/external_files.py | 75 +++++++++++++++++ requirements.txt | 1 + tests/test2.yaml | 12 +++ 5 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 esphome/external_files.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bf754464d..ee7c70bb0f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,7 @@ RUN \ openssh-client=1:9.2p1-2+deb12u1 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ + libmagic1=1:5.44-3 \ patch=2.7.6-7; \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ apt-get install -y --no-install-recommends \ diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 1b7c654b0b..c11021fc9c 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,15 +1,23 @@ +from __future__ import annotations + import logging +import hashlib import io from pathlib import Path import re import requests +from magic import Magic + +from PIL import Image from esphome import core from esphome.components import font +from esphome import external_files import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( + __version__, CONF_DITHER, CONF_FILE, CONF_ICON, @@ -19,6 +27,7 @@ from esphome.const import ( CONF_RESIZE, CONF_SOURCE, CONF_TYPE, + CONF_URL, ) from esphome.core import CORE, HexInt @@ -43,34 +52,74 @@ IMAGE_TYPE = { CONF_USE_TRANSPARENCY = "use_transparency" # If the MDI file cannot be downloaded within this time, abort. -MDI_DOWNLOAD_TIMEOUT = 30 # seconds +IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" +SOURCE_WEB = "web" + Image_ = image_ns.class_("Image") -def _compute_local_icon_path(value) -> Path: - base_dir = Path(CORE.data_dir) / DOMAIN / "mdi" +def _compute_local_icon_path(value: dict) -> Path: + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" return base_dir / f"{value[CONF_ICON]}.svg" -def download_mdi(value): - mdi_id = value[CONF_ICON] - path = _compute_local_icon_path(value) - if path.is_file(): - return value - url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" - _LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) +def _compute_local_image_path(value: dict) -> Path: + url = value[CONF_URL] + 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 download_content(url: str, path: Path) -> None: + if not external_files.has_remote_file_changed(url, path): + _LOGGER.debug("Remote file has not changed %s", url) + return + + _LOGGER.debug( + "Remote file has changed, downloading from %s to %s", + url, + path, + ) + try: - req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) + req = requests.get( + url, + timeout=IMAGE_DOWNLOAD_TIMEOUT, + headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, + ) req.raise_for_status() except requests.exceptions.RequestException as e: - raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") + raise cv.Invalid(f"Could not download from {url}: {e}") path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(req.content) + + +def download_mdi(value): + validate_cairosvg_installed(value) + + mdi_id = value[CONF_ICON] + path = _compute_local_icon_path(value) + + url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" + + download_content(url, path) + + return value + + +def download_image(value): + url = value[CONF_URL] + path = _compute_local_image_path(value) + + download_content(url, path) + return value @@ -139,6 +188,13 @@ def validate_file_shorthand(value): CONF_ICON: icon, } ) + if value.startswith("http://") or value.startswith("https://"): + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_WEB, + CONF_URL: value, + } + ) return FILE_SCHEMA( { CONF_SOURCE: SOURCE_LOCAL, @@ -160,10 +216,18 @@ MDI_SCHEMA = cv.All( download_mdi, ) +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.string, + }, + download_image, +) + TYPED_FILE_SCHEMA = cv.typed_schema( { SOURCE_LOCAL: LOCAL_SCHEMA, SOURCE_MDI: MDI_SCHEMA, + SOURCE_WEB: WEB_SCHEMA, }, key=CONF_SOURCE, ) @@ -201,9 +265,7 @@ IMAGE_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) -def load_svg_image(file: str, resize: tuple[int, int]): - from PIL import Image - +def load_svg_image(file: bytes, resize: tuple[int, int]): # This import is only needed in case of SVG images; adding it # to the top would force configurations not using SVG to also have it # installed for no reason. @@ -212,19 +274,17 @@ def load_svg_image(file: str, resize: tuple[int, int]): if resize: req_width, req_height = resize svg_image = svg2png( - url=file, + file, output_width=req_width, output_height=req_height, ) else: - svg_image = svg2png(url=file) + svg_image = svg2png(file) return Image.open(io.BytesIO(svg_image)) async def to_code(config): - from PIL import Image - conf_file = config[CONF_FILE] if conf_file[CONF_SOURCE] == SOURCE_LOCAL: @@ -233,17 +293,26 @@ async def to_code(config): elif conf_file[CONF_SOURCE] == SOURCE_MDI: path = _compute_local_icon_path(conf_file).as_posix() + elif conf_file[CONF_SOURCE] == SOURCE_WEB: + path = _compute_local_image_path(conf_file).as_posix() + try: - resize = config.get(CONF_RESIZE) - if path.lower().endswith(".svg"): - image = load_svg_image(path, resize) - else: - image = Image.open(path) - if resize: - image.thumbnail(resize) + with open(path, "rb") as f: + file_contents = f.read() except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") + mime = Magic(mime=True) + file_type = mime.from_buffer(file_contents) + + resize = config.get(CONF_RESIZE) + if "svg" in file_type: + image = load_svg_image(file_contents, resize) + else: + image = Image.open(io.BytesIO(file_contents)) + if resize: + image.thumbnail(resize) + width, height = image.size if CONF_RESIZE not in config and (width > 500 or height > 500): diff --git a/esphome/external_files.py b/esphome/external_files.py new file mode 100644 index 0000000000..5b476286f3 --- /dev/null +++ b/esphome/external_files.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import logging +from pathlib import Path +import os +from datetime import datetime +import requests +import esphome.config_validation as cv +from esphome.core import CORE, TimePeriodSeconds + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@landonr"] + +NETWORK_TIMEOUT = 30 + +IF_MODIFIED_SINCE = "If-Modified-Since" +CACHE_CONTROL = "Cache-Control" +CACHE_CONTROL_MAX_AGE = "max-age=" +CONTENT_DISPOSITION = "content-disposition" +TEMP_DIR = "temp" + + +def has_remote_file_changed(url, local_file_path): + if os.path.exists(local_file_path): + _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) + try: + local_modification_time = os.path.getmtime(local_file_path) + local_modification_time_str = datetime.utcfromtimestamp( + local_modification_time + ).strftime("%a, %d %b %Y %H:%M:%S GMT") + + headers = { + IF_MODIFIED_SINCE: local_modification_time_str, + CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", + } + response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT) + + _LOGGER.debug( + "has_remote_file_changed: File %s, Local modified %s, response code %d", + local_file_path, + local_modification_time_str, + response.status_code, + ) + + if response.status_code == 304: + _LOGGER.debug( + "has_remote_file_changed: File not modified since %s", + local_modification_time_str, + ) + return False + _LOGGER.debug("has_remote_file_changed: File modified") + return True + except requests.exceptions.RequestException as e: + raise cv.Invalid( + f"Could not check if {url} has changed, please check if file exists " + f"({e})" + ) + + _LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path) + return True + + +def is_file_recent(file_path: str, refresh: TimePeriodSeconds) -> bool: + if os.path.exists(file_path): + creation_time = os.path.getctime(file_path) + current_time = datetime.now().timestamp() + return current_time - creation_time <= refresh.total_seconds + return False + + +def compute_local_file_dir(domain: str) -> Path: + base_directory = Path(CORE.data_dir) / domain + base_directory.mkdir(parents=True, exist_ok=True) + + return base_directory diff --git a/requirements.txt b/requirements.txt index 4203ce6304..abbdcf66d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.5.5 zeroconf==0.127.0 +python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/tests/test2.yaml b/tests/test2.yaml index aaf14eaa57..b258b103d3 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -753,6 +753,18 @@ image: file: pnglogo.png type: RGB565 use_transparency: no + - id: web_svg_image + file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + resize: 256x48 + type: TRANSPARENT_BINARY + - id: web_tiff_image + file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + type: RGB24 + resize: 48x48 + - id: web_redirect_image + file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + type: RGB24 + resize: 48x48 - id: mdi_alert file: mdi:alert-circle-outline From 2076db1ccd6a258b24aab00d54cc743575aaca24 Mon Sep 17 00:00:00 2001 From: Ilia Sotnikov Date: Thu, 23 Nov 2023 21:15:58 +0200 Subject: [PATCH 044/436] Pillow: bump to 10.1.0 (#5815) --- esphome/components/font/__init__.py | 8 ++++---- requirements_optional.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 7e34dff22d..22a5f6b2c5 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -67,13 +67,13 @@ def validate_pillow_installed(value): except ImportError as err: raise cv.Invalid( "Please install the pillow python package to use this feature. " - '(pip install "pillow==10.0.1")' + '(pip install "pillow==10.1.0")' ) from err - if version.parse(PIL.__version__) != version.parse("10.0.1"): + if version.parse(PIL.__version__) != version.parse("10.1.0"): raise cv.Invalid( - "Please update your pillow installation to 10.0.1. " - '(pip install "pillow==10.0.1")' + "Please update your pillow installation to 10.1.0. " + '(pip install "pillow==10.1.0")' ) return value diff --git a/requirements_optional.txt b/requirements_optional.txt index 40c27f8547..bc4ea08c92 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,3 +1,3 @@ -pillow==10.0.1 +pillow==10.1.0 cairosvg==2.7.1 cryptography==41.0.4 From 1204b4f1bd96b636086d7118e7f79550d86c37e0 Mon Sep 17 00:00:00 2001 From: Landon Rohatensky Date: Thu, 23 Nov 2023 11:10:33 -0800 Subject: [PATCH 045/436] Allow images to be downloaded from URLs (#5214) Co-authored-by: guillempages Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 1 + esphome/components/image/__init__.py | 121 +++++++++++++++++++++------ esphome/external_files.py | 75 +++++++++++++++++ requirements.txt | 1 + tests/test2.yaml | 12 +++ 5 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 esphome/external_files.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bf754464d..ee7c70bb0f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,7 @@ RUN \ openssh-client=1:9.2p1-2+deb12u1 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ + libmagic1=1:5.44-3 \ patch=2.7.6-7; \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ apt-get install -y --no-install-recommends \ diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 1b7c654b0b..c11021fc9c 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,15 +1,23 @@ +from __future__ import annotations + import logging +import hashlib import io from pathlib import Path import re import requests +from magic import Magic + +from PIL import Image from esphome import core from esphome.components import font +from esphome import external_files import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( + __version__, CONF_DITHER, CONF_FILE, CONF_ICON, @@ -19,6 +27,7 @@ from esphome.const import ( CONF_RESIZE, CONF_SOURCE, CONF_TYPE, + CONF_URL, ) from esphome.core import CORE, HexInt @@ -43,34 +52,74 @@ IMAGE_TYPE = { CONF_USE_TRANSPARENCY = "use_transparency" # If the MDI file cannot be downloaded within this time, abort. -MDI_DOWNLOAD_TIMEOUT = 30 # seconds +IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" +SOURCE_WEB = "web" + Image_ = image_ns.class_("Image") -def _compute_local_icon_path(value) -> Path: - base_dir = Path(CORE.data_dir) / DOMAIN / "mdi" +def _compute_local_icon_path(value: dict) -> Path: + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" return base_dir / f"{value[CONF_ICON]}.svg" -def download_mdi(value): - mdi_id = value[CONF_ICON] - path = _compute_local_icon_path(value) - if path.is_file(): - return value - url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" - _LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) +def _compute_local_image_path(value: dict) -> Path: + url = value[CONF_URL] + 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 download_content(url: str, path: Path) -> None: + if not external_files.has_remote_file_changed(url, path): + _LOGGER.debug("Remote file has not changed %s", url) + return + + _LOGGER.debug( + "Remote file has changed, downloading from %s to %s", + url, + path, + ) + try: - req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) + req = requests.get( + url, + timeout=IMAGE_DOWNLOAD_TIMEOUT, + headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, + ) req.raise_for_status() except requests.exceptions.RequestException as e: - raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") + raise cv.Invalid(f"Could not download from {url}: {e}") path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(req.content) + + +def download_mdi(value): + validate_cairosvg_installed(value) + + mdi_id = value[CONF_ICON] + path = _compute_local_icon_path(value) + + url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" + + download_content(url, path) + + return value + + +def download_image(value): + url = value[CONF_URL] + path = _compute_local_image_path(value) + + download_content(url, path) + return value @@ -139,6 +188,13 @@ def validate_file_shorthand(value): CONF_ICON: icon, } ) + if value.startswith("http://") or value.startswith("https://"): + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_WEB, + CONF_URL: value, + } + ) return FILE_SCHEMA( { CONF_SOURCE: SOURCE_LOCAL, @@ -160,10 +216,18 @@ MDI_SCHEMA = cv.All( download_mdi, ) +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.string, + }, + download_image, +) + TYPED_FILE_SCHEMA = cv.typed_schema( { SOURCE_LOCAL: LOCAL_SCHEMA, SOURCE_MDI: MDI_SCHEMA, + SOURCE_WEB: WEB_SCHEMA, }, key=CONF_SOURCE, ) @@ -201,9 +265,7 @@ IMAGE_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) -def load_svg_image(file: str, resize: tuple[int, int]): - from PIL import Image - +def load_svg_image(file: bytes, resize: tuple[int, int]): # This import is only needed in case of SVG images; adding it # to the top would force configurations not using SVG to also have it # installed for no reason. @@ -212,19 +274,17 @@ def load_svg_image(file: str, resize: tuple[int, int]): if resize: req_width, req_height = resize svg_image = svg2png( - url=file, + file, output_width=req_width, output_height=req_height, ) else: - svg_image = svg2png(url=file) + svg_image = svg2png(file) return Image.open(io.BytesIO(svg_image)) async def to_code(config): - from PIL import Image - conf_file = config[CONF_FILE] if conf_file[CONF_SOURCE] == SOURCE_LOCAL: @@ -233,17 +293,26 @@ async def to_code(config): elif conf_file[CONF_SOURCE] == SOURCE_MDI: path = _compute_local_icon_path(conf_file).as_posix() + elif conf_file[CONF_SOURCE] == SOURCE_WEB: + path = _compute_local_image_path(conf_file).as_posix() + try: - resize = config.get(CONF_RESIZE) - if path.lower().endswith(".svg"): - image = load_svg_image(path, resize) - else: - image = Image.open(path) - if resize: - image.thumbnail(resize) + with open(path, "rb") as f: + file_contents = f.read() except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") + mime = Magic(mime=True) + file_type = mime.from_buffer(file_contents) + + resize = config.get(CONF_RESIZE) + if "svg" in file_type: + image = load_svg_image(file_contents, resize) + else: + image = Image.open(io.BytesIO(file_contents)) + if resize: + image.thumbnail(resize) + width, height = image.size if CONF_RESIZE not in config and (width > 500 or height > 500): diff --git a/esphome/external_files.py b/esphome/external_files.py new file mode 100644 index 0000000000..5b476286f3 --- /dev/null +++ b/esphome/external_files.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import logging +from pathlib import Path +import os +from datetime import datetime +import requests +import esphome.config_validation as cv +from esphome.core import CORE, TimePeriodSeconds + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@landonr"] + +NETWORK_TIMEOUT = 30 + +IF_MODIFIED_SINCE = "If-Modified-Since" +CACHE_CONTROL = "Cache-Control" +CACHE_CONTROL_MAX_AGE = "max-age=" +CONTENT_DISPOSITION = "content-disposition" +TEMP_DIR = "temp" + + +def has_remote_file_changed(url, local_file_path): + if os.path.exists(local_file_path): + _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) + try: + local_modification_time = os.path.getmtime(local_file_path) + local_modification_time_str = datetime.utcfromtimestamp( + local_modification_time + ).strftime("%a, %d %b %Y %H:%M:%S GMT") + + headers = { + IF_MODIFIED_SINCE: local_modification_time_str, + CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", + } + response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT) + + _LOGGER.debug( + "has_remote_file_changed: File %s, Local modified %s, response code %d", + local_file_path, + local_modification_time_str, + response.status_code, + ) + + if response.status_code == 304: + _LOGGER.debug( + "has_remote_file_changed: File not modified since %s", + local_modification_time_str, + ) + return False + _LOGGER.debug("has_remote_file_changed: File modified") + return True + except requests.exceptions.RequestException as e: + raise cv.Invalid( + f"Could not check if {url} has changed, please check if file exists " + f"({e})" + ) + + _LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path) + return True + + +def is_file_recent(file_path: str, refresh: TimePeriodSeconds) -> bool: + if os.path.exists(file_path): + creation_time = os.path.getctime(file_path) + current_time = datetime.now().timestamp() + return current_time - creation_time <= refresh.total_seconds + return False + + +def compute_local_file_dir(domain: str) -> Path: + base_directory = Path(CORE.data_dir) / domain + base_directory.mkdir(parents=True, exist_ok=True) + + return base_directory diff --git a/requirements.txt b/requirements.txt index 7a3bb421ed..2c2bf1ba19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.5.2 zeroconf==0.123.0 +python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/tests/test2.yaml b/tests/test2.yaml index bfc886eaa4..91fb554146 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -752,6 +752,18 @@ image: file: pnglogo.png type: RGB565 use_transparency: no + - id: web_svg_image + file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + resize: 256x48 + type: TRANSPARENT_BINARY + - id: web_tiff_image + file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + type: RGB24 + resize: 48x48 + - id: web_redirect_image + file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + type: RGB24 + resize: 48x48 - id: mdi_alert file: mdi:alert-circle-outline From 711faab329d5ff22bb0264def03461c3f1d77ca7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 24 Nov 2023 10:24:25 +1300 Subject: [PATCH 046/436] Bump version to 2023.11.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5f6e9f5eb1..5596cbcd36 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.3" +__version__ = "2023.11.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 1762204b0039386a561a489c37138e6197ffdd32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Nov 2023 23:49:55 +0100 Subject: [PATCH 047/436] dashboard: set nodelay on the websocket to avoid a delay seeing log messages (#5802) --- esphome/dashboard/web_server.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 7c5f653b5b..9a9ccb462b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -27,6 +27,7 @@ import tornado.process import tornado.queues import tornado.web import tornado.websocket +import tornado.httputil import yaml from tornado.log import access_log @@ -136,7 +137,15 @@ def websocket_method(name): @websocket_class class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): - def __init__(self, application, request, **kwargs): + """Base class for ESPHome websocket commands.""" + + def __init__( + self, + application: tornado.web.Application, + request: tornado.httputil.HTTPServerRequest, + **kwargs: Any, + ) -> None: + """Initialize the websocket.""" super().__init__(application, request, **kwargs) self._proc = None self._queue = None @@ -145,6 +154,12 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # use Popen() with a reading thread instead self._use_popen = os.name == "nt" + def open(self, *args: str, **kwargs: str) -> None: + """Handle new WebSocket connection.""" + # Ensure messages from the subprocess are sent immediately + # to avoid a 200-500ms delay when nodelay is not set. + self.set_nodelay(True) + @authenticated async def on_message( # pylint: disable=invalid-overridden-method self, message: str From 9f8a896e13136809ae0f7e585246db4be06d3d1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:02:44 -0600 Subject: [PATCH 048/436] Bump aioesphomeapi from 18.5.5 to 18.5.7 (#5822) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index abbdcf66d5..af53dc8e94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.5 +aioesphomeapi==18.5.7 zeroconf==0.127.0 python-magic==0.4.27 From 5c31bec8c20626c0fb2558f08145ca1ab46e85be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Nov 2023 00:29:08 +0100 Subject: [PATCH 049/436] Ensure names containing characters other than `a-z` `A-Z` `0-9` or `_` are unique (#5810) --- esphome/core/helpers.cpp | 11 +++++++---- esphome/helpers.py | 5 ++++- tests/unit_tests/test_helpers.py | 6 +++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 714a1642f8..c95c0470de 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -278,10 +278,13 @@ std::string str_snake_case(const std::string &str) { return result; } std::string str_sanitize(const std::string &str) { - std::string out; - std::copy_if(str.begin(), str.end(), std::back_inserter(out), [](const char &c) { - return c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); - }); + std::string out = str; + std::replace_if( + out.begin(), out.end(), + [](const char &c) { + return !(c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); + }, + '_'); return out; } std::string str_snprintf(const char *fmt, size_t len, ...) { diff --git a/esphome/helpers.py b/esphome/helpers.py index 4012b2067f..00416b591f 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -357,6 +357,9 @@ def snake_case(value): return value.replace(" ", "_").lower() +_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9_]") + + def sanitize(value): """Same behaviour as `helpers.cpp` method `str_sanitize`.""" - return re.sub("[^-_0-9a-zA-Z]", r"", value) + return _DISALLOWED_CHARS.sub("_", value) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 67fabd7af8..79d39901f0 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -258,9 +258,9 @@ def test_snake_case(text, expected): "text, expected", ( ("foo_bar", "foo_bar"), - ('!"§$%&/()=?foo_bar', "foo_bar"), - ('foo_!"§$%&/()=?bar', "foo_bar"), - ('foo_bar!"§$%&/()=?', "foo_bar"), + ('!"§$%&/()=?foo_bar', "___________foo_bar"), + ('foo_!"§$%&/()=?bar', "foo____________bar"), + ('foo_bar!"§$%&/()=?', "foo_bar___________"), ), ) def test_sanitize(text, expected): From f456603c1b6e3428aa7204e7a58ca068b3a72353 Mon Sep 17 00:00:00 2001 From: Vincent Schmandt Date: Fri, 24 Nov 2023 05:31:07 +0100 Subject: [PATCH 050/436] Add ENS160 Sensor (#4243) Co-authored-by: Keith Burzinski Co-authored-by: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ens160/__init__.py | 1 + esphome/components/ens160/ens160.cpp | 321 ++++++++++++++++++++++++++ esphome/components/ens160/ens160.h | 60 +++++ esphome/components/ens160/sensor.py | 88 +++++++ tests/test3.1.yaml | 11 +- tests/test3.yaml | 1 - 7 files changed, 478 insertions(+), 5 deletions(-) create mode 100644 esphome/components/ens160/__init__.py create mode 100644 esphome/components/ens160/ens160.cpp create mode 100644 esphome/components/ens160/ens160.h create mode 100644 esphome/components/ens160/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index dd1586d039..af23f679c8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -90,6 +90,7 @@ esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/* @jesserockz esphome/components/emc2101/* @ellull +esphome/components/ens160/* @vincentscode esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @Rapsssito @jesserockz diff --git a/esphome/components/ens160/__init__.py b/esphome/components/ens160/__init__.py new file mode 100644 index 0000000000..d26770a89d --- /dev/null +++ b/esphome/components/ens160/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@vincentscode"] diff --git a/esphome/components/ens160/ens160.cpp b/esphome/components/ens160/ens160.cpp new file mode 100644 index 0000000000..c7a6ccbb73 --- /dev/null +++ b/esphome/components/ens160/ens160.cpp @@ -0,0 +1,321 @@ +// ENS160 sensor with I2C interface from ScioSense +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-7-ENS160-Datasheet.pdf +// +// Implementation based on: +// https://github.com/sciosense/ENS160_driver + +#include "ens160.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ens160 { + +static const char *const TAG = "ens160"; + +static const uint8_t ENS160_BOOTING = 10; + +static const uint16_t ENS160_PART_ID = 0x0160; + +static const uint8_t ENS160_REG_PART_ID = 0x00; +static const uint8_t ENS160_REG_OPMODE = 0x10; +static const uint8_t ENS160_REG_CONFIG = 0x11; +static const uint8_t ENS160_REG_COMMAND = 0x12; +static const uint8_t ENS160_REG_TEMP_IN = 0x13; +static const uint8_t ENS160_REG_DATA_STATUS = 0x20; +static const uint8_t ENS160_REG_DATA_AQI = 0x21; +static const uint8_t ENS160_REG_DATA_TVOC = 0x22; +static const uint8_t ENS160_REG_DATA_ECO2 = 0x24; + +static const uint8_t ENS160_REG_GPR_READ_0 = 0x48; +static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4; + +static const uint8_t ENS160_COMMAND_NOP = 0x00; +static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC; +static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E; + +static const uint8_t ENS160_OPMODE_RESET = 0xF0; +static const uint8_t ENS160_OPMODE_IDLE = 0x01; +static const uint8_t ENS160_OPMODE_STD = 0x02; + +static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80; +static const uint8_t ENS160_DATA_STATUS_STATER = 0x40; +static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C; +static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02; +static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01; + +// helps remove reserved bits in aqi data register +static const uint8_t ENS160_DATA_AQI = 0x07; + +void ENS160Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ENS160..."); + + // check part_id + uint16_t part_id; + if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast(&part_id), 2)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + if (part_id != ENS160_PART_ID) { + this->error_code_ = INVALID_ID; + this->mark_failed(); + return; + } + + // set mode to reset + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + delay(ENS160_BOOTING); + + // check status + uint8_t status_value; + if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + this->validity_flag_ = static_cast((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + + if (this->validity_flag_ == INVALID_OUTPUT) { + this->error_code_ = VALIDITY_INVALID; + this->mark_failed(); + return; + } + + // set mode to idle + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + // clear command + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + + // read firmware version + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + uint8_t version_data[3]; + if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + this->firmware_ver_major_ = version_data[0]; + this->firmware_ver_minor_ = version_data[1]; + this->firmware_ver_build_ = version_data[2]; + + // set mode to standard + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + + // read opmode and check standard mode is achieved before finishing Setup + uint8_t op_mode; + if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + + if (op_mode != ENS160_OPMODE_STD) { + this->error_code_ = STD_OPMODE_FAILED; + this->mark_failed(); + return; + } +} + +void ENS160Component::update() { + uint8_t status_value, data_ready; + + if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { + ESP_LOGW(TAG, "Error reading status register"); + this->status_set_warning(); + return; + } + + // verbose status logging + ESP_LOGV(TAG, "Status: ENS160 STATAS bit 0x%x", + (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS); + ESP_LOGV(TAG, "Status: ENS160 STATER bit 0x%x", + (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER); + ESP_LOGV(TAG, "Status: ENS160 VALIDITY FLAG 0x%02x", (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + ESP_LOGV(TAG, "Status: ENS160 NEWDAT bit 0x%x", + (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT); + ESP_LOGV(TAG, "Status: ENS160 NEWGPR bit 0x%x", + (ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR); + + data_ready = ENS160_DATA_STATUS_NEWDAT & status_value; + this->validity_flag_ = static_cast((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + + switch (validity_flag_) { + case NORMAL_OPERATION: + if (data_ready != ENS160_DATA_STATUS_NEWDAT) { + ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready"); + return; + } + break; + case INITIAL_STARTUP: + if (!this->initial_startup_) { + this->initial_startup_ = true; + ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on"); + } + return; + case WARMING_UP: + if (!this->warming_up_) { + this->warming_up_ = true; + ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes"); + this->send_env_data_(); + } + return; + case INVALID_OUTPUT: + ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output"); + this->status_set_warning(); + return; + } + + // read new data + uint16_t data_eco2; + if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast(&data_eco2), 2)) { + ESP_LOGW(TAG, "Error reading eCO2 data register"); + this->status_set_warning(); + return; + } + if (this->co2_ != nullptr) { + this->co2_->publish_state(data_eco2); + } + + uint16_t data_tvoc; + if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast(&data_tvoc), 2)) { + ESP_LOGW(TAG, "Error reading TVOC data register"); + this->status_set_warning(); + return; + } + if (this->tvoc_ != nullptr) { + this->tvoc_->publish_state(data_tvoc); + } + + uint8_t data_aqi; + if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) { + ESP_LOGW(TAG, "Error reading AQI data register"); + this->status_set_warning(); + return; + } + if (this->aqi_ != nullptr) { + // remove reserved bits, just in case they are used in future + data_aqi = ENS160_DATA_AQI & data_aqi; + + this->aqi_->publish_state(data_aqi); + } + + this->status_clear_warning(); + + // set temperature and humidity compensation data + this->send_env_data_(); +} + +void ENS160Component::send_env_data_() { + if (this->temperature_ == nullptr && this->humidity_ == nullptr) + return; + + float temperature = NAN; + if (this->temperature_ != nullptr) + temperature = this->temperature_->state; + + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated"); + return; + } else { + ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature); + } + + float humidity = NAN; + if (this->humidity_ != nullptr) + humidity = this->humidity_->state; + + if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated"); + return; + } else { + ESP_LOGV(TAG, "External humidity compensation: %.1f%%", humidity); + } + + uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f); + uint16_t h = (uint16_t) (humidity * 512.0f); + + uint8_t data[4]; + data[0] = t & 0xff; + data[1] = (t >> 8) & 0xff; + data[2] = h & 0xff; + data[3] = (h >> 8) & 0xff; + + if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) { + ESP_LOGE(TAG, "Error writing compensation values"); + this->status_set_warning(); + return; + } +} + +void ENS160Component::dump_config() { + ESP_LOGCONFIG(TAG, "ENS160:"); + + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication failed! Is the sensor connected?"); + break; + case READ_FAILED: + ESP_LOGE(TAG, "Error reading from register"); + break; + case WRITE_FAILED: + ESP_LOGE(TAG, "Error writing to register"); + break; + case INVALID_ID: + ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?"); + break; + case VALIDITY_INVALID: + ESP_LOGE(TAG, "Invalid Device Status - No valid output"); + break; + case STD_OPMODE_FAILED: + ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode"); + break; + case NONE: + ESP_LOGD(TAG, "Setup successful"); + break; + } + ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_, + this->firmware_ver_build_); + + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "CO2 Sensor:", this->co2_); + LOG_SENSOR(" ", "TVOC Sensor:", this->tvoc_); + LOG_SENSOR(" ", "AQI Sensor:", this->aqi_); + + if (this->temperature_ != nullptr && this->humidity_ != nullptr) { + LOG_SENSOR(" ", " Temperature Compensation:", this->temperature_); + LOG_SENSOR(" ", " Humidity Compensation:", this->humidity_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: Not configured"); + } +} + +} // namespace ens160 +} // namespace esphome diff --git a/esphome/components/ens160/ens160.h b/esphome/components/ens160/ens160.h new file mode 100644 index 0000000000..88bc8e3501 --- /dev/null +++ b/esphome/components/ens160/ens160.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ens160 { + +class ENS160Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void set_co2(sensor::Sensor *co2) { co2_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + void set_aqi(sensor::Sensor *aqi) { aqi_ = aqi; } + + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void send_env_data_(); + + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + INVALID_ID, + VALIDITY_INVALID, + READ_FAILED, + WRITE_FAILED, + STD_OPMODE_FAILED, + } error_code_{NONE}; + + enum ValidityFlag { + NORMAL_OPERATION = 0, + WARMING_UP, + INITIAL_STARTUP, + INVALID_OUTPUT, + } validity_flag_; + + bool warming_up_{false}; + bool initial_startup_{false}; + + uint8_t firmware_ver_major_{0}; + uint8_t firmware_ver_minor_{0}; + uint8_t firmware_ver_build_{0}; + + sensor::Sensor *co2_{nullptr}; + sensor::Sensor *tvoc_{nullptr}; + sensor::Sensor *aqi_{nullptr}; + + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *temperature_{nullptr}; +}; + +} // namespace ens160 +} // namespace esphome diff --git a/esphome/components/ens160/sensor.py b/esphome/components/ens160/sensor.py new file mode 100644 index 0000000000..55f0ff7b6f --- /dev/null +++ b/esphome/components/ens160/sensor.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ECO2, + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + CONF_TVOC, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + ICON_CHEMICAL_WEAPON, + ICON_MOLECULE_CO2, + ICON_RADIATOR, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_BILLION, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@vincentscode"] +DEPENDENCIES = ["i2c"] + +ens160_ns = cg.esphome_ns.namespace("ens160") +ENS160Component = ens160_ns.class_( + "ENS160Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONF_AQI = "aqi" +CONF_COMPENSATION = "compensation" +UNIT_INDEX = "index" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ENS160Component), + cv.Required(CONF_ECO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_AQI): sensor.sensor_schema( + unit_of_measurement=UNIT_INDEX, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), + cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x53)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + sens = await sensor.new_sensor(config[CONF_ECO2]) + cg.add(var.set_co2(sens)) + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) + sens = await sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi(sens)) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + sens = await cg.get_variable(compensation_config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 151e53fd62..9000636f63 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -225,6 +225,13 @@ sensor: name: "ADE7953 Reactive Power B" update_interval: 1s + - platform: ens160 + eco2: + name: "ENS160 eCO2" + tvoc: + name: "ENS160 Total Volatile Organic Compounds" + aqi: + name: "ENS160 Air Quality Index" - platform: tmp102 name: TMP102 Temperature - platform: hm3301 @@ -424,7 +431,6 @@ switch: direction: BACKWARD id: test_motor - custom_component: lambda: |- auto s = new CustomComponent(); @@ -613,7 +619,6 @@ mcp23017: mcp23008: id: mcp23008_hub - light: - platform: hbridge name: Icicle Lights @@ -633,7 +638,6 @@ ttp229_bsf: sdo_pin: D2 scl_pin: D1 - display: - platform: max7219digit cs_pin: GPIO15 @@ -645,7 +649,6 @@ display: lambda: |- it.printdigit("hello"); - http_request: useragent: esphome/device timeout: 10s diff --git a/tests/test3.yaml b/tests/test3.yaml index 41ded7ee39..0a405a2841 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -480,7 +480,6 @@ sensor: name: PZEMDC Power energy: name: PZEMDC Energy - - platform: pmsx003 uart_id: uart_9 type: PMSX003 From 9f84b6390dd7808ac6f6a5e9c68f44ebf0c3c417 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:27:56 +0100 Subject: [PATCH 051/436] Bump actions/github-script from 6.4.1 to 7.0.1 (#5803) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/needs-docs.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/needs-docs.yml b/.github/workflows/needs-docs.yml index 5019d64752..628b5cc5e3 100644 --- a/.github/workflows/needs-docs.yml +++ b/.github/workflows/needs-docs.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check for needs-docs label - uses: actions/github-script@v6.4.1 + uses: actions/github-script@v7.0.1 with: script: | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e23db521a..69456619e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -203,7 +203,7 @@ jobs: needs: [deploy-manifest] steps: - name: Trigger Workflow - uses: actions/github-script@v6.4.1 + uses: actions/github-script@v7.0.1 with: github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} script: | From 91f1aa05ad31d94d79d92e516dab4719b70331fe Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 25 Nov 2023 00:18:36 +0100 Subject: [PATCH 052/436] Run all tests when local testing. (#5717) --- script/test | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/script/test b/script/test index 36a58cd75a..e227c17f9f 100755 --- a/script/test +++ b/script/test @@ -6,12 +6,6 @@ cd "$(dirname "$0")/.." set -x -esphome compile tests/test1.yaml -esphome compile tests/test2.yaml -esphome compile tests/test3.yaml -esphome compile tests/test3.1.yaml -esphome compile tests/test4.yaml -esphome compile tests/test5.yaml -esphome compile tests/test6.yaml -esphome compile tests/test7.yaml -esphome compile tests/test8.yaml +for f in ./tests/test*.yaml; do + esphome compile $f +done From 636ee2b59713f0f0bad72565658d1f3a176ff0a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:06:57 -0600 Subject: [PATCH 053/436] Bump aioesphomeapi from 18.5.7 to 18.5.9 (#5830) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index af53dc8e94..3d096ffd52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.7 +aioesphomeapi==18.5.9 zeroconf==0.127.0 python-magic==0.4.27 From c43518c391120866d31163f6890773a475b0124d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 25 Nov 2023 14:56:32 +1100 Subject: [PATCH 054/436] Allow split uart pin inversion for ESP-IDF (#5831) --- esphome/components/uart/__init__.py | 3 ++- tests/test5.yaml | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 36f2bb5851..9005422ce6 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -75,12 +75,13 @@ def validate_rx_pin(value): def validate_invert_esp32(config): if ( CORE.is_esp32 + and CORE.using_arduino and CONF_TX_PIN in config and CONF_RX_PIN in config and config[CONF_TX_PIN][CONF_INVERTED] != config[CONF_RX_PIN][CONF_INVERTED] ): raise cv.Invalid( - "Different invert values for TX and RX pin are not (yet) supported for ESP32." + "Different invert values for TX and RX pin are not supported for ESP32 when using Arduino." ) return config diff --git a/tests/test5.yaml b/tests/test5.yaml index 82c201f017..46cedcabd2 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -41,7 +41,9 @@ uart: rx_pin: 3 baud_rate: 9600 - id: uart_2 - tx_pin: 17 + tx_pin: + number: 17 + inverted: true rx_pin: 16 baud_rate: 19200 From ccd7f0661c3fa3fa283dd59058a0779ea1360df2 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sat, 25 Nov 2023 09:38:45 +0100 Subject: [PATCH 055/436] Add `is_detected()` for Nextion displays (#5825) --- esphome/components/nextion/nextion.cpp | 3 ++- esphome/components/nextion/nextion_base.h | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 0134595050..92afdfe3dc 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -93,7 +93,8 @@ bool Nextion::check_connect_() { connect_info.push_back(response.substr(start, end - start)); } - if (connect_info.size() == 7) { + this->is_detected_ = (connect_info.size() == 7); + if (this->is_detected_) { ESP_LOGN(TAG, "Received connect_info %zu", connect_info.size()); this->device_model_ = connect_info[2]; diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index a24fd74060..ffc431ed13 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -48,10 +48,12 @@ class NextionBase { bool is_sleeping() { return this->is_sleeping_; } bool is_setup() { return this->is_setup_; } + bool is_detected() { return this->is_detected_; } protected: bool is_setup_ = false; bool is_sleeping_ = false; + bool is_detected_ = false; }; } // namespace nextion From dbdcb39af903963efb6ba9e2106f42ad8495ac92 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:48:38 +1100 Subject: [PATCH 056/436] Pull PollingComponent up from individual display drivers to Display. (#5444) --- .../components/addressable_light/addressable_light_display.h | 2 +- esphome/components/addressable_light/display.py | 1 - esphome/components/display/__init__.py | 3 ++- esphome/components/display/display.h | 2 +- esphome/components/ili9xxx/display.py | 1 - esphome/components/ili9xxx/ili9xxx_display.h | 3 +-- esphome/components/inkplate6/display.py | 1 - esphome/components/inkplate6/inkplate.h | 2 +- esphome/components/lcd_base/__init__.py | 1 - esphome/components/max7219/display.py | 1 - esphome/components/max7219digit/display.py | 3 +-- esphome/components/max7219digit/max7219digit.h | 3 +-- esphome/components/nextion/display.py | 1 - esphome/components/pcd8544/display.py | 1 - esphome/components/pcd8544/pcd_8544.h | 3 +-- esphome/components/pvvx_mithermometer/display/__init__.py | 1 - esphome/components/ssd1306_base/__init__.py | 1 - esphome/components/ssd1306_base/ssd1306_base.h | 2 +- esphome/components/ssd1322_base/__init__.py | 1 - esphome/components/ssd1322_base/ssd1322_base.h | 2 +- esphome/components/ssd1325_base/__init__.py | 1 - esphome/components/ssd1325_base/ssd1325_base.h | 2 +- esphome/components/ssd1327_base/__init__.py | 1 - esphome/components/ssd1327_base/ssd1327_base.h | 2 +- esphome/components/ssd1331_base/__init__.py | 1 - esphome/components/ssd1331_base/ssd1331_base.h | 2 +- esphome/components/ssd1351_base/__init__.py | 1 - esphome/components/ssd1351_base/ssd1351_base.h | 2 +- esphome/components/st7735/display.py | 1 - esphome/components/st7735/st7735.h | 3 +-- esphome/components/st7789v/display.py | 1 - esphome/components/st7789v/st7789v.h | 3 +-- esphome/components/st7920/display.py | 1 - esphome/components/st7920/st7920.h | 3 +-- esphome/components/tm1621/display.py | 1 - esphome/components/tm1637/display.py | 1 - esphome/components/tm1638/display.py | 1 - esphome/components/waveshare_epaper/display.py | 1 - esphome/components/waveshare_epaper/waveshare_epaper.h | 3 +-- 39 files changed, 19 insertions(+), 47 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index 8893c39be6..f47389fd05 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -10,7 +10,7 @@ namespace esphome { namespace addressable_light { -class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent { +class AddressableLightDisplay : public display::DisplayBuffer { public: light::AddressableLight *get_light() const { return this->light_; } diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py index 2f9b8cf455..327ec8296a 100644 --- a/esphome/components/addressable_light/display.py +++ b/esphome/components/addressable_light/display.py @@ -45,7 +45,6 @@ async def to_code(config): cg.add(var.set_height(config[CONF_HEIGHT])) cg.add(var.set_light(wrapped_light)) - await cg.register_component(var, config) await display.register_display(var, config) if pixel_mapper := config.get(CONF_PIXEL_MAPPER): diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index b7a8508fc8..9f4e922a37 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -58,7 +58,7 @@ BASIC_DISPLAY_SCHEMA = cv.Schema( { cv.Optional(CONF_LAMBDA): cv.lambda_, } -) +).extend(cv.polling_component_schema("1s")) FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( { @@ -116,6 +116,7 @@ async def setup_display_core_(var, config): async def register_display(var, config): + await cg.register_component(var, config) await setup_display_core_(var, config) diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 350fd40f26..7ce6d179ef 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -163,7 +163,7 @@ class BaseFont { virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; }; -class Display { +class Display : public PollingComponent { public: /// Fill the entire screen with the given color. virtual void fill(Color color); diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index ec96d38cf8..6882d254e1 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -115,7 +115,6 @@ async def to_code(config): rhs = MODELS[config[CONF_MODEL]].new() var = cg.Pvariable(config[CONF_ID], rhs) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index e43585afeb..ae7c83e61f 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -19,8 +19,7 @@ enum ILI9XXXColorMode { #define ILI9XXXDisplay_DATA_RATE spi::DATA_RATE_40MHZ #endif // ILI9XXXDisplay_DATA_RATE -class ILI9XXXDisplay : public PollingComponent, - public display::DisplayBuffer, +class ILI9XXXDisplay : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index f05169ea2e..a1ecfdd1d6 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -110,7 +110,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 565bd74710..307d9671e6 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -17,7 +17,7 @@ enum InkplateModel : uint8_t { INKPLATE_6_V2 = 3, }; -class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { +class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { public: const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 92fd0b5563..693211c6fe 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -52,7 +52,6 @@ LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def setup_lcd_display(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) if CONF_USER_CHARACTERS in config: diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index 391d033f24..13807b0dbd 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -29,7 +29,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await spi.register_spi_device(var, config) await display.register_display(var, config) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index 8db9123a39..779e385ab1 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -39,7 +39,7 @@ CHIP_MODES = { max7219_ns = cg.esphome_ns.namespace("max7219digit") MAX7219Component = max7219_ns.class_( - "MAX7219Component", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "MAX7219Component", spi.SPIDevice, display.DisplayBuffer, cg.PollingComponent ) MAX7219ComponentRef = MAX7219Component.operator("ref") @@ -78,7 +78,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await spi.register_spi_device(var, config) await display.register_display(var, config) diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 93d2af21f9..ead8033803 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -25,8 +25,7 @@ class MAX7219Component; using max7219_writer_t = std::function; -class MAX7219Component : public PollingComponent, - public display::DisplayBuffer, +class MAX7219Component : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 0831b12f8a..1ac0364b8b 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -71,7 +71,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await uart.register_uart_device(var, config) if CONF_BRIGHTNESS in config: diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index b4c8f432cf..d7e72d1c81 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -39,7 +39,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index 9a69a9fec7..cfdb96de61 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -7,8 +7,7 @@ namespace esphome { namespace pcd8544 { -class PCD8544 : public PollingComponent, - public display::DisplayBuffer, +class PCD8544 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/pvvx_mithermometer/display/__init__.py b/esphome/components/pvvx_mithermometer/display/__init__.py index d935638933..70c568c1e3 100644 --- a/esphome/components/pvvx_mithermometer/display/__init__.py +++ b/esphome/components/pvvx_mithermometer/display/__init__.py @@ -38,7 +38,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await ble_client.register_ble_node(var, config) cg.add(var.set_disconnect_delay(config[CONF_DISCONNECT_DELAY].total_milliseconds)) diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 55239dfcb8..1fe74dfcb5 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -74,7 +74,6 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1306(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 34b76d284d..2e09755863 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -24,7 +24,7 @@ enum SSD1306Model { SSD1305_MODEL_128_64, }; -class SSD1306 : public PollingComponent, public display::DisplayBuffer { +class SSD1306 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py index 97fb0d2a74..471c874986 100644 --- a/esphome/components/ssd1322_base/__init__.py +++ b/esphome/components/ssd1322_base/__init__.py @@ -33,7 +33,6 @@ SSD1322_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1322(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1322_base/ssd1322_base.h b/esphome/components/ssd1322_base/ssd1322_base.h index d672b298d6..9f4d39976c 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.h +++ b/esphome/components/ssd1322_base/ssd1322_base.h @@ -11,7 +11,7 @@ enum SSD1322Model { SSD1322_MODEL_256_64 = 0, }; -class SSD1322 : public PollingComponent, public display::DisplayBuffer { +class SSD1322 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 1a6f7fb519..e66cfbc684 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -37,7 +37,6 @@ SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1325(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index 8ba6a56c8b..ae033e582b 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -15,7 +15,7 @@ enum SSD1325Model { SSD1327_MODEL_128_128, }; -class SSD1325 : public PollingComponent, public display::DisplayBuffer { +class SSD1325 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py index af2eb3489d..7f2259cf32 100644 --- a/esphome/components/ssd1327_base/__init__.py +++ b/esphome/components/ssd1327_base/__init__.py @@ -26,7 +26,6 @@ SSD1327_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1327(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1327_base/ssd1327_base.h b/esphome/components/ssd1327_base/ssd1327_base.h index 5639beb828..207023a3d3 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.h +++ b/esphome/components/ssd1327_base/ssd1327_base.h @@ -11,7 +11,7 @@ enum SSD1327Model { SSD1327_MODEL_128_128 = 0, }; -class SSD1327 : public PollingComponent, public display::DisplayBuffer { +class SSD1327 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py index 169c0eed1a..80162979fc 100644 --- a/esphome/components/ssd1331_base/__init__.py +++ b/esphome/components/ssd1331_base/__init__.py @@ -18,7 +18,6 @@ SSD1331_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1331(var, config): - await cg.register_component(var, config) await display.register_display(var, config) if CONF_RESET_PIN in config: diff --git a/esphome/components/ssd1331_base/ssd1331_base.h b/esphome/components/ssd1331_base/ssd1331_base.h index be5713f208..719bfc1f8b 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.h +++ b/esphome/components/ssd1331_base/ssd1331_base.h @@ -7,7 +7,7 @@ namespace esphome { namespace ssd1331_base { -class SSD1331 : public PollingComponent, public display::DisplayBuffer { +class SSD1331 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py index 2988dd4bf3..150d89afed 100644 --- a/esphome/components/ssd1351_base/__init__.py +++ b/esphome/components/ssd1351_base/__init__.py @@ -27,7 +27,6 @@ SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1351(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h index 2f1e0237cd..62777a60a0 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.h +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -12,7 +12,7 @@ enum SSD1351Model { SSD1351_MODEL_128_128, }; -class SSD1351 : public PollingComponent, public display::DisplayBuffer { +class SSD1351 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index 652d31662d..4ff5cafaf8 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -69,7 +69,6 @@ CONFIG_SCHEMA = cv.All( async def setup_st7735(var, config): - await cg.register_component(var, config) await display.register_display(var, config) if CONF_RESET_PIN in config: diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 3baa9b083a..37fe673962 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -32,8 +32,7 @@ enum ST7735Model { ST7735_INITR_18REDTAB = INITR_18REDTAB }; -class ST7735 : public PollingComponent, - public display::DisplayBuffer, +class ST7735 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index a4c08974c6..41970afd26 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -158,7 +158,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 22093301e2..29ea315979 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -107,8 +107,7 @@ static const uint8_t ST7789_MADCTL_GS = 0x01; static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; -class ST7789V : public PollingComponent, - public display::DisplayBuffer, +class ST7789V : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py index 9b544fa644..1267e2ad63 100644 --- a/esphome/components/st7920/display.py +++ b/esphome/components/st7920/display.py @@ -28,7 +28,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await spi.register_spi_device(var, config) if CONF_LAMBDA in config: diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index c00b7cf5e0..c9fdad454d 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -11,8 +11,7 @@ class ST7920; using st7920_writer_t = std::function; -class ST7920 : public PollingComponent, - public display::DisplayBuffer, +class ST7920 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/tm1621/display.py b/esphome/components/tm1621/display.py index edbc5f6928..a82b680f62 100644 --- a/esphome/components/tm1621/display.py +++ b/esphome/components/tm1621/display.py @@ -28,7 +28,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) cs = await cg.gpio_pin_expression(config[CONF_CS_PIN]) diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py index 609c62fd10..dcbc64332a 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -34,7 +34,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) diff --git a/esphome/components/tm1638/display.py b/esphome/components/tm1638/display.py index 6339983674..2fb8dc7a55 100644 --- a/esphome/components/tm1638/display.py +++ b/esphome/components/tm1638/display.py @@ -33,7 +33,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index eb0faadc02..519b07fca2 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -148,7 +148,6 @@ async def to_code(config): else: raise NotImplementedError() - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index b3325d69eb..c800d29643 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -7,8 +7,7 @@ namespace esphome { namespace waveshare_epaper { -class WaveshareEPaper : public PollingComponent, - public display::DisplayBuffer, +class WaveshareEPaper : public display::DisplayBuffer, public spi::SPIDevice { public: From 2e6d01ddff06e2b5f006b01ca16df740fcf6446c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:54:12 +1100 Subject: [PATCH 057/436] Implement variable length single word SPI writes. (#5678) --- esphome/components/spi/spi.cpp | 14 +++++++----- esphome/components/spi/spi.h | 16 ++++++++++++++ esphome/components/spi/spi_esp_idf.cpp | 30 ++++++++++++++++++-------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 935399500f..9d06ac0e45 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -77,15 +77,19 @@ 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) { +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); } + +uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { // Clock starts out at idle level this->clk_pin_->digital_write(clock_polarity_); uint8_t out_data = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i != num_bits; i++) { uint8_t shift; if (bit_order_ == BIT_ORDER_MSB_FIRST) { - shift = 7 - i; + shift = num_bits - 1 - i; } else { shift = i; } @@ -94,7 +98,7 @@ uint8_t SPIDelegateBitBash::transfer(uint8_t data) { // sampling on leading edge this->sdo_pin_->digital_write(data & (1 << shift)); this->cycle_clock_(); - out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift; + out_data |= uint16_t(this->sdi_pin_->digital_read()) << shift; this->clk_pin_->digital_write(!this->clock_polarity_); this->cycle_clock_(); this->clk_pin_->digital_write(this->clock_polarity_); @@ -104,7 +108,7 @@ uint8_t SPIDelegateBitBash::transfer(uint8_t data) { this->clk_pin_->digital_write(!this->clock_polarity_); this->sdo_pin_->digital_write(data & (1 << shift)); this->cycle_clock_(); - out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift; + out_data |= uint16_t(this->sdi_pin_->digital_read()) << shift; this->clk_pin_->digital_write(this->clock_polarity_); } } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 107ffb7cb5..0eb4cd7eb6 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -199,6 +199,15 @@ class SPIDelegate { rxbuf[i] = this->transfer(txbuf[i]); } + /** + * write a variable length data item, up to 16 bits. + * @param data The data to send. Should be LSB-aligned (i.e. top bits will be discarded.) + * @param num_bits The number of bits to send + */ + virtual void write(uint16_t data, size_t num_bits) { + esph_log_e("spi_device", "variable length write not implemented"); + } + // write 16 bits virtual void write16(uint16_t data) { if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { @@ -270,6 +279,10 @@ class SPIDelegateBitBash : public SPIDelegate { uint8_t transfer(uint8_t data) override; + void write(uint16_t data, size_t num_bits) override; + + void write16(uint16_t data) override { this->write(data, 16); }; + protected: GPIOPin *clk_pin_; GPIOPin *sdo_pin_; @@ -284,6 +297,7 @@ class SPIDelegateBitBash : public SPIDelegate { continue; this->last_transition_ += this->wait_cycle_; } + uint16_t transfer_(uint16_t data, size_t num_bits); }; class SPIBus { @@ -408,6 +422,8 @@ class SPIDevice : public SPIClient { void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); } + void write(uint16_t data, size_t num_bits) { this->delegate_->write(data, num_bits); }; + void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); } void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index f9e4bfcca6..03ab298019 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -72,7 +72,11 @@ class SPIDelegateHw : public SPIDelegate { desc.rxlength = this->write_only_ ? 0 : partial * 8; desc.tx_buffer = txbuf; desc.rx_buffer = rxbuf; - esp_err_t const err = spi_device_transmit(this->handle_, &desc); + // polling is used as it has about 10% less overhead than queuing an interrupt transfer + esp_err_t err = spi_device_polling_start(this->handle_, &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } if (err != ESP_OK) { ESP_LOGE(TAG, "Transmit failed - err %X", err); break; @@ -85,6 +89,21 @@ class SPIDelegateHw : public SPIDelegate { } } + void write(uint16_t data, size_t num_bits) override { + spi_transaction_ext_t desc = {}; + desc.command_bits = num_bits; + desc.base.flags = SPI_TRANS_VARIABLE_CMD; + desc.base.cmd = data; + esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } + + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + } + } + void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); } uint8_t transfer(uint8_t data) override { @@ -93,14 +112,7 @@ class SPIDelegateHw : public SPIDelegate { return rxbuf; } - void write16(uint16_t data) override { - if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { - uint16_t txbuf = SPI_SWAP_DATA_TX(data, 16); - this->transfer((uint8_t *) &txbuf, nullptr, 2); - } else { - this->transfer((uint8_t *) &data, nullptr, 2); - } - } + void write16(uint16_t data) override { this->write(data, 16); } void write_array(const uint8_t *ptr, size_t length) override { this->transfer(ptr, nullptr, length); } From 0a7d3c367b9e494b24baf66d5373370718ec9fca Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:36:52 +1100 Subject: [PATCH 058/436] Add 'enable_at_startup' feature to power_supply (#5826) --- esphome/components/power_supply/__init__.py | 4 +++ .../components/power_supply/power_supply.cpp | 29 ++++++++----------- .../components/power_supply/power_supply.h | 3 +- tests/test1.1.yaml | 13 +++++---- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index f7dd8bca84..6735eddff3 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -8,6 +8,8 @@ power_supply_ns = cg.esphome_ns.namespace("power_supply") PowerSupply = power_supply_ns.class_("PowerSupply", cg.Component) MULTI_CONF = True +CONF_ENABLE_ON_BOOT = "enable_on_boot" + CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(PowerSupply), @@ -18,6 +20,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_KEEP_ON_TIME, default="10s" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ENABLE_ON_BOOT, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -30,5 +33,6 @@ async def to_code(config): cg.add(var.set_pin(pin)) cg.add(var.set_enable_time(config[CONF_ENABLE_TIME])) cg.add(var.set_keep_on_time(config[CONF_KEEP_ON_TIME])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add_define("USE_POWER_SUPPLY") diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index c4d157615a..7474075302 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -11,47 +11,42 @@ void PowerSupply::setup() { this->pin_->setup(); this->pin_->digital_write(false); - this->enabled_ = false; + if (this->enable_on_boot_) + this->request_high_power(); } void PowerSupply::dump_config() { ESP_LOGCONFIG(TAG, "Power Supply:"); LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, " Time to enable: %" PRIu32 " ms", this->enable_time_); ESP_LOGCONFIG(TAG, " Keep on time: %.1f s", this->keep_on_time_ / 1000.0f); + if (this->enable_on_boot_) + ESP_LOGCONFIG(TAG, " Enabled at startup: True"); } float PowerSupply::get_setup_priority() const { return setup_priority::IO; } -bool PowerSupply::is_enabled() const { return this->enabled_; } +bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; } void PowerSupply::request_high_power() { - this->cancel_timeout("power-supply-off"); - this->pin_->digital_write(true); - if (this->active_requests_ == 0) { - // we need to enable the power supply. - // cancel old timeout if it exists because we now definitely have a high power mode. + this->cancel_timeout("power-supply-off"); ESP_LOGD(TAG, "Enabling power supply."); + this->pin_->digital_write(true); delay(this->enable_time_); } - this->enabled_ = true; - // increase active requests this->active_requests_++; } void PowerSupply::unrequest_high_power() { - this->active_requests_--; - if (this->active_requests_ < 0) { - // we're just going to use 0 as our new counter. - this->active_requests_ = 0; - } - if (this->active_requests_ == 0) { - // set timeout for power supply off + ESP_LOGW(TAG, "Invalid call to unrequest_high_power"); + return; + } + this->active_requests_--; + if (this->active_requests_ == 0) { this->set_timeout("power-supply-off", this->keep_on_time_, [this]() { ESP_LOGD(TAG, "Disabling power supply."); this->pin_->digital_write(false); - this->enabled_ = false; }); } } diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index 49d905ba3a..0b06105ae9 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -13,6 +13,7 @@ class PowerSupply : public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void set_enable_time(uint32_t enable_time) { enable_time_ = enable_time; } void set_keep_on_time(uint32_t keep_on_time) { keep_on_time_ = keep_on_time; } + void set_enable_on_boot(bool enable_on_boot) { enable_on_boot_ = enable_on_boot; } /// Is this power supply currently on? bool is_enabled() const; @@ -35,7 +36,7 @@ class PowerSupply : public Component { protected: GPIOPin *pin_; - bool enabled_{false}; + bool enable_on_boot_{false}; uint32_t enable_time_; uint32_t keep_on_time_; int16_t active_requests_{0}; // use signed integer to make catching negative requests easier. diff --git a/tests/test1.1.yaml b/tests/test1.1.yaml index f4ad89897b..e2e7bd5d63 100644 --- a/tests/test1.1.yaml +++ b/tests/test1.1.yaml @@ -44,12 +44,13 @@ network: e131: power_supply: - id: atx_power_supply - enable_time: 20ms - keep_on_time: 10s - pin: - number: 13 - inverted: true + - id: atx_power_supply + enable_time: 20ms + keep_on_time: 10s + enable_on_boot: true + pin: + number: 13 + inverted: true i2c: sda: 21 From 1324d9e39a4f7d850ba9fc013f3d0e412e4fa5b9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:45:26 +1300 Subject: [PATCH 059/436] Voice Assistant improvements (#5827) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 2 + .../i2s_audio/speaker/i2s_audio_speaker.h | 2 + esphome/components/speaker/speaker.h | 2 + .../voice_assistant/voice_assistant.cpp | 108 +++++++++++------- .../voice_assistant/voice_assistant.h | 3 + 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index ed13e6b458..e729cdf954 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -220,6 +220,8 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length) { return index; } +bool I2SAudioSpeaker::has_buffered_data() const { return uxQueueMessagesWaiting(this->buffer_queue_) > 0; } + } // namespace i2s_audio } // namespace esphome diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index b075722e1b..20c36a69d3 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -56,6 +56,8 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud size_t play(const uint8_t *data, size_t length) override; + bool has_buffered_data() const override; + protected: void start_(); // void stop_(); diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 3f520e3c5e..b494873160 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -18,6 +18,8 @@ class Speaker { virtual void start() = 0; virtual void stop() = 0; + virtual bool has_buffered_data() const = 0; + bool is_running() const { return this->state_ == STATE_RUNNING; } protected: diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 9b13a71039..c0e706305d 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -273,28 +273,27 @@ void VoiceAssistant::loop() { bool playing = false; #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + ssize_t received_len = 0; if (this->speaker_buffer_index_ + RECEIVE_SIZE < SPEAKER_BUFFER_SIZE) { - auto len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE); - if (len > 0) { - this->speaker_buffer_index_ += len; - this->speaker_buffer_size_ += len; + received_len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE); + if (received_len > 0) { + this->speaker_buffer_index_ += received_len; + this->speaker_buffer_size_ += received_len; + this->speaker_bytes_received_ += received_len; } } else { - ESP_LOGW(TAG, "Receive buffer full"); - } - if (this->speaker_buffer_size_ > 0) { - size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); - if (written > 0) { - memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); - this->speaker_buffer_size_ -= written; - this->speaker_buffer_index_ -= written; - this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); - } else { - ESP_LOGW(TAG, "Speaker buffer full"); - } + ESP_LOGD(TAG, "Receive buffer full"); } + // Build a small buffer of audio before sending to the speaker + if (this->speaker_bytes_received_ > RECEIVE_SIZE * 4) + this->write_speaker_(); if (this->wait_for_stream_end_) { this->cancel_timeout("playing"); + if (this->stream_ended_ && received_len < 0) { + ESP_LOGD(TAG, "End of audio stream received"); + this->cancel_timeout("speaker-timeout"); + this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); + } break; // We dont want to timeout here as the STREAM_END event will take care of that. } playing = this->speaker_->is_running(); @@ -316,14 +315,26 @@ void VoiceAssistant::loop() { case State::RESPONSE_FINISHED: { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + if (this->speaker_buffer_size_ > 0) { + this->write_speaker_(); + break; + } + if (this->speaker_->has_buffered_data() || this->speaker_->is_running()) { + break; + } + ESP_LOGD(TAG, "Speaker has finished outputting all audio"); this->speaker_->stop(); this->cancel_timeout("speaker-timeout"); this->cancel_timeout("playing"); this->speaker_buffer_size_ = 0; this->speaker_buffer_index_ = 0; + this->speaker_bytes_received_ = 0; memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); + this->wait_for_stream_end_ = false; + this->stream_ended_ = false; + + this->tts_stream_end_trigger_->trigger(); } - this->wait_for_stream_end_ = false; #endif this->set_state_(State::IDLE, State::IDLE); break; @@ -333,6 +344,20 @@ void VoiceAssistant::loop() { } } +void VoiceAssistant::write_speaker_() { + if (this->speaker_buffer_size_ > 0) { + size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); + if (written > 0) { + memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); + this->speaker_buffer_size_ -= written; + this->speaker_buffer_index_ -= written; + this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); + } else { + ESP_LOGD(TAG, "Speaker buffer full, trying again next loop"); + } + } +} + void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { if (!subscribe) { if (this->api_client_ == nullptr || client != this->api_client_) { @@ -503,21 +528,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); - this->start_trigger_->trigger(); + this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_END: { ESP_LOGD(TAG, "Wake word detected"); - this->wake_word_detected_trigger_->trigger(); + this->defer([this]() { this->wake_word_detected_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_STT_START: ESP_LOGD(TAG, "STT started"); - this->listening_trigger_->trigger(); + this->defer([this]() { this->listening_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_END: { - this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); std::string text; for (auto arg : msg.data) { if (arg.name == "text") { @@ -529,12 +553,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); - this->stt_end_trigger_->trigger(text); + this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); break; } case api::enums::VOICE_ASSISTANT_INTENT_START: ESP_LOGD(TAG, "Intent started"); - this->intent_start_trigger_->trigger(); + this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { @@ -542,7 +566,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->conversation_id_ = std::move(arg.value); } } - this->intent_end_trigger_->trigger(); + this->defer([this]() { this->intent_end_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -557,10 +581,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); - this->tts_start_trigger_->trigger(text); + this->defer([this, text]() { + this->tts_start_trigger_->trigger(text); #ifdef USE_SPEAKER - this->speaker_->start(); + this->speaker_->start(); #endif + }); break; } case api::enums::VOICE_ASSISTANT_TTS_END: { @@ -575,14 +601,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); + this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { - this->media_player_->make_call().set_media_url(url).perform(); - } + if (this->media_player_ != nullptr) { + this->media_player_->make_call().set_media_url(url).perform(); + } #endif + this->tts_end_trigger_->trigger(url); + }); State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; this->set_state_(new_state, new_state); - this->tts_end_trigger_->trigger(url); break; } case api::enums::VOICE_ASSISTANT_RUN_END: { @@ -599,7 +627,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->set_state_(State::IDLE, State::IDLE); } } - this->end_trigger_->trigger(); + this->defer([this]() { this->end_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_ERROR: { @@ -617,8 +645,10 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } else if (code == "wake-provider-missing" || code == "wake-engine-missing") { // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. - this->request_stop(); - this->error_trigger_->trigger(code, message); + this->defer([this, code, message]() { + this->request_stop(); + this->error_trigger_->trigger(code, message); + }); return; } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); @@ -626,32 +656,32 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->signal_stop_(); this->set_state_(State::STOP_MICROPHONE, State::IDLE); } - this->error_trigger_->trigger(code, message); + this->defer([this, code, message]() { this->error_trigger_->trigger(code, message); }); break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER this->wait_for_stream_end_ = true; ESP_LOGD(TAG, "TTS stream start"); - this->tts_stream_start_trigger_->trigger(); + this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { - this->set_state_(State::RESPONSE_FINISHED, State::IDLE); #ifdef USE_SPEAKER + this->stream_ended_ = true; ESP_LOGD(TAG, "TTS stream end"); - this->tts_stream_end_trigger_->trigger(); #endif break; } case api::enums::VOICE_ASSISTANT_STT_VAD_START: ESP_LOGD(TAG, "Starting STT by VAD"); - this->stt_vad_start_trigger_->trigger(); + this->defer([this]() { this->stt_vad_start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_VAD_END: ESP_LOGD(TAG, "STT by VAD end"); - this->stt_vad_end_trigger_->trigger(); + this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); + this->defer([this]() { this->stt_vad_end_trigger_->trigger(); }); break; default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index f6dcd1c563..66bf4c3c57 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -156,11 +156,14 @@ class VoiceAssistant : public Component { microphone::Microphone *mic_{nullptr}; #ifdef USE_SPEAKER + void write_speaker_(); speaker::Speaker *speaker_{nullptr}; uint8_t *speaker_buffer_; size_t speaker_buffer_index_{0}; size_t speaker_buffer_size_{0}; + size_t speaker_bytes_received_{0}; bool wait_for_stream_end_{false}; + bool stream_ended_{false}; #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; From 15180ee1e2f9f7254fa277c37379372dc2b43f0a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:35:31 +1300 Subject: [PATCH 060/436] mcp3008: Tidy up and fix auto load bug (#5842) --- esphome/components/mcp3008/mcp3008.cpp | 22 ++------ esphome/components/mcp3008/mcp3008.h | 25 +-------- esphome/components/mcp3008/sensor.py | 39 -------------- esphome/components/mcp3008/sensor/__init__.py | 53 +++++++++++++++++++ .../mcp3008/sensor/mcp3008_sensor.cpp | 27 ++++++++++ .../mcp3008/sensor/mcp3008_sensor.h | 31 +++++++++++ 6 files changed, 115 insertions(+), 82 deletions(-) delete mode 100644 esphome/components/mcp3008/sensor.py create mode 100644 esphome/components/mcp3008/sensor/__init__.py create mode 100644 esphome/components/mcp3008/sensor/mcp3008_sensor.cpp create mode 100644 esphome/components/mcp3008/sensor/mcp3008_sensor.h diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index 81abc4f012..aed48456b2 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -1,4 +1,6 @@ #include "mcp3008.h" + +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -32,28 +34,10 @@ float MCP3008::read_data(uint8_t pin) { this->disable(); - int data = data_msb << 8 | data_lsb; + uint16_t data = encode_uint16(data_msb, data_lsb); return data / 1023.0f; } -MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage) - : PollingComponent(1000), parent_(parent), pin_(pin), reference_voltage_(reference_voltage) {} - -float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } - -void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } -void MCP3008Sensor::dump_config() { - ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); - ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); - ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); -} -float MCP3008Sensor::sample() { - float value_v = this->parent_->read_data(pin_); - value_v = (value_v * this->reference_voltage_); - return value_v; -} -void MCP3008Sensor::update() { this->publish_state(this->sample()); } - } // namespace mcp3008 } // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h index 5d8b823111..baf8d7c152 100644 --- a/esphome/components/mcp3008/mcp3008.h +++ b/esphome/components/mcp3008/mcp3008.h @@ -1,10 +1,8 @@ #pragma once +#include "esphome/components/spi/spi.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/voltage_sampler/voltage_sampler.h" namespace esphome { namespace mcp3008 { @@ -14,31 +12,10 @@ class MCP3008 : public Component, spi::DATA_RATE_75KHZ> { // Running at the slowest max speed supported by the // mcp3008. 2.7v = 75ksps public: - MCP3008() = default; - void setup() override; void dump_config() override; float get_setup_priority() const override; float read_data(uint8_t pin); - - protected: -}; - -class MCP3008Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { - public: - MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage); - - void set_reference_voltage(float reference_voltage) { reference_voltage_ = reference_voltage; } - void setup() override; - void update() override; - void dump_config() override; - float get_setup_priority() const override; - float sample() override; - - protected: - MCP3008 *parent_; - uint8_t pin_; - float reference_voltage_; }; } // namespace mcp3008 diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py deleted file mode 100644 index dd5141484b..0000000000 --- a/esphome/components/mcp3008/sensor.py +++ /dev/null @@ -1,39 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import sensor, voltage_sampler -from esphome.const import CONF_ID, CONF_NUMBER -from . import mcp3008_ns, MCP3008 - -AUTO_LOAD = ["voltage_sampler"] - -DEPENDENCIES = ["mcp3008"] - -MCP3008Sensor = mcp3008_ns.class_( - "MCP3008Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler -) -CONF_REFERENCE_VOLTAGE = "reference_voltage" -CONF_MCP3008_ID = "mcp3008_id" - -CONFIG_SCHEMA = ( - sensor.sensor_schema(MCP3008Sensor) - .extend( - { - cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, - } - ) - .extend(cv.polling_component_schema("1s")) -) - - -async def to_code(config): - parent = await cg.get_variable(config[CONF_MCP3008_ID]) - var = cg.new_Pvariable( - config[CONF_ID], - parent, - config[CONF_NUMBER], - config[CONF_REFERENCE_VOLTAGE], - ) - await cg.register_component(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/mcp3008/sensor/__init__.py b/esphome/components/mcp3008/sensor/__init__.py new file mode 100644 index 0000000000..c56965d517 --- /dev/null +++ b/esphome/components/mcp3008/sensor/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_ID, + CONF_NUMBER, + UNIT_VOLT, + STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_VOLTAGE, +) + +from .. import mcp3008_ns, MCP3008 + +AUTO_LOAD = ["voltage_sampler"] + +DEPENDENCIES = ["mcp3008"] + +MCP3008Sensor = mcp3008_ns.class_( + "MCP3008Sensor", + sensor.Sensor, + cg.PollingComponent, + voltage_sampler.VoltageSampler, + cg.Parented.template(MCP3008), +) +CONF_REFERENCE_VOLTAGE = "reference_voltage" +CONF_MCP3008_ID = "mcp3008_id" + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MCP3008Sensor, + unit_of_measurement=UNIT_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + ) + .extend( + { + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_MCP3008_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp new file mode 100644 index 0000000000..df2a8735f8 --- /dev/null +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp @@ -0,0 +1,27 @@ +#include "mcp3008_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3008 { + +static const char *const TAG = "mcp3008.sensor"; + +float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MCP3008Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); +} + +float MCP3008Sensor::sample() { + float value_v = this->parent_->read_data(pin_); + value_v = (value_v * this->reference_voltage_); + return value_v; +} + +void MCP3008Sensor::update() { this->publish_state(this->sample()); } + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.h b/esphome/components/mcp3008/sensor/mcp3008_sensor.h new file mode 100644 index 0000000000..ebaeab966f --- /dev/null +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" + +#include "../mcp3008.h" + +namespace esphome { +namespace mcp3008 { + +class MCP3008Sensor : public PollingComponent, + public sensor::Sensor, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + void set_pin(uint8_t pin) { this->pin_ = pin; } + + void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; + + protected: + uint8_t pin_; + float reference_voltage_; +}; + +} // namespace mcp3008 +} // namespace esphome From a15a812466eb1111792e4e5ecf2eb4cfdcf05e3b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:43:41 +1300 Subject: [PATCH 061/436] Fix missing include in remote_base (#5843) --- esphome/components/remote_base/remote_base.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 40c699e8ea..095f95053f 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -1,6 +1,8 @@ #include "remote_base.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace remote_base { From f63f722afb7fb016dbff1faa32386ccd64ec45fa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:44:09 +1300 Subject: [PATCH 062/436] Create GT911 Touchscreen component (#4027) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/gt911/__init__.py | 6 + .../gt911/binary_sensor/__init__.py | 31 +++++ .../gt911/binary_sensor/gt911_button.cpp | 27 ++++ .../gt911/binary_sensor/gt911_button.h | 28 ++++ .../components/gt911/touchscreen/__init__.py | 46 +++++++ .../gt911/touchscreen/gt911_touchscreen.cpp | 122 ++++++++++++++++++ .../gt911/touchscreen/gt911_touchscreen.h | 39 ++++++ .../components/touchscreen/touchscreen.cpp | 5 + esphome/components/touchscreen/touchscreen.h | 1 + tests/test4.yaml | 8 ++ 11 files changed, 314 insertions(+) create mode 100644 esphome/components/gt911/__init__.py create mode 100644 esphome/components/gt911/binary_sensor/__init__.py create mode 100644 esphome/components/gt911/binary_sensor/gt911_button.cpp create mode 100644 esphome/components/gt911/binary_sensor/gt911_button.h create mode 100644 esphome/components/gt911/touchscreen/__init__.py create mode 100644 esphome/components/gt911/touchscreen/gt911_touchscreen.cpp create mode 100644 esphome/components/gt911/touchscreen/gt911_touchscreen.h diff --git a/CODEOWNERS b/CODEOWNERS index af23f679c8..2cd08a780e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -119,6 +119,7 @@ esphome/components/graph/* @synco esphome/components/gree/* @orestismers esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte +esphome/components/gt911/* @clydebarrow @jesserockz esphome/components/haier/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior diff --git a/esphome/components/gt911/__init__.py b/esphome/components/gt911/__init__.py new file mode 100644 index 0000000000..1f7ecd1d5e --- /dev/null +++ b/esphome/components/gt911/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@jesserockz", "@clydebarrow"] +DEPENDENCIES = ["i2c"] + +gt911_ns = cg.esphome_ns.namespace("gt911") diff --git a/esphome/components/gt911/binary_sensor/__init__.py b/esphome/components/gt911/binary_sensor/__init__.py new file mode 100644 index 0000000000..18f5c49dbd --- /dev/null +++ b/esphome/components/gt911/binary_sensor/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_INDEX + +from .. import gt911_ns +from ..touchscreen import GT911Touchscreen, GT911ButtonListener + +CONF_GT911_ID = "gt911_id" + +GT911Button = gt911_ns.class_( + "GT911Button", + binary_sensor.BinarySensor, + cg.Component, + GT911ButtonListener, + cg.Parented.template(GT911Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(GT911Button).extend( + { + cv.GenerateID(CONF_GT911_ID): cv.use_id(GT911Touchscreen), + cv.Optional(CONF_INDEX, default=0): cv.int_range(min=0, max=3), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_GT911_ID]) + cg.add(var.set_index(config[CONF_INDEX])) diff --git a/esphome/components/gt911/binary_sensor/gt911_button.cpp b/esphome/components/gt911/binary_sensor/gt911_button.cpp new file mode 100644 index 0000000000..35ffaecefc --- /dev/null +++ b/esphome/components/gt911/binary_sensor/gt911_button.cpp @@ -0,0 +1,27 @@ +#include "gt911_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gt911 { + +static const char *const TAG = "GT911.binary_sensor"; + +void GT911Button::setup() { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); +} + +void GT911Button::dump_config() { + LOG_BINARY_SENSOR("", "GT911 Button", this); + ESP_LOGCONFIG(TAG, " Index: %u", this->index_); +} + +void GT911Button::update_button(uint8_t index, bool state) { + if (index != this->index_) + return; + + this->publish_state(state); +} + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/binary_sensor/gt911_button.h b/esphome/components/gt911/binary_sensor/gt911_button.h new file mode 100644 index 0000000000..556ed65f91 --- /dev/null +++ b/esphome/components/gt911/binary_sensor/gt911_button.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/gt911/touchscreen/gt911_touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace gt911 { + +class GT911Button : public binary_sensor::BinarySensor, + public Component, + public GT911ButtonListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_index(uint8_t index) { this->index_ = index; } + + void update_button(uint8_t index, bool state) override; + + protected: + uint8_t index_; +}; + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py new file mode 100644 index 0000000000..295e32b1b1 --- /dev/null +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_ROTATION +from .. import gt911_ns + + +GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") +GT911Touchscreen = gt911_ns.class_( + "GT911Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +ROTATIONS = { + 0: touchscreen.TouchRotation.ROTATE_0_DEGREES, + 90: touchscreen.TouchRotation.ROTATE_90_DEGREES, + 180: touchscreen.TouchRotation.ROTATE_180_DEGREES, + 270: touchscreen.TouchRotation.ROTATE_270_DEGREES, +} +CONFIG_SCHEMA = ( + touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GT911Touchscreen), + cv.Optional(CONF_ROTATION): cv.enum(ROTATIONS), + cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x5D)) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + if CONF_ROTATION in config: + cg.add(var.set_rotation(ROTATIONS[config[CONF_ROTATION]])) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp new file mode 100644 index 0000000000..4d3e7e7903 --- /dev/null +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -0,0 +1,122 @@ +#include "gt911_touchscreen.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gt911 { + +static const char *const TAG = "gt911.touchscreen"; + +static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; +static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; +static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; +static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; +static const size_t MAX_TOUCHES = 5; // max number of possible touches reported + +#define ERROR_CHECK(err) \ + if ((err) != i2c::ERROR_OK) { \ + ESP_LOGE(TAG, "Failed to communicate!"); \ + this->status_set_warning(); \ + return; \ + } + +void IRAM_ATTR HOT Store::gpio_intr(Store *store) { store->available = true; } + +void GT911Touchscreen::setup() { + i2c::ErrorCode err; + ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); + // datasheet says NOT to use pullup/down on the int line. + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + + // check the configuration of the int line. + uint8_t data; + err = this->write(GET_SWITCHES, 2); + if (err == i2c::ERROR_OK) { + err = this->read(&data, 1); + if (err == i2c::ERROR_OK) { + ESP_LOGD(TAG, "Read from switches: 0x%02X", data); + this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, + (data & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + } + } + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to communicate!"); + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); +} + +void GT911Touchscreen::loop() { + i2c::ErrorCode err; + touchscreen::TouchPoint tp; + uint8_t touch_state = 0; + uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte + + if (!this->store_.available) + return; + this->store_.available = false; + + err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); + ERROR_CHECK(err); + err = this->read(&touch_state, 1); + ERROR_CHECK(err); + this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE)); + + if ((touch_state & 0x80) == 0) + return; + uint8_t num_of_touches = touch_state & 0x07; + if (num_of_touches == 0) + this->send_release_(); + if (num_of_touches > MAX_TOUCHES) // should never happen + return; + + err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); + ERROR_CHECK(err); + // num_of_touches is guaranteed to be 0..5. Also read the key data + err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); + ERROR_CHECK(err); + + for (uint8_t i = 0; i != num_of_touches; i++) { + tp.id = data[i][0]; + uint16_t x = encode_uint16(data[i][2], data[i][1]); + uint16_t y = encode_uint16(data[i][4], data[i][3]); + + switch (this->rotation_) { + case touchscreen::ROTATE_0_DEGREES: + tp.x = x; + tp.y = y; + break; + case touchscreen::ROTATE_90_DEGREES: + tp.x = y; + tp.y = this->display_width_ - x; + break; + case touchscreen::ROTATE_180_DEGREES: + tp.x = this->display_width_ - x; + tp.y = this->display_height_ - y; + break; + case touchscreen::ROTATE_270_DEGREES: + tp.x = this->display_height_ - y; + tp.y = x; + break; + } + this->defer([this, tp]() { this->send_touch_(tp); }); + } + auto keys = data[num_of_touches][0]; + for (size_t i = 0; i != 4; i++) { + for (auto *listener : this->button_listeners_) + listener->update_button(i, (keys & (1 << i)) != 0); + } +} + +void GT911Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + ESP_LOGCONFIG(TAG, " Rotation: %d", (int) this->rotation_); +} + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h new file mode 100644 index 0000000000..dc9248bb4a --- /dev/null +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace gt911 { + +struct Store { + volatile bool available; + + static void gpio_intr(Store *store); +}; + +class GT911ButtonListener { + public: + virtual void update_button(uint8_t index, bool state) = 0; +}; + +class GT911Touchscreen : public touchscreen::Touchscreen, public Component, public i2c::I2CDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + void set_rotation(touchscreen::TouchRotation rotation) { this->rotation_ = rotation; } + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } + + protected: + InternalGPIOPin *interrupt_pin_; + Store store_; + std::vector button_listeners_; +}; + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 2eaa736171..20828aad8b 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -18,6 +18,11 @@ void Touchscreen::set_display(display::Display *display) { } } +void Touchscreen::send_release_() { + for (auto *listener : this->touch_listeners_) + listener->release(); +} + void Touchscreen::send_touch_(TouchPoint tp) { ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y); this->touch_trigger_.trigger(tp); diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 24b3191880..6e07bcfea0 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -41,6 +41,7 @@ class Touchscreen { protected: /// Call this function to send touch points to the `on_touch` listener and the binary_sensors. void send_touch_(TouchPoint tp); + void send_release_(); uint16_t display_width_; uint16_t display_height_; diff --git a/tests/test4.yaml b/tests/test4.yaml index c27dbb65ac..63bf5bf0ae 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -405,6 +405,10 @@ binary_sensor: y_max: 100 on_press: - logger.log: Touched + - platform: gt911 + id: touch_key_911 + index: 0 + - platform: gpio name: MaxIn Pin 4 @@ -725,6 +729,10 @@ touchscreen: - logger.log: format: Touch at (%d, %d) args: [touch.x, touch.y] + - platform: gt911 + interrupt_pin: GPIO3 + display: inkplate_display + i2s_audio: i2s_lrclk_pin: GPIO26 From 3b77f05cc9c25c5b51678af112558b124d66a78f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:57:40 +1300 Subject: [PATCH 063/436] Add 'voice_assistant.connected' condition (#5845) --- esphome/components/voice_assistant/__init__.py | 12 ++++++++++++ esphome/components/voice_assistant/voice_assistant.h | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index d05f39072c..59aef901f2 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -57,6 +57,9 @@ StopAction = voice_assistant_ns.class_( IsRunningCondition = voice_assistant_ns.class_( "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) ) +ConnectedCondition = voice_assistant_ns.class_( + "ConnectedCondition", automation.Condition, cg.Parented.template(VoiceAssistant) +) def tts_stream_validate(config): @@ -298,3 +301,12 @@ async def voice_assistant_is_running_to_code(config, condition_id, template_arg, var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +@register_condition( + "voice_assistant.connected", ConnectedCondition, VOICE_ASSISTANT_ACTION_SCHEMA +) +async def voice_assistant_connected_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 66bf4c3c57..f9325dff54 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -222,6 +222,11 @@ template class IsRunningCondition : public Condition, pub bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; +template class ConnectedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->get_api_connection() != nullptr; } +}; + extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace voice_assistant From 019315afa0191a6c41a3d03274d2b9d5c7b8468d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:07:48 +0000 Subject: [PATCH 064/436] Bump aioesphomeapi from 18.5.9 to 19.1.2 (#5844) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3d096ffd52..09f5c303bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.9 +aioesphomeapi==19.1.2 zeroconf==0.127.0 python-magic==0.4.27 From 460362b11f891e4bd316046c6fb7fca9083ff80c Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 27 Nov 2023 22:09:57 +0100 Subject: [PATCH 065/436] Nextion - Standardizing log messages (#5837) --- esphome/components/nextion/nextion.cpp | 38 +++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 92afdfe3dc..81e905b979 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -433,8 +433,10 @@ void Nextion::process_nextion_commands_() { uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Got touch page=%u component=%u type=%s", page_id, component_id, - touch_event ? "PRESS" : "RELEASE"); + ESP_LOGD(TAG, "Got touch event:"); + ESP_LOGD(TAG, " page_id: %u", page_id); + ESP_LOGD(TAG, " component_id: %u", component_id); + ESP_LOGD(TAG, " event type: %s", touch_event ? "PRESS" : "RELEASE"); for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } @@ -448,7 +450,7 @@ void Nextion::process_nextion_commands_() { } uint8_t page_id = to_process[0]; - ESP_LOGD(TAG, "Got new page=%u", page_id); + ESP_LOGD(TAG, "Got new page: %u", page_id); this->page_callback_.call(page_id); break; } @@ -466,7 +468,10 @@ void Nextion::process_nextion_commands_() { uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Got touch at x=%u y=%u type=%s", x, y, touch_event ? "PRESS" : "RELEASE"); + ESP_LOGD(TAG, "Got touch event:"); + ESP_LOGD(TAG, " x: %u", x); + ESP_LOGD(TAG, " y: %u", y); + ESP_LOGD(TAG, " type: %s", touch_event ? "PRESS" : "RELEASE"); break; } @@ -588,7 +593,9 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - ESP_LOGN(TAG, "Got Switch variable_name=%s value=%d", variable_name.c_str(), to_process[0] != 0); + ESP_LOGN(TAG, "Got Switch:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", to_process[0] != 0); for (auto *switchtype : this->switchtype_) { switchtype->process_bool(variable_name, to_process[index] != 0); @@ -619,7 +626,9 @@ void Nextion::process_nextion_commands_() { value += to_process[i + index + 1] << (8 * i); } - ESP_LOGN(TAG, "Got sensor variable_name=%s value=%d", variable_name.c_str(), value); + ESP_LOGN(TAG, "Got sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", value); for (auto *sensor : this->sensortype_) { sensor->process_sensor(variable_name, value); @@ -651,7 +660,9 @@ void Nextion::process_nextion_commands_() { text_value = to_process.substr(index); - ESP_LOGN(TAG, "Got Text Sensor variable_name=%s value=%s", variable_name.c_str(), text_value.c_str()); + ESP_LOGN(TAG, "Got Text Sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %s", text_value.c_str()); // NextionTextSensorResponseQueue *nq = new NextionTextSensorResponseQueue; // nq->variable_name = variable_name; @@ -682,7 +693,9 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - ESP_LOGN(TAG, "Got Binary Sensor variable_name=%s value=%d", variable_name.c_str(), to_process[index] != 0); + ESP_LOGN(TAG, "Got Binary Sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", to_process[index] != 0); for (auto *binarysensortype : this->binarysensortype_) { binarysensortype->process_bool(&variable_name[0], to_process[index] != 0); @@ -772,7 +785,10 @@ void Nextion::set_nextion_sensor_state(int queue_type, const std::string &name, } void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state) { - ESP_LOGN(TAG, "Received state for variable %s, state %lf for queue type %d", name.c_str(), state, queue_type); + ESP_LOGN(TAG, "Received state:"); + ESP_LOGN(TAG, " variable: %s", name.c_str()); + ESP_LOGN(TAG, " state: %lf", state); + ESP_LOGN(TAG, " queue type: %d", queue_type); switch (queue_type) { case NextionQueueType::SENSOR: { @@ -809,7 +825,9 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s } void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { - ESP_LOGD(TAG, "Received state for variable %s, state %s", name.c_str(), state.c_str()); + ESP_LOGD(TAG, "Received state:"); + ESP_LOGD(TAG, " variable: %s", name.c_str()); + ESP_LOGD(TAG, " state: %s", state.c_str()); for (auto *sensor : this->textsensortype_) { if (name == sensor->get_variable_name()) { From 8e4b9c3c1e38f8aead90f19aa726143e86aad1a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:45:26 +1300 Subject: [PATCH 066/436] Voice Assistant improvements (#5827) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 2 + .../i2s_audio/speaker/i2s_audio_speaker.h | 2 + esphome/components/speaker/speaker.h | 2 + .../voice_assistant/voice_assistant.cpp | 108 +++++++++++------- .../voice_assistant/voice_assistant.h | 3 + 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index ed13e6b458..e729cdf954 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -220,6 +220,8 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length) { return index; } +bool I2SAudioSpeaker::has_buffered_data() const { return uxQueueMessagesWaiting(this->buffer_queue_) > 0; } + } // namespace i2s_audio } // namespace esphome diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index b075722e1b..20c36a69d3 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -56,6 +56,8 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud size_t play(const uint8_t *data, size_t length) override; + bool has_buffered_data() const override; + protected: void start_(); // void stop_(); diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 3f520e3c5e..b494873160 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -18,6 +18,8 @@ class Speaker { virtual void start() = 0; virtual void stop() = 0; + virtual bool has_buffered_data() const = 0; + bool is_running() const { return this->state_ == STATE_RUNNING; } protected: diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 9b13a71039..c0e706305d 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -273,28 +273,27 @@ void VoiceAssistant::loop() { bool playing = false; #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + ssize_t received_len = 0; if (this->speaker_buffer_index_ + RECEIVE_SIZE < SPEAKER_BUFFER_SIZE) { - auto len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE); - if (len > 0) { - this->speaker_buffer_index_ += len; - this->speaker_buffer_size_ += len; + received_len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE); + if (received_len > 0) { + this->speaker_buffer_index_ += received_len; + this->speaker_buffer_size_ += received_len; + this->speaker_bytes_received_ += received_len; } } else { - ESP_LOGW(TAG, "Receive buffer full"); - } - if (this->speaker_buffer_size_ > 0) { - size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); - if (written > 0) { - memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); - this->speaker_buffer_size_ -= written; - this->speaker_buffer_index_ -= written; - this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); - } else { - ESP_LOGW(TAG, "Speaker buffer full"); - } + ESP_LOGD(TAG, "Receive buffer full"); } + // Build a small buffer of audio before sending to the speaker + if (this->speaker_bytes_received_ > RECEIVE_SIZE * 4) + this->write_speaker_(); if (this->wait_for_stream_end_) { this->cancel_timeout("playing"); + if (this->stream_ended_ && received_len < 0) { + ESP_LOGD(TAG, "End of audio stream received"); + this->cancel_timeout("speaker-timeout"); + this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); + } break; // We dont want to timeout here as the STREAM_END event will take care of that. } playing = this->speaker_->is_running(); @@ -316,14 +315,26 @@ void VoiceAssistant::loop() { case State::RESPONSE_FINISHED: { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + if (this->speaker_buffer_size_ > 0) { + this->write_speaker_(); + break; + } + if (this->speaker_->has_buffered_data() || this->speaker_->is_running()) { + break; + } + ESP_LOGD(TAG, "Speaker has finished outputting all audio"); this->speaker_->stop(); this->cancel_timeout("speaker-timeout"); this->cancel_timeout("playing"); this->speaker_buffer_size_ = 0; this->speaker_buffer_index_ = 0; + this->speaker_bytes_received_ = 0; memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); + this->wait_for_stream_end_ = false; + this->stream_ended_ = false; + + this->tts_stream_end_trigger_->trigger(); } - this->wait_for_stream_end_ = false; #endif this->set_state_(State::IDLE, State::IDLE); break; @@ -333,6 +344,20 @@ void VoiceAssistant::loop() { } } +void VoiceAssistant::write_speaker_() { + if (this->speaker_buffer_size_ > 0) { + size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); + if (written > 0) { + memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); + this->speaker_buffer_size_ -= written; + this->speaker_buffer_index_ -= written; + this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); + } else { + ESP_LOGD(TAG, "Speaker buffer full, trying again next loop"); + } + } +} + void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { if (!subscribe) { if (this->api_client_ == nullptr || client != this->api_client_) { @@ -503,21 +528,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); - this->start_trigger_->trigger(); + this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_END: { ESP_LOGD(TAG, "Wake word detected"); - this->wake_word_detected_trigger_->trigger(); + this->defer([this]() { this->wake_word_detected_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_STT_START: ESP_LOGD(TAG, "STT started"); - this->listening_trigger_->trigger(); + this->defer([this]() { this->listening_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_END: { - this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); std::string text; for (auto arg : msg.data) { if (arg.name == "text") { @@ -529,12 +553,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); - this->stt_end_trigger_->trigger(text); + this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); break; } case api::enums::VOICE_ASSISTANT_INTENT_START: ESP_LOGD(TAG, "Intent started"); - this->intent_start_trigger_->trigger(); + this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { @@ -542,7 +566,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->conversation_id_ = std::move(arg.value); } } - this->intent_end_trigger_->trigger(); + this->defer([this]() { this->intent_end_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -557,10 +581,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); - this->tts_start_trigger_->trigger(text); + this->defer([this, text]() { + this->tts_start_trigger_->trigger(text); #ifdef USE_SPEAKER - this->speaker_->start(); + this->speaker_->start(); #endif + }); break; } case api::enums::VOICE_ASSISTANT_TTS_END: { @@ -575,14 +601,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); + this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { - this->media_player_->make_call().set_media_url(url).perform(); - } + if (this->media_player_ != nullptr) { + this->media_player_->make_call().set_media_url(url).perform(); + } #endif + this->tts_end_trigger_->trigger(url); + }); State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; this->set_state_(new_state, new_state); - this->tts_end_trigger_->trigger(url); break; } case api::enums::VOICE_ASSISTANT_RUN_END: { @@ -599,7 +627,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->set_state_(State::IDLE, State::IDLE); } } - this->end_trigger_->trigger(); + this->defer([this]() { this->end_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_ERROR: { @@ -617,8 +645,10 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } else if (code == "wake-provider-missing" || code == "wake-engine-missing") { // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. - this->request_stop(); - this->error_trigger_->trigger(code, message); + this->defer([this, code, message]() { + this->request_stop(); + this->error_trigger_->trigger(code, message); + }); return; } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); @@ -626,32 +656,32 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->signal_stop_(); this->set_state_(State::STOP_MICROPHONE, State::IDLE); } - this->error_trigger_->trigger(code, message); + this->defer([this, code, message]() { this->error_trigger_->trigger(code, message); }); break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER this->wait_for_stream_end_ = true; ESP_LOGD(TAG, "TTS stream start"); - this->tts_stream_start_trigger_->trigger(); + this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { - this->set_state_(State::RESPONSE_FINISHED, State::IDLE); #ifdef USE_SPEAKER + this->stream_ended_ = true; ESP_LOGD(TAG, "TTS stream end"); - this->tts_stream_end_trigger_->trigger(); #endif break; } case api::enums::VOICE_ASSISTANT_STT_VAD_START: ESP_LOGD(TAG, "Starting STT by VAD"); - this->stt_vad_start_trigger_->trigger(); + this->defer([this]() { this->stt_vad_start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_VAD_END: ESP_LOGD(TAG, "STT by VAD end"); - this->stt_vad_end_trigger_->trigger(); + this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); + this->defer([this]() { this->stt_vad_end_trigger_->trigger(); }); break; default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index f6dcd1c563..66bf4c3c57 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -156,11 +156,14 @@ class VoiceAssistant : public Component { microphone::Microphone *mic_{nullptr}; #ifdef USE_SPEAKER + void write_speaker_(); speaker::Speaker *speaker_{nullptr}; uint8_t *speaker_buffer_; size_t speaker_buffer_index_{0}; size_t speaker_buffer_size_{0}; + size_t speaker_bytes_received_{0}; bool wait_for_stream_end_{false}; + bool stream_ended_{false}; #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; From ff97639f791963bb9e5f35318be960a100f024b0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:43:41 +1300 Subject: [PATCH 067/436] Fix missing include in remote_base (#5843) --- esphome/components/remote_base/remote_base.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 40c699e8ea..095f95053f 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -1,6 +1,8 @@ #include "remote_base.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace remote_base { From 687f5ca6330c7b96e92823bd28f9d68731404152 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:57:40 +1300 Subject: [PATCH 068/436] Add 'voice_assistant.connected' condition (#5845) --- esphome/components/voice_assistant/__init__.py | 12 ++++++++++++ esphome/components/voice_assistant/voice_assistant.h | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index d05f39072c..59aef901f2 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -57,6 +57,9 @@ StopAction = voice_assistant_ns.class_( IsRunningCondition = voice_assistant_ns.class_( "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) ) +ConnectedCondition = voice_assistant_ns.class_( + "ConnectedCondition", automation.Condition, cg.Parented.template(VoiceAssistant) +) def tts_stream_validate(config): @@ -298,3 +301,12 @@ async def voice_assistant_is_running_to_code(config, condition_id, template_arg, var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +@register_condition( + "voice_assistant.connected", ConnectedCondition, VOICE_ASSISTANT_ACTION_SCHEMA +) +async def voice_assistant_connected_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 66bf4c3c57..f9325dff54 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -222,6 +222,11 @@ template class IsRunningCondition : public Condition, pub bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; +template class ConnectedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->get_api_connection() != nullptr; } +}; + extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace voice_assistant From 28a3cddde374a07c880622cd13095ce7dfbbec4d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:14:26 +1300 Subject: [PATCH 069/436] Bump version to 2023.11.5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5596cbcd36..2e693d0ea0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.4" +__version__ = "2023.11.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4e6d3729e178999daea09421e42ccf022c768509 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Nov 2023 16:39:24 -0600 Subject: [PATCH 070/436] dashboard: Small cleanups to dashboard (#5841) --- esphome/dashboard/entries.py | 2 +- esphome/dashboard/settings.py | 24 +++--- esphome/dashboard/web_server.py | 135 +++++++++++++++++--------------- 3 files changed, 87 insertions(+), 74 deletions(-) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index 8ccfa795d5..ad139b830b 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -262,7 +262,7 @@ class DashboardEntry: self.state = EntryState.UNKNOWN self._to_dict: dict[str, Any] | None = None - def __repr__(self): + def __repr__(self) -> str: """Return the representation of this entry.""" return ( f"DashboardEntry(path={self.path} " diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 61718298d2..1a5b1620e8 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -23,45 +23,45 @@ class DashboardSettings: self.cookie_secret: str | None = None self.absolute_config_dir: Path | None = None - def parse_args(self, args): + def parse_args(self, args: Any) -> None: self.on_ha_addon: bool = args.ha_addon - password: str = args.password or os.getenv("PASSWORD", "") + password = args.password or os.getenv("PASSWORD") or "" if not self.on_ha_addon: - self.username: str = args.username or os.getenv("USERNAME", "") + self.username = args.username or os.getenv("USERNAME") or "" self.using_password = bool(password) if self.using_password: self.password_hash = password_hash(password) - self.config_dir: str = args.configuration - self.absolute_config_dir: Path = Path(self.config_dir).resolve() + self.config_dir = args.configuration + self.absolute_config_dir = Path(self.config_dir).resolve() CORE.config_path = os.path.join(self.config_dir, ".") @property - def relative_url(self): - return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/") + def relative_url(self) -> str: + return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL") or "/" @property def status_use_ping(self): return get_bool_env("ESPHOME_DASHBOARD_USE_PING") @property - def status_use_mqtt(self): + def status_use_mqtt(self) -> bool: return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT") @property - def using_ha_addon_auth(self): + def using_ha_addon_auth(self) -> bool: if not self.on_ha_addon: return False return not get_bool_env("DISABLE_HA_AUTHENTICATION") @property - def using_auth(self): + def using_auth(self) -> bool: return self.using_password or self.using_ha_addon_auth @property - def streamer_mode(self): + def streamer_mode(self) -> bool: return get_bool_env("ESPHOME_STREAMER_MODE") - def check_password(self, username, password): + def check_password(self, username: str, password: str) -> bool: if not self.using_auth: return True if username != self.username: diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9a9ccb462b..9bbf0b28dc 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -14,12 +14,14 @@ import shutil import subprocess import threading from pathlib import Path -from typing import Any +from typing import Any, Callable, TypeVar +from collections.abc import Iterable import tornado import tornado.concurrent import tornado.gen import tornado.httpserver +import tornado.httputil import tornado.ioloop import tornado.iostream import tornado.netutil @@ -27,9 +29,9 @@ import tornado.process import tornado.queues import tornado.web import tornado.websocket -import tornado.httputil import yaml from tornado.log import access_log +from yaml.nodes import Node from esphome import const, platformio_api, yaml_util from esphome.helpers import get_bool_env, mkdir_p @@ -54,7 +56,7 @@ cookie_authenticated_yes = b"yes" settings = DASHBOARD.settings -def template_args(): +def template_args() -> dict[str, Any]: version = const.__version__ if "b" in version: docs_link = "https://beta.esphome.io/" @@ -73,9 +75,12 @@ def template_args(): } -def authenticated(func): +T = TypeVar("T", bound=Callable[..., Any]) + + +def authenticated(func: T) -> T: @functools.wraps(func) - def decorator(self, *args, **kwargs): + def decorator(self, *args: Any, **kwargs: Any): if not is_authenticated(self): self.redirect("./login") return None @@ -209,7 +214,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) @property - def is_process_active(self): + def is_process_active(self) -> bool: return self._proc is not None and self._proc.returncode is None @websocket_method("stdin") @@ -398,7 +403,7 @@ class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): class SerialPortRequestHandler(BaseHandler): @authenticated - async def get(self): + async def get(self) -> None: ports = await asyncio.get_running_loop().run_in_executor(None, get_serial_ports) data = [] for port in ports: @@ -418,7 +423,7 @@ class SerialPortRequestHandler(BaseHandler): class WizardRequestHandler(BaseHandler): @authenticated - def post(self): + def post(self) -> None: from esphome import wizard kwargs = { @@ -449,7 +454,7 @@ class WizardRequestHandler(BaseHandler): class ImportRequestHandler(BaseHandler): @authenticated - def post(self): + def post(self) -> None: from esphome.components.dashboard_import import import_config dashboard = DASHBOARD @@ -504,7 +509,7 @@ class ImportRequestHandler(BaseHandler): class DownloadListRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + def get(self, configuration: str | None = None) -> None: storage_path = ext_storage_path(configuration) storage_json = StorageJSON.load(storage_path) if storage_json is None: @@ -512,26 +517,29 @@ class DownloadListRequestHandler(BaseHandler): return from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS - from esphome.components.esp32 import get_download_types as esp32_types - from esphome.components.esp8266 import get_download_types as esp8266_types - from esphome.components.libretiny import get_download_types as libretiny_types - from esphome.components.rp2040 import get_download_types as rp2040_types downloads = [] - platform = storage_json.target_platform.lower() + platform: str = storage_json.target_platform.lower() if platform == const.PLATFORM_RP2040: + from esphome.components.rp2040 import get_download_types as rp2040_types + downloads = rp2040_types(storage_json) elif platform == const.PLATFORM_ESP8266: + from esphome.components.esp8266 import get_download_types as esp8266_types + downloads = esp8266_types(storage_json) elif platform.upper() in ESP32_VARIANTS: + from esphome.components.esp32 import get_download_types as esp32_types + downloads = esp32_types(storage_json) - elif platform == const.PLATFORM_BK72XX: - downloads = libretiny_types(storage_json) - elif platform == const.PLATFORM_RTL87XX: + elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX): + from esphome.components.libretiny import ( + get_download_types as libretiny_types, + ) + downloads = libretiny_types(storage_json) else: - self.send_error(418) - return + raise ValueError(f"Unknown platform {platform}") self.set_status(200) self.set_header("content-type", "application/json") @@ -551,7 +559,7 @@ class DownloadBinaryRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration: str | None = None): + async def get(self, configuration: str | None = None) -> None: """Download a binary file.""" loop = asyncio.get_running_loop() compressed = self.get_argument("compressed", "0") == "1" @@ -618,7 +626,7 @@ class DownloadBinaryRequestHandler(BaseHandler): class EsphomeVersionHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: self.set_header("Content-Type", "application/json") self.write(json.dumps({"version": const.__version__})) self.finish() @@ -626,7 +634,7 @@ class EsphomeVersionHandler(BaseHandler): class ListDevicesHandler(BaseHandler): @authenticated - async def get(self): + async def get(self) -> None: dashboard = DASHBOARD await dashboard.entries.async_request_update_entries() entries = dashboard.entries.async_all() @@ -656,7 +664,7 @@ class ListDevicesHandler(BaseHandler): class MainRequestHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: begin = bool(self.get_argument("begin", False)) self.render( @@ -669,7 +677,7 @@ class MainRequestHandler(BaseHandler): class PrometheusServiceDiscoveryHandler(BaseHandler): @authenticated - async def get(self): + async def get(self) -> None: dashboard = DASHBOARD await dashboard.entries.async_request_update_entries() entries = dashboard.entries.async_all() @@ -698,29 +706,34 @@ class PrometheusServiceDiscoveryHandler(BaseHandler): class BoardsRequestHandler(BaseHandler): @authenticated - def get(self, platform: str): - from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS - from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS - from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS - from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS - from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS - - platform_to_boards = { - const.PLATFORM_ESP32: ESP32_BOARDS, - const.PLATFORM_ESP8266: ESP8266_BOARDS, - const.PLATFORM_RP2040: RP2040_BOARDS, - const.PLATFORM_BK72XX: BK72XX_BOARDS, - const.PLATFORM_RTL87XX: RTL87XX_BOARDS, - } + def get(self, platform: str) -> None: # filter all ESP32 variants by requested platform if platform.startswith("esp32"): + from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS + boards = { k: v - for k, v in platform_to_boards[const.PLATFORM_ESP32].items() + for k, v in ESP32_BOARDS.items() if v[const.KEY_VARIANT] == platform.upper() } + elif platform == const.PLATFORM_ESP8266: + from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS + + boards = ESP8266_BOARDS + elif platform == const.PLATFORM_RP2040: + from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS + + boards = RP2040_BOARDS + elif platform == const.PLATFORM_BK72XX: + from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS + + boards = BK72XX_BOARDS + elif platform == const.PLATFORM_RTL87XX: + from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS + + boards = RTL87XX_BOARDS else: - boards = platform_to_boards[platform] + raise ValueError(f"Unknown platform {platform}") # map to a {board_name: board_title} dict platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} @@ -734,7 +747,7 @@ class BoardsRequestHandler(BaseHandler): class PingRequestHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: dashboard = DASHBOARD dashboard.ping_request.set() if settings.status_use_mqtt: @@ -754,7 +767,7 @@ class PingRequestHandler(BaseHandler): class InfoRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration=None): + async def get(self, configuration: str | None = None) -> None: yaml_path = settings.rel_path(configuration) dashboard = DASHBOARD entry = dashboard.entries.get(yaml_path) @@ -770,7 +783,7 @@ class InfoRequestHandler(BaseHandler): class EditRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration: str | None = None): + async def get(self, configuration: str | None = None) -> None: """Get the content of a file.""" loop = asyncio.get_running_loop() filename = settings.rel_path(configuration) @@ -788,7 +801,7 @@ class EditRequestHandler(BaseHandler): @authenticated @bind_config - async def post(self, configuration: str | None = None): + async def post(self, configuration: str | None = None) -> None: """Write the content of a file.""" loop = asyncio.get_running_loop() config_file = settings.rel_path(configuration) @@ -805,7 +818,7 @@ class EditRequestHandler(BaseHandler): class DeleteRequestHandler(BaseHandler): @authenticated @bind_config - def post(self, configuration=None): + def post(self, configuration: str | None = None) -> None: config_file = settings.rel_path(configuration) storage_path = ext_storage_path(configuration) @@ -825,20 +838,20 @@ class DeleteRequestHandler(BaseHandler): class UndoDeleteRequestHandler(BaseHandler): @authenticated @bind_config - def post(self, configuration=None): + def post(self, configuration: str | None = None) -> None: config_file = settings.rel_path(configuration) trash_path = trash_storage_path() shutil.move(os.path.join(trash_path, configuration), config_file) class LoginHandler(BaseHandler): - def get(self): + def get(self) -> None: if is_authenticated(self): self.redirect("./") else: self.render_login_page() - def render_login_page(self, error=None): + def render_login_page(self, error: str | None = None) -> None: self.render( "login.template.html", error=error, @@ -847,7 +860,7 @@ class LoginHandler(BaseHandler): **template_args(), ) - def post_ha_addon_login(self): + def post_ha_addon_login(self) -> None: import requests headers = { @@ -874,7 +887,7 @@ class LoginHandler(BaseHandler): self.set_status(401) self.render_login_page(error="Invalid username or password") - def post_native_login(self): + def post_native_login(self) -> None: username = self.get_argument("username", "") password = self.get_argument("password", "") if settings.check_password(username, password): @@ -887,7 +900,7 @@ class LoginHandler(BaseHandler): self.set_status(401) self.render_login_page(error=error_str) - def post(self): + def post(self) -> None: if settings.using_ha_addon_auth: self.post_ha_addon_login() else: @@ -896,14 +909,14 @@ class LoginHandler(BaseHandler): class LogoutHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: self.clear_cookie("authenticated") self.redirect("./login") class SecretKeysRequestHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: filename = None for secret_filename in const.SECRETS_FILES: @@ -923,10 +936,10 @@ class SecretKeysRequestHandler(BaseHandler): class SafeLoaderIgnoreUnknown(FastestAvailableSafeLoader): - def ignore_unknown(self, node): + def ignore_unknown(self, node: Node) -> str: return f"{node.tag} {node.value}" - def construct_yaml_binary(self, node) -> str: + def construct_yaml_binary(self, node: Node) -> str: return super().construct_yaml_binary(node).decode("ascii") @@ -939,7 +952,7 @@ SafeLoaderIgnoreUnknown.add_constructor( class JsonConfigRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration=None): + async def get(self, configuration: str | None = None) -> None: filename = settings.rel_path(configuration) if not os.path.isfile(filename): self.send_error(404) @@ -959,7 +972,7 @@ class JsonConfigRequestHandler(BaseHandler): self.finish() -def get_base_frontend_path(): +def get_base_frontend_path() -> str: if ENV_DEV not in os.environ: import esphome_dashboard @@ -973,12 +986,12 @@ def get_base_frontend_path(): return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) -def get_static_path(*args): +def get_static_path(*args: Iterable[str]) -> str: return os.path.join(get_base_frontend_path(), "static", *args) @functools.cache -def get_static_file_url(name): +def get_static_file_url(name: str) -> str: base = f"./static/{name}" if ENV_DEV in os.environ: @@ -997,7 +1010,7 @@ def get_static_file_url(name): def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application: - def log_function(handler): + def log_function(handler: tornado.web.RequestHandler) -> None: if handler.get_status() < 400: log_method = access_log.info From 4b6fbd5db016ee2e50a4b8bb2f416c2d6c4e7e59 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Mon, 27 Nov 2023 23:43:03 +0100 Subject: [PATCH 071/436] Pylontech integration (solar battery bank) (#4688) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/pylontech/__init__.py | 46 +++++++++ esphome/components/pylontech/pylontech.cpp | 91 +++++++++++++++++ esphome/components/pylontech/pylontech.h | 53 ++++++++++ .../components/pylontech/sensor/__init__.py | 97 +++++++++++++++++++ .../pylontech/sensor/pylontech_sensor.cpp | 60 ++++++++++++ .../pylontech/sensor/pylontech_sensor.h | 32 ++++++ .../pylontech/text_sensor/__init__.py | 41 ++++++++ .../text_sensor/pylontech_text_sensor.cpp | 40 ++++++++ .../text_sensor/pylontech_text_sensor.h | 26 +++++ tests/test4.yaml | 41 ++++++++ 11 files changed, 528 insertions(+) create mode 100644 esphome/components/pylontech/__init__.py create mode 100644 esphome/components/pylontech/pylontech.cpp create mode 100644 esphome/components/pylontech/pylontech.h create mode 100644 esphome/components/pylontech/sensor/__init__.py create mode 100644 esphome/components/pylontech/sensor/pylontech_sensor.cpp create mode 100644 esphome/components/pylontech/sensor/pylontech_sensor.h create mode 100644 esphome/components/pylontech/text_sensor/__init__.py create mode 100644 esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp create mode 100644 esphome/components/pylontech/text_sensor/pylontech_text_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 2cd08a780e..e6bc53ee6f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,7 @@ esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz +esphome/components/pylontech/* @functionpointer esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje esphome/components/qwiic_pir/* @kahrendt diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py new file mode 100644 index 0000000000..56fac92e89 --- /dev/null +++ b/esphome/components/pylontech/__init__.py @@ -0,0 +1,46 @@ +import logging +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@functionpointer"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +CONF_PYLONTECH_ID = "pylontech_id" +CONF_BATTERY = "battery" + +pylontech_ns = cg.esphome_ns.namespace("pylontech") +PylontechComponent = pylontech_ns.class_( + "PylontechComponent", cg.PollingComponent, uart.UARTDevice +) +PylontechBattery = pylontech_ns.class_("PylontechBattery") + +CV_NUM_BATTERIES = cv.int_range(1, 6) + +PYLONTECH_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_PYLONTECH_ID): cv.use_id(PylontechComponent), + cv.Required(CONF_BATTERY): CV_NUM_BATTERIES, + } +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PylontechComponent), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp new file mode 100644 index 0000000000..4bfa876110 --- /dev/null +++ b/esphome/components/pylontech/pylontech.cpp @@ -0,0 +1,91 @@ +#include "pylontech.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech"; +static const int MAX_DATA_LENGTH_BYTES = 256; +static const uint8_t ASCII_LF = 0x0A; + +PylontechComponent::PylontechComponent() {} + +void PylontechComponent::dump_config() { + this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); + ESP_LOGCONFIG(TAG, "pylontech:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Connection with pylontech failed!"); + } + + for (PylontechListener *listener : this->listeners_) { + listener->dump_config(); + } + + LOG_UPDATE_INTERVAL(this); +} + +void PylontechComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up pylontech..."); + while (this->available() != 0) { + this->read(); + } +} + +void PylontechComponent::update() { this->write_str("pwr\n"); } + +void PylontechComponent::loop() { + uint8_t data; + + // pylontech sends a lot of data very suddenly + // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_[buffer_index_write_] += (char) data; + if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || + buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { + // complete line received + buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; + } + } + } + + // only process one line per call of loop() to not block esphome for too long + if (buffer_index_read_ != buffer_index_write_) { + this->process_line_(buffer_[buffer_index_read_]); + buffer_[buffer_index_read_].clear(); + buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS; + } +} + +void PylontechComponent::process_line_(std::string &buffer) { + ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str()); + // clang-format off + // example line to parse: + // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St + // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // clang-format on + + PylontechListener::LineContents l{}; + const int parsed = sscanf( // NOLINT + buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %d %*s", // NOLINT + &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT + l.curr_st, l.temp_st, &l.coulomb, &l.mostempr); // NOLINT + + if (l.bat_num <= 0) { + ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + if (parsed != 14) { + ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + + for (PylontechListener *listener : this->listeners_) { + listener->on_line_read(&l); + } +} + +float PylontechComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h new file mode 100644 index 0000000000..3282cb4d9f --- /dev/null +++ b/esphome/components/pylontech/pylontech.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace pylontech { + +static const uint8_t NUM_BUFFERS = 20; +static const uint8_t TEXT_SENSOR_MAX_LEN = 8; + +class PylontechListener { + public: + struct LineContents { + int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr; + char base_st[TEXT_SENSOR_MAX_LEN], volt_st[TEXT_SENSOR_MAX_LEN], curr_st[TEXT_SENSOR_MAX_LEN], + temp_st[TEXT_SENSOR_MAX_LEN]; + }; + + virtual void on_line_read(LineContents *line); + virtual void dump_config(); +}; + +class PylontechComponent : public PollingComponent, public uart::UARTDevice { + public: + PylontechComponent(); + + /// Schedule data readings. + void update() override; + /// Read data once available + void loop() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + void register_listener(PylontechListener *listener) { this->listeners_.push_back(listener); } + + protected: + void process_line_(std::string &buffer); + + // ring buffer + std::string buffer_[NUM_BUFFERS]; + int buffer_index_write_ = 0; + int buffer_index_read_ = 0; + + std::vector listeners_{}; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py new file mode 100644 index 0000000000..0423f3370c --- /dev/null +++ b/esphome/components/pylontech/sensor/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_VOLTAGE, + CONF_CURRENT, + CONF_TEMPERATURE, + UNIT_VOLT, + UNIT_AMPERE, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_BATTERY, + UNIT_CELSIUS, + UNIT_PERCENT, + CONF_ID, +) + +from .. import ( + CONF_PYLONTECH_ID, + PYLONTECH_COMPONENT_SCHEMA, + CONF_BATTERY, + pylontech_ns, +) + +PylontechSensor = pylontech_ns.class_("PylontechSensor", cg.Component) + +CONF_COULOMB = "coulomb" +CONF_TEMPERATURE_LOW = "temperature_low" +CONF_TEMPERATURE_HIGH = "temperature_high" +CONF_VOLTAGE_LOW = "voltage_low" +CONF_VOLTAGE_HIGH = "voltage_high" +CONF_MOS_TEMPERATURE = "mos_temperature" + +TYPES: dict[str, cv.Schema] = { + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_TEMPERATURE_LOW: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_TEMPERATURE_HIGH: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_VOLTAGE_LOW: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_VOLTAGE_HIGH: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_COULOMB: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + ), + CONF_MOS_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), +} + +CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PylontechSensor)} +).extend({cv.Optional(marker): schema for marker, schema in TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) + bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) + + for marker in TYPES: + if marker_config := config.get(marker): + sens = await sensor.new_sensor(marker_config) + cg.add(getattr(bat, f"set_{marker}_sensor")(sens)) + + cg.add(paren.register_listener(bat)) diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.cpp b/esphome/components/pylontech/sensor/pylontech_sensor.cpp new file mode 100644 index 0000000000..5b5db0731e --- /dev/null +++ b/esphome/components/pylontech/sensor/pylontech_sensor.cpp @@ -0,0 +1,60 @@ +#include "pylontech_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech.sensor"; + +PylontechSensor::PylontechSensor(int8_t bat_num) { this->bat_num_ = bat_num; } + +void PylontechSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Pylontech Sensor:"); + ESP_LOGCONFIG(TAG, " Battery %d", this->bat_num_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Temperature low", this->temperature_low_sensor_); + LOG_SENSOR(" ", "Temperature high", this->temperature_high_sensor_); + LOG_SENSOR(" ", "Voltage low", this->voltage_low_sensor_); + LOG_SENSOR(" ", "Voltage high", this->voltage_high_sensor_); + LOG_SENSOR(" ", "Coulomb", this->coulomb_sensor_); + LOG_SENSOR(" ", "MOS Temperature", this->mos_temperature_sensor_); +} + +void PylontechSensor::on_line_read(PylontechListener::LineContents *line) { + if (this->bat_num_ != line->bat_num) { + return; + } + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(((float) line->volt) / 1000.0f); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(((float) line->curr) / 1000.0f); + } + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(((float) line->tempr) / 1000.0f); + } + if (this->temperature_low_sensor_ != nullptr) { + this->temperature_low_sensor_->publish_state(((float) line->tlow) / 1000.0f); + } + if (this->temperature_high_sensor_ != nullptr) { + this->temperature_high_sensor_->publish_state(((float) line->thigh) / 1000.0f); + } + if (this->voltage_low_sensor_ != nullptr) { + this->voltage_low_sensor_->publish_state(((float) line->vlow) / 1000.0f); + } + if (this->voltage_high_sensor_ != nullptr) { + this->voltage_high_sensor_->publish_state(((float) line->vhigh) / 1000.0f); + } + if (this->coulomb_sensor_ != nullptr) { + this->coulomb_sensor_->publish_state(line->coulomb); + } + if (this->mos_temperature_sensor_ != nullptr) { + this->mos_temperature_sensor_->publish_state(((float) line->mostempr) / 1000.0f); + } +} + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.h b/esphome/components/pylontech/sensor/pylontech_sensor.h new file mode 100644 index 0000000000..8986adc26c --- /dev/null +++ b/esphome/components/pylontech/sensor/pylontech_sensor.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../pylontech.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace pylontech { + +class PylontechSensor : public PylontechListener, public Component { + public: + PylontechSensor(int8_t bat_num); + void dump_config() override; + + SUB_SENSOR(voltage) + SUB_SENSOR(current) + SUB_SENSOR(temperature) + SUB_SENSOR(temperature_low) + SUB_SENSOR(temperature_high) + SUB_SENSOR(voltage_low) + SUB_SENSOR(voltage_high) + + SUB_SENSOR(coulomb) + SUB_SENSOR(mos_temperature) + + void on_line_read(LineContents *line) override; + + protected: + int8_t bat_num_; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/text_sensor/__init__.py b/esphome/components/pylontech/text_sensor/__init__.py new file mode 100644 index 0000000000..d6ccc678f8 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID + +from .. import ( + CONF_PYLONTECH_ID, + PYLONTECH_COMPONENT_SCHEMA, + CONF_BATTERY, + pylontech_ns, +) + +PylontechTextSensor = pylontech_ns.class_("PylontechTextSensor", cg.Component) + +CONF_BASE_STATE = "base_state" +CONF_VOLTAGE_STATE = "voltage_state" +CONF_CURRENT_STATE = "current_state" +CONF_TEMPERATURE_STATE = "temperature_state" + +MARKERS: list[str] = [ + CONF_BASE_STATE, + CONF_VOLTAGE_STATE, + CONF_CURRENT_STATE, + CONF_TEMPERATURE_STATE, +] + +CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PylontechTextSensor)} +).extend({cv.Optional(marker): text_sensor.text_sensor_schema() for marker in MARKERS}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) + bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) + + for marker in MARKERS: + if marker_config := config.get(marker): + var = await text_sensor.new_text_sensor(marker_config) + cg.add(getattr(bat, f"set_{marker}_text_sensor")(var)) + + cg.add(paren.register_listener(bat)) diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp new file mode 100644 index 0000000000..9e894bc570 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp @@ -0,0 +1,40 @@ +#include "pylontech_text_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech.textsensor"; + +PylontechTextSensor::PylontechTextSensor(int8_t bat_num) { this->bat_num_ = bat_num; } + +void PylontechTextSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Pylontech Text Sensor:"); + ESP_LOGCONFIG(TAG, " Battery %d", this->bat_num_); + LOG_TEXT_SENSOR(" ", "Base state", this->base_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Voltage state", this->voltage_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Current state", this->current_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Temperature state", this->temperature_state_text_sensor_); +} + +void PylontechTextSensor::on_line_read(PylontechListener::LineContents *line) { + if (this->bat_num_ != line->bat_num) { + return; + } + if (this->base_state_text_sensor_ != nullptr) { + this->base_state_text_sensor_->publish_state(std::string(line->base_st)); + } + if (this->voltage_state_text_sensor_ != nullptr) { + this->voltage_state_text_sensor_->publish_state(std::string(line->volt_st)); + } + if (this->current_state_text_sensor_ != nullptr) { + this->current_state_text_sensor_->publish_state(std::string(line->curr_st)); + } + if (this->temperature_state_text_sensor_ != nullptr) { + this->temperature_state_text_sensor_->publish_state(std::string(line->temp_st)); + } +} + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h new file mode 100644 index 0000000000..a685512ed5 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../pylontech.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace pylontech { + +class PylontechTextSensor : public PylontechListener, public Component { + public: + PylontechTextSensor(int8_t bat_num); + void dump_config() override; + + SUB_TEXT_SENSOR(base_state) + SUB_TEXT_SENSOR(voltage_state) + SUB_TEXT_SENSOR(current_state) + SUB_TEXT_SENSOR(temperature_state) + + void on_line_read(LineContents *line) override; + + protected: + int8_t bat_num_; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 63bf5bf0ae..69d9211969 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -99,6 +99,12 @@ pipsolar: id: inverter0 uart_id: uart115200 +pylontech: + - id: pylontech0 + uart_id: uart115200 + - id: pylontech1 + uart_id: uart115200 + sx1509: - id: sx1509_hub address: 0x3E @@ -113,6 +119,30 @@ dac7678: internal_reference: true sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world @@ -589,6 +619,17 @@ number: name: Tuya Number Copy text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state - platform: pipsolar pipsolar_id: inverter0 device_mode: From b8ee0dedec521b3edf87ca8bef2dbc1e59dea69a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:28:24 +1300 Subject: [PATCH 072/436] Fix write_speaker without speaker in config (#5847) --- esphome/components/voice_assistant/voice_assistant.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index c0e706305d..29fc664342 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -344,6 +344,7 @@ void VoiceAssistant::loop() { } } +#ifdef USE_SPEAKER void VoiceAssistant::write_speaker_() { if (this->speaker_buffer_size_ > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); @@ -357,6 +358,7 @@ void VoiceAssistant::write_speaker_() { } } } +#endif void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { if (!subscribe) { From 175f00f41bc8305cfa65bef7142ba7cdd4ee8140 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:28:24 +1300 Subject: [PATCH 073/436] Fix write_speaker without speaker in config (#5847) --- esphome/components/voice_assistant/voice_assistant.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index c0e706305d..29fc664342 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -344,6 +344,7 @@ void VoiceAssistant::loop() { } } +#ifdef USE_SPEAKER void VoiceAssistant::write_speaker_() { if (this->speaker_buffer_size_ > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); @@ -357,6 +358,7 @@ void VoiceAssistant::write_speaker_() { } } } +#endif void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { if (!subscribe) { From ed9fd173a9f791d0d77ef28e68117fa04c6f8e23 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:31:55 +1300 Subject: [PATCH 074/436] Bump version to 2023.11.6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 2e693d0ea0..6f8d83170e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.5" +__version__ = "2023.11.6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From a66dec738dba1bc1c4712ac561e731f7c5fedf00 Mon Sep 17 00:00:00 2001 From: Lucas Prim Date: Mon, 27 Nov 2023 20:40:39 -0300 Subject: [PATCH 075/436] Implement deep sleep and clear screen on Waveshare 7.5in B V3 (#5239) --- .../waveshare_epaper/waveshare_epaper.cpp | 20 ++++++++++--------- .../waveshare_epaper/waveshare_epaper.h | 4 ++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index f52808d295..53bfa57f4f 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1329,6 +1329,7 @@ void WaveshareEPaper7P5InBV2::dump_config() { LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P5InBV3::initialize() { this->init_display_(); } bool WaveshareEPaper7P5InBV3::wait_until_idle_() { if (this->busy_pin_ == nullptr) { return true; @@ -1341,12 +1342,13 @@ bool WaveshareEPaper7P5InBV3::wait_until_idle_() { ESP_LOGI(TAG, "Timeout while displaying image!"); return false; } + App.feed_wdt(); delay(10); } delay(200); // NOLINT return true; }; -void WaveshareEPaper7P5InBV3::initialize() { +void WaveshareEPaper7P5InBV3::init_display_() { this->reset_(); // COMMAND POWER SETTING @@ -1402,8 +1404,6 @@ void WaveshareEPaper7P5InBV3::initialize() { this->data(0x00); this->data(0x00); - this->wait_until_idle_(); - uint8_t lut_vcom_7_i_n5_v2[] = { 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0xF, 0x1, 0xF, 0x1, 0x2, 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, @@ -1449,24 +1449,26 @@ void WaveshareEPaper7P5InBV3::initialize() { this->command(0x24); // LUTBB for (count = 0; count < 42; count++) this->data(lut_bb_7_i_n5_v2[count]); - - this->command(0x10); - for (uint32_t i = 0; i < 800 * 480 / 8; i++) { - this->data(0xFF); - } }; void HOT WaveshareEPaper7P5InBV3::display() { + this->init_display_(); uint32_t buf_len = this->get_buffer_length_(); + this->command(0x10); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(0xFF); + } + this->command(0x13); // Start Transmission delay(2); for (uint32_t i = 0; i < buf_len; i++) { - this->data(~(this->buffer_[i])); + this->data(this->buffer_[i]); } this->command(0x12); // Display Refresh delay(100); // NOLINT this->wait_until_idle_(); + this->deep_sleep(); } int WaveshareEPaper7P5InBV3::get_width_internal() { return 800; } int WaveshareEPaper7P5InBV3::get_height_internal() { return 480; } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index c800d29643..f6ccf90861 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -429,6 +429,8 @@ class WaveshareEPaper7P5InBV3 : public WaveshareEPaper { this->data(0xA5); } + void clear_screen(); + protected: int get_width_internal() override; @@ -444,6 +446,8 @@ class WaveshareEPaper7P5InBV3 : public WaveshareEPaper { delay(200); // NOLINT } }; + + void init_display_(); }; class WaveshareEPaper7P5InBC : public WaveshareEPaper { From 496c29aa044bde02ce9081abafbe40a6db3a9798 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:42:22 +1100 Subject: [PATCH 076/436] Fix ESP-IDF uart initialisation sequence to match Espressif docs. (#5838) --- .../uart/uart_component_esp_idf.cpp | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 9b519c4568..c78626fa26 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -84,28 +84,9 @@ void IDFUARTComponent::setup() { return; } - err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, - /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will - block task until all data have been sent out.*/ - 0, - /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), - /* Flags used to allocate the interrupt. */ 0); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) invert |= UART_SIGNAL_TXD_INV; @@ -119,6 +100,25 @@ void IDFUARTComponent::setup() { return; } + err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, + /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will + block task until all data have been sent out.*/ + 0, + /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), + /* Flags used to allocate the interrupt. */ 0); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + xSemaphoreGive(this->lock_); } From ab1cc0ed6e26bb332637c2f0ce6d773ece2b37d3 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 01:24:43 +0100 Subject: [PATCH 077/436] Nextion - Align strings on `dump_config` (#5824) --- esphome/components/nextion/nextion.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 81e905b979..02ba94fcab 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -129,15 +129,15 @@ void Nextion::dump_config() { ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); if (this->touch_sleep_timeout_ != 0) { - ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); + ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); } if (this->wake_up_page_ != -1) { - ESP_LOGCONFIG(TAG, " Wake Up Page : %d", this->wake_up_page_); + ESP_LOGCONFIG(TAG, " Wake Up Page: %d", this->wake_up_page_); } if (this->start_up_page_ != -1) { - ESP_LOGCONFIG(TAG, " Start Up Page : %d", this->start_up_page_); + ESP_LOGCONFIG(TAG, " Start Up Page: %d", this->start_up_page_); } } From 993cd55b1df3f8c8cb6035e39612dfa6c7781c54 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:42:03 +1100 Subject: [PATCH 078/436] Speed up (and fix) ili9xxx display component. (#5406) --- esphome/components/ili9xxx/display.py | 107 ++++-- .../components/ili9xxx/ili9xxx_display.cpp | 344 +++++++----------- esphome/components/ili9xxx/ili9xxx_display.h | 106 ++++-- esphome/components/ili9xxx/ili9xxx_init.h | 27 ++ tests/test1.yaml | 13 + 5 files changed, 317 insertions(+), 280 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 6882d254e1..f321b2ed63 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, pins from esphome.components import display, spi, font +from esphome.components.display import validate_rotation from esphome.core import CORE, HexInt from esphome.const import ( CONF_COLOR_PALETTE, @@ -13,6 +14,9 @@ from esphome.const import ( CONF_PAGES, CONF_RESET_PIN, CONF_DIMENSIONS, + CONF_WIDTH, + CONF_HEIGHT, + CONF_ROTATION, ) DEPENDENCIES = ["spi"] @@ -26,28 +30,35 @@ def AUTO_LOAD(): CODEOWNERS = ["@nielsnl68", "@clydebarrow"] -ili9XXX_ns = cg.esphome_ns.namespace("ili9xxx") -ili9XXXSPI = ili9XXX_ns.class_( +ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") +ILI9XXXDisplay = ili9xxx_ns.class_( "ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer ) -ILI9XXXColorMode = ili9XXX_ns.enum("ILI9XXXColorMode") +ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode") +ColorOrder = display.display_ns.enum("ColorMode") MODELS = { - "M5STACK": ili9XXX_ns.class_("ILI9XXXM5Stack", ili9XXXSPI), - "M5CORE": ili9XXX_ns.class_("ILI9XXXM5CORE", ili9XXXSPI), - "TFT_2.4": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), - "TFT_2.4R": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), - "ILI9341": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), - "ILI9342": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), - "ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI), - "ILI9481-18": ili9XXX_ns.class_("ILI9XXXILI948118", ili9XXXSPI), - "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), - "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), - "ILI9488_A": ili9XXX_ns.class_("ILI9XXXILI9488A", ili9XXXSPI), - "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), - "S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI), - "S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI), + "M5STACK": ili9xxx_ns.class_("ILI9XXXM5Stack", ILI9XXXDisplay), + "M5CORE": ili9xxx_ns.class_("ILI9XXXM5CORE", ILI9XXXDisplay), + "TFT_2.4": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), + "TFT_2.4R": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay), + "ILI9341": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), + "ILI9342": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay), + "ILI9481": ili9xxx_ns.class_("ILI9XXXILI9481", ILI9XXXDisplay), + "ILI9481-18": ili9xxx_ns.class_("ILI9XXXILI948118", ILI9XXXDisplay), + "ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay), + "ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay), + "ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay), + "ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay), + "ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay), + "S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay), + "S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay), +} + +COLOR_ORDERS = { + "RGB": ColorOrder.COLOR_ORDER_RGB, + "BGR": ColorOrder.COLOR_ORDER_BGR, } COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") @@ -55,6 +66,14 @@ COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" CONF_INVERT_DISPLAY = "invert_display" +CONF_INVERT_COLORS = "invert_colors" +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" +CONF_SWAP_XY = "swap_xy" +CONF_COLOR_ORDER = "color_order" +CONF_OFFSET_HEIGHT = "offset_height" +CONF_OFFSET_WIDTH = "offset_width" +CONF_TRANSFORM = "transform" def _validate(config): @@ -77,6 +96,7 @@ def _validate(config): "TFT_2.4R", "ILI9341", "ILI9342", + "ST7789V", ]: raise cv.Invalid( "Provided model can't run on ESP8266. Use an ESP32 with PSRAM onboard" @@ -88,9 +108,19 @@ CONFIG_SCHEMA = cv.All( font.validate_pillow_installed, display.FULL_DISPLAY_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(ili9XXXSPI), + cv.GenerateID(): cv.declare_id(ILI9XXXDisplay), cv.Required(CONF_MODEL): cv.enum(MODELS, upper=True, space="_"), - cv.Optional(CONF_DIMENSIONS): cv.dimensions, + cv.Optional(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_, + cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_, + } + ), + ), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_LED_PIN): cv.invalid( @@ -101,7 +131,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list( cv.file_ ), - cv.Optional(CONF_INVERT_DISPLAY): cv.boolean, + cv.Optional(CONF_INVERT_DISPLAY): cv.invalid( + "'invert_display' has been replaced by 'invert_colors'" + ), + cv.Optional(CONF_INVERT_COLORS): cv.boolean, + cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True), + cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation, + cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), } ) .extend(cv.polling_component_schema("1s")) @@ -119,6 +161,13 @@ async def to_code(config): await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) + if CONF_COLOR_ORDER in config: + cg.add(var.set_color_order(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( @@ -131,9 +180,17 @@ async def to_code(config): cg.add(var.set_reset_pin(reset)) if CONF_DIMENSIONS in config: - cg.add( - var.set_dimentions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]) - ) + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + cg.add( + var.set_offsets( + dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT] + ) + ) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) rhs = None if config[CONF_COLOR_PALETTE] == "GRAYSCALE": @@ -178,5 +235,5 @@ async def to_code(config): prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_palette(prog_arr)) - if CONF_INVERT_DISPLAY in config: - cg.add(var.invert_display(config[CONF_INVERT_DISPLAY])) + if CONF_INVERT_COLORS in config: + cg.add(var.invert_colors(config[CONF_INVERT_COLORS])) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 902a9e6245..b315c8be87 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -8,11 +8,31 @@ namespace esphome { namespace ili9xxx { static const char *const TAG = "ili9xxx"; +static const uint16_t SPI_SETUP_US = 100; // estimated fixed overhead in microseconds for an SPI write +static const uint16_t SPI_MAX_BLOCK_SIZE = 4092; // Max size of continuous SPI transfer + +// store a 16 bit value in a buffer, big endian. +static inline void put16_be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} void ILI9XXXDisplay::setup() { + ESP_LOGD(TAG, "Setting up ILI9xxx"); + this->setup_pins_(); - this->initialize(); - this->command(this->pre_invertdisplay_ ? ILI9XXX_INVON : ILI9XXX_INVOFF); + this->init_lcd_(); + + this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF); + // custom x/y transform and color order + uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB; + if (this->swap_xy_) + mad |= MADCTL_MV; + if (this->mirror_x_) + mad |= MADCTL_MX; + if (this->mirror_y_) + mad |= MADCTL_MY; + this->send_command(ILI9XXX_MADCTL, &mad, 1); this->x_low_ = this->width_; this->y_low_ = this->height_; @@ -47,6 +67,8 @@ void ILI9XXXDisplay::setup_pins_() { void ILI9XXXDisplay::dump_config() { LOG_DISPLAY("", "ili9xxx", this); + ESP_LOGCONFIG(TAG, " Width Offset: %u", this->offset_x_); + ESP_LOGCONFIG(TAG, " Height Offset: %u", this->offset_y_); switch (this->buffer_color_mode_) { case BITS_8_INDEXED: ESP_LOGCONFIG(TAG, " Color mode: 8bit Indexed"); @@ -64,8 +86,12 @@ void ILI9XXXDisplay::dump_config() { ESP_LOGCONFIG(TAG, " Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); + ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_)); + ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_)); + ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_)); if (this->is_failed()) { ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!"); @@ -141,12 +167,14 @@ void HOT ILI9XXXDisplay::draw_absolute_pixel_internal(int x, int y, Color color) } if (updated) { // low and high watermark may speed up drawing from buffer - this->x_low_ = (x < this->x_low_) ? x : this->x_low_; - this->y_low_ = (y < this->y_low_) ? y : this->y_low_; - this->x_high_ = (x > this->x_high_) ? x : this->x_high_; - this->y_high_ = (y > this->y_high_) ? y : this->y_high_; - // ESP_LOGVV(TAG, "=>>> pixel (x:%d, y:%d) (xl:%d, xh:%d, yl:%d, yh:%d", x, y, this->x_low_, this->x_high_, - // this->y_low_, this->y_high_); + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; } } @@ -165,59 +193,82 @@ void ILI9XXXDisplay::update() { } void ILI9XXXDisplay::display_() { - // we will only update the changed window to the display - uint16_t w = this->x_high_ - this->x_low_ + 1; // NOLINT - uint16_t h = this->y_high_ - this->y_low_ + 1; // NOLINT - uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); - + uint8_t transfer_buffer[ILI9XXX_TRANSFER_BUFFER_SIZE]; // check if something was displayed if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { ESP_LOGV(TAG, "Nothing to display"); return; } - set_addr_window_(this->x_low_, this->y_low_, w, h); + // we will only update the changed rows to the display + size_t const w = this->x_high_ - this->x_low_ + 1; + size_t const h = this->y_high_ - this->y_low_ + 1; + size_t mhz = this->data_rate_ / 1000000; + // estimate time for a single write + size_t sw_time = this->width_ * h * 16 / mhz + this->width_ * h * 2 / SPI_MAX_BLOCK_SIZE * SPI_SETUP_US * 2; + // estimate time for multiple writes + size_t mw_time = (w * h * 16) / mhz + w * h * 2 / ILI9XXX_TRANSFER_BUFFER_SIZE * SPI_SETUP_US; ESP_LOGV(TAG, "Start display(xlow:%d, ylow:%d, xhigh:%d, yhigh:%d, width:%d, " - "heigth:%d, start_pos:%" PRId32 ")", - this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, start_pos); - - this->start_data_(); - for (uint16_t row = 0; row < h; row++) { - uint32_t pos = start_pos + (row * width_); - uint32_t rem = w; - - while (rem > 0) { - uint32_t sz = std::min(rem, ILI9XXX_TRANSFER_BUFFER_SIZE); - // ESP_LOGVV(TAG, "Send to display(pos:%d, rem:%d, zs:%d)", pos, rem, sz); - buffer_to_transfer_(pos, sz); - if (this->is_18bitdisplay_) { - for (uint32_t i = 0; i < sz; ++i) { - uint16_t color_val = transfer_buffer_[i]; - - uint8_t red = color_val & 0x1F; - uint8_t green = (color_val & 0x7E0) >> 5; - uint8_t blue = (color_val & 0xF800) >> 11; - - uint8_t pass_buff[3]; - - pass_buff[2] = (uint8_t) ((red / 32.0) * 64) << 2; - pass_buff[1] = (uint8_t) green << 2; - pass_buff[0] = (uint8_t) ((blue / 32.0) * 64) << 2; - - this->write_array(pass_buff, sizeof(pass_buff)); - } - } else { - this->write_array16(transfer_buffer_, sz); + "height:%d, mode=%d, 18bit=%d, sw_time=%dus, mw_time=%dus)", + this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, this->buffer_color_mode_, + this->is_18bitdisplay_, sw_time, mw_time); + auto now = millis(); + this->enable(); + if (this->buffer_color_mode_ == BITS_16 && !this->is_18bitdisplay_ && sw_time < mw_time) { + // 16 bit mode maps directly to display format + ESP_LOGV(TAG, "Doing single write of %d bytes", this->width_ * h * 2); + set_addr_window_(0, this->y_low_, this->width_ - 1, this->y_high_); + this->write_array(this->buffer_ + this->y_low_ * this->width_ * 2, h * this->width_ * 2); + } else { + ESP_LOGV(TAG, "Doing multiple write"); + size_t rem = h * w; // remaining number of pixels to write + set_addr_window_(this->x_low_, this->y_low_, this->x_high_, this->y_high_); + size_t idx = 0; // index into transfer_buffer + size_t pixel = 0; // pixel number offset + size_t pos = this->y_low_ * this->width_ + this->x_low_; + while (rem-- != 0) { + uint16_t color_val; + switch (this->buffer_color_mode_) { + case BITS_8: + color_val = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos++])); + break; + case BITS_8_INDEXED: + color_val = display::ColorUtil::color_to_565( + display::ColorUtil::index8_to_color_palette888(this->buffer_[pos++], this->palette_)); + break; + default: // case BITS_16: + color_val = (buffer_[pos * 2] << 8) + buffer_[pos * 2 + 1]; + pos++; + break; + } + if (this->is_18bitdisplay_) { + transfer_buffer[idx++] = (uint8_t) ((color_val & 0xF800) >> 8); // Blue + transfer_buffer[idx++] = (uint8_t) ((color_val & 0x7E0) >> 3); // Green + transfer_buffer[idx++] = (uint8_t) (color_val << 3); // Red + } else { + put16_be(transfer_buffer + idx, color_val); + idx += 2; + } + if (idx == ILI9XXX_TRANSFER_BUFFER_SIZE) { + this->write_array(transfer_buffer, idx); + idx = 0; + App.feed_wdt(); + } + // end of line? Skip to the next. + if (++pixel == w) { + pixel = 0; + pos += this->width_ - w; } - pos += sz; - rem -= sz; } - App.feed_wdt(); + // flush any balance. + if (idx != 0) { + this->write_array(transfer_buffer, idx); + } } - this->end_data_(); - + this->disable(); + ESP_LOGV(TAG, "Data write took %dms", (unsigned) (millis() - now)); // invalidate watermarks this->x_low_ = this->width_; this->y_low_ = this->height_; @@ -225,26 +276,6 @@ void ILI9XXXDisplay::display_() { this->y_high_ = 0; } -uint32_t ILI9XXXDisplay::buffer_to_transfer_(uint32_t pos, uint32_t sz) { - for (uint32_t i = 0; i < sz; ++i) { - switch (this->buffer_color_mode_) { - case BITS_8_INDEXED: - transfer_buffer_[i] = display::ColorUtil::color_to_565( - display::ColorUtil::index8_to_color_palette888(this->buffer_[pos + i], this->palette_)); - break; - case BITS_16: - transfer_buffer_[i] = ((uint16_t) this->buffer_[(pos + i) * 2] << 8) | this->buffer_[((pos + i) * 2) + 1]; - continue; - break; - default: - transfer_buffer_[i] = - display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos + i])); - break; - } - } - return sz; -} - // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color // values per bit is huge uint32_t ILI9XXXDisplay::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } @@ -303,11 +334,11 @@ void ILI9XXXDisplay::reset_() { } } -void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) { +void ILI9XXXDisplay::init_lcd_() { uint8_t cmd, x, num_args; - const uint8_t *addr = init_cmd; - while ((cmd = progmem_read_byte(addr++)) > 0) { - x = progmem_read_byte(addr++); + const uint8_t *addr = this->init_sequence_; + while ((cmd = *addr++) > 0) { + x = *addr++; num_args = x & 0x7F; send_command(cmd, addr, num_args); addr += num_args; @@ -316,27 +347,29 @@ void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) { } } -void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) { - uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1); - this->command(ILI9XXX_CASET); // Column address set - this->start_data_(); - this->write_byte(x1 >> 8); - this->write_byte(x1); - this->write_byte(x2 >> 8); - this->write_byte(x2); - this->end_data_(); - this->command(ILI9XXX_PASET); // Row address set - this->start_data_(); - this->write_byte(y1 >> 8); - this->write_byte(y1); - this->write_byte(y2 >> 8); - this->write_byte(y2); - this->end_data_(); - this->command(ILI9XXX_RAMWR); // Write to RAM +// Tell the display controller where we want to draw pixels. +// when called, the SPI should have already been enabled, only the D/C pin will be toggled here. +void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + uint8_t buf[4]; + this->dc_pin_->digital_write(false); + this->write_byte(ILI9XXX_CASET); // Column address set + put16_be(buf, x1 + this->offset_x_); + put16_be(buf + 2, x2 + this->offset_x_); + this->dc_pin_->digital_write(true); + this->write_array(buf, sizeof buf); + this->dc_pin_->digital_write(false); + this->write_byte(ILI9XXX_PASET); // Row address set + put16_be(buf, y1 + this->offset_y_); + put16_be(buf + 2, y2 + this->offset_y_); + this->dc_pin_->digital_write(true); + this->write_array(buf, sizeof buf); + this->dc_pin_->digital_write(false); + this->write_byte(ILI9XXX_RAMWR); // Write to RAM + this->dc_pin_->digital_write(true); } -void ILI9XXXDisplay::invert_display(bool invert) { - this->pre_invertdisplay_ = invert; +void ILI9XXXDisplay::invert_colors(bool invert) { + this->pre_invertcolors_ = invert; if (is_ready()) { this->command(invert ? ILI9XXX_INVON : ILI9XXX_INVOFF); } @@ -345,132 +378,5 @@ void ILI9XXXDisplay::invert_display(bool invert) { int ILI9XXXDisplay::get_width_internal() { return this->width_; } int ILI9XXXDisplay::get_height_internal() { return this->height_; } -// M5Stack display -void ILI9XXXM5Stack::initialize() { - this->init_lcd_(INITCMD_M5STACK); - if (this->width_ == 0) - this->width_ = 320; - if (this->height_ == 0) - this->height_ = 240; - this->pre_invertdisplay_ = true; -} - -// M5CORE display // Based on the configuration settings of M5stact's M5GFX code. -void ILI9XXXM5CORE::initialize() { - this->init_lcd_(INITCMD_M5CORE); - if (this->width_ == 0) - this->width_ = 320; - if (this->height_ == 0) - this->height_ = 240; - this->pre_invertdisplay_ = true; -} - -// 24_TFT display -void ILI9XXXILI9341::initialize() { - this->init_lcd_(INITCMD_ILI9341); - if (this->width_ == 0) - this->width_ = 240; - if (this->height_ == 0) - this->height_ = 320; -} -// 24_TFT rotated display -void ILI9XXXILI9342::initialize() { - this->init_lcd_(INITCMD_ILI9341); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 240; - } -} - -// 35_TFT display -void ILI9XXXILI9481::initialize() { - this->init_lcd_(INITCMD_ILI9481); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } -} - -void ILI9XXXILI948118::initialize() { - this->init_lcd_(INITCMD_ILI9481_18); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 480; - } - this->is_18bitdisplay_ = true; -} - -// 35_TFT display -void ILI9XXXILI9486::initialize() { - this->init_lcd_(INITCMD_ILI9486); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } -} -// 40_TFT display -void ILI9XXXILI9488::initialize() { - this->init_lcd_(INITCMD_ILI9488); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } - this->is_18bitdisplay_ = true; -} -// 40_TFT display -void ILI9XXXILI9488A::initialize() { - this->init_lcd_(INITCMD_ILI9488_A); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } - this->is_18bitdisplay_ = true; -} -// 40_TFT display -void ILI9XXXST7796::initialize() { - this->init_lcd_(INITCMD_ST7796); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 480; - } -} - -// 24_TFT rotated display -void ILI9XXXS3Box::initialize() { - this->init_lcd_(INITCMD_S3BOX); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 240; - } -} - -// 24_TFT rotated display -void ILI9XXXS3BoxLite::initialize() { - this->init_lcd_(INITCMD_S3BOXLITE); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 240; - } - this->pre_invertdisplay_ = true; -} - } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index ae7c83e61f..db1274450a 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -7,7 +7,7 @@ namespace esphome { namespace ili9xxx { -const uint32_t ILI9XXX_TRANSFER_BUFFER_SIZE = 64; +const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6 enum ILI9XXXColorMode { BITS_8 = 0x08, @@ -23,20 +23,47 @@ class ILI9XXXDisplay : public display::DisplayBuffer, public spi::SPIDevice { public: + ILI9XXXDisplay() = default; + ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height, bool invert_colors) + : init_sequence_{init_sequence}, width_{width}, height_{height}, pre_invertcolors_{invert_colors} { + uint8_t cmd, num_args, bits; + const uint8_t *addr = init_sequence; + while ((cmd = *addr++) != 0) { + num_args = *addr++ & 0x7F; + if (cmd == ILI9XXX_MADCTL) { + bits = *addr; + this->swap_xy_ = (bits & MADCTL_MV) != 0; + this->mirror_x_ = (bits & MADCTL_MX) != 0; + this->mirror_y_ = (bits & MADCTL_MY) != 0; + this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + break; + } + addr += num_args; + } + } + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_palette(const uint8_t *palette) { this->palette_ = palette; } void set_buffer_color_mode(ILI9XXXColorMode color_mode) { this->buffer_color_mode_ = color_mode; } - void set_dimentions(int16_t width, int16_t height) { + void set_dimensions(int16_t width, int16_t height) { this->height_ = height; this->width_ = width; } - void invert_display(bool invert); + void set_offsets(int16_t offset_x, int16_t offset_y) { + this->offset_x_ = offset_x; + this->offset_y_ = offset_y; + } + void invert_colors(bool invert); void command(uint8_t value); void data(uint8_t value); void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); uint8_t read_command(uint8_t command_byte, uint8_t index); + void set_color_order(display::ColorOrder color_order) { this->color_order_ = color_order; } + void set_swap_xy(bool swap_xy) { this->swap_xy_ = swap_xy; } + void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } + void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } void update() override; @@ -50,16 +77,17 @@ class ILI9XXXDisplay : public display::DisplayBuffer, protected: void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); - virtual void initialize() = 0; void display_(); - void init_lcd_(const uint8_t *init_cmd); - void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); - + void init_lcd_(); + void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2); void reset_(); + uint8_t const *init_sequence_{}; int16_t width_{0}; ///< Display width as modified by current rotation int16_t height_{0}; ///< Display height as modified by current rotation + int16_t offset_x_{0}; + int16_t offset_y_{0}; uint16_t x_low_{0}; uint16_t y_low_{0}; uint16_t x_high_{0}; @@ -77,10 +105,6 @@ class ILI9XXXDisplay : public display::DisplayBuffer, void start_data_(); void end_data_(); - uint16_t transfer_buffer_[ILI9XXX_TRANSFER_BUFFER_SIZE]; - - uint32_t buffer_to_transfer_(uint32_t pos, uint32_t sz); - GPIOPin *reset_pin_{nullptr}; GPIOPin *dc_pin_{nullptr}; GPIOPin *busy_pin_{nullptr}; @@ -88,77 +112,87 @@ class ILI9XXXDisplay : public display::DisplayBuffer, bool prossing_update_ = false; bool need_update_ = false; bool is_18bitdisplay_ = false; - bool pre_invertdisplay_ = false; + bool pre_invertcolors_ = false; + display::ColorOrder color_order_{}; + bool swap_xy_{}; + bool mirror_x_{}; + bool mirror_y_{}; }; //----------- M5Stack display -------------- class ILI9XXXM5Stack : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240, true) {} }; //----------- M5Stack display -------------- class ILI9XXXM5CORE : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240, true) {} +}; + +//----------- ST7789V display -------------- +class ILI9XXXST7789V : public ILI9XXXDisplay { + public: + ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320, false) {} }; //----------- ILI9XXX_24_TFT display -------------- class ILI9XXXILI9341 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320, false) {} }; //----------- ILI9XXX_24_TFT rotated display -------------- class ILI9XXXILI9342 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240, false) {} }; //----------- ILI9XXX_??_TFT rotated display -------------- class ILI9XXXILI9481 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320, false) {} }; //----------- ILI9481 in 18 bit mode -------------- class ILI9XXXILI948118 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480, true) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9486 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320, false) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9488 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9488() : ILI9XXXDisplay(INITCMD_ILI9488, 480, 320, true) {} }; //----------- ILI9XXX_35_TFT origin colors rotated display -------------- class ILI9XXXILI9488A : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320, true) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXST7796 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480, false) {} }; class ILI9XXXS3Box : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240, false) {} }; class ILI9XXXS3BoxLite : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {} }; } // namespace ili9xxx diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index e3be9389b7..a74824052f 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -289,6 +289,33 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0x00 // End of list }; +static const uint8_t PROGMEM INITCMD_ST7789V[] = { + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR + ILI9XXX_DFUNCTR, 2, 0x0A, 0x82, + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR2, 5, 0x0C, 0x0C, 0x00, 0x33, 0x33, + ILI9XXX_ETMOD, 1, 0x35, 0xBB, 1, 0x28, + ILI9XXX_PWCTR1 , 1, 0x0C, // Power control VRH[5:0] + ILI9XXX_PWCTR3 , 2, 0x01, 0xFF, + ILI9XXX_PWCTR4 , 1, 0x10, + ILI9XXX_PWCTR5 , 1, 0x20, + ILI9XXX_IFCTR , 1, 0x0F, + ILI9XXX_PWSET, 2, 0xA4, 0xA1, + ILI9XXX_GMCTRP1 , 14, + 0xd0, 0x00, 0x02, 0x07, 0x0a, + 0x28, 0x32, 0x44, 0x42, 0x06, 0x0e, + 0x12, 0x14, 0x17, + ILI9XXX_GMCTRN1 , 14, + 0xd0, 0x00, 0x02, 0x07, 0x0a, + 0x28, 0x31, 0x54, 0x47, + 0x0e, 0x1c, 0x17, 0x1b, + 0x1e, + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + // clang-format on } // namespace ili9xxx } // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 61d28faf73..0849d8aeb6 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1554,6 +1554,8 @@ sensor: memory_address: 0x7d name: Adres sensor +psram: + esp32_touch: setup_mode: false iir_filter: 10ms @@ -2992,6 +2994,12 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 @@ -3000,6 +3008,11 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + dimensions: + width: 320 + height: 240 + offset_width: 20 + offset_height: 10 model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 From 1e772718580754ddf3ad3958f49e94859bdc5ed6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:07:29 +1300 Subject: [PATCH 079/436] Fix regex for 'byte' custom CI check (#5851) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index d8c2f3053f..cc9bdcadbb 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -458,7 +458,7 @@ def lint_no_removed_in_idf_conversions(fname, match): @lint_re_check( - r"[^\w\d]byte\s+[\w\d]+\s*=", + r"[^\w\d]byte +[\w\d]+\s*=", include=cpp_include, exclude={ "esphome/components/tuya/tuya.h", From a8bc5ef46f24ce0d6f10614cbf7b2f1b045cce50 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:50:02 +1100 Subject: [PATCH 080/436] Pass through additional arguments to create number (#5849) --- esphome/components/number/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index e6ad545d70..07164be5ce 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -257,8 +257,8 @@ async def register_number( ) -async def new_number(config, *, min_value: float, max_value: float, step: float): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_number(config, *args, min_value: float, max_value: float, step: float): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_number( var, config, min_value=min_value, max_value=max_value, step=step ) From d1be686c54afa1fcf2b46b9f615e168252c7730e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:14:59 +0100 Subject: [PATCH 081/436] Nextion `on_touch` trigger (#5833) --- esphome/components/nextion/automation.h | 9 +++++++++ esphome/components/nextion/display.py | 19 +++++++++++++++++++ esphome/components/nextion/nextion.cpp | 5 +++++ esphome/components/nextion/nextion.h | 7 +++++++ 4 files changed, 40 insertions(+) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 210d7b2e2b..f51fe6b4f8 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -33,5 +33,14 @@ class PageTrigger : public Trigger { } }; +class TouchTrigger : public Trigger { + public: + explicit TouchTrigger(Nextion *nextion) { + nextion->add_touch_event_callback([this](uint8_t page_id, uint8_t component_id, bool touch_event) { + this->trigger(page_id, component_id, touch_event); + }); + } +}; + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 1ac0364b8b..92e85ad28a 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_LAMBDA, CONF_BRIGHTNESS, CONF_TRIGGER_ID, + CONF_ON_TOUCH, ) from esphome.core import CORE from . import Nextion, nextion_ns, nextion_ref @@ -31,6 +32,7 @@ SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template()) +TouchTrigger = nextion_ns.class_("TouchTrigger", automation.Trigger.template()) CONFIG_SCHEMA = ( display.BASIC_DISPLAY_SCHEMA.extend( @@ -58,6 +60,11 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PageTrigger), } ), + cv.Optional(CONF_ON_TOUCH): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TouchTrigger), + } + ), cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, cv.Optional(CONF_START_UP_PAGE): cv.positive_int, @@ -119,3 +126,15 @@ async def to_code(config): for conf in config.get(CONF_ON_PAGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(cg.uint8, "x")], conf) + + for conf in config.get(CONF_ON_TOUCH, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.uint8, "page_id"), + (cg.uint8, "component_id"), + (cg.bool_, "touch_event"), + ], + conf, + ) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 02ba94fcab..fcc0d97655 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -167,6 +167,10 @@ void Nextion::add_new_page_callback(std::function &&callback) { this->page_callback_.add(std::move(callback)); } +void Nextion::add_touch_event_callback(std::function &&callback) { + this->touch_callback_.add(std::move(callback)); +} + void Nextion::update_all_components() { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return; @@ -440,6 +444,7 @@ void Nextion::process_nextion_commands_() { for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } + this->touch_callback_.call(page_id, component_id, touch_event != 0); break; } case 0x66: { // Nextion initiated new page event return data. diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index f3a13d2170..a90c91db8a 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -732,6 +732,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void add_new_page_callback(std::function &&callback); + /** Add a callback to be notified when Nextion has a touch event. + * + * @param callback The void() callback. + */ + void add_touch_event_callback(std::function &&callback); + void update_all_components(); /** @@ -885,6 +891,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager sleep_callback_{}; CallbackManager wake_callback_{}; CallbackManager page_callback_{}; + CallbackManager touch_callback_{}; optional writer_; float brightness_{1.0}; From 2f888ff7c5737246bf277b5b71406cfe72804914 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:50:14 +0100 Subject: [PATCH 082/436] Nextion colors parameters (#5699) * Add `foreground` color - Adds `set_component_foreground_color` and `set_component_pressed_foreground_color` which does the same as `set_component_font_color` and `set_component_pressed_font_color` but with a more intuitive name, as this can be used for any component and not only the ones with a text (font). - I've also reviewed some docstring when related to colors. * Add numeric color to drawing methods Should I've used uint32_t instead? In order to keep consistency? * component color support to uint6_t This is the right format and is now consistent with colors on drawings. I'm keeping uint32_t also to avoid breaking changes. * Enforces uint16_t for colors uint32_t is incorrect for Nextion display colors. * Fix clang-format --- esphome/components/nextion/nextion.h | 305 ++++++++++++++---- esphome/components/nextion/nextion_base.h | 2 + .../components/nextion/nextion_commands.cpp | 80 ++++- .../components/nextion/nextion_component.cpp | 4 +- 4 files changed, 307 insertions(+), 84 deletions(-) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index a90c91db8a..f188708f35 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -95,16 +95,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** * Set the background color of a component. * @param component The component name. - * @param color The color (as a uint32_t). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_background_color("button", 0xFF0000); + * it.set_component_background_color("button", 63488); * ``` * * This will change the background color of the component `button` to red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_background_color(const char *component, uint32_t color); + void set_component_background_color(const char *component, uint16_t color); /** * Set the background color of a component. * @param component The component name. @@ -115,9 +117,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_background_color("button", "RED"); * ``` * - * This will change the background color of the component `button` to blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the background color of the component `button` to red. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_background_color(const char *component, const char *color); /** @@ -127,26 +128,29 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_background_color("button", color); + * auto blue = Color(0, 0, 255); + * it.set_component_background_color("button", blue); * ``` * - * This will change the background color of the component `button` to what color contains. + * This will change the background color of the component `button` to blue. */ void set_component_background_color(const char *component, Color color) override; /** * Set the pressed background color of a component. * @param component The component name. - * @param color The color (as a int). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_pressed_background_color("button", 0xFF0000 ); + * it.set_component_pressed_background_color("button", 63488); * ``` * * This will change the pressed background color of the component `button` to red. This is the background color that * is shown when the component is pressed. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_pressed_background_color(const char *component, uint32_t color); + void set_component_pressed_background_color(const char *component, uint16_t color); /** * Set the pressed background color of a component. * @param component The component name. @@ -157,10 +161,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_pressed_background_color("button", "RED"); * ``` * - * This will change the pressed background color of the component `button` to blue. This is the background color that - * is shown when the component is pressed. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_pressed_background_color(const char *component, const char *color); /** @@ -170,15 +173,102 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_pressed_background_color("button", color); + * auto red = Color(255, 0, 0); + * it.set_component_pressed_background_color("button", red); * ``` * - * This will change the pressed background color of the component `button` to blue. This is the background color that - * is shown when the component is pressed. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. */ void set_component_pressed_background_color(const char *component, Color color) override; + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as a uint16_t). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", 63488); + * ``` + * + * This will change the foreground color of the component `button` to red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_foreground_color(const char *component, uint16_t color); + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as a string). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", "RED"); + * ``` + * + * This will change the foreground color of the component `button` to red. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. + */ + void set_component_foreground_color(const char *component, const char *color); + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", Color::BLACK); + * ``` + * + * This will change the foreground color of the component `button` to black. + */ + void set_component_foreground_color(const char *component, Color color) override; + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as a uint16_t). + * + * Example: + * ```cpp + * it.set_component_pressed_foreground_color("button", 63488 ); + * ``` + * + * This will change the pressed foreground color of the component `button` to red. This is the foreground color that + * is shown when the component is pressed. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_pressed_foreground_color(const char *component, uint16_t color); + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as a string). + * + * Example: + * ```cpp + * it.set_component_pressed_foreground_color("button", "RED"); + * ``` + * + * This will change the pressed foreground color of the component `button` to red. This is the foreground color that + * is shown when the component is pressed. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. + */ + void set_component_pressed_foreground_color(const char *component, const char *color); + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * auto blue = Color(0, 0, 255); + * it.set_component_pressed_foreground_color("button", blue); + * ``` + * + * This will change the pressed foreground color of the component `button` to blue. This is the foreground color that + * is shown when the component is pressed. + */ + void set_component_pressed_foreground_color(const char *component, Color color) override; /** * Set the picture id of a component. @@ -210,16 +300,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** * Set the font color of a component. * @param component The component name. - * @param color The color (as a uint32_t ). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_font_color("textview", 0xFF0000); + * it.set_component_font_color("textview", 63488); * ``` * * This will change the font color of the component `textview` to a red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_font_color(const char *component, uint32_t color); + void set_component_font_color(const char *component, uint16_t color); /** * Set the font color of a component. * @param component The component name. @@ -230,9 +322,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_font_color("textview", "RED"); * ``` * - * This will change the font color of the component `textview` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the font color of the component `textview` to a red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_font_color(const char *component, const char *color); /** @@ -242,27 +333,27 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_font_color("textview", color); + * it.set_component_font_color("textview", Color::BLACK); * ``` * - * This will change the font color of the component `textview` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the font color of the component `textview` to black. */ void set_component_font_color(const char *component, Color color) override; /** * Set the pressed font color of a component. * @param component The component name. - * @param color The color (as a uint32_t). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_pressed_font_color("button", 0xFF0000); + * it.set_component_pressed_font_color("button", 63488); * ``` * * This will change the pressed font color of the component `button` to a red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_pressed_font_color(const char *component, uint32_t color); + void set_component_pressed_font_color(const char *component, uint16_t color); /** * Set the pressed font color of a component. * @param component The component name. @@ -273,9 +364,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_pressed_font_color("button", "RED"); * ``` * - * This will change the pressed font color of the component `button` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the pressed font color of the component `button` to a red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_pressed_font_color(const char *component, const char *color); /** @@ -285,12 +375,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_pressed_font_color("button", color); + * it.set_component_pressed_font_color("button", Color::BLACK); * ``` * - * This will change the pressed font color of the component `button` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the pressed font color of the component `button` to black. */ void set_component_pressed_font_color(const char *component, Color color) override; /** @@ -420,6 +508,25 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Displays the picture who has the id `2` at the x coordinates `15` and y coordinates `25`. */ void display_picture(int picture_id, int x_start, int y_start); + /** + * Fill a rectangle with a color. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width to draw. + * @param height The height to draw. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * fill_area(50, 50, 100, 100, 63488); + * ``` + * + * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with + * the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void fill_area(int x1, int y1, int width, int height, uint16_t color); /** * Fill a rectangle with a color. * @param x1 The starting x coordinate. @@ -434,8 +541,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * ``` * * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with - * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to - * convert color codes to Nextion HMI colors + * the red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void fill_area(int x1, int y1, int width, int height, const char *color); /** @@ -448,14 +555,33 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * fill_area(50, 50, 100, 100, color); + * auto blue = Color(0, 0, 255); + * fill_area(50, 50, 100, 100, blue); * ``` * * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with - * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to - * convert color codes to Nextion HMI colors + * blue color. */ void fill_area(int x1, int y1, int width, int height, Color color); + /** + * Draw a line on the screen. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param x2 The ending x coordinate. + * @param y2 The ending y coordinate. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.line(50, 50, 75, 75, 63488); + * ``` + * + * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate + * `75` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void line(int x1, int y1, int x2, int y2, uint16_t color); /** * Draw a line on the screen. * @param x1 The starting x coordinate. @@ -466,13 +592,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.line(50, 50, 75, 75, "17013"); + * it.line(50, 50, 75, 75, "BLUE"); * ``` * * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate - * `75` with the color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * `75` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void line(int x1, int y1, int x2, int y2, const char *color); /** @@ -485,15 +610,33 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.line(50, 50, 75, 75, "17013"); + * auto blue = Color(0, 0, 255); + * it.line(50, 50, 75, 75, blue); * ``` * * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate - * `75` with the color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * `75` with blue color. */ void line(int x1, int y1, int x2, int y2, Color color); + /** + * Draw a rectangle outline. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.rectangle(25, 35, 40, 50, 63488); + * ``` + * + * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a + * length of `50` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void rectangle(int x1, int y1, int width, int height, uint16_t color); /** * Draw a rectangle outline. * @param x1 The starting x coordinate. @@ -504,13 +647,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.rectangle(25, 35, 40, 50, "17013"); + * it.rectangle(25, 35, 40, 50, "BLUE"); * ``` * * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a - * length of `50` with color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * length of `50` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void rectangle(int x1, int y1, int width, int height, const char *color); /** @@ -523,21 +665,31 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.rectangle(25, 35, 40, 50, "17013"); + * auto blue = Color(0, 0, 255); + * it.rectangle(25, 35, 40, 50, blue); * ``` * * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a - * length of `50` with color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * length of `50` with blue color. */ void rectangle(int x1, int y1, int width, int height, Color color); + /** + * Draw a circle outline + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (number). + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void circle(int center_x, int center_y, int radius, uint16_t color); /** * Draw a circle outline * @param center_x The center x coordinate. * @param center_y The center y coordinate. * @param radius The circle radius. * @param color The color to draw with (as a string). + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void circle(int center_x, int center_y, int radius, const char *color); /** @@ -548,6 +700,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @param color The color to draw with (as Color). */ void circle(int center_x, int center_y, int radius, Color color); + /** + * Draw a filled circled. + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.filled_cricle(25, 25, 10, 63488); + * ``` + * + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void filled_circle(int center_x, int center_y, int radius, uint16_t color); /** * Draw a filled circled. * @param center_x The center x coordinate. @@ -557,12 +726,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.filled_cricle(25, 25, 10, "17013"); + * it.filled_cricle(25, 25, 10, "BLUE"); * ``` * - * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void filled_circle(int center_x, int center_y, int radius, const char *color); /** @@ -574,12 +742,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.filled_cricle(25, 25, 10, color); + * auto blue = Color(0, 0, 255); + * it.filled_cricle(25, 25, 10, blue); * ``` * - * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with blue color. */ void filled_circle(int center_x, int center_y, int radius, Color color); diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index ffc431ed13..b5729a1df1 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -39,6 +39,8 @@ class NextionBase { virtual void set_component_background_color(const char *component, Color color) = 0; virtual void set_component_pressed_background_color(const char *component, Color color) = 0; + virtual void set_component_foreground_color(const char *component, Color color) = 0; + virtual void set_component_pressed_foreground_color(const char *component, Color color) = 0; virtual void set_component_font_color(const char *component, Color color) = 0; virtual void set_component_pressed_font_color(const char *component, Color color) = 0; virtual void set_component_font(const char *component, uint8_t font_id) = 0; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 35530d1a7f..0722ed6f4e 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -53,9 +53,9 @@ void Nextion::set_protocol_reparse_mode(bool active_mode) { this->write_array(to_send, sizeof(to_send)); } -// Set Colors -void Nextion::set_component_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu32, component, color); +// Set Colors - Background +void Nextion::set_component_background_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu16, component, color); } void Nextion::set_component_background_color(const char *component, const char *color) { @@ -67,8 +67,9 @@ void Nextion::set_component_background_color(const char *component, Color color) display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pressed_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu32, component, +// Set Colors - Background (pressed) +void Nextion::set_component_pressed_background_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu16, component, color); } @@ -81,16 +82,38 @@ void Nextion::set_component_pressed_background_color(const char *component, Colo display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pic(const char *component, uint8_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%d", component, pic_id); +// Set Colors - Foreground +void Nextion::set_component_foreground_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%" PRIu16, component, color); } -void Nextion::set_component_picc(const char *component, uint8_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%d", component, pic_id); +void Nextion::set_component_foreground_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%s", component, color); } -void Nextion::set_component_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu32, component, color); +void Nextion::set_component_foreground_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%d", component, + display::ColorUtil::color_to_565(color)); +} + +// Set Colors - Foreground (pressed) +void Nextion::set_component_pressed_foreground_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%" PRIu16, component, + color); +} + +void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", " %s.pco2=%s", component, color); +} + +void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +// Set Colors - Font +void Nextion::set_component_font_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_font_color(const char *component, const char *color) { @@ -102,8 +125,9 @@ void Nextion::set_component_font_color(const char *component, Color color) { display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pressed_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu32, component, color); +// Set Colors - Font (pressed) +void Nextion::set_component_pressed_font_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { @@ -115,6 +139,15 @@ void Nextion::set_component_pressed_font_color(const char *component, Color colo display::ColorUtil::color_to_565(color)); } +// Set picture +void Nextion::set_component_pic(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%d", component, pic_id); +} + +void Nextion::set_component_picc(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%d", component, pic_id); +} + void Nextion::set_component_text_printf(const char *component, const char *format, ...) { va_list arg; va_start(arg, format); @@ -193,6 +226,10 @@ void Nextion::display_picture(int picture_id, int x_start, int y_start) { this->add_no_result_to_queue_with_printf_("display_picture", "pic %d, %d, %d", x_start, y_start, picture_id); } +void Nextion::fill_area(int x1, int y1, int width, int height, uint16_t color) { + this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%" PRIu16, x1, y1, width, height, color); +} + void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%s", x1, y1, width, height, color); } @@ -202,6 +239,10 @@ void Nextion::fill_area(int x1, int y1, int width, int height, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::line(int x1, int y1, int x2, int y2, uint16_t color) { + this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%" PRIu16, x1, y1, x2, y2, color); +} + void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); } @@ -211,6 +252,11 @@ void Nextion::line(int x1, int y1, int x2, int y2, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::rectangle(int x1, int y1, int width, int height, uint16_t color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%" PRIu16, x1, y1, x1 + width, y1 + height, + color); +} + void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); } @@ -220,6 +266,10 @@ void Nextion::rectangle(int x1, int y1, int width, int height, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::circle(int center_x, int center_y, int radius, uint16_t color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%" PRIu16, center_x, center_y, radius, color); +} + void Nextion::circle(int center_x, int center_y, int radius, const char *color) { this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%s", center_x, center_y, radius, color); } @@ -229,6 +279,10 @@ void Nextion::circle(int center_x, int center_y, int radius, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::filled_circle(int center_x, int center_y, int radius, uint16_t color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%" PRIu16, center_x, center_y, radius, color); +} + void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%s", center_x, center_y, radius, color); } diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index bbb2cf6cb2..cfb4e3600c 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -99,11 +99,11 @@ void NextionComponent::update_component_settings(bool force_update) { this->bco2_needs_update_ = false; } if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) { - this->nextion_->set_component_font_color(this->variable_name_.c_str(), this->pco_); + this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_); this->pco_needs_update_ = false; } if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) { - this->nextion_->set_component_pressed_font_color(this->variable_name_.c_str(), this->pco2_); + this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_); this->pco2_needs_update_ = false; } From ad5f6b5687b90aad4e1fb667a8eb7ef5e16ecc79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Nov 2023 23:11:17 -0600 Subject: [PATCH 083/436] dashboard: fix supervisor auth doing I/O in the event loop (#5807) --- esphome/dashboard/web_server.py | 62 ++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9bbf0b28dc..4552aebf7b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -13,9 +13,9 @@ import secrets import shutil import subprocess import threading -from pathlib import Path -from typing import Any, Callable, TypeVar from collections.abc import Iterable +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, TypeVar import tornado import tornado.concurrent @@ -45,12 +45,17 @@ from .util.file import write_file from .util.subprocess import async_run_system_command from .util.text import friendly_name_slugify +if TYPE_CHECKING: + from requests import Response + + _LOGGER = logging.getLogger(__name__) ENV_DEV = "ESPHOME_DASHBOARD_DEV" +COOKIE_AUTHENTICATED_YES = b"yes" -cookie_authenticated_yes = b"yes" +AUTH_COOKIE_NAME = "authenticated" settings = DASHBOARD.settings @@ -89,18 +94,18 @@ def authenticated(func: T) -> T: return decorator -def is_authenticated(request_handler): +def is_authenticated(handler: BaseHandler) -> bool: + """Check if the request is authenticated.""" if settings.on_ha_addon: # Handle ingress - disable auth on ingress port # X-HA-Ingress is automatically stripped on the non-ingress server in nginx - header = request_handler.request.headers.get("X-HA-Ingress", "NO") + header = handler.request.headers.get("X-HA-Ingress", "NO") if str(header) == "YES": return True + if settings.using_auth: - return ( - request_handler.get_secure_cookie("authenticated") - == cookie_authenticated_yes - ) + return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES + return True @@ -860,38 +865,45 @@ class LoginHandler(BaseHandler): **template_args(), ) - def post_ha_addon_login(self) -> None: + def _make_supervisor_auth_request(self) -> Response: + """Make a request to the supervisor auth endpoint.""" import requests - headers = { - "X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN"), - } - + headers = {"X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN")} data = { "username": self.get_argument("username", ""), "password": self.get_argument("password", ""), } + return requests.post( + "http://supervisor/auth", headers=headers, json=data, timeout=30 + ) + + async def post_ha_addon_login(self) -> None: + loop = asyncio.get_running_loop() try: - req = requests.post( - "http://supervisor/auth", headers=headers, json=data, timeout=30 - ) - if req.status_code == 200: - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") - return + req = await loop.run_in_executor(None, self._make_supervisor_auth_request) except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Error during Hass.io auth request: %s", err) self.set_status(500) self.render_login_page(error="Internal server error") return + + if req.status_code == 200: + self._set_authenticated() + self.redirect("/") + return self.set_status(401) self.render_login_page(error="Invalid username or password") + def _set_authenticated(self) -> None: + """Set the authenticated cookie.""" + self.set_secure_cookie(AUTH_COOKIE_NAME, COOKIE_AUTHENTICATED_YES) + def post_native_login(self) -> None: username = self.get_argument("username", "") password = self.get_argument("password", "") if settings.check_password(username, password): - self.set_secure_cookie("authenticated", cookie_authenticated_yes) + self._set_authenticated() self.redirect("./") return error_str = ( @@ -900,9 +912,9 @@ class LoginHandler(BaseHandler): self.set_status(401) self.render_login_page(error=error_str) - def post(self) -> None: + async def post(self): if settings.using_ha_addon_auth: - self.post_ha_addon_login() + await self.post_ha_addon_login() else: self.post_native_login() @@ -910,7 +922,7 @@ class LoginHandler(BaseHandler): class LogoutHandler(BaseHandler): @authenticated def get(self) -> None: - self.clear_cookie("authenticated") + self.clear_cookie(AUTH_COOKIE_NAME) self.redirect("./login") From 3940c6ac4ef5c91685b0e4320750f63369e2a4ab Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 06:25:20 +0100 Subject: [PATCH 084/436] Improve reliability of Nextion TFT uploads (Arduino) (#5683) Co-authored-by: Keith Burzinski --- .../nextion/nextion_upload_arduino.cpp | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index d1f9f44c2b..3337ff9b50 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -41,7 +41,7 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) http->setRedirectLimit(3); #endif -#endif +#endif // USE_ESP8266 char range_header[64]; sprintf(range_header, "bytes=%d-%d", range_start, range_end); @@ -62,6 +62,7 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { ++tries; if (!begin_status) { ESP_LOGD(TAG, "upload_by_chunks_: connection failed"); + delay(500); // NOLINT continue; } @@ -84,10 +85,10 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { std::string recv_string; size_t size = 0; - int sent = 0; + int fetched = 0; int range = range_end - range_start; - while (sent < range) { + while (fetched < range) { size = http->getStreamPtr()->available(); if (!size) { App.feed_wdt(); @@ -95,28 +96,38 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { continue; } int c = http->getStreamPtr()->readBytes( - &this->transfer_buffer_[sent], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size)); - sent += c; + &this->transfer_buffer_[fetched], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size)); + fetched += c; } http->end(); - ESP_LOGN(TAG, "this->content_length_ %d sent %d", this->content_length_, sent); + ESP_LOGN(TAG, "Fetched %d of %d bytes", fetched, this->content_length_); + + // upload fetched segments to the display in 4KB chunks + int write_len; for (int i = 0; i < range; i += 4096) { - this->write_array(&this->transfer_buffer_[i], 4096); - this->content_length_ -= 4096; - ESP_LOGN(TAG, "this->content_length_ %d range %d range_end %d range_start %d", this->content_length_, range, - range_end, range_start); + App.feed_wdt(); + write_len = this->content_length_ < 4096 ? this->content_length_ : 4096; + this->write_array(&this->transfer_buffer_[i], write_len); + this->content_length_ -= write_len; + ESP_LOGD(TAG, "Uploaded %0.2f %%; %d bytes remaining", + 100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_); if (!this->upload_first_chunk_sent_) { this->upload_first_chunk_sent_ = true; delay(500); // NOLINT - App.feed_wdt(); } - this->recv_ret_string_(recv_string, 2048, true); - if (recv_string[0] == 0x08) { + this->recv_ret_string_(recv_string, 4096, true); + if (recv_string[0] != 0x05) { // 0x05 == "ok" + ESP_LOGD(TAG, "recv_string [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + } + + // handle partial upload request + if (recv_string[0] == 0x08 && recv_string.size() == 5) { uint32_t result = 0; - for (int i = 0; i < 4; ++i) { - result += static_cast(recv_string[i + 1]) << (8 * i); + for (int j = 0; j < 4; ++j) { + result += static_cast(recv_string[j + 1]) << (8 * j); } if (result > 0) { ESP_LOGD(TAG, "Nextion reported new range %d", result); @@ -126,10 +137,14 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { } recv_string.clear(); } + return range_end + 1; } bool Nextion::upload_tft() { + ESP_LOGD(TAG, "Nextion TFT upload requested"); + ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + if (this->is_updating_) { ESP_LOGD(TAG, "Currently updating"); return false; @@ -162,7 +177,7 @@ bool Nextion::upload_tft() { if (!begin_status) { this->is_updating_ = false; - ESP_LOGD(TAG, "connection failed"); + ESP_LOGD(TAG, "Connection failed"); ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); return false; @@ -237,7 +252,9 @@ bool Nextion::upload_tft() { this->recv_ret_string_(response, 2000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. - ESP_LOGD(TAG, "Upgrade response is %s %zu", response.c_str(), response.length()); + ESP_LOGD(TAG, "Upgrade response is [%s] - %zu bytes", + format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + response.length()); for (size_t i = 0; i < response.length(); i++) { ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]); @@ -256,17 +273,17 @@ bool Nextion::upload_tft() { if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0) { chunk_size = this->content_length_; } else { - if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand - int chunk = int((ESP.getFreeHeap() - 32768) / 4096); - chunk_size = chunk * 4096; + if (ESP.getFreeHeap() > 81920) { // Ensure some FreeHeap to other things and limit chunk size + chunk_size = ESP.getFreeHeap() - 65536; + chunk_size = int(chunk_size / 4096) * 4096; chunk_size = chunk_size > 65536 ? 65536 : chunk_size; - } else if (ESP.getFreeHeap() < 10240) { + } else if (ESP.getFreeHeap() < 32768) { chunk_size = 4096; } } #else // NOLINTNEXTLINE(readability-static-accessed-through-instance) - uint32_t chunk_size = ESP.getFreeHeap() < 10240 ? 4096 : 8192; + uint32_t chunk_size = ESP.getFreeHeap() < 16384 ? 4096 : 8192; #endif if (this->transfer_buffer_ == nullptr) { From af8258168bb4e82a78bcd623ef72c83b59bf1a0e Mon Sep 17 00:00:00 2001 From: Sean Brogan Date: Mon, 27 Nov 2023 23:00:48 -0800 Subject: [PATCH 085/436] Add Pro Check Universal sensor support. (#5798) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mopeka_pro_check/mopeka_pro_check.cpp | 3 ++- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index e543ceb864..f79e40bb4e 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -54,7 +54,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP && static_cast(manu_data.data[0]) != LIPPERT_BOTTOM_UP && - static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP) { + static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP && + static_cast(manu_data.data[0]) != PRO_UNIVERSAL) { ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); return false; } diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index b5dff153e7..8b4d47e4c6 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -17,7 +17,9 @@ enum SensorType { TOP_DOWN_AIR_ABOVE = 0x04, BOTTOM_UP_WATER = 0x05, LIPPERT_BOTTOM_UP = 0x06, - PLUS_BOTTOM_UP = 0x08 + PLUS_BOTTOM_UP = 0x08, + PRO_UNIVERSAL = 0xC // Pro Check Universal + // all other values are reserved }; From 087733c2fd87746d471f75e55e9a3aadb8cbc796 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 19:23:55 +0000 Subject: [PATCH 086/436] Bump aioesphomeapi from 19.1.2 to 19.1.7 (#5859) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 09f5c303bb..2af36b1339 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.1.2 +aioesphomeapi==19.1.7 zeroconf==0.127.0 python-magic==0.4.27 From 6424f831e278e984f46da0d11953fb99613c600e Mon Sep 17 00:00:00 2001 From: Alexander Dimitrov Date: Tue, 28 Nov 2023 23:17:16 +0200 Subject: [PATCH 087/436] Pn532 non blocking scan (#5191) --- esphome/components/pn532/pn532.cpp | 58 +++++++++++++++++++++- esphome/components/pn532/pn532.h | 11 ++++ esphome/components/pn532_i2c/pn532_i2c.cpp | 23 ++++----- esphome/components/pn532_i2c/pn532_i2c.h | 1 + esphome/components/pn532_spi/pn532_spi.cpp | 46 +++++------------ esphome/components/pn532_spi/pn532_spi.h | 1 + 6 files changed, 92 insertions(+), 48 deletions(-) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index cc28d7078b..8088e6c022 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -127,8 +127,18 @@ void PN532::loop() { if (!this->requested_read_) return; + auto ready = this->read_ready_(false); + if (ready == WOULDBLOCK) + return; + + bool success = false; std::vector read; - bool success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read); + + if (ready == READY) { + success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read); + } else { + this->send_ack_(); // abort still running InListPassiveTarget + } this->requested_read_ = false; @@ -286,12 +296,58 @@ bool PN532::read_ack_() { return matches; } +void PN532::send_ack_() { + ESP_LOGV(TAG, "Sending ACK for abort"); + this->write_data({0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00}); + delay(10); +} void PN532::send_nack_() { ESP_LOGV(TAG, "Sending NACK for retransmit"); this->write_data({0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00}); delay(10); } +enum PN532ReadReady PN532::read_ready_(bool block) { + if (this->rd_ready_ == READY) { + if (block) { + this->rd_start_time_ = 0; + this->rd_ready_ = WOULDBLOCK; + } + return READY; + } + + if (!this->rd_start_time_) { + this->rd_start_time_ = millis(); + } + + while (true) { + if (this->is_read_ready()) { + this->rd_ready_ = READY; + break; + } + + if (millis() - this->rd_start_time_ > 100) { + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + this->rd_ready_ = TIMEOUT; + break; + } + + if (!block) { + this->rd_ready_ = WOULDBLOCK; + break; + } + + yield(); + } + + auto rdy = this->rd_ready_; + if (block || rdy == TIMEOUT) { + this->rd_start_time_ = 0; + this->rd_ready_ = WOULDBLOCK; + } + return rdy; +} + void PN532::turn_off_rf_() { ESP_LOGV(TAG, "Turning RF field OFF"); this->write_command_({ diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 8ae215dfd9..8194d86477 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -20,6 +20,12 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; static const uint8_t PN532_COMMAND_POWERDOWN = 0x16; +enum PN532ReadReady { + WOULDBLOCK = 0, + TIMEOUT, + READY, +}; + class PN532BinarySensor; class PN532 : public PollingComponent { @@ -54,8 +60,11 @@ class PN532 : public PollingComponent { void turn_off_rf_(); bool write_command_(const std::vector &data); bool read_ack_(); + void send_ack_(); void send_nack_(); + enum PN532ReadReady read_ready_(bool block); + virtual bool is_read_ready() = 0; virtual bool write_data(const std::vector &data) = 0; virtual bool read_data(std::vector &data, uint8_t len) = 0; virtual bool read_response(uint8_t command, std::vector &data) = 0; @@ -91,6 +100,8 @@ class PN532 : public PollingComponent { std::vector triggers_ontagremoved_; std::vector current_uid_; nfc::NdefMessage *next_task_message_to_write_; + uint32_t rd_start_time_{0}; + enum PN532ReadReady rd_ready_ { WOULDBLOCK }; enum NfcTask { READ = 0, CLEAN, diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index e7c99e94b0..b306222a21 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -12,6 +12,14 @@ namespace pn532_i2c { static const char *const TAG = "pn532_i2c"; +bool PN532I2C::is_read_ready() { + uint8_t ready; + if (!this->read_bytes_raw(&ready, 1)) { + return false; + } + return ready == 0x01; +} + bool PN532I2C::write_data(const std::vector &data) { return this->write(data.data(), data.size()) == i2c::ERROR_OK; } @@ -19,19 +27,8 @@ bool PN532I2C::write_data(const std::vector &data) { bool PN532I2C::read_data(std::vector &data, uint8_t len) { delay(1); - std::vector ready; - ready.resize(1); - uint32_t start_time = millis(); - while (true) { - if (this->read_bytes_raw(ready.data(), 1)) { - if (ready[0] == 0x01) - break; - } - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } data.resize(len + 1); diff --git a/esphome/components/pn532_i2c/pn532_i2c.h b/esphome/components/pn532_i2c/pn532_i2c.h index 95cf8eeb36..00c0df206d 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.h +++ b/esphome/components/pn532_i2c/pn532_i2c.h @@ -14,6 +14,7 @@ class PN532I2C : public pn532::PN532, public i2c::I2CDevice { void dump_config() override; protected: + bool is_read_ready() override; bool write_data(const std::vector &data) override; bool read_data(std::vector &data, uint8_t len) override; bool read_response(uint8_t command, std::vector &data) override; diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index be58f265b9..d55d8161d8 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -21,6 +21,14 @@ void PN532Spi::setup() { PN532::setup(); } +bool PN532Spi::is_read_ready() { + this->enable(); + this->write_byte(0x02); + bool ready = this->read_byte() == 0x01; + this->disable(); + return ready; +} + bool PN532Spi::write_data(const std::vector &data) { this->enable(); delay(2); @@ -34,24 +42,8 @@ bool PN532Spi::write_data(const std::vector &data) { } bool PN532Spi::read_data(std::vector &data, uint8_t len) { - ESP_LOGV(TAG, "Waiting for ready byte..."); - - uint32_t start_time = millis(); - while (true) { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - bool ready = this->read_byte() == 0x01; - this->disable(); - if (ready) - break; - ESP_LOGV(TAG, "Not ready yet..."); - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } // Read data (transmission from the PN532 to the host) @@ -72,22 +64,8 @@ bool PN532Spi::read_data(std::vector &data, uint8_t len) { bool PN532Spi::read_response(uint8_t command, std::vector &data) { ESP_LOGV(TAG, "Reading response"); - uint32_t start_time = millis(); - while (true) { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - bool ready = this->read_byte() == 0x01; - this->disable(); - if (ready) - break; - ESP_LOGV(TAG, "Not ready yet..."); - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } this->enable(); diff --git a/esphome/components/pn532_spi/pn532_spi.h b/esphome/components/pn532_spi/pn532_spi.h index 2d8312813d..b7adca22e9 100644 --- a/esphome/components/pn532_spi/pn532_spi.h +++ b/esphome/components/pn532_spi/pn532_spi.h @@ -18,6 +18,7 @@ class PN532Spi : public pn532::PN532, void dump_config() override; protected: + bool is_read_ready() override; bool write_data(const std::vector &data) override; bool read_data(std::vector &data, uint8_t len) override; bool read_response(uint8_t command, std::vector &data) override; From 391eff8fd52b08854e5a6c55e2659c26fb96c847 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:42:35 +1100 Subject: [PATCH 088/436] Add Chamberlain/HomEntry HE60R garage door opener (#5834) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/he60r/__init__.py | 1 + esphome/components/he60r/cover.py | 47 +++++ esphome/components/he60r/he60r.cpp | 265 +++++++++++++++++++++++++++ esphome/components/he60r/he60r.h | 47 +++++ tests/test4.yaml | 13 ++ 6 files changed, 374 insertions(+) create mode 100644 esphome/components/he60r/__init__.py create mode 100644 esphome/components/he60r/cover.py create mode 100644 esphome/components/he60r/he60r.cpp create mode 100644 esphome/components/he60r/he60r.h diff --git a/CODEOWNERS b/CODEOWNERS index e6bc53ee6f..de80806eac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -124,6 +124,7 @@ esphome/components/haier/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann +esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode diff --git a/esphome/components/he60r/__init__.py b/esphome/components/he60r/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/he60r/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/he60r/cover.py b/esphome/components/he60r/cover.py new file mode 100644 index 0000000000..fd4c746016 --- /dev/null +++ b/esphome/components/he60r/cover.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import cover, uart +from esphome.const import ( + CONF_CLOSE_DURATION, + CONF_ID, + CONF_OPEN_DURATION, +) + +he60r_ns = cg.esphome_ns.namespace("he60r") +HE60rCover = he60r_ns.class_("HE60rCover", cover.Cover, cg.Component) + +CONFIG_SCHEMA = ( + cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(HE60rCover), + cv.Optional( + CONF_OPEN_DURATION, default="15s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_CLOSE_DURATION, default="15s" + ): cv.positive_time_period_milliseconds, + } + ) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "he60r", + baud_rate=1200, + require_tx=True, + require_rx=True, + data_bits=8, + parity="EVEN", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cover.register_cover(var, config) + await uart.register_uart_device(var, config) + + cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) + cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp new file mode 100644 index 0000000000..d6e6122b1b --- /dev/null +++ b/esphome/components/he60r/he60r.cpp @@ -0,0 +1,265 @@ +#include "he60r.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace he60r { + +static const char *const TAG = "he60r.cover"; +static const uint8_t QUERY_BYTE = 0x38; +static const uint8_t TOGGLE_BYTE = 0x30; + +using namespace esphome::cover; + +void HE60rCover::setup() { + auto restore = this->restore_state_(); + + if (restore.has_value()) { + restore->apply(this); + this->publish_state(false); + } else { + // if no other information, assume half open + this->position = 0.5f; + } + this->current_operation = COVER_OPERATION_IDLE; + this->last_recompute_time_ = this->start_dir_time_ = millis(); + this->set_interval(300, [this]() { this->update_(); }); +} + +CoverTraits HE60rCover::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_stop(true); + traits.set_supports_position(true); + traits.set_supports_toggle(true); + traits.set_is_assumed_state(false); + return traits; +} + +void HE60rCover::dump_config() { + LOG_COVER("", "HE60R Cover", this); + this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8); + ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); + auto restore = this->restore_state_(); + if (restore.has_value()) + ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f)); +} + +void HE60rCover::endstop_reached_(CoverOperation operation) { + const uint32_t now = millis(); + + this->set_current_operation_(COVER_OPERATION_IDLE); + auto new_position = operation == COVER_OPERATION_OPENING ? COVER_OPEN : COVER_CLOSED; + if (new_position != this->position || this->current_operation != COVER_OPERATION_IDLE) { + this->position = new_position; + this->current_operation = COVER_OPERATION_IDLE; + if (this->last_command_ == operation) { + float dur = (now - this->start_dir_time_) / 1e3f; + ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), + operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur); + } + this->publish_state(); + } +} + +void HE60rCover::set_current_operation_(cover::CoverOperation operation) { + if (this->current_operation != operation) { + this->current_operation = operation; + if (operation != COVER_OPERATION_IDLE) + this->last_recompute_time_ = millis(); + this->publish_state(); + } +} + +void HE60rCover::process_rx_(uint8_t data) { + ESP_LOGV(TAG, "Process RX data %X", data); + if (!this->query_seen_) { + this->query_seen_ = data == QUERY_BYTE; + if (!this->query_seen_) + ESP_LOGD(TAG, "RX Byte %02X", data); + return; + } + switch (data) { + case 0xB5: // at closed endstop, jammed? + case 0xF5: // at closed endstop, jammed? + case 0x55: // at closed endstop + this->next_direction_ = COVER_OPERATION_OPENING; + this->endstop_reached_(COVER_OPERATION_CLOSING); + break; + + case 0x52: // at opened endstop + this->next_direction_ = COVER_OPERATION_CLOSING; + this->endstop_reached_(COVER_OPERATION_OPENING); + break; + + case 0x51: // travelling up after encountering obstacle + case 0x01: // travelling up + case 0x11: // travelling up, triggered by remote + this->set_current_operation_(COVER_OPERATION_OPENING); + this->next_direction_ = COVER_OPERATION_IDLE; + break; + + case 0x44: // travelling down + case 0x14: // travelling down, triggered by remote + this->next_direction_ = COVER_OPERATION_IDLE; + this->set_current_operation_(COVER_OPERATION_CLOSING); + break; + + case 0x86: // Stopped, jammed? + case 0x16: // stopped midway while opening, by remote + case 0x06: // stopped midway while opening + this->next_direction_ = COVER_OPERATION_CLOSING; + this->set_current_operation_(COVER_OPERATION_IDLE); + break; + + case 0x10: // stopped midway while closing, by remote + case 0x00: // stopped midway while closing + this->next_direction_ = COVER_OPERATION_OPENING; + this->set_current_operation_(COVER_OPERATION_IDLE); + break; + + default: + break; + } +} + +void HE60rCover::update_() { + if (toggles_needed_ != 0) { + if ((this->counter_++ & 0x3) == 0) { + toggles_needed_--; + ESP_LOGD(TAG, "Writing byte 0x30, still needed=%d", toggles_needed_); + this->write_byte(TOGGLE_BYTE); + } else { + this->write_byte(QUERY_BYTE); + } + } else { + this->write_byte(QUERY_BYTE); + this->counter_ = 0; + } + if (this->current_operation != COVER_OPERATION_IDLE) { + this->recompute_position_(); + + // if we initiated the move, check if we reached the target position + if (this->last_command_ != COVER_OPERATION_IDLE) { + if (this->is_at_target_()) { + this->start_direction_(COVER_OPERATION_IDLE); + } + } + } +} + +void HE60rCover::loop() { + uint8_t data; + + while (this->available() > 0) { + if (this->read_byte(&data)) { + this->process_rx_(data); + } + } +} + +void HE60rCover::control(const CoverCall &call) { + if (call.get_stop()) { + this->start_direction_(COVER_OPERATION_IDLE); + } else if (call.get_toggle().has_value()) { + // toggle action logic: OPEN - STOP - CLOSE + if (this->last_command_ != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->toggles_needed_++; + } + } else if (call.get_position().has_value()) { + // go to position action + auto pos = *call.get_position(); + // are we at the target? + if (pos == this->position) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->target_position_ = pos; + this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); + } + } +} + +/** + * Check if the cover has reached or passed the target position. This is used only + * for partial open/close requests - endstops are used for full open/close. + * @return True if the cover has reached or passed its target position. For full open/close target always return false. + */ +bool HE60rCover::is_at_target_() const { + // equality of floats is fraught with peril - this is reliable since the values are 0.0 or 1.0 which are + // exactly representable. + if (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED) + return false; + // aiming for an intermediate position - exact comparison here will not work and we need to allow for overshoot + switch (this->last_command_) { + case COVER_OPERATION_OPENING: + return this->position >= this->target_position_; + case COVER_OPERATION_CLOSING: + return this->position <= this->target_position_; + case COVER_OPERATION_IDLE: + return this->current_operation == COVER_OPERATION_IDLE; + default: + return true; + } +} +void HE60rCover::start_direction_(CoverOperation dir) { + this->last_command_ = dir; + if (this->current_operation == dir) + return; + ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(), + dir == COVER_OPERATION_OPENING ? "OPEN" + : dir == COVER_OPERATION_CLOSING ? "CLOSE" + : "STOP"); + + if (dir == this->next_direction_) { + // either moving and needs to stop, or stopped and will move correctly on one trigger + this->toggles_needed_ = 1; + } else { + if (this->current_operation == COVER_OPERATION_IDLE) { + // if stopped, but will go the wrong way, need 3 triggers. + this->toggles_needed_ = 3; + } else { + // just stop and reverse + this->toggles_needed_ = 2; + } + ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); + } + this->start_dir_time_ = millis(); +} + +void HE60rCover::recompute_position_() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + + const uint32_t now = millis(); + float dir; + float action_dur; + + switch (this->current_operation) { + case COVER_OPERATION_OPENING: + dir = 1.0f; + action_dur = this->open_duration_; + break; + case COVER_OPERATION_CLOSING: + dir = -1.0f; + action_dur = this->close_duration_; + break; + default: + return; + } + + if (now > this->last_recompute_time_) { + auto diff = now - last_recompute_time_; + auto delta = dir * diff / action_dur; + // make sure our guesstimate never reaches full open or close. + this->position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f); + ESP_LOGD(TAG, "Recompute %dms, dir=%f, action_dur=%f, delta=%f, pos=%f", (int) diff, dir, action_dur, delta, + this->position); + this->last_recompute_time_ = now; + this->publish_state(); + } +} + +} // namespace he60r +} // namespace esphome diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h new file mode 100644 index 0000000000..624b61fc65 --- /dev/null +++ b/esphome/components/he60r/he60r.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace he60r { + +class HE60rCover : public cover::Cover, public Component, public uart::UARTDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } + void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } + + cover::CoverTraits get_traits() override; + + protected: + void update_(); + void control(const cover::CoverCall &call) override; + bool is_at_target_() const; + void start_direction_(cover::CoverOperation dir); + void update_operation_(cover::CoverOperation dir); + void endstop_reached_(cover::CoverOperation operation); + void recompute_position_(); + void set_current_operation_(cover::CoverOperation operation); + void process_rx_(uint8_t data); + + uint32_t open_duration_{0}; + uint32_t close_duration_{0}; + uint32_t toggles_needed_{0}; + cover::CoverOperation next_direction_{cover::COVER_OPERATION_IDLE}; + cover::CoverOperation last_command_{cover::COVER_OPERATION_IDLE}; + uint32_t last_recompute_time_{0}; + uint32_t start_dir_time_{0}; + float target_position_{0}; + bool query_seen_{}; + uint8_t counter_{}; +}; + +} // namespace he60r +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 69d9211969..65aab7cdde 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -58,6 +58,12 @@ uart: tx_pin: GPIO22 rx_pin: GPIO23 baud_rate: 9600 + - id: uart_he60r + tx_pin: 22 + rx_pin: 23 + baud_rate: 1200 + parity: EVEN + ota: safe_mode: true @@ -528,6 +534,13 @@ cover: - platform: copy source_id: tuya_cover name: Tuya Cover copy + - platform: he60r + uart_id: uart_he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s + display: - platform: addressable_light From 6cf4412e7b5210c3f5fadd960f1f99179682b8b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:49:42 -1000 Subject: [PATCH 089/436] Bump aioesphomeapi from 19.1.7 to 19.2.1 (#5863) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2af36b1339..574b1c0a69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.1.7 +aioesphomeapi==19.2.1 zeroconf==0.127.0 python-magic==0.4.27 From 782854ab365ba4c50dc29a228b73f0d63a421ef1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:51:14 -1000 Subject: [PATCH 090/436] Bump tornado from 6.3.3 to 6.4 (#5862) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 574b1c0a69..841c5a97cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ voluptuous==0.13.1 PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 -tornado==6.3.3 +tornado==6.4 tzlocal==5.2 # from time tzdata>=2021.1 # from time pyserial==3.5 From 788f1b60e2449ebf1ebc84884ab8570bd6eec9c6 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 2 Dec 2023 22:58:25 -0800 Subject: [PATCH 091/436] add missing ifdef to pvvx_mithermometer (#5880) --- esphome/components/pvvx_mithermometer/display/pvvx_display.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index fc200f7d71..d192e62430 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -13,7 +13,9 @@ void PVVXDisplay::dump_config() { ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); ESP_LOGCONFIG(TAG, " Characteristic UUID : %s", this->char_uuid_.to_string().c_str()); ESP_LOGCONFIG(TAG, " Auto clear : %s", YESNO(this->auto_clear_enabled_)); +#ifdef USE_TIME ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); +#endif ESP_LOGCONFIG(TAG, " Disconnect delay : %" PRIu32 "ms", this->disconnect_delay_ms_); LOG_UPDATE_INTERVAL(this); } From e271faa5441bcfef99c30d4ef1c864db8556027e Mon Sep 17 00:00:00 2001 From: Mike La Spina Date: Mon, 4 Dec 2023 17:15:01 -0600 Subject: [PATCH 092/436] Fix un-initialized version string (#5865) --- esphome/components/ld2420/ld2420.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 2780503776..2b50c7a1d4 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -255,12 +255,11 @@ class LD2420Component : public Component, public uart::UARTDevice { uint16_t gate_energy_[LD2420_TOTAL_GATES]; CmdReplyT cmd_reply_; - uint32_t timeout_; uint32_t max_distance_gate_; uint32_t min_distance_gate_; uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY}; bool cmd_active_{false}; - char ld2420_firmware_ver_[8]; + char ld2420_firmware_ver_[8]{"v0.0.0"}; bool presence_{false}; bool calibration_{false}; uint16_t distance_{0}; From 29dcc4031f9cc0d6b1044003da1ac72bc7e71c93 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 4 Dec 2023 15:41:33 -0800 Subject: [PATCH 093/436] fix a01nyub data reading (#5882) Co-authored-by: Samuel Sieb --- esphome/components/a01nyub/a01nyub.cpp | 49 ++++++++++---------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/esphome/components/a01nyub/a01nyub.cpp b/esphome/components/a01nyub/a01nyub.cpp index 75cb276f84..d0bc89a0c9 100644 --- a/esphome/components/a01nyub/a01nyub.cpp +++ b/esphome/components/a01nyub/a01nyub.cpp @@ -8,50 +8,37 @@ namespace esphome { namespace a01nyub { static const char *const TAG = "a01nyub.sensor"; -static const uint8_t MAX_DATA_LENGTH_BYTES = 4; void A01nyubComponent::loop() { uint8_t data; while (this->available() > 0) { - if (this->read_byte(&data)) { - buffer_.push_back(data); + this->read_byte(&data); + if (this->buffer_.empty() && (data != 0xff)) + continue; + buffer_.push_back(data); + if (this->buffer_.size() == 4) this->check_buffer_(); - } } } void A01nyubComponent::check_buffer_() { - if (this->buffer_.size() >= MAX_DATA_LENGTH_BYTES) { - size_t i; - for (i = 0; i < this->buffer_.size(); i++) { - // Look for the first packet - if (this->buffer_[i] == 0xFF) { - if (i + 1 + 3 < this->buffer_.size()) { // Packet is not complete - return; // Wait for completion - } - - uint8_t checksum = (this->buffer_[i] + this->buffer_[i + 1] + this->buffer_[i + 2]) & 0xFF; - if (this->buffer_[i + 3] == checksum) { - float distance = (this->buffer_[i + 1] << 8) + this->buffer_[i + 2]; - if (distance > 280) { - float meters = distance / 1000.0; - ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); - this->publish_state(meters); - } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); - } - } - break; - } + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; + if (this->buffer_[3] == checksum) { + float distance = (this->buffer_[1] << 8) + this->buffer_[2]; + if (distance > 280) { + float meters = distance / 1000.0; + ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); + this->publish_state(meters); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); } - this->buffer_.clear(); + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); } + this->buffer_.clear(); } -void A01nyubComponent::dump_config() { - ESP_LOGCONFIG(TAG, "A01nyub Sensor:"); - LOG_SENSOR(" ", "Distance", this); -} +void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); } } // namespace a01nyub } // namespace esphome From 0c71685d55b8af17fd7a8fa95e7cd3ee8e99af27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:42:02 +0900 Subject: [PATCH 094/436] Bump pytest-asyncio from 0.21.1 to 0.23.2 (#5888) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index d2ce98cc8c..c4fa12d0e7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ pre-commit pytest==7.4.3 pytest-cov==4.1.0 pytest-mock==3.12.0 -pytest-asyncio==0.21.1 +pytest-asyncio==0.23.2 asyncmock==0.4.2 hypothesis==5.49.0 From c2183eb7f07bb5a398359f4c2ebfd7c05a45f6b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:42:19 +0900 Subject: [PATCH 095/436] Bump zeroconf from 0.127.0 to 0.128.0 (#5889) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 841c5a97cd..7261972e01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==19.2.1 -zeroconf==0.127.0 +zeroconf==0.128.0 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From 2a740963bad3c27141bb429e361ef141a8dc68d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:42:37 +0900 Subject: [PATCH 096/436] Bump pylint from 2.17.6 to 3.0.2 (#5592) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c4fa12d0e7..cb96f79587 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.17.6 +pylint==3.0.2 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating black==23.11.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating From 657a7070cbbcbf6fa0c61cf271056e263229a578 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:43:07 +0900 Subject: [PATCH 097/436] Bump voluptuous from 0.13.1 to 0.14.1 (#5784) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7261972e01..7d37954656 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -voluptuous==0.13.1 +voluptuous==0.14.1 PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 From b8fe4f8d56f1d3590d7d2e132d211837a52c27fc Mon Sep 17 00:00:00 2001 From: Fabio Pugliese Ornellas Date: Mon, 4 Dec 2023 23:50:01 +0000 Subject: [PATCH 098/436] Security improvement: Support wifi ap_timeout=0s (disable) (#5887) --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d023405728..519489097a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -164,7 +164,7 @@ void WiFiComponent::loop() { #ifdef USE_WIFI_AP if (this->has_ap() && !this->ap_setup_) { - if (now - this->last_connected_ > this->ap_timeout_) { + if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) { ESP_LOGI(TAG, "Starting fallback AP!"); this->setup_ap_config_(); #ifdef USE_CAPTIVE_PORTAL From df5394d51c73748b1d545a629c61e3787469ee97 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:52:02 +1100 Subject: [PATCH 099/436] Suppress full config output of "esphome config" when -q option is used. (#5852) --- esphome/__main__.py | 3 ++- esphome/core/__init__.py | 2 ++ esphome/log.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index e5456cf8e5..0796dead43 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -389,7 +389,8 @@ def command_config(args, config): output = re.sub( r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output ) - safe_print(output) + if not CORE.quiet: + safe_print(output) _LOGGER.info("Configuration is valid!") return 0 diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 60bd17b481..3c3b27a79d 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -522,6 +522,8 @@ class EsphomeCore: self.component_ids = set() # Whether ESPHome was started in verbose mode self.verbose = False + # Whether ESPHome was started in quiet mode + self.quiet = False def reset(self): self.dashboard = False diff --git a/esphome/log.py b/esphome/log.py index b5d72e774c..23dc453d32 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -78,6 +78,7 @@ def setup_log( CORE.verbose = True elif quiet: log_level = logging.CRITICAL + CORE.quiet = True else: log_level = logging.INFO logging.basicConfig(level=log_level) From d9792b0d923ef640019db418b21e78993b7be312 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:56:53 +1100 Subject: [PATCH 100/436] Checks for pins used in multiple places (#5666) --- esphome/components/adc/__init__.py | 26 +- esphome/components/esp32/gpio.py | 37 +- esphome/components/esp8266/gpio.py | 27 +- esphome/components/host/gpio.py | 21 +- esphome/components/i2c/__init__.py | 5 +- esphome/components/libretiny/gpio.py | 24 +- esphome/components/max6956/__init__.py | 18 +- esphome/components/mcp23016/__init__.py | 18 +- esphome/components/mcp23xxx_base/__init__.py | 20 +- esphome/components/pca9554/__init__.py | 19 +- esphome/components/pcf8574/__init__.py | 18 +- esphome/components/rp2040/gpio.py | 22 +- esphome/components/sn74hc595/__init__.py | 22 +- esphome/config.py | 48 +- esphome/const.py | 1 + esphome/core/__init__.py | 3 + esphome/pins.py | 185 ++++++- esphome/util.py | 26 - tests/test1.1.yaml | 20 +- tests/test1.yaml | 539 ++++++++++++++----- tests/test11.5.yaml | 93 +++- tests/test2.yaml | 76 ++- tests/test3.1.yaml | 121 +++-- tests/test3.yaml | 169 ++++-- tests/test4.yaml | 289 +++++++--- tests/test5.yaml | 54 +- tests/test8.yaml | 9 +- 27 files changed, 1335 insertions(+), 575 deletions(-) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index bad5cf74ef..952fbdd9b9 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ANALOG, CONF_INPUT +from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER from esphome.core import CORE from esphome.components.esp32 import get_esp32_variant @@ -152,7 +152,8 @@ def validate_adc_pin(value): return cv.only_on_rp2040("TEMPERATURE") if CORE.is_esp32: - value = pins.internal_gpio_input_pin_number(value) + conf = pins.internal_gpio_input_pin_schema(value) + value = conf[CONF_NUMBER] variant = get_esp32_variant() if ( variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL @@ -166,24 +167,23 @@ def validate_adc_pin(value): ): raise cv.Invalid(f"{variant} doesn't support ADC on this pin") - return pins.internal_gpio_input_pin_schema(value) + return conf if CORE.is_esp8266: - value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( - value - ) - - if value != 17: # A0 - raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") - return pins.gpio_pin_schema( + conf = pins.gpio_pin_schema( {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) + if conf[CONF_NUMBER] != 17: # A0 + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") + return conf + if CORE.is_rp2040: - value = pins.internal_gpio_input_pin_number(value) - if value not in (26, 27, 28, 29): + conf = pins.internal_gpio_input_pin_schema(value) + number = conf[CONF_NUMBER] + if number not in (26, 27, 28, 29): raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") - return pins.internal_gpio_input_pin_schema(value) + return conf if CORE.is_libretiny: return pins.gpio_pin_schema( diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index a53649e3e4..16f99f2b15 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -3,15 +3,13 @@ from typing import Any from esphome.const import ( CONF_ID, - CONF_INPUT, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OPEN_DRAIN, CONF_OUTPUT, - CONF_PULLDOWN, - CONF_PULLUP, CONF_IGNORE_STRAPPING_WARNING, + PLATFORM_ESP32, ) from esphome import pins from esphome.core import CORE @@ -33,7 +31,6 @@ from .const import ( 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 @@ -42,7 +39,6 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support 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 - ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) @@ -161,33 +157,22 @@ DRIVE_STRENGTHS = { } gpio_num_t = cg.global_ns.enum("gpio_num_t") - CONF_DRIVE_STRENGTH = "drive_strength" ESP32_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, - cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( - cv.float_with_unit("current", "mA", optional_unit=True), - cv.enum(DRIVE_STRENGTHS), - ), - }, + pins.gpio_base_schema(ESP32InternalGPIOPin, validate_gpio_pin).extend( + { + cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, + cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( + cv.float_with_unit("current", "mA", optional_unit=True), + cv.enum(DRIVE_STRENGTHS), + ), + } + ), validate_supports, ) -@pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA) async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index e75578cc16..c42bc9204f 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, + PLATFORM_ESP8266, ) from esphome import pins from esphome.core import CORE, coroutine_with_priority @@ -21,10 +22,8 @@ import esphome.codegen as cg from . import boards from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns - _LOGGER = logging.getLogger(__name__) - ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) @@ -124,6 +123,8 @@ def validate_supports(value): (True, False, False, False, False), # OUTPUT (False, True, False, False, False), + # INPUT and OUTPUT, e.g. for i2c + (True, True, False, False, False), # INPUT_PULLUP (True, False, False, True, False), # INPUT_PULLDOWN_16 @@ -142,21 +143,11 @@ def validate_supports(value): ESP8266_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(ESP8266GPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, + pins.gpio_base_schema( + ESP8266GPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), + ), validate_supports, ) @@ -167,7 +158,7 @@ class PinInitialState: level: int = 255 -@pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA) async def esp8266_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index d523d28ee5..180919de4f 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -17,10 +17,8 @@ import esphome.codegen as cg from .const import host_ns - _LOGGER = logging.getLogger(__name__) - HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) @@ -45,21 +43,10 @@ def validate_gpio_pin(value): return _translate_pin(value) -HOST_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(HostGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, +HOST_PIN_SCHEMA = pins.gpio_base_schema( + HostGPIOPin, + validate_gpio_pin, + modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN, CONF_PULLUP, CONF_PULLDOWN], ) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 676190b0e5..0a1f049b93 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -39,9 +39,8 @@ def _bus_declare_type(value): raise NotImplementedError -pin_with_input_and_output_support = cv.All( - pins.internal_gpio_pin_number({CONF_INPUT: True}), - pins.internal_gpio_pin_number({CONF_OUTPUT: True}), +pin_with_input_and_output_support = pins.internal_gpio_pin_number( + {CONF_OUTPUT: True, CONF_INPUT: True} ) diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index ba9bfffcc9..1d7b37cc9b 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -186,25 +186,11 @@ def validate_gpio_usage(value): return value -BASE_PIN_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ArduinoInternalGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, -) - -BASE_PIN_SCHEMA.add_extra(validate_gpio_usage) +BASE_PIN_SCHEMA = pins.gpio_base_schema( + ArduinoInternalGPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), +).add_extra(validate_gpio_usage) async def component_pin_to_code(config): diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index 77e0d37e76..bb71dba8bf 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -74,20 +74,14 @@ def validate_mode(value): CONF_MAX6956 = "max6956" -MAX6956_PIN_SCHEMA = cv.All( +MAX6956_PIN_SCHEMA = pins.gpio_base_schema( + MAX6956GPIOPin, + cv.int_range(min=4, max=31), + modes=[CONF_INPUT, CONF_PULLUP, CONF_OUTPUT], + mode_validator=validate_mode, +).extend( { - cv.GenerateID(): cv.declare_id(MAX6956GPIOPin), cv.Required(CONF_MAX6956): cv.use_id(MAX6956), - cv.Required(CONF_NUMBER): cv.int_range(min=4, max=31), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index c1209a9627..55722e3ae0 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -45,19 +45,15 @@ def validate_mode(value): CONF_MCP23016 = "mcp23016" -MCP23016_PIN_SCHEMA = cv.All( +MCP23016_PIN_SCHEMA = pins.gpio_base_schema( + MCP23016GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(MCP23016GPIOPin), cv.Required(CONF_MCP23016): cv.use_id(MCP23016), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 7bcd5c84fc..1e41a8ddff 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -54,20 +54,16 @@ def validate_mode(value): CONF_MCP23XXX = "mcp23xxx" -MCP23XXX_PIN_SCHEMA = cv.All( + +MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( + MCP23XXXGPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(MCP23XXXGPIOPin), cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, cv.Optional(CONF_INTERRUPT, default="NO_INTERRUPT"): cv.enum( MCP23XXX_INTERRUPT_MODES, upper=True ), diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index fd52fafc5d..da31dbd9d9 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -52,20 +52,15 @@ def validate_mode(value): return value -PCA9554_PIN_SCHEMA = cv.All( +PCA9554_PIN_SCHEMA = pins.gpio_base_schema( + PCA9554GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, +).extend( { - cv.GenerateID(): cv.declare_id(PCA9554GPIOPin), cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, + } ) diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index d44ac28364..ebf112b85b 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -48,19 +48,15 @@ def validate_mode(value): return value -PCF8574_PIN_SCHEMA = cv.All( +PCF8574_PIN_SCHEMA = pins.gpio_base_schema( + PCF8574GPIOPin, + cv.int_range(min=0, max=17), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(PCF8574GPIOPin), cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=17), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 4823a6d22a..6ba0975a2c 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( - CONF_ANALOG, CONF_ID, CONF_INPUT, CONF_INVERTED, @@ -11,6 +10,7 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, + CONF_ANALOG, ) from esphome.core import CORE from esphome import pins @@ -78,22 +78,10 @@ def validate_supports(value): RP2040_PIN_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(RP2040GPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } + pins.gpio_base_schema( + RP2040GPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), ), validate_supports, ) diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index e7ba45175c..11a6747656 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -5,7 +5,6 @@ from esphome.components import spi from esphome.const import ( CONF_ID, CONF_SPI_ID, - CONF_MODE, CONF_NUMBER, CONF_INVERTED, CONF_DATA_PIN, @@ -35,7 +34,6 @@ CONF_LATCH_PIN = "latch_pin" CONF_OE_PIN = "oe_pin" CONF_SR_COUNT = "sr_count" - CONFIG_SCHEMA = cv.Any( cv.Schema( { @@ -88,24 +86,20 @@ async def to_code(config): def _validate_output_mode(value): - if value is not True: + if value.get(CONF_OUTPUT) is not True: raise cv.Invalid("Only output mode is supported") return value -SN74HC595_PIN_SCHEMA = cv.All( +SN74HC595_PIN_SCHEMA = pins.gpio_base_schema( + SN74HC595GPIOPin, + cv.int_range(min=0, max=2047), + modes=[CONF_OUTPUT], + mode_validator=_validate_output_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(SN74HC595GPIOPin), cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=2048, max_included=False), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_OUTPUT, default=True): cv.All( - cv.boolean, _validate_output_mode - ), - }, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/config.py b/esphome/config.py index a980358186..6f644cee14 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -7,6 +7,7 @@ import re from typing import Optional, Union from contextlib import contextmanager +import contextvars import voluptuous as vol @@ -53,6 +54,7 @@ def iter_components(config): ConfigPath = list[Union[str, int]] +path_context = contextvars.ContextVar("Config path") def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool: @@ -489,6 +491,7 @@ class SchemaValidationStep(ConfigValidationStep): def run(self, result: Config) -> None: if self.comp.config_schema is None: return + token = path_context.set(self.path) with result.catch_error(self.path): if self.comp.is_platform: # Remove 'platform' key for validation @@ -507,6 +510,7 @@ class SchemaValidationStep(ConfigValidationStep): validated = schema(self.conf) result.set_by_path(self.path, validated) + path_context.reset(token) result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) @@ -652,37 +656,24 @@ class FinalValidateValidationStep(ConfigValidationStep): if self.comp.final_validate_schema is not None: self.comp.final_validate_schema(conf) - fconf = fv.full_config.get() - - def _check_pins(c): - for value in c.values(): - if not isinstance(value, dict): - continue - for key, ( - _, - _, - pin_final_validate, - ) in pins.PIN_SCHEMA_REGISTRY.items(): - if ( - key != CORE.target_platform - and key in value - and pin_final_validate is not None - ): - pin_final_validate(fconf, value) - - # Check for pin configs and a final_validate schema in the pin registry - confs = conf - if not isinstance( - confs, list - ): # Handle components like SPI that have a list instead of MULTI_CONF - confs = [conf] - for c in confs: - if c: # Some component have None or empty schemas - _check_pins(c) - fv.full_config.reset(token) +class PinUseValidationCheck(ConfigValidationStep): + """Check for pin reuse""" + + priority = -30 # Should happen after component final validations + + def __init__(self) -> None: + pass + + def run(self, result: Config) -> None: + if result.errors: + # If result already has errors, skip this step + return + pins.PIN_SCHEMA_REGISTRY.final_validate(result) + + def validate_config(config, command_line_substitutions) -> Config: result = Config() @@ -778,6 +769,7 @@ def validate_config(config, command_line_substitutions) -> Config: for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) + result.add_validation_step(PinUseValidationCheck()) result.run_validation_steps() diff --git a/esphome/const.py b/esphome/const.py index 2487d7b64c..2bc088c063 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -46,6 +46,7 @@ CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" CONF_AFTER = "after" +CONF_ALLOW_OTHER_USES = "allow_other_uses" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" CONF_ANALOG = "analog" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 3c3b27a79d..58ae23e139 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -526,6 +526,8 @@ class EsphomeCore: self.quiet = False def reset(self): + from esphome.pins import PIN_SCHEMA_REGISTRY + self.dashboard = False self.name = None self.friendly_name = None @@ -545,6 +547,7 @@ class EsphomeCore: self.platformio_options = {} self.loaded_integrations = set() self.component_ids = set() + PIN_SCHEMA_REGISTRY.reset() @property def address(self) -> Optional[str]: diff --git a/esphome/pins.py b/esphome/pins.py index 0035bea4f0..e2fd8e98e2 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,5 +1,7 @@ import operator from functools import reduce +import esphome.config_validation as cv +from esphome.core import CORE, ID from esphome.const import ( CONF_INPUT, @@ -10,16 +12,120 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, CONF_IGNORE_STRAPPING_WARNING, + CONF_ALLOW_OTHER_USES, + CONF_INVERTED, ) -from esphome.util import PinRegistry -from esphome.core import CORE + + +class PinRegistry(dict): + def __init__(self): + super().__init__() + self.pins_used = {} + + def reset(self): + self.pins_used = {} + + def get_count(self, key, number): + """ + Get the number of places a given pin is used. + :param key: The ID of the defining component + :param number: The pin number + :return: The number of places the pin is used. + """ + pin_key = (key, number) + return self.pins_used[pin_key] if pin_key in self.pins_used else 0 + + def register(self, name, schema, final_validate=None): + """ + Register a pin schema + :param name: + :param schema: + :param final_validate: + :return: + """ + + def decorator(fun): + self[name] = (fun, schema, final_validate) + return fun + + return decorator + + def validate(self, conf, key=None): + """ + Validate a pin against a registered schema + :param conf The pin config + :param key: an optional scalar key (e.g. platform) + :return: The transformed result + """ + from esphome.config import path_context + + key = self.get_key(conf) if key is None else key + # Element 1 is the pin validation function + # evaluate here so a validation failure skips the rest + result = self[key][1](conf) + if CONF_NUMBER in result: + # key maps to the pin schema + if isinstance(key, ID): + key = key.id + pin_key = (key, result[CONF_NUMBER]) + if pin_key not in self.pins_used: + self.pins_used[pin_key] = [] + # client_id identifies the instance of the providing component + client_id = result.get(key) + self.pins_used[pin_key].append((path_context.get(), client_id, result)) + # return the validated pin config + return result + + def get_key(self, conf): + """ + Is there a key in conf corresponding to a registered pin schema? + If not, fall back to the default platform schema. + :param conf The config for the component + :return: the schema key + """ + keys = list(filter(lambda k: k in conf, self)) + return keys[0] if keys else CORE.target_platform + + def get_to_code(self, key): + """ + Return the code generator function for a pin schema, stored as tuple element 0 + :param conf: The pin config + :param key An optional specific key + :return: The awaitable coroutine + """ + key = self.get_key(key) if isinstance(key, dict) else key + return self[key][0] + + def final_validate(self, fconf): + """ + Run the final validation for all pins, and check for reuse + :param fconf: The full config + """ + for (key, _), pin_list in self.pins_used.items(): + count = len(pin_list) # number of places same pin used. + final_val_fun = self[key][2] # final validation function + for pin_path, client_id, pin_config in pin_list: + with fconf.catch_error([cv.ROOT_CONFIG_PATH] + pin_path): + if final_val_fun is not None: + # Get the containing path of the config providing this pin. + parent_path = fconf.get_path_for_id(client_id)[:-1] + parent_config = fconf.get_config_for_path(parent_path) + final_val_fun(pin_config, parent_config) + allow_others = pin_config.get(CONF_ALLOW_OTHER_USES, False) + if count != 1 and not allow_others: + raise cv.Invalid( + f"Pin {pin_config[CONF_NUMBER]} is used in multiple places" + ) + if count == 1 and allow_others: + raise cv.Invalid( + f"Pin {pin_config[CONF_NUMBER]} incorrectly sets {CONF_ALLOW_OTHER_USES}: true" + ) + PIN_SCHEMA_REGISTRY = PinRegistry() def _set_mode(value, default_mode): - import esphome.config_validation as cv - if CONF_MODE not in value: return {**value, CONF_MODE: default_mode} mode = value[CONF_MODE] @@ -65,20 +171,26 @@ def _schema_creator(default_mode, internal: bool = False): if not isinstance(value, dict): return validator({CONF_NUMBER: value}) value = _set_mode(value, default_mode) - if not internal: - for key, entry in PIN_SCHEMA_REGISTRY.items(): - if key != CORE.target_platform and key in value: - return entry[1](value) - return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value) + if internal: + return PIN_SCHEMA_REGISTRY.validate(value, CORE.target_platform) + return PIN_SCHEMA_REGISTRY.validate(value) return validator def _internal_number_creator(mode): def validator(value): - value_d = {CONF_NUMBER: value} + if isinstance(value, dict): + if CONF_MODE in value or CONF_INVERTED in value: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + value_d = value + else: + value_d = {CONF_NUMBER: value} value_d = _set_mode(value_d, mode) - return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value_d)[CONF_NUMBER] + return PIN_SCHEMA_REGISTRY.validate(value_d, CORE.target_platform)[CONF_NUMBER] return validator @@ -149,8 +261,6 @@ internal_gpio_input_pullup_pin_number = _internal_number_creator( def check_strapping_pin(conf, strapping_pin_list, logger): - import esphome.config_validation as cv - num = conf[CONF_NUMBER] if num in strapping_pin_list and not conf.get(CONF_IGNORE_STRAPPING_WARNING): logger.warning( @@ -161,3 +271,52 @@ def check_strapping_pin(conf, strapping_pin_list, logger): # mitigate undisciplined use of strapping: if num not in strapping_pin_list and conf.get(CONF_IGNORE_STRAPPING_WARNING): raise cv.Invalid(f"GPIO{num} is not a strapping pin") + + +GPIO_STANDARD_MODES = ( + CONF_INPUT, + CONF_OUTPUT, + CONF_OPEN_DRAIN, + CONF_PULLUP, + CONF_PULLDOWN, +) + + +def gpio_validate_modes(value): + if not value[CONF_INPUT] and not value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be input or output") + return value + + +def gpio_base_schema( + pin_type, + number_validator, + modes=GPIO_STANDARD_MODES, + mode_validator=gpio_validate_modes, + invertable=True, +): + """ + Generate a base gpio pin schema + :param pin_type: The type for the pin variable + :param number_validator: A validator for the pin number + :param modes: The available modes, default is all standard modes + :param mode_validator: A validator function for the pin mode + :param invertable: If the pin supports hardware inversion + :return: A schema for the pin + """ + mode_default = len(modes) == 1 + mode_dict = dict( + map(lambda m: (cv.Optional(m, default=mode_default), cv.boolean), modes) + ) + + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(pin_type), + cv.Required(CONF_NUMBER): number_validator, + cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean, + cv.Optional(CONF_MODE, default={}): cv.All(mode_dict, mode_validator), + } + ) + if invertable: + return schema.extend({cv.Optional(CONF_INVERTED, default=False): cv.boolean}) + return schema diff --git a/esphome/util.py b/esphome/util.py index d9c8502e0e..d5a4c60570 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -57,32 +57,6 @@ class SimpleRegistry(dict): return decorator -def _final_validate(parent_id_key, fun): - def validator(fconf, pin_config): - import esphome.config_validation as cv - - parent_path = fconf.get_path_for_id(pin_config[parent_id_key])[:-1] - parent_config = fconf.get_config_for_path(parent_path) - - pin_path = fconf.get_path_for_id(pin_config[const.CONF_ID])[:-1] - with cv.prepend_path([cv.ROOT_CONFIG_PATH] + pin_path): - fun(pin_config, parent_config) - - return validator - - -class PinRegistry(dict): - def register(self, name, schema, final_validate=None): - if final_validate is not None: - final_validate = _final_validate(name, final_validate) - - def decorator(fun): - self[name] = (fun, schema, final_validate) - return fun - - return decorator - - def safe_print(message="", end="\n"): from esphome.core import CORE diff --git a/tests/test1.1.yaml b/tests/test1.1.yaml index e2e7bd5d63..c71aa6e0ef 100644 --- a/tests/test1.1.yaml +++ b/tests/test1.1.yaml @@ -54,7 +54,9 @@ power_supply: i2c: sda: 21 - scl: 22 + scl: + number: 22 + allow_other_uses: true scan: true frequency: 100kHz setup_priority: -100 @@ -86,7 +88,9 @@ light: - platform: fastled_clockless id: addr1 chipset: WS2811 - pin: GPIO23 + pin: + allow_other_uses: true + number: GPIO23 num_leds: 60 rgb_order: BRG max_refresh_rate: 20ms @@ -168,8 +172,12 @@ light: - platform: fastled_spi id: addr2 chipset: WS2801 - data_pin: GPIO23 - clock_pin: GPIO22 + data_pin: + allow_other_uses: true + number: GPIO23 + clock_pin: + number: GPIO22 + allow_other_uses: true data_rate: 2MHz num_leds: 60 rgb_order: BRG @@ -190,7 +198,9 @@ light: variant: SK6812 method: ESP32_I2S_0 num_leds: 60 - pin: GPIO23 + pin: + allow_other_uses: true + number: GPIO23 - platform: partition name: Partition Light segments: diff --git a/tests/test1.yaml b/tests/test1.yaml index 0849d8aeb6..c90cdf6d90 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -181,8 +181,12 @@ mqtt: - light.turn_off: ${roomname}_lights i2c: - sda: 21 - scl: 22 + sda: + allow_other_uses: true + number: 21 + scl: + allow_other_uses: true + number: 22 scan: true frequency: 100kHz setup_priority: -100 @@ -190,15 +194,23 @@ i2c: spi: id: spi_bus - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO21 + mosi_pin: + allow_other_uses: true + number: GPIO22 + miso_pin: + allow_other_uses: true + number: GPIO23 uart: - tx_pin: + allow_other_uses: true number: GPIO22 inverted: true rx_pin: + allow_other_uses: true number: GPIO23 inverted: true baud_rate: 115200 @@ -220,18 +232,30 @@ uart: - lambda: UARTDebug::log_int(direction, bytes, ','); - lambda: UARTDebug::log_binary(direction, bytes, ';'); - id: ld2410_uart - tx_pin: 18 - rx_pin: 23 + tx_pin: + allow_other_uses: true + number: 18 + rx_pin: + allow_other_uses: true + number: 23 baud_rate: 256000 parity: NONE stop_bits: 1 - id: dfrobot_mmwave_uart - tx_pin: 14 - rx_pin: 27 + tx_pin: + allow_other_uses: true + number: 14 + rx_pin: + allow_other_uses: true + number: 27 baud_rate: 115200 - id: ld2420_uart - tx_pin: 17 - rx_pin: 16 + tx_pin: + allow_other_uses: true + number: 17 + rx_pin: + allow_other_uses: true + number: 16 baud_rate: 115200 parity: NONE stop_bits: 1 @@ -282,12 +306,16 @@ power_supply: keep_on_time: 10s pin: number: 13 + allow_other_uses: true inverted: true deep_sleep: run_duration: 20s sleep_duration: 50s - wakeup_pin: GPIO2 + wakeup_pin: + allow_other_uses: true + number: GPIO2 + ignore_strapping_warning: true wakeup_pin_mode: INVERT_WAKEUP ads1115: @@ -295,11 +323,18 @@ ads1115: i2c_id: i2c_bus dallas: - pin: GPIO23 + pin: + allow_other_uses: true + number: GPIO23 as3935_spi: - cs_pin: GPIO12 - irq_pin: GPIO13 + cs_pin: + ignore_strapping_warning: true + allow_other_uses: true + number: GPIO12 + irq_pin: + allow_other_uses: true + number: GPIO13 esp32_ble: io_capability: keyboard_only @@ -339,16 +374,24 @@ bedjet: time_id: sntp_time mcp23s08: - id: mcp23s08_hub - cs_pin: GPIO12 + cs_pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true deviceaddress: 0 mcp23s17: - id: mcp23s17_hub - cs_pin: GPIO12 + cs_pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true deviceaddress: 1 micronova: - enable_rx_pin: 4 + enable_rx_pin: + allow_other_uses: true + number: 4 uart_id: uart_0 dfrobot_sen0395: @@ -539,7 +582,9 @@ sensor: name: NIR i2c_id: i2c_bus - platform: atm90e26 - cs_pin: 5 + cs_pin: + allow_other_uses: true + number: 5 voltage: name: Line Voltage current: @@ -558,7 +603,9 @@ sensor: gain_voltage: 26400 gain_ct: 31251 - platform: atm90e32 - cs_pin: 5 + cs_pin: + allow_other_uses: true + number: 5 phase_a: voltage: name: EMON Line Voltage A @@ -675,7 +722,9 @@ sensor: index: 1 name: Living Room Temperature 2 - platform: dht - pin: GPIO26 + pin: + allow_other_uses: true + number: GPIO26 temperature: id: dht_temperature name: Living Room Temperature 3 @@ -692,7 +741,9 @@ sensor: update_interval: 15s i2c_id: i2c_bus - platform: duty_cycle - pin: GPIO25 + pin: + allow_other_uses: true + number: GPIO25 name: Duty Cycle Sensor - platform: ee895 co2: @@ -721,9 +772,15 @@ sensor: update_interval: 15s i2c_id: i2c_bus - platform: hlw8012 - sel_pin: 5 - cf_pin: 14 - cf1_pin: 13 + sel_pin: + allow_other_uses: true + number: 5 + cf_pin: + allow_other_uses: true + number: 14 + cf1_pin: + allow_other_uses: true + number: 13 current: name: HLW8012 Current voltage: @@ -772,7 +829,9 @@ sensor: max_pressure: 15 temperature: name: Honeywell temperature - cs_pin: GPIO5 + cs_pin: + allow_other_uses: true + number: GPIO5 - platform: honeywellabp2_i2c pressure: name: Honeywell2 pressure @@ -806,8 +865,12 @@ sensor: i2c_id: i2c_bus - platform: hx711 name: HX711 Value - dout_pin: GPIO23 - clk_pin: GPIO25 + dout_pin: + allow_other_uses: true + number: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO25 gain: 128 update_interval: 15s - platform: ina219 @@ -880,22 +943,30 @@ sensor: i2c_id: i2c_bus - platform: max6675 name: Living Room Temperature - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 15s - platform: max31855 name: Den Temperature - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 15s reference_temperature: name: MAX31855 Internal Temperature - platform: max31856 name: BBQ Temperature - cs_pin: GPIO17 + cs_pin: + allow_other_uses: true + number: GPIO17 update_interval: 15s mains_filter: 50Hz - platform: max31865 name: Water Tank Temperature - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 15s reference_resistance: 430 Ω rtd_nominal_resistance: 100 Ω @@ -1007,7 +1078,10 @@ sensor: i2c_id: i2c_bus - platform: pulse_counter name: Pulse Counter - pin: GPIO12 + pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true count_mode: rising_edge: INCREMENT falling_edge: DECREMENT @@ -1016,7 +1090,10 @@ sensor: - platform: pulse_meter name: Pulse Meter id: pulse_meter_sensor - pin: GPIO12 + pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true internal_filter: 100ms timeout: 2 min on_value: @@ -1039,9 +1116,15 @@ sensor: - platform: rotary_encoder name: Rotary Encoder id: rotary_encoder1 - pin_a: GPIO23 - pin_b: GPIO25 - pin_reset: GPIO25 + pin_a: + allow_other_uses: true + number: GPIO23 + pin_b: + allow_other_uses: true + number: GPIO25 + pin_reset: + allow_other_uses: true + number: GPIO25 filters: - or: - debounce: 0.1s @@ -1064,7 +1147,9 @@ sensor: - display_menu.up: - platform: pulse_width name: Pulse Width - pin: GPIO12 + pin: + allow_other_uses: true + number: GPIO12 - platform: sm300d2 uart_id: uart_0 co2: @@ -1247,9 +1332,12 @@ sensor: address: 0x48 i2c_id: i2c_bus - platform: ultrasonic - trigger_pin: GPIO25 + trigger_pin: + allow_other_uses: true + number: GPIO25 echo_pin: number: GPIO23 + allow_other_uses: true inverted: true name: Ultrasonic Sensor timeout: 5.5m @@ -1296,9 +1384,14 @@ sensor: pin: number: GPIO04 mode: INPUT + allow_other_uses: true - platform: zyaura - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: + allow_other_uses: true + number: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO4 co2: name: ZyAura CO2 temperature: @@ -1572,6 +1665,7 @@ binary_sensor: mcp23xxx: mcp23s08_hub # Use pin number 1 number: 1 + allow_other_uses: true # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false @@ -1581,6 +1675,7 @@ binary_sensor: mcp23xxx: mcp23s17_hub # Use pin number 1 number: 1 + allow_other_uses: true # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false @@ -1589,13 +1684,16 @@ binary_sensor: pin: mcp23xxx: mcp23s17_hub # Use pin number 1 + allow_other_uses: true number: 1 # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false interrupt: FALLING - platform: gpio - pin: GPIO9 + pin: + allow_other_uses: true + number: GPIO9 name: Living Room Window device_class: window filters: @@ -1664,11 +1762,13 @@ binary_sensor: - platform: gpio pin: number: GPIO9 + allow_other_uses: true mode: INPUT_PULLUP name: Living Room Window 2 - platform: gpio pin: number: GPIO9 + allow_other_uses: true mode: INPUT_OUTPUT_OPEN_DRAIN name: Living Room Button - platform: status @@ -1747,6 +1847,7 @@ binary_sensor: pin: mcp23xxx: mcp23017_hub number: 1 + allow_other_uses: true mode: INPUT inverted: true - platform: gpio @@ -1767,6 +1868,7 @@ binary_sensor: name: Speed Fan Cycle binary sensor" pin: number: 18 + allow_other_uses: true mode: input: true pulldown: true @@ -1891,42 +1993,66 @@ tlc59208f: i2c_id: i2c_bus my9231: - data_pin: GPIO12 - clock_pin: GPIO14 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 num_channels: 6 num_chips: 2 bit_depth: 16 sm2235: - data_pin: GPIO4 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO4 + clock_pin: + allow_other_uses: true + number: GPIO5 max_power_color_channels: 9 max_power_white_channels: 9 sm2335: - data_pin: GPIO4 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO4 + clock_pin: + allow_other_uses: true + number: GPIO5 max_power_color_channels: 9 max_power_white_channels: 9 bp1658cj: - data_pin: GPIO3 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO3 + clock_pin: + allow_other_uses: true + number: GPIO5 max_power_color_channels: 4 max_power_white_channels: 6 bp5758d: - data_pin: GPIO3 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO3 + clock_pin: + allow_other_uses: true + number: GPIO5 output: - platform: gpio - pin: GPIO26 + pin: + allow_other_uses: true + number: GPIO26 id: gpio_26 power_supply: atx_power_supply inverted: false - platform: ledc - pin: 19 + pin: + allow_other_uses: true + number: 19 id: gpio_19 frequency: 1500Hz channel: 14 @@ -1996,6 +2122,7 @@ output: pin: pcf8574: pcf8574_hub number: 0 + #allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2003,6 +2130,7 @@ output: pin: pca9554: pca9554_hub number: 0 + #allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2010,6 +2138,7 @@ output: pin: mcp23xxx: mcp23017_hub number: 0 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2017,6 +2146,7 @@ output: pin: mcp23xxx: mcp23008_hub number: 0 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2076,14 +2206,22 @@ output: channel: 3 - platform: slow_pwm id: id24 - pin: GPIO26 + pin: + allow_other_uses: true + number: GPIO26 period: 15s - platform: ac_dimmer id: dimmer1 - gate_pin: GPIO5 - zero_cross_pin: GPIO26 + gate_pin: + allow_other_uses: true + number: GPIO5 + zero_cross_pin: + allow_other_uses: true + number: GPIO26 - platform: esp32_dac - pin: GPIO25 + pin: + allow_other_uses: true + number: GPIO25 id: dac_output - platform: mcp4725 id: mcp4725_dac_output @@ -2147,9 +2285,15 @@ output: current: 10 - platform: x9c id: test_x9c - cs_pin: GPIO25 - inc_pin: GPIO26 - ud_pin: GPIO27 + cs_pin: + allow_other_uses: true + number: GPIO25 + inc_pin: + allow_other_uses: true + number: GPIO26 + ud_pin: + allow_other_uses: true + number: GPIO27 initial_value: 0.5 light: @@ -2256,7 +2400,9 @@ light: warm_white_color_temperature: 500 mireds remote_transmitter: - - pin: 32 + - pin: + allow_other_uses: true + number: 32 carrier_duty_percent: 100% climate: @@ -2440,6 +2586,7 @@ switch: mcp23xxx: mcp23s08_hub # Use pin number 0 number: 0 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2448,10 +2595,13 @@ switch: mcp23xxx: mcp23s17_hub # Use pin number 0 number: 1 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio - pin: GPIO25 + pin: + allow_other_uses: true + number: GPIO25 name: Living Room Dehumidifier icon: "mdi:restart" inverted: true @@ -2834,12 +2984,24 @@ display: id: my_lcd_gpio dimensions: 18x4 data_pins: - - GPIO19 - - GPIO21 - - GPIO22 - - GPIO23 - enable_pin: GPIO23 - rs_pin: GPIO25 + - + allow_other_uses: true + number: GPIO19 + - + allow_other_uses: true + number: GPIO21 + - + allow_other_uses: true + number: GPIO22 + - + allow_other_uses: true + number: GPIO23 + enable_pin: + allow_other_uses: true + number: GPIO23 + rs_pin: + allow_other_uses: true + number: GPIO25 lambda: |- it.print("Hello World!"); - platform: lcd_pcf8574 @@ -2860,13 +3022,19 @@ display: it.print("Hello World!"); i2c_id: i2c_bus - platform: max7219 - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 num_chips: 1 lambda: |- it.print("01234567"); - platform: tm1637 - clk_pin: GPIO23 - dio_pin: GPIO25 + clk_pin: + allow_other_uses: true + number: GPIO23 + dio_pin: + allow_other_uses: true + number: GPIO25 intensity: 3 lambda: |- it.print("1234"); @@ -2874,6 +3042,7 @@ display: clk_pin: mcp23xxx: mcp23017_hub number: 1 + allow_other_uses: true dio_pin: mcp23xxx: mcp23017_hub number: 2 @@ -2883,15 +3052,23 @@ display: lambda: |- it.print("1234"); - platform: pcd8544 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 contrast: 60 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1306_i2c model: SSD1306_128X64 - reset_pin: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 address: 0x3C id: display1 contrast: 60% @@ -2912,28 +3089,48 @@ display: i2c_id: i2c_bus - platform: ssd1306_spi model: SSD1306 128x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1322_spi model: SSD1322 256x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1325_spi model: SSD1325 128x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1327_i2c model: SSD1327 128X128 - reset_pin: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 address: 0x3D id: display1327 brightness: 60% @@ -2947,29 +3144,53 @@ display: i2c_id: i2c_bus - platform: ssd1327_spi model: SSD1327 128x128 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1331_spi - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1351_spi model: SSD1351 128x128 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7789v model: TTGO TDisplay 135x240 - cs_pin: GPIO5 - dc_pin: GPIO16 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO16 + reset_pin: + allow_other_uses: true + number: GPIO23 backlight_pin: no lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); @@ -2977,15 +3198,22 @@ display: width: 128 height: 64 cs_pin: + allow_other_uses: true number: GPIO23 inverted: true lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 model: INITR_BLACKTAB - cs_pin: GPIO5 - dc_pin: GPIO16 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO16 + reset_pin: + allow_other_uses: true + number: GPIO23 rotation: 0 device_width: 128 device_height: 160 @@ -3001,10 +3229,16 @@ display: mirror_x: true mirror_y: false model: TFT 2.4 - cs_pin: GPIO5 - dc_pin: GPIO4 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO4 color_palette: GRAYSCALE - reset_pin: GPIO22 + reset_pin: + allow_other_uses: true + number: GPIO22 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx @@ -3014,9 +3248,15 @@ display: offset_width: 20 offset_height: 10 model: TFT 2.4 - cs_pin: GPIO5 - dc_pin: GPIO4 - reset_pin: GPIO22 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO4 + reset_pin: + allow_other_uses: true + number: GPIO22 auto_clear_enabled: false rotation: 90 lambda: |- @@ -3041,10 +3281,18 @@ display: it.print_battery(true); - platform: tm1621 id: tm1621_display - cs_pin: GPIO17 - data_pin: GPIO5 - read_pin: GPIO23 - write_pin: GPIO18 + cs_pin: + allow_other_uses: true + number: GPIO17 + data_pin: + allow_other_uses: true + number: GPIO5 + read_pin: + allow_other_uses: true + number: GPIO23 + write_pin: + allow_other_uses: true + number: GPIO18 lambda: |- it.printf(0, "%.1f", id(dht_temperature).state); it.display_celsius(true); @@ -3053,12 +3301,18 @@ display: tm1651: id: tm1651_battery - clk_pin: GPIO23 - dio_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO23 + dio_pin: + allow_other_uses: true + number: GPIO23 remote_receiver: id: rcvr - pin: GPIO32 + pin: + allow_other_uses: true + number: GPIO32 dump: all on_coolix: then: @@ -3068,11 +3322,16 @@ remote_receiver: delay: !lambda "return uint32_t(x.code) + x.protocol;" status_led: - pin: GPIO2 + pin: + allow_other_uses: true + number: GPIO2 + ignore_strapping_warning: true pn532_spi: id: pn532_bs - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 1s on_tag: - lambda: |- @@ -3094,7 +3353,9 @@ rdm6300: uart_id: uart_0 rc522_spi: - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 1s on_tag: - lambda: |- @@ -3287,9 +3548,15 @@ mcp23016: stepper: - platform: a4988 id: my_stepper - step_pin: GPIO23 - dir_pin: GPIO25 - sleep_pin: GPIO25 + step_pin: + allow_other_uses: true + number: GPIO23 + dir_pin: + allow_other_uses: true + number: GPIO25 + sleep_pin: + allow_other_uses: true + number: GPIO25 max_speed: 250 steps/s acceleration: 100 steps/s^2 deceleration: 200 steps/s^2 @@ -3397,14 +3664,26 @@ text_sensor: sn74hc595: - id: sn74hc595_hub - data_pin: GPIO21 - clock_pin: GPIO23 - latch_pin: GPIO22 - oe_pin: GPIO32 + data_pin: + allow_other_uses: true + number: GPIO21 + clock_pin: + allow_other_uses: true + number: GPIO23 + latch_pin: + allow_other_uses: true + number: GPIO22 + oe_pin: + allow_other_uses: true + number: GPIO32 sr_count: 2 - id: sn74hc595_hub_2 - latch_pin: GPIO22 - oe_pin: GPIO32 + latch_pin: + allow_other_uses: true + number: GPIO22 + oe_pin: + allow_other_uses: true + number: GPIO32 sr_count: 2 spi_id: spi_bus @@ -3456,8 +3735,12 @@ canbus: } - platform: esp32_can id: esp32_internal_can - rx_pin: GPIO04 - tx_pin: GPIO05 + rx_pin: + allow_other_uses: true + number: GPIO04 + tx_pin: + allow_other_uses: true + number: GPIO05 can_id: 4 bit_rate: 50kbps on_frame: diff --git a/tests/test11.5.yaml b/tests/test11.5.yaml index 685487e871..2a9b40c5c3 100644 --- a/tests/test11.5.yaml +++ b/tests/test11.5.yaml @@ -44,25 +44,42 @@ uart: rx_pin: 3 baud_rate: 9600 - id: uart_2 - tx_pin: 17 - rx_pin: 16 + tx_pin: + allow_other_uses: true + number: 17 + rx_pin: + allow_other_uses: true + number: 16 baud_rate: 19200 i2c: + sda: + number: 21 + allow_other_uses: true frequency: 100khz spi: - id: spi_1 - clk_pin: 12 - mosi_pin: 13 - miso_pin: 14 + clk_pin: + allow_other_uses: true + number: 12 + mosi_pin: + allow_other_uses: true + number: 13 + miso_pin: + allow_other_uses: true + number: 14 - id: spi_2 - clk_pin: 32 + clk_pin: + allow_other_uses: true + number: 32 mosi_pin: 33 modbus: uart_id: uart_1 - flow_control_pin: 5 + flow_control_pin: + allow_other_uses: true + number: 5 id: mod_bus1 modbus_controller: @@ -229,9 +246,15 @@ binary_sensor: lambda: return x[0] & 1; tlc5947: - data_pin: GPIO12 - clock_pin: GPIO14 - lat_pin: GPIO15 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 + lat_pin: + allow_other_uses: true + number: GPIO15 gp8403: - id: gp8403_5v @@ -417,7 +440,9 @@ sensor: - platform: adc id: adc_sensor_p32 name: ADC pin 32 - pin: 32 + pin: + allow_other_uses: true + number: 32 attenuation: 11db update_interval: 1s - platform: internal_temperature @@ -584,7 +609,9 @@ sensor: name: Kuntze temperature - platform: ade7953_i2c - irq_pin: 16 + irq_pin: + allow_other_uses: true + number: 16 voltage: name: ADE7953 Voltage current_a: @@ -612,7 +639,9 @@ sensor: - platform: ade7953_spi spi_id: spi_1 cs_pin: 04 - irq_pin: 16 + irq_pin: + allow_other_uses: true + number: 16 voltage: name: ADE7953 Voltage current_a: @@ -683,7 +712,9 @@ switch: display: - platform: tm1638 id: primarydisplay - stb_pin: 5 #TM1638 STB + stb_pin: + allow_other_uses: true + number: 5 #TM1638 STB clk_pin: 18 #TM1638 CLK dio_pin: 23 #TM1638 DIO update_interval: 5s @@ -728,20 +759,32 @@ text_sensor: sn74hc165: id: sn74hc165_hub - data_pin: GPIO12 - clock_pin: GPIO14 - load_pin: GPIO27 - clock_inhibit_pin: GPIO26 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 + load_pin: + number: GPIO27 + clock_inhibit_pin: + number: GPIO26 sr_count: 4 matrix_keypad: id: keypad rows: - - pin: 21 + - pin: + allow_other_uses: true + number: 21 - pin: 19 columns: - - pin: 17 - - pin: 16 + - pin: + allow_other_uses: true + number: 17 + - pin: + allow_other_uses: true + number: 16 keys: "1234" key_collector: @@ -753,14 +796,18 @@ key_collector: light: - platform: esp32_rmt_led_strip id: led_strip - pin: 13 + pin: + allow_other_uses: true + number: 13 num_leds: 60 rmt_channel: 6 rgb_order: GRB chipset: ws2812 - platform: esp32_rmt_led_strip id: led_strip2 - pin: 15 + pin: + allow_other_uses: true + number: 15 num_leds: 60 rmt_channel: 2 rgb_order: RGB diff --git a/tests/test2.yaml b/tests/test2.yaml index b258b103d3..e5358781df 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -17,11 +17,17 @@ substitutions: ethernet: type: LAN8720 - mdc_pin: GPIO23 - mdio_pin: GPIO25 + mdc_pin: + allow_other_uses: true + number: GPIO23 + mdio_pin: + allow_other_uses: true + number: GPIO25 clk_mode: GPIO0_IN phy_addr: 0 - power_pin: GPIO25 + power_pin: + allow_other_uses: true + number: GPIO25 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 @@ -37,18 +43,32 @@ mdns: api: i2c: - sda: 21 - scl: 22 + sda: + allow_other_uses: true + number: 21 + scl: + allow_other_uses: true + number: 22 scan: false spi: - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO21 + mosi_pin: + allow_other_uses: true + number: GPIO22 + miso_pin: + allow_other_uses: true + number: GPIO23 uart: - tx_pin: GPIO22 - rx_pin: GPIO23 + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 baud_rate: 115200 # Specifically added for testing debug with no after: definition. debug: @@ -73,21 +93,29 @@ deep_sleep: gpio_wakeup_reason: 10s touch_wakeup_reason: 15s sleep_duration: 50s - wakeup_pin: GPIO2 + wakeup_pin: + allow_other_uses: true + number: GPIO2 wakeup_pin_mode: INVERT_WAKEUP as3935_i2c: - irq_pin: GPIO12 + irq_pin: + allow_other_uses: true + number: GPIO12 mcp3008: - id: mcp3008_hub - cs_pin: GPIO12 + cs_pin: + allow_other_uses: true + number: GPIO12 output: - platform: ac_dimmer id: dimmer1 gate_pin: GPIO5 - zero_cross_pin: GPIO12 + zero_cross_pin: + allow_other_uses: true + number: GPIO12 sensor: - platform: homeassistant @@ -534,7 +562,9 @@ binary_sensor: name: Mi Motion Sensor 2 Button - platform: gpio id: gpio_set_retry_test - pin: GPIO9 + pin: + allow_other_uses: true + number: GPIO9 on_press: then: - lambda: |- @@ -601,7 +631,9 @@ xiaomi_rtcgq02lm: bindkey: "48403ebe2d385db8d0c187f81e62cb64" status_led: - pin: GPIO2 + pin: + allow_other_uses: true + number: GPIO2 text_sensor: - platform: version @@ -704,9 +736,13 @@ script: stepper: - platform: uln2003 id: my_stepper - pin_a: GPIO23 + pin_a: + allow_other_uses: true + number: GPIO23 pin_b: GPIO27 - pin_c: GPIO25 + pin_c: + allow_other_uses: true + number: GPIO25 pin_d: GPIO26 sleep_when_done: false step_mode: HALF_STEP @@ -731,7 +767,9 @@ display: offset_height: 35 offset_width: 0 dc_pin: GPIO13 - reset_pin: GPIO9 + reset_pin: + allow_other_uses: true + number: GPIO9 image: - id: binary_image diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 9000636f63..63ef4e8ce0 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -29,14 +29,24 @@ web_server: version: 2 i2c: - sda: 4 - scl: 5 + sda: + allow_other_uses: true + number: 4 + scl: + allow_other_uses: true + number: 5 scan: false spi: - clk_pin: GPIO12 - mosi_pin: GPIO13 - miso_pin: GPIO14 + clk_pin: + allow_other_uses: true + number: GPIO12 + mosi_pin: + allow_other_uses: true + number: GPIO13 + miso_pin: + allow_other_uses: true + number: GPIO14 ota: @@ -52,7 +62,9 @@ sensor: name: VL53L0x Distance address: 0x29 update_interval: 60s - enable_pin: GPIO13 + enable_pin: + allow_other_uses: true + number: GPIO13 timeout: 200us - platform: apds9960 type: clear @@ -170,7 +182,9 @@ sensor: name: Custom Sensor - platform: ade7953_i2c - irq_pin: GPIO16 + irq_pin: + allow_other_uses: true + number: GPIO16 voltage: name: ADE7953 Voltage id: ade7953_voltage @@ -199,8 +213,12 @@ sensor: update_interval: 1s - platform: ade7953_spi - cs_pin: GPIO04 - irq_pin: GPIO16 + cs_pin: + allow_other_uses: true + number: GPIO04 + irq_pin: + allow_other_uses: true + number: GPIO16 voltage: name: ADE7953 Voltage current_a: @@ -360,8 +378,12 @@ text_sensor: name: Custom Text Sensor sm2135: - data_pin: GPIO12 - clock_pin: GPIO14 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 rgb_current: 20mA cw_current: 60mA @@ -379,6 +401,7 @@ switch: pin: mcp23xxx: mcp23017_hub number: 0 + allow_other_uses: true mode: OUTPUT interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - platform: gpio @@ -386,11 +409,14 @@ switch: pin: mcp23xxx: mcp23008_hub number: 0 + allow_other_uses: true mode: OUTPUT interlock: *interlock - platform: gpio id: gpio_switch3 - pin: GPIO1 + pin: + allow_other_uses: true + number: GPIO1 interlock: *interlock - platform: custom lambda: |- @@ -440,10 +466,18 @@ custom_component: stepper: - platform: uln2003 id: my_stepper - pin_a: GPIO12 - pin_b: GPIO13 - pin_c: GPIO14 - pin_d: GPIO15 + pin_a: + allow_other_uses: true + number: GPIO12 + pin_b: + allow_other_uses: true + number: GPIO13 + pin_c: + allow_other_uses: true + number: GPIO14 + pin_d: + allow_other_uses: true + number: GPIO15 sleep_when_done: false step_mode: HALF_STEP max_speed: 250 steps/s @@ -451,8 +485,12 @@ stepper: deceleration: inf - platform: a4988 id: my_stepper2 - step_pin: GPIO1 - dir_pin: GPIO2 + step_pin: + allow_other_uses: true + number: GPIO1 + dir_pin: + allow_other_uses: true + number: GPIO2 max_speed: 0.1 steps/s acceleration: 10 steps/s^2 deceleration: 10 steps/s^2 @@ -556,11 +594,14 @@ cover: output: - platform: esp8266_pwm id: out - pin: D3 + pin: + number: D3 frequency: 50Hz - platform: esp8266_pwm id: out2 - pin: D4 + pin: + allow_other_uses: true + number: D4 - platform: custom type: binary lambda: |- @@ -572,7 +613,9 @@ output: - platform: sigma_delta_output id: sddac update_interval: 60s - pin: D4 + pin: + allow_other_uses: true + number: D4 turn_on_action: then: - logger.log: "Turned on" @@ -593,7 +636,9 @@ output: outputs: - id: custom_float - platform: slow_pwm - pin: GPIO5 + pin: + allow_other_uses: true + number: GPIO5 id: my_slow_pwm period: 15s restart_cycle_on_state_change: false @@ -635,12 +680,18 @@ servo: ttp229_lsf: ttp229_bsf: - sdo_pin: D2 - scl_pin: D1 + sdo_pin: + allow_other_uses: true + number: D2 + scl_pin: + allow_other_uses: true + number: D1 display: - platform: max7219digit - cs_pin: GPIO15 + cs_pin: + allow_other_uses: true + number: GPIO15 num_chips: 4 rotate_chip: 0 intensity: 10 @@ -666,10 +717,20 @@ button: name: Restart Button (Factory Default Settings) cd74hc4067: - pin_s0: GPIO12 - pin_s1: GPIO13 - pin_s2: GPIO14 - pin_s3: GPIO15 + pin_s0: + allow_other_uses: true + number: GPIO12 + pin_s1: + allow_other_uses: true + number: GPIO13 + pin_s2: + allow_other_uses: true + number: GPIO14 + pin_s3: + allow_other_uses: true + number: GPIO15 adc128s102: - cs_pin: GPIO12 + cs_pin: + allow_other_uses: true + number: GPIO12 diff --git a/tests/test3.yaml b/tests/test3.yaml index 0a405a2841..6b1757c2ad 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -223,55 +223,102 @@ uart: tx_pin: number: GPIO1 inverted: true - rx_pin: GPIO3 + allow_other_uses: true + rx_pin: + allow_other_uses: true + number: GPIO3 baud_rate: 115200 - id: uart_2 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_3 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 4800 - id: uart_4 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_5 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_6 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_7 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 38400 - id: uart_8 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 4800 parity: NONE stop_bits: 2 # Specifically added for testing debug with no options at all. debug: - id: uart_9 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_10 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_11 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_12 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 modbus: @@ -748,13 +795,19 @@ binary_sensor: - platform: gpio id: bin1 - pin: 1 + pin: + allow_other_uses: true + number: 1 - platform: gpio id: bin2 - pin: 2 + pin: + allow_other_uses: true + number: 2 - platform: gpio id: bin3 - pin: 3 + pin: + allow_other_uses: true + number: 3 globals: - id: my_global_string @@ -762,11 +815,15 @@ globals: initial_value: '""' remote_receiver: - pin: GPIO12 + pin: + allow_other_uses: true + number: GPIO12 dump: [] status_led: - pin: GPIO2 + pin: + allow_other_uses: true + number: GPIO2 text_sensor: - platform: daly_bms @@ -819,13 +876,19 @@ script: switch: - platform: gpio id: gpio_switch1 - pin: 1 + pin: + allow_other_uses: true + number: 1 - platform: gpio id: gpio_switch2 - pin: 2 + pin: + allow_other_uses: true + number: 2 - platform: gpio id: gpio_switch3 - pin: 3 + pin: + allow_other_uses: true + number: 3 - platform: nextion id: r0 @@ -1023,13 +1086,18 @@ sprinkler: output: - platform: esp8266_pwm id: out - pin: D3 + pin: + number: D3 frequency: 50Hz - platform: esp8266_pwm id: out2 - pin: D4 + pin: + allow_other_uses: true + number: D4 - platform: slow_pwm - pin: GPIO5 + pin: + allow_other_uses: true + number: GPIO5 id: my_slow_pwm period: 15s restart_cycle_on_state_change: false @@ -1039,7 +1107,9 @@ e131: light: - platform: neopixelbus name: Neopixelbus Light - pin: GPIO1 + pin: + allow_other_uses: true + number: GPIO1 type: GRBW variant: SK6812 method: ESP8266_UART0 @@ -1071,6 +1141,12 @@ light: max_brightness: 500 firmware: "51.6" uart_id: uart_11 + nrst_pin: + number: 5 + allow_other_uses: true + boot0_pin: + number: 4 + allow_other_uses: true sim800l: uart_id: uart_4 @@ -1096,8 +1172,12 @@ dfplayer: logger.log: Playback finished event tm1651: id: tm1651_battery - clk_pin: D6 - dio_pin: D5 + clk_pin: + allow_other_uses: true + number: D6 + dio_pin: + allow_other_uses: true + number: D5 rf_bridge: uart_id: uart_5 @@ -1150,7 +1230,9 @@ display: lambda: 'ESP_LOGD("display","Display shows new page %u", x);' fingerprint_grow: - sensing_pin: 4 + sensing_pin: + allow_other_uses: true + number: 4 password: 0x12FE37DC new_password: 0xA65B9840 on_finger_scan_matched: @@ -1184,7 +1266,9 @@ dsmr: decryption_key: 00112233445566778899aabbccddeeff uart_id: uart_6 max_telegram_length: 1000 - request_pin: D5 + request_pin: + allow_other_uses: true + number: D5 request_interval: 20s receive_timeout: 100ms @@ -1197,8 +1281,11 @@ qr_code: value: https://esphome.io/index.html lightwaverf: - read_pin: 13 - write_pin: 14 + read_pin: + number: 13 + write_pin: + allow_other_uses: true + number: 14 alarm_control_panel: - platform: template diff --git a/tests/test4.yaml b/tests/test4.yaml index 65aab7cdde..a3dfc6aceb 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -10,11 +10,17 @@ substitutions: ethernet: type: LAN8720 - mdc_pin: GPIO23 - mdio_pin: GPIO25 + mdc_pin: + allow_other_uses: true + number: GPIO23 + mdio_pin: + allow_other_uses: true + number: GPIO25 clk_mode: GPIO0_IN phy_addr: 0 - power_pin: GPIO25 + power_pin: + allow_other_uses: true + number: GPIO25 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 @@ -34,29 +40,49 @@ mqtt: api: i2c: - sda: 21 - scl: 22 + sda: + allow_other_uses: true + number: 21 + scl: + allow_other_uses: true + number: 22 scan: false spi: - id: spi_id_1 - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO21 + mosi_pin: + allow_other_uses: true + number: GPIO22 + miso_pin: + allow_other_uses: true + number: GPIO23 interface: hardware - id: spi_id_2 - clk_pin: GPIO32 - mosi_pin: GPIO33 + clk_pin: + number: GPIO32 + mosi_pin: + number: GPIO33 interface: hardware uart: - id: uart115200 - tx_pin: GPIO22 - rx_pin: GPIO23 + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 baud_rate: 115200 - id: uart9600 - tx_pin: GPIO22 - rx_pin: GPIO23 + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 baud_rate: 9600 - id: uart_he60r tx_pin: 22 @@ -91,6 +117,7 @@ tuya: status_pin: number: 14 inverted: true + allow_other_uses: true select: - platform: tuya @@ -117,7 +144,9 @@ sx1509: mcp3204: spi_id: spi_id_1 - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 dac7678: address: 0x4A @@ -510,7 +539,9 @@ light: id: led_matrix_32x8 name: led_matrix_32x8 chipset: WS2812B - pin: GPIO15 + pin: + allow_other_uses: true + number: GPIO15 num_leds: 256 rgb_order: GRB default_transition_length: 0s @@ -566,20 +597,36 @@ display: - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 2.13in-ttgo-b1 full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 2.90in full_update_every: 30 reset_duration: 200ms @@ -587,20 +634,36 @@ display: it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 2.90inv2 full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 1.54in-m5coreink-m09 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); @@ -610,15 +673,54 @@ display: partial_updating: false update_interval: 60s - ckv_pin: GPIO1 - sph_pin: GPIO1 - gmod_pin: GPIO1 - gpio0_enable_pin: GPIO1 - oe_pin: GPIO1 - spv_pin: GPIO1 - powerup_pin: GPIO1 - wakeup_pin: GPIO1 - vcom_pin: GPIO1 + display_data_1_pin: + number: 5 + allow_other_uses: true + display_data_2_pin: + number: 18 + allow_other_uses: true + display_data_3_pin: + number: 19 + allow_other_uses: true + display_data_5_pin: + number: 25 + allow_other_uses: true + display_data_4_pin: + number: 23 + allow_other_uses: true + display_data_6_pin: + number: 26 + allow_other_uses: true + display_data_7_pin: + number: 27 + allow_other_uses: true + ckv_pin: + allow_other_uses: true + number: GPIO1 + sph_pin: + allow_other_uses: true + number: GPIO1 + gmod_pin: + allow_other_uses: true + number: GPIO1 + gpio0_enable_pin: + allow_other_uses: true + number: GPIO1 + oe_pin: + allow_other_uses: true + number: GPIO1 + spv_pin: + allow_other_uses: true + number: GPIO1 + powerup_pin: + allow_other_uses: true + number: GPIO1 + wakeup_pin: + allow_other_uses: true + number: GPIO1 + vcom_pin: + allow_other_uses: true + number: GPIO1 number: - platform: tuya @@ -706,18 +808,54 @@ output: id: dac7678_1_ch7 esp32_camera: name: ESP-32 Camera - data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19] - vsync_pin: GPIO22 - href_pin: GPIO26 - pixel_clock_pin: GPIO21 + data_pins: + - number: GPIO17 + allow_other_uses: true + - number: GPIO35 + allow_other_uses: true + - + number: GPIO34 + - + number: GPIO5 + allow_other_uses: true + - + number: GPIO39 + - + number: GPIO18 + allow_other_uses: true + - + number: GPIO36 + allow_other_uses: true + - + number: GPIO19 + allow_other_uses: true + vsync_pin: + allow_other_uses: true + number: GPIO22 + href_pin: + allow_other_uses: true + number: GPIO26 + pixel_clock_pin: + allow_other_uses: true + number: GPIO21 external_clock: - pin: GPIO27 + pin: + allow_other_uses: true + number: GPIO27 frequency: 20MHz i2c_pins: - sda: GPIO25 - scl: GPIO23 - reset_pin: GPIO15 - power_down_pin: GPIO1 + sda: + allow_other_uses: true + number: GPIO25 + scl: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO15 + power_down_pin: + allow_other_uses: true + number: GPIO1 resolution: 640x480 jpeg_quality: 10 @@ -748,8 +886,12 @@ button: touchscreen: - platform: ektf2232 - interrupt_pin: GPIO36 - rts_pin: GPIO5 + interrupt_pin: + allow_other_uses: true + number: GPIO36 + rts_pin: + allow_other_uses: true + number: GPIO5 display: inkplate_display on_touch: - logger.log: @@ -759,8 +901,11 @@ touchscreen: - platform: xpt2046 id: xpt_touchscreen spi_id: spi_id_2 - cs_pin: 17 - interrupt_pin: 16 + cs_pin: + allow_other_uses: true + number: 17 + interrupt_pin: + number: 16 display: inkplate_display update_interval: 50ms report_interval: 1s @@ -777,7 +922,9 @@ touchscreen: - platform: lilygo_t5_47 id: lilygo_touchscreen - interrupt_pin: GPIO36 + interrupt_pin: + allow_other_uses: true + number: GPIO36 display: inkplate_display on_touch: - logger.log: @@ -789,16 +936,26 @@ touchscreen: i2s_audio: - i2s_lrclk_pin: GPIO26 - i2s_bclk_pin: GPIO27 - i2s_mclk_pin: GPIO25 + i2s_lrclk_pin: + allow_other_uses: true + number: GPIO26 + i2s_bclk_pin: + allow_other_uses: true + number: GPIO27 + i2s_mclk_pin: + allow_other_uses: true + number: GPIO25 media_player: - platform: i2s_audio name: None dac_type: external - i2s_dout_pin: GPIO25 - mute_pin: GPIO14 + i2s_dout_pin: + allow_other_uses: true + number: GPIO25 + mute_pin: + allow_other_uses: true + number: GPIO14 on_state: - media_player.play: - media_player.play_media: http://localhost/media.mp3 @@ -827,12 +984,16 @@ prometheus: microphone: - platform: i2s_audio id: mic_id_adc - adc_pin: GPIO35 + adc_pin: + allow_other_uses: true + number: GPIO35 adc_type: internal - platform: i2s_audio id: mic_id_external - i2s_din_pin: GPIO23 + i2s_din_pin: + allow_other_uses: true + number: GPIO23 adc_type: external pdm: false @@ -840,7 +1001,9 @@ speaker: - platform: i2s_audio id: speaker_id dac_type: external - i2s_dout_pin: GPIO25 + i2s_dout_pin: + allow_other_uses: true + number: GPIO25 mode: mono voice_assistant: diff --git a/tests/test5.yaml b/tests/test5.yaml index 46cedcabd2..bf4247fb92 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -42,17 +42,27 @@ uart: baud_rate: 9600 - id: uart_2 tx_pin: + allow_other_uses: true number: 17 inverted: true - rx_pin: 16 + rx_pin: + allow_other_uses: true + number: 16 baud_rate: 19200 i2c: + sda: + allow_other_uses: true + number: 21 + scl: + number: 22 frequency: 100khz modbus: uart_id: uart_1 - flow_control_pin: 5 + flow_control_pin: + allow_other_uses: true + number: 5 id: mod_bus1 modbus_controller: @@ -214,9 +224,15 @@ binary_sensor: lambda: return x[0] & 1; tlc5947: - data_pin: GPIO12 - clock_pin: GPIO14 - lat_pin: GPIO15 + data_pin: + number: GPIO12 + allow_other_uses: true + clock_pin: + allow_other_uses: true + number: GPIO14 + lat_pin: + allow_other_uses: true + number: GPIO15 gp8403: - id: gp8403_5v @@ -614,7 +630,9 @@ switch: display: - platform: tm1638 id: primarydisplay - stb_pin: 5 #TM1638 STB + stb_pin: + allow_other_uses: true + number: 5 #TM1638 STB clk_pin: 18 #TM1638 CLK dio_pin: 23 #TM1638 DIO update_interval: 5s @@ -659,8 +677,12 @@ text_sensor: sn74hc165: id: sn74hc165_hub - data_pin: GPIO12 - clock_pin: GPIO14 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 load_pin: GPIO27 clock_inhibit_pin: GPIO26 sr_count: 4 @@ -668,11 +690,17 @@ sn74hc165: matrix_keypad: id: keypad rows: - - pin: 21 + - pin: + allow_other_uses: true + number: 21 - pin: 19 columns: - - pin: 17 - - pin: 16 + - pin: + allow_other_uses: true + number: 17 + - pin: + allow_other_uses: true + number: 16 keys: "1234" has_pulldowns: true @@ -692,7 +720,9 @@ light: chipset: ws2812 - platform: esp32_rmt_led_strip id: led_strip2 - pin: 15 + pin: + allow_other_uses: true + number: 15 num_leds: 60 rmt_channel: 2 rgb_order: RGB diff --git a/tests/test8.yaml b/tests/test8.yaml index cbac2cb833..5e4a41080a 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -57,7 +57,9 @@ display: model: ili9342 cs_pin: GPIO5 dc_pin: GPIO4 - reset_pin: GPIO48 + reset_pin: + number: GPIO48 + allow_other_uses: true i2c: scl: GPIO18 @@ -68,7 +70,10 @@ touchscreen: interrupt_pin: number: GPIO3 ignore_strapping_warning: true - reset_pin: GPIO48 + allow_other_uses: false + reset_pin: + number: GPIO48 + allow_other_uses: true binary_sensor: - platform: tt21100 From 4d3730b50eb9d8a0997d2b3289d5873c8fa1911c Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 5 Dec 2023 02:09:44 +0100 Subject: [PATCH 101/436] Nextion support to idf with `cinttypes` (#5876) --- esphome/components/nextion/nextion.cpp | 1 + esphome/components/nextion/nextion_commands.cpp | 1 + esphome/components/nextion/nextion_upload_idf.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fcc0d97655..15f1d02841 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -2,6 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include namespace esphome { namespace nextion { diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 0722ed6f4e..3cefc6618a 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -1,6 +1,7 @@ #include "nextion.h" #include "esphome/core/util.h" #include "esphome/core/log.h" +#include namespace esphome { namespace nextion { diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 58f5659ade..84da7bb1be 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -11,6 +11,7 @@ #include #include +#include namespace esphome { namespace nextion { From 1d0fb59208cb8e2ede49ae004a9d119e44dcb9a8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 4 Dec 2023 20:50:40 -0600 Subject: [PATCH 102/436] Fix test4.yaml after #5666 (#5890) --- tests/test4.yaml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test4.yaml b/tests/test4.yaml index a3dfc6aceb..05870d3b8f 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -79,18 +79,21 @@ uart: - id: uart9600 tx_pin: allow_other_uses: true - number: GPIO22 + number: GPIO25 rx_pin: allow_other_uses: true - number: GPIO23 + number: GPIO26 baud_rate: 9600 - id: uart_he60r - tx_pin: 22 - rx_pin: 23 + tx_pin: + number: GPIO18 + allow_other_uses: true + rx_pin: + number: GPIO36 + allow_other_uses: true baud_rate: 1200 parity: EVEN - ota: safe_mode: true port: 3286 @@ -115,7 +118,7 @@ tuya: time_id: sntp_time uart_id: uart115200 status_pin: - number: 14 + number: GPIO5 inverted: true allow_other_uses: true @@ -954,7 +957,6 @@ media_player: allow_other_uses: true number: GPIO25 mute_pin: - allow_other_uses: true number: GPIO14 on_state: - media_player.play: From 7fd08fb8163206534ca94251669b1cdc7f2cc655 Mon Sep 17 00:00:00 2001 From: Fabio Pugliese Ornellas Date: Tue, 5 Dec 2023 04:17:43 +0000 Subject: [PATCH 103/436] Fix template text component length check (#5881) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Keith Burzinski --- esphome/components/template/text/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/template/text/__init__.py b/esphome/components/template/text/__init__.py index a82664ee15..0f228a3c6b 100644 --- a/esphome/components/template/text/__init__.py +++ b/esphome/components/template/text/__init__.py @@ -39,8 +39,8 @@ def validate(config): ) with cv.prepend_path(CONF_MIN_LENGTH): - if config[CONF_MIN_LENGTH] >= config[CONF_MAX_LENGTH]: - raise cv.Invalid("min_length must be less than max_length") + if config[CONF_MIN_LENGTH] > config[CONF_MAX_LENGTH]: + raise cv.Invalid("min_length must be less than or equal to max_length") return config From 89d7cdf86b0c71531e3867b33a79e607fed95bc0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 5 Dec 2023 02:21:12 -0600 Subject: [PATCH 104/436] RC522 - Fix error counter error (#5873) --- esphome/components/rc522/rc522.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 4e74020e4c..e2146dd14e 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -397,8 +397,10 @@ RC522::StatusCode RC522::await_transceive_() { back_length_ = 0; ESP_LOGW(TAG, "Communication with the MFRC522 might be down, reset in %d", 10 - error_counter_); // todo: trigger reset? - if (error_counter_++ > 10) + if (error_counter_++ >= 10) { setup(); + error_counter_ = 0; // reset the error counter + } return STATUS_TIMEOUT; } From 8f70ef24a22f0c03b4e1e3c4aef84cf748b56688 Mon Sep 17 00:00:00 2001 From: Subhash Chandra Date: Wed, 6 Dec 2023 06:34:17 +0530 Subject: [PATCH 105/436] feat(packages): support removing components (#5821) --- .devcontainer/devcontainer.json | 1 + esphome/config.py | 39 ++++- esphome/config_helpers.py | 28 ++- esphome/config_validation.py | 6 +- esphome/yaml_util.py | 7 +- .../component_tests/packages/test_packages.py | 164 +++++++++++++++++- 6 files changed, 240 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c8f94cb6bb..7abcb43417 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -37,6 +37,7 @@ "!secret scalar", "!lambda scalar", "!extend scalar", + "!remove scalar", "!include_dir_named scalar", "!include_dir_list scalar", "!include_dir_merge_list scalar", diff --git a/esphome/config.py b/esphome/config.py index 6f644cee14..3461223490 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -26,7 +26,7 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict -from esphome.config_helpers import Extend +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 @@ -345,6 +345,12 @@ class LoadValidationStep(ConfigValidationStep): path + [CONF_ID], ) continue + if isinstance(p_id, Remove): + result.add_str_error( + f"Source for removal of ID '{p_id.value}' was not found.", + path + [CONF_ID], + ) + continue result.add_str_error("No platform specified! See 'platform' key.", path) continue # Remove temp output path and construct new one @@ -634,6 +640,35 @@ class IDPassValidationStep(ConfigValidationStep): ) +class RemoveReferenceValidationStep(ConfigValidationStep): + """ + Make sure all !remove references have been removed from the config. + Any left overs mean the merge step couldn't find corresponding previously existing id/key + """ + + def run(self, result: Config) -> None: + if result.errors: + # If result already has errors, skip this step + return + + def recursive_check_remove_tag(config: Config, path: ConfigPath = None): + path = path or [] + + if isinstance(config, Remove): + result.add_str_error( + f"Source for removal at '{'->'.join([str(p) for p in path])}' was not found.", + path, + ) + elif isinstance(config, list): + for i, item in enumerate(config): + recursive_check_remove_tag(item, path + [i]) + elif isinstance(config, dict): + for key, value in config.items(): + recursive_check_remove_tag(value, path + [key]) + + recursive_check_remove_tag(result) + + class FinalValidateValidationStep(ConfigValidationStep): """Run final_validate_schema for all components.""" @@ -771,6 +806,8 @@ def validate_config(config, command_line_substitutions) -> Config: result.add_validation_step(IDPassValidationStep()) result.add_validation_step(PinUseValidationCheck()) + result.add_validation_step(RemoveReferenceValidationStep()) + result.run_validation_steps() return result diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index e1d63775bb..ac52c6ede2 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -22,6 +22,22 @@ class Extend: return isinstance(b, Extend) and self.value == b.value +class Remove: + def __init__(self, value=None): + self.value = value + + def __str__(self): + return f"!remove {self.value}" + + def __eq__(self, b): + """ + Check if two Remove objects contain the same ID. + + Only used in unit tests. + """ + return isinstance(b, Remove) and self.value == b.value + + def read_config_file(path: str) -> str: if CORE.vscode and ( not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) @@ -48,7 +64,10 @@ def merge_config(full_old, full_new): return new res = old.copy() for k, v in new.items(): - res[k] = merge(old[k], v) if k in old else v + if isinstance(v, Remove) and k in old: + del res[k] + else: + res[k] = merge(old[k], v) if k in old else v return res if isinstance(new, list): if not isinstance(old, list): @@ -59,6 +78,7 @@ def merge_config(full_old, full_new): for i, v in enumerate(res) if CONF_ID in v and isinstance(v[CONF_ID], str) } + ids_to_delete = [] for v in new: if CONF_ID in v: new_id = v[CONF_ID] @@ -68,9 +88,15 @@ def merge_config(full_old, full_new): v[CONF_ID] = new_id res[ids[new_id]] = merge(res[ids[new_id]], v) continue + elif isinstance(new_id, Remove): + new_id = new_id.value + if new_id in ids: + ids_to_delete.append(ids[new_id]) + continue else: ids[new_id] = len(res) res.append(v) + res = [v for i, v in enumerate(res) if i not in ids_to_delete] return res if new is None: return old diff --git a/esphome/config_validation.py b/esphome/config_validation.py index eb347d0a4d..ad2ee11512 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -13,7 +13,7 @@ import voluptuous as vol from esphome import core import esphome.codegen as cg -from esphome.config_helpers import Extend +from esphome.config_helpers import Extend, Remove from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, @@ -532,6 +532,10 @@ def declare_id(type): if isinstance(value, Extend): raise Invalid(f"Source for extension of ID '{value.value}' was not found.") + + if isinstance(value, Remove): + raise Invalid(f"Source for Removal of ID '{value.value}' was not found.") + return core.ID(validate_id_name(value), is_declaration=True, type=type) return validator diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index a954415d12..f0f755dd61 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -10,7 +10,7 @@ import yaml import yaml.constructor from esphome import core -from esphome.config_helpers import read_config_file, Extend +from esphome.config_helpers import read_config_file, Extend, Remove from esphome.core import ( EsphomeError, IPAddress, @@ -362,6 +362,10 @@ class ESPHomeLoader(FastestAvailableSafeLoader): def construct_extend(self, node): return Extend(str(node.value)) + @_add_data_ref + def construct_remove(self, node): + return Remove(str(node.value)) + ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor( @@ -394,6 +398,7 @@ ESPHomeLoader.add_constructor( ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) ESPHomeLoader.add_constructor("!extend", ESPHomeLoader.construct_extend) +ESPHomeLoader.add_constructor("!remove", ESPHomeLoader.construct_remove) def load_yaml(fname, clear_secrets=True): diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0e24d78f5c..01cf55872c 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -20,7 +20,7 @@ from esphome.const import ( CONF_WIFI, ) from esphome.components.packages import do_packages_pass -from esphome.config_helpers import Extend +from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv # Test strings @@ -349,3 +349,165 @@ def test_package_merge_by_missing_id(): actual = do_packages_pass(config) assert actual == expected + + +def test_package_list_remove_by_id(): + """ + Ensures that components with matching IDs are removed correctly. + + In this test, two sensors are defined in a package, and one of them is removed at the top level. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + # "package2": { + # CONF_SENSOR: [ + # { + # CONF_ID: Remove(TEST_SENSOR_ID_1), + # } + # ], + # }, + }, + CONF_SENSOR: [ + { + CONF_ID: Remove(TEST_SENSOR_ID_1), + }, + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_multiple_package_list_remove_by_id(): + """ + Ensures that components with matching IDs are removed correctly. + + In this test, two sensors are defined in a package, and one of them is removed in another package. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + "package2": { + CONF_SENSOR: [ + { + CONF_ID: Remove(TEST_SENSOR_ID_1), + } + ], + }, + }, + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_dict_remove_by_id(basic_wifi, basic_esphome): + """ + Ensures that components with missing IDs are removed from dict. + """ + """ + Ensures that the top-level configuration takes precedence over duplicate keys defined in a package. + + In this test, CONF_SSID should be overwritten by that defined in the top-level config. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + CONF_WIFI: Remove(), + } + + expected = { + CONF_ESPHOME: basic_esphome, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_remove_by_missing_id(): + """ + Ensures that components with missing IDs are not merged. + """ + + config = { + CONF_PACKAGES: { + "sensors": { + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]}, + ] + } + }, + "missing_key": Remove(), + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 10.0}]}, + {CONF_ID: Remove(TEST_SENSOR_ID_2), CONF_FILTERS: [{CONF_OFFSET: 146.0}]}, + ], + } + + expected = { + "missing_key": Remove(), + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 42.0}], + }, + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 10.0}], + }, + { + CONF_ID: Remove(TEST_SENSOR_ID_2), + CONF_FILTERS: [{CONF_OFFSET: 146.0}], + }, + ], + } + + actual = do_packages_pass(config) + assert actual == expected From be07463fbd8bdb8464cd0d6f48bf46678148061b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Dec 2023 15:06:05 -1000 Subject: [PATCH 106/436] dashboard: Add some basic tests for the dashboard (#5870) --- tests/dashboard/common.py | 6 ++ tests/dashboard/fixtures/conf/pico.yaml | 47 +++++++++++++++ tests/dashboard/test_web_server.py | 80 +++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 tests/dashboard/common.py create mode 100644 tests/dashboard/fixtures/conf/pico.yaml create mode 100644 tests/dashboard/test_web_server.py diff --git a/tests/dashboard/common.py b/tests/dashboard/common.py new file mode 100644 index 0000000000..f84c03aad8 --- /dev/null +++ b/tests/dashboard/common.py @@ -0,0 +1,6 @@ +import pathlib + + +def get_fixture_path(filename: str) -> pathlib.Path: + """Get path of fixture.""" + return pathlib.Path(__file__).parent.joinpath("fixtures", filename) diff --git a/tests/dashboard/fixtures/conf/pico.yaml b/tests/dashboard/fixtures/conf/pico.yaml new file mode 100644 index 0000000000..cf5b5b75bf --- /dev/null +++ b/tests/dashboard/fixtures/conf/pico.yaml @@ -0,0 +1,47 @@ +substitutions: + name: picoproxy + friendly_name: Pico Proxy + +esphome: + name: ${name} + friendly_name: ${friendly_name} + project: + name: esphome.bluetooth-proxy + version: "1.0" + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ap: + +api: +logger: +ota: +improv_serial: + +dashboard_import: + package_import_url: github://esphome/firmware/bluetooth-proxy/esp32-generic.yaml@main + +button: + - platform: factory_reset + id: resetf + - platform: safe_mode + name: Safe Mode Boot + entity_category: diagnostic + +sensor: + - platform: template + id: pm11 + name: "pm 1.0µm" + lambda: return 1.0; + - platform: template + id: pm251 + name: "pm 2.5µm" + lambda: return 2.5; + - platform: template + id: pm101 + name: "pm 10µm" + lambda: return 10; diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py new file mode 100644 index 0000000000..a61850abf3 --- /dev/null +++ b/tests/dashboard/test_web_server.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import asyncio +import json +import os +from unittest.mock import Mock + +import pytest +import pytest_asyncio +from tornado.httpclient import AsyncHTTPClient, HTTPResponse +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from tornado.testing import bind_unused_port + +from esphome.dashboard import web_server +from esphome.dashboard.core import DASHBOARD + +from .common import get_fixture_path + + +class DashboardTestHelper: + def __init__(self, io_loop: IOLoop, client: AsyncHTTPClient, port: int) -> None: + self.io_loop = io_loop + self.client = client + self.port = port + + async def fetch(self, path: str, **kwargs) -> HTTPResponse: + """Get a response for the given path.""" + if path.lower().startswith(("http://", "https://")): + url = path + else: + url = f"http://127.0.0.1:{self.port}{path}" + future = self.client.fetch(url, raise_error=True, **kwargs) + result = await future + return result + + +@pytest_asyncio.fixture() +async def dashboard() -> DashboardTestHelper: + sock, port = bind_unused_port() + args = Mock( + ha_addon=True, + configuration=get_fixture_path("conf"), + port=port, + ) + DASHBOARD.settings.parse_args(args) + app = web_server.make_app() + http_server = HTTPServer(app) + http_server.add_sockets([sock]) + await DASHBOARD.async_setup() + os.environ["DISABLE_HA_AUTHENTICATION"] = "1" + assert DASHBOARD.settings.using_password is False + assert DASHBOARD.settings.on_ha_addon is True + assert DASHBOARD.settings.using_auth is False + task = asyncio.create_task(DASHBOARD.async_run()) + client = AsyncHTTPClient() + io_loop = IOLoop(make_current=False) + yield DashboardTestHelper(io_loop, client, port) + task.cancel() + sock.close() + client.close() + io_loop.close() + + +@pytest.mark.asyncio +async def test_main_page(dashboard: DashboardTestHelper) -> None: + response = await dashboard.fetch("/") + assert response.code == 200 + + +@pytest.mark.asyncio +async def test_devices_page(dashboard: DashboardTestHelper) -> None: + response = await dashboard.fetch("/devices") + assert response.code == 200 + assert response.headers["content-type"] == "application/json" + json_data = json.loads(response.body.decode()) + configured_devices = json_data["configured"] + first_device = configured_devices[0] + assert first_device["name"] == "pico" + assert first_device["configuration"] == "pico.yaml" From f026f49415e0b3e6c7ab01d708cf91e5868f0275 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 6 Dec 2023 07:55:07 +0100 Subject: [PATCH 107/436] Nextion exit reparse mode on startup (#5868) Co-authored-by: Keith Burzinski --- esphome/components/nextion/base_component.py | 1 + esphome/components/nextion/display.py | 7 +++++-- esphome/components/nextion/nextion.cpp | 7 ++++++- esphome/components/nextion/nextion.h | 17 +++++++++++++++++ esphome/components/nextion/nextion_commands.cpp | 1 + 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index f1c3a1d227..5bd6643cb8 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -29,6 +29,7 @@ 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" def NextionName(value): diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 92e85ad28a..fd61dfa2be 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -21,6 +21,7 @@ from .base_component import ( CONF_WAKE_UP_PAGE, CONF_START_UP_PAGE, CONF_AUTO_WAKE_ON_TOUCH, + CONF_EXIT_REPARSE_ON_START, ) CODEOWNERS = ["@senexcrenshaw"] @@ -69,6 +70,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, cv.Optional(CONF_START_UP_PAGE): cv.positive_int, cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, + cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("5s")) @@ -106,8 +108,9 @@ async def to_code(config): if CONF_START_UP_PAGE in config: cg.add(var.set_start_up_page_internal(config[CONF_START_UP_PAGE])) - if CONF_AUTO_WAKE_ON_TOUCH in config: - cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + + cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START])) await display.register_display(var, config) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 15f1d02841..29dcfa6cef 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -48,6 +48,9 @@ bool Nextion::check_connect_() { this->ignore_is_setup_ = true; this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating + if (this->exit_reparse_on_start_) { + this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); + } this->send_command_("connect"); this->comok_sent_ = millis(); @@ -127,7 +130,8 @@ void Nextion::dump_config() { ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); - ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); + ESP_LOGCONFIG(TAG, " Wake On Touch: %s", YESNO(this->auto_wake_on_touch_)); + ESP_LOGCONFIG(TAG, " Exit reparse: %s", YESNO(this->exit_reparse_on_start_)); if (this->touch_sleep_timeout_ != 0) { ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); @@ -248,6 +252,7 @@ void Nextion::loop() { } this->set_auto_wake_on_touch(this->auto_wake_on_touch_); + this->set_exit_reparse_on_start(this->exit_reparse_on_start_); if (this->touch_sleep_timeout_ != 0) { this->set_touch_sleep_timeout(this->touch_sleep_timeout_); diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index f188708f35..acbf394fc6 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -815,6 +815,19 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * The display will wake up by touch. */ void set_auto_wake_on_touch(bool auto_wake); + /** + * Sets if Nextion should exit the active reparse mode before the "connect" command is sent + * @param exit_reparse True or false. When exit_reparse is true, the exit reparse command + * will be sent before requesting the connection from Nextion. + * + * Example: + * ```cpp + * it.set_exit_reparse_on_start(true); + * ``` + * + * The display will be requested to leave active reparse mode before setup. + */ + void set_exit_reparse_on_start(bool exit_reparse); /** * Sets Nextion mode between sleep and awake * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. @@ -943,6 +956,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; } void set_start_up_page_internal(uint8_t start_up_page) { this->start_up_page_ = start_up_page; } void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } + void set_exit_reparse_on_start_internal(bool exit_reparse_on_start) { + this->exit_reparse_on_start_ = exit_reparse_on_start; + } protected: std::deque nextion_queue_; @@ -966,6 +982,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe int wake_up_page_ = -1; int start_up_page_ = -1; bool auto_wake_on_touch_ = true; + bool exit_reparse_on_start_ = false; /** * Manually send a raw command to the display and don't wait for an acknowledgement packet. diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 3cefc6618a..8512ea5573 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -53,6 +53,7 @@ void Nextion::set_protocol_reparse_mode(bool active_mode) { this->write_str("connect"); this->write_array(to_send, sizeof(to_send)); } +void Nextion::set_exit_reparse_on_start(bool exit_reparse) { this->exit_reparse_on_start_ = exit_reparse; } // Set Colors - Background void Nextion::set_component_background_color(const char *component, uint16_t color) { From c53874788a8db88e03cf45764d7e1b1b1a822ac3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 11:04:20 -1000 Subject: [PATCH 108/436] Bump aioesphomeapi from 19.2.1 to 19.3.0 (#5895) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7d37954656..cde39175da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.2.1 +aioesphomeapi==19.3.0 zeroconf==0.128.0 python-magic==0.4.27 From 049a7a0113e64ae122df9aa6172a54672ebd6656 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:57:06 +0100 Subject: [PATCH 109/436] Add framework info to Nextion log tags (#5864) --- esphome/components/nextion/nextion_upload_arduino.cpp | 2 +- esphome/components/nextion/nextion_upload_idf.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 3337ff9b50..e3d0903d09 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -15,7 +15,7 @@ namespace esphome { namespace nextion { -static const char *const TAG = "nextion_upload"; +static const char *const TAG = "nextion.upload.arduino"; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 84da7bb1be..57bb9c45e8 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -15,7 +15,7 @@ namespace esphome { namespace nextion { -static const char *const TAG = "nextion_upload"; +static const char *const TAG = "nextion.upload.idf"; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 From 51428dcbc27b86819aa4a1decad45a13a78af159 Mon Sep 17 00:00:00 2001 From: mtl010957 Date: Wed, 6 Dec 2023 19:31:27 -0500 Subject: [PATCH 110/436] Handle case where using enetity level name: None with MQTT (#5897) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mqtt/mqtt_component.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index f9f8c850e9..af4d6f13a5 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -76,7 +76,11 @@ bool MQTTComponent::send_discovery_() { this->send_discovery(root, config); // Fields from EntityBase - root[MQTT_NAME] = this->friendly_name(); + if (this->get_entity()->has_own_name()) { + root[MQTT_NAME] = this->friendly_name(); + } else { + root[MQTT_NAME] = ""; + } if (this->is_disabled_by_default()) root[MQTT_ENABLED_BY_DEFAULT] = false; if (!this->get_icon().empty()) From a6f1701902355489bdfcb7dee53671abf063d1aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 01:08:50 +0000 Subject: [PATCH 111/436] Bump actions/setup-python from 4.7.1 to 5.0.0 (#5896) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/sync-device-classes.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index aa8dd6d894..18a2485dbb 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v4.7.0 + uses: actions/setup-python@v5.0.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 51f47d39aa..8fe8bbdc52 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Set up Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: "3.9" - name: Set up Docker Buildx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d1daf922f..8182f92f94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69456619e9..625a8c8ecb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Set up Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: "3.x" - name: Set up python environment @@ -80,7 +80,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Set up Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: "3.9" diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 88edb63546..d45784bf7f 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -22,7 +22,7 @@ jobs: path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: 3.11 From 0906559afe55d1e233d4ead938c6f439482ff29e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:38:55 +0000 Subject: [PATCH 112/436] Bump zeroconf from 0.128.0 to 0.128.4 (#5906) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cde39175da..39715232d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==19.3.0 -zeroconf==0.128.0 +zeroconf==0.128.4 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From b62c099d54eb24c3dff0e53aa3561b524a9c512b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:22:41 +1100 Subject: [PATCH 113/436] Fix 18 bit displays. (#5908) --- esphome/components/ili9xxx/ili9xxx_display.h | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index db1274450a..bf4889afe1 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -30,13 +30,24 @@ class ILI9XXXDisplay : public display::DisplayBuffer, const uint8_t *addr = init_sequence; while ((cmd = *addr++) != 0) { num_args = *addr++ & 0x7F; - if (cmd == ILI9XXX_MADCTL) { - bits = *addr; - this->swap_xy_ = (bits & MADCTL_MV) != 0; - this->mirror_x_ = (bits & MADCTL_MX) != 0; - this->mirror_y_ = (bits & MADCTL_MY) != 0; - this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; - break; + bits = *addr; + switch (cmd) { + case ILI9XXX_MADCTL: { + this->swap_xy_ = (bits & MADCTL_MV) != 0; + this->mirror_x_ = (bits & MADCTL_MX) != 0; + this->mirror_y_ = (bits & MADCTL_MY) != 0; + this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + break; + } + + case ILI9XXX_PIXFMT: { + if ((bits & 0xF) == 6) + this->is_18bitdisplay_ = true; + break; + } + + default: + break; } addr += num_args; } From 86e6a8a5033acaf9d1e83a7ec65624fab1f276ce Mon Sep 17 00:00:00 2001 From: Clemens Date: Tue, 12 Dec 2023 00:28:16 +0100 Subject: [PATCH 114/436] fix RGBW Mode on RP2040 (#5907) --- esphome/components/rp2040_pio_led_strip/led_strip.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index ce1836306f..c04419a9bf 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -70,9 +70,10 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { // assemble bits in buffer to 32 bit words with ex for GBR: 0bGGGGGGGGRRRRRRRRBBBBBBBB00000000 for (int i = 0; i < this->num_leds_; i++) { - uint8_t c1 = this->buf_[(i * 3) + 0]; - uint8_t c2 = this->buf_[(i * 3) + 1]; - uint8_t c3 = this->buf_[(i * 3) + 2]; + uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + uint8_t c1 = this->buf_[(i * multiplier) + 0]; + uint8_t c2 = this->buf_[(i * multiplier) + 1]; + uint8_t c3 = this->buf_[(i * multiplier) + 2]; uint8_t w = this->is_rgbw_ ? this->buf_[(i * 4) + 3] : 0; uint32_t color = encode_uint32(c1, c2, c3, w); pio_sm_put_blocking(this->pio_, this->sm_, color); From b30430b0bda4f12e72281c987183d7c7a45cc659 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Tue, 12 Dec 2023 14:15:59 +1100 Subject: [PATCH 115/436] Add graphical display menu (#4105) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Alex Hermann --- CODEOWNERS | 1 + esphome/components/display/display.cpp | 7 + esphome/components/display/display.h | 15 ++ .../display_menu_base/display_menu_base.cpp | 12 + .../display_menu_base/display_menu_base.h | 5 + .../display_menu_base/menu_item.cpp | 23 ++ .../components/display_menu_base/menu_item.h | 4 + .../graphical_display_menu/__init__.py | 96 +++++++ .../graphical_display_menu.cpp | 243 ++++++++++++++++++ .../graphical_display_menu.h | 84 ++++++ esphome/core/defines.h | 1 + tests/test1.yaml | 116 ++++++++- 12 files changed, 598 insertions(+), 9 deletions(-) create mode 100644 esphome/components/graphical_display_menu/__init__.py create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.cpp create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.h diff --git a/CODEOWNERS b/CODEOWNERS index de80806eac..43b7647fe6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -116,6 +116,7 @@ esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/gree/* @orestismers esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 22454aeddb..88ee64ea55 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in } #endif // USE_QR_CODE +#ifdef USE_GRAPHICAL_DISPLAY_MENU +void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) { + Rect rect(x, y, width, height); + menu->draw(this, &rect); +} +#endif // USE_GRAPHICAL_DISPLAY_MENU + void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, int *height) { int x_offset, baseline; diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 7ce6d179ef..3afcfb9528 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -17,6 +17,10 @@ #include "esphome/components/qr_code/qr_code.h" #endif +#ifdef USE_GRAPHICAL_DISPLAY_MENU +#include "esphome/components/graphical_display_menu/graphical_display_menu.h" +#endif + namespace esphome { namespace display { @@ -392,6 +396,17 @@ class Display : public PollingComponent { void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); #endif +#ifdef USE_GRAPHICAL_DISPLAY_MENU + /** + * @param x The x coordinate of the upper left corner + * @param y The y coordinate of the upper left corner + * @param menu The GraphicalDisplayMenu to draw + * @param width Width of the menu + * @param height Height of the menu + */ + void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height); +#endif // USE_GRAPHICAL_DISPLAY_MENU + /** Get the text bounds of the given string. * * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp index 57da3cec35..0bfee338ca 100644 --- a/esphome/components/display_menu_base/display_menu_base.cpp +++ b/esphome/components/display_menu_base/display_menu_base.cpp @@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() { this->process_initial_(); + this->on_before_show(); + if (this->active_ && this->editing_) this->finish_editing_(); @@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() { } this->draw_and_update(); + + this->on_after_show(); } void DisplayMenuComponent::show() { @@ -196,18 +200,26 @@ void DisplayMenuComponent::show() { this->process_initial_(); + this->on_before_show(); + if (!this->active_) { this->active_ = true; this->draw_and_update(); } + + this->on_after_show(); } void DisplayMenuComponent::hide() { if (this->check_healthy_and_active_()) { + this->on_before_hide(); + if (this->editing_) this->finish_editing_(); this->active_ = false; this->update(); + + this->on_after_hide(); } } diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h index 46bb0a8192..6208fcd3b4 100644 --- a/esphome/components/display_menu_base/display_menu_base.h +++ b/esphome/components/display_menu_base/display_menu_base.h @@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component { update(); } + virtual void on_before_show(){}; + virtual void on_after_show(){}; + virtual void on_before_hide(){}; + virtual void on_after_hide(){}; + uint8_t rows_; bool active_; MenuMode mode_; diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index bbe6ec0e89..2c7f34c493 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -5,6 +5,29 @@ namespace esphome { namespace display_menu_base { +const LogString *menu_item_type_to_string(MenuItemType type) { + switch (type) { + case MenuItemType::MENU_ITEM_LABEL: + return LOG_STR("MENU_ITEM_LABEL"); + case MenuItemType::MENU_ITEM_MENU: + return LOG_STR("MENU_ITEM_MENU"); + case MenuItemType::MENU_ITEM_BACK: + return LOG_STR("MENU_ITEM_BACK"); + case MenuItemType::MENU_ITEM_SELECT: + return LOG_STR("MENU_ITEM_SELECT"); + case MenuItemType::MENU_ITEM_NUMBER: + return LOG_STR("MENU_ITEM_NUMBER"); + case MenuItemType::MENU_ITEM_SWITCH: + return LOG_STR("MENU_ITEM_SWITCH"); + case MenuItemType::MENU_ITEM_COMMAND: + return LOG_STR("MENU_ITEM_COMMAND"); + case MenuItemType::MENU_ITEM_CUSTOM: + return LOG_STR("MENU_ITEM_CUSTOM"); + default: + return LOG_STR("UNKNOWN"); + } +} + void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index a30f31e88f..36de146031 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -14,6 +14,7 @@ #endif #include +#include "esphome/core/log.h" namespace esphome { namespace display_menu_base { @@ -29,6 +30,9 @@ enum MenuItemType { MENU_ITEM_CUSTOM, }; +/// @brief Returns a string representation of a menu item type suitable for logging +const LogString *menu_item_type_to_string(MenuItemType type); + class MenuItem; class MenuItemMenu; using value_getter_t = std::function; diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py new file mode 100644 index 0000000000..dc49358efd --- /dev/null +++ b/esphome/components/graphical_display_menu/__init__.py @@ -0,0 +1,96 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, font, color +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome import automation, core + +from esphome.components.display_menu_base import ( + DISPLAY_MENU_BASE_SCHEMA, + DisplayMenuComponent, + display_menu_to_code, +) + +CONF_DISPLAY = "display" +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") +GraphicalDisplayMenu = graphical_display_menu_ns.class_( + "GraphicalDisplayMenu", DisplayMenuComponent +) +GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const") +MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments") +MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator( + "const" +) +GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_( + "GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger +) + +CODEOWNERS = ["@MrMDavidson"] + +AUTO_LOAD = ["display_menu_base"] + +CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu), + cv.Optional(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), + cv.Required(CONF_FONT): cv.use_id(font.Font), + cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_ON_REDRAW): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + GraphicalDisplayMenuOnRedrawTrigger + ) + } + ), + } + ) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if display_config := config.get(CONF_DISPLAY): + drawing_display = await cg.get_variable(display_config) + cg.add(var.set_display(drawing_display)) + + menu_font = await cg.get_variable(config[CONF_FONT]) + cg.add(var.set_font(menu_font)) + + if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None: + if isinstance(menu_item_value_config, core.Lambda): + template_ = await cg.templatable( + menu_item_value_config, + [(MenuItemValueArgumentsConstPtr, "it")], + cg.std_string, + ) + cg.add(var.set_menu_item_value(template_)) + else: + cg.add(var.set_menu_item_value(menu_item_value_config)) + + if foreground_color_config := config.get(CONF_FOREGROUND_COLOR): + foreground_color = await cg.get_variable(foreground_color_config) + cg.add(var.set_foreground_color(foreground_color)) + + if background_color_config := config.get(CONF_BACKGROUND_COLOR): + background_color = await cg.get_variable(background_color_config) + cg.add(var.set_background_color(background_color)) + + for conf in config.get(CONF_ON_REDRAW, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf + ) + + await display_menu_to_code(var, config) + + cg.add_define("USE_GRAPHICAL_DISPLAY_MENU") diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp new file mode 100644 index 0000000000..2e4c14fb7b --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -0,0 +1,243 @@ +#include "graphical_display_menu.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include "esphome/components/display/display.h" + +namespace esphome { +namespace graphical_display_menu { + +static const char *const TAG = "graphical_display_menu"; + +void GraphicalDisplayMenu::setup() { + if (this->display_ != nullptr) { + display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); }; + this->display_page_ = make_unique(writer); + } + + if (!this->menu_item_value_.has_value()) { + this->menu_item_value_ = [](const MenuItemValueArguments *it) { + std::string label = " "; + if (it->is_item_selected && it->is_menu_editing) { + label.append(">"); + label.append(it->item->get_value_text()); + label.append("<"); + } else { + label.append("("); + label.append(it->item->get_value_text()); + label.append(")"); + } + return label; + }; + } + + display_menu_base::DisplayMenuComponent::setup(); +} + +void GraphicalDisplayMenu::dump_config() { + ESP_LOGCONFIG(TAG, "Graphical Display Menu"); + ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr)); + ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr)); + ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick"); + ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_)); + ESP_LOGCONFIG(TAG, "Menu items:"); + for (size_t i = 0; i < this->displayed_item_->items_size(); i++) { + auto *item = this->displayed_item_->get_item(i); + ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(), + LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())), + YESNO(item->get_immediate_edit())); + } +} + +void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; } + +void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; } + +void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; } +void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; } + +void GraphicalDisplayMenu::on_before_show() { + if (this->display_ != nullptr) { + this->previous_display_page_ = this->display_->get_active_page(); + this->display_->show_page(this->display_page_.get()); + this->display_->clear(); + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::on_before_hide() { + if (this->previous_display_page_ != nullptr) { + this->display_->show_page((display::DisplayPage *) this->previous_display_page_); + this->display_->clear(); + this->update(); + this->previous_display_page_ = nullptr; + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::draw_and_update() { + this->update(); + + // If we're in advanced drawing mode we won't have a display and will instead require the update callback to do + // our drawing + if (this->display_ != nullptr) { + draw_menu(); + } +} + +void GraphicalDisplayMenu::draw_menu() { + if (this->display_ == nullptr) { + ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode"); + return; + } + display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height()); + this->draw_menu_internal_(this->display_, &bounds); +} + +void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) { + this->draw_menu_internal_(display, bounds); +} + +void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) { + int total_height = 0; + int y_padding = 2; + bool scroll_menu_items = false; + std::vector menu_dimensions; + int number_items_fit_to_screen = 0; + const int max_item_index = this->displayed_item_->items_size() - 1; + + for (size_t i = 0; i <= max_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); + + menu_dimensions.push_back(item_dimensions); + total_height += item_dimensions.h + (i == 0 ? 0 : y_padding); + + if (total_height <= bounds->h) { + number_items_fit_to_screen++; + } else { + // Scroll the display if the selected item or the item immediately after it overflows + if ((selected) || (i == this->cursor_index_ + 1)) { + scroll_menu_items = true; + } + } + } + + // Determine what items to draw + int first_item_index = 0; + int last_item_index = max_item_index; + + if (number_items_fit_to_screen <= 1) { + // If only one item can fit to the bounds draw the current cursor item + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + first_item_index = this->cursor_index_; + } else { + if (scroll_menu_items) { + // Attempt to draw the item after the current item (+1 for equality check in the draw loop) + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + + // Go back through the measurements to determine how many prior items we can fit + int height_left_to_use = bounds->h; + for (int i = last_item_index; i >= 0; i--) { + const display::Rect item_dimensions = menu_dimensions[i]; + height_left_to_use -= (item_dimensions.h + y_padding); + + if (height_left_to_use <= 0) { + // Ran out of space - this is our first item to draw + first_item_index = i; + break; + } + } + const int items_to_draw = last_item_index - first_item_index; + // Dont't draw last item partially if it is the selected item + if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) && + (first_item_index < max_item_index)) { + first_item_index++; + } + } + } + + // Render the items into the view port + display->start_clipping(*bounds); + + int y_offset = bounds->y; + for (size_t i = first_item_index; i <= last_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + display::Rect dimensions = menu_dimensions[i]; + + dimensions.y = y_offset; + dimensions.x = bounds->x; + this->draw_item(display, item, &dimensions, selected); + + y_offset = dimensions.y + dimensions.h + y_padding; + } + + display->end_clipping(); +} + +display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + display::Rect dimensions(0, 0, 0, 0); + + if (selected) { + // TODO: Support selection glyph + dimensions.w += 0; + dimensions.h += 0; + } + + std::string label = item->get_text(); + if (item->has_value()) { + // Append to label + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + int x1; + int y1; + int width; + int height; + display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height); + + dimensions.w = std::min((int16_t) width, bounds->w); + dimensions.h = std::min((int16_t) height, bounds->h); + + return dimensions; +} + +inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + const auto background_color = selected ? this->foreground_color_ : this->background_color_; + const auto foreground_color = selected ? this->background_color_ : this->foreground_color_; + + // int background_width = std::max(bounds->width, available_width); + int background_width = bounds->w; + + if (selected) { + display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color); + } + + std::string label = item->get_text(); + if (item->has_value()) { + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str()); +} + +void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) { + ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific " + "draw_item should be called."); +} + +void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); } + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h new file mode 100644 index 0000000000..96f2bd79fd --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/color.h" +#include "esphome/components/display_menu_base/display_menu_base.h" +#include "esphome/components/display_menu_base/menu_item.h" +#include "esphome/core/automation.h" +#include + +namespace esphome { + +// forward declare from display namespace +namespace display { +class Display; +class DisplayPage; +class BaseFont; +class Rect; +} // namespace display + +namespace graphical_display_menu { + +const Color COLOR_ON(255, 255, 255, 255); +const Color COLOR_OFF(0, 0, 0, 0); + +struct MenuItemValueArguments { + MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) { + this->item = item; + this->is_item_selected = is_item_selected; + this->is_menu_editing = is_menu_editing; + } + + const display_menu_base::MenuItem *item; + bool is_item_selected; + bool is_menu_editing; +}; + +class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { + public: + void setup() override; + void dump_config() override; + + void set_display(display::Display *display); + void set_font(display::BaseFont *font); + template void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; } + void set_foreground_color(Color foreground_color); + void set_background_color(Color background_color); + + void add_on_redraw_callback(std::function &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); } + + void draw(display::Display *display, const display::Rect *bounds); + + protected: + void draw_and_update() override; + void draw_menu() override; + void draw_menu_internal_(display::Display *display, const display::Rect *bounds); + void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override; + virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + void update() override; + + void on_before_show() override; + void on_before_hide() override; + + std::unique_ptr display_page_{nullptr}; + const display::DisplayPage *previous_display_page_{nullptr}; + display::Display *display_{nullptr}; + display::BaseFont *font_{nullptr}; + TemplatableValue menu_item_value_; + Color foreground_color_{COLOR_ON}; + Color background_color_{COLOR_OFF}; + + CallbackManager on_redraw_callbacks_{}; +}; + +class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { + public: + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { + parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + } +}; + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b93b8c9270..e75abdb88f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_UART_DEBUGGER #define USE_WIFI #define USE_WIFI_AP +#define USE_GRAPHICAL_DISPLAY_MENU // Arduino-specific feature flags #ifdef USE_ARDUINO diff --git a/tests/test1.yaml b/tests/test1.yaml index c90cdf6d90..559b7ab8fc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1141,10 +1141,12 @@ sensor: value: !lambda "return -1;" on_clockwise: - logger.log: Clockwise - - display_menu.down: + - display_menu.down: test_lcd_menu + - display_menu.down: test_graphical_display_menu on_anticlockwise: - logger.log: Anticlockwise - - display_menu.up: + - display_menu.up: test_lcd_menu + - display_menu.up: test_graphical_display_menu - platform: pulse_width name: Pulse Width pin: @@ -1781,13 +1783,22 @@ binary_sensor: on_press: - if: condition: - display_menu.is_active: + display_menu.is_active: test_lcd_menu then: - - display_menu.enter: + - display_menu.enter: test_lcd_menu else: - - display_menu.left: - - display_menu.right: - - display_menu.show: + - display_menu.left: test_lcd_menu + - display_menu.right: test_lcd_menu + - display_menu.show: test_lcd_menu + - if: + condition: + display_menu.is_active: test_graphical_display_menu + then: + - display_menu.enter: test_graphical_display_menu + else: + - display_menu.left: test_graphical_display_menu + - display_menu.right: test_graphical_display_menu + - display_menu.show: test_graphical_display_menu - platform: template name: Garage Door Open id: garage_door @@ -3204,6 +3215,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 + id: st7735_display model: INITR_BLACKTAB cs_pin: allow_other_uses: true @@ -3997,6 +4009,7 @@ ld2420: uart_id: ld2420_uart lcd_menu: + id: test_lcd_menu display_id: my_lcd_gpio mark_back: 0x5e mark_selected: 0x3e @@ -4028,7 +4041,7 @@ lcd_menu: text: Show Main on_value: then: - - display_menu.show_main: + - display_menu.show_main: test_lcd_menu - type: select text: Enum Item immediate_edit: true @@ -4058,7 +4071,7 @@ lcd_menu: text: Hide on_value: then: - - display_menu.hide: + - display_menu.hide: test_lcd_menu - type: switch text: Switch switch: my_switch @@ -4078,6 +4091,91 @@ lcd_menu: then: lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +graphical_display_menu: + id: test_graphical_display_menu + display: st7735_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: 'Back' + - type: label + - type: menu + text: 'Submenu 1' + items: + - type: back + text: 'Back' + - type: menu + text: 'Submenu 21' + items: + - type: back + text: 'Back' + - type: command + text: 'Show Main' + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: 'Enum Item' + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: 'Number' + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: 'Hide' + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: 'Switch' + switch: my_switch + on_text: 'Bright' + off_text: 'Dark' + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' + alarm_control_panel: - platform: template id: alarmcontrolpanel1 From 39d026299e6834fbd3182f0f717e5c96414b053b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 12 Dec 2023 05:48:21 +0100 Subject: [PATCH 116/436] Added on_image callback to ESP32 Cam (#4860) --- esphome/components/esp32_camera/__init__.py | 17 +++++++++++++++ .../components/esp32_camera/esp32_camera.cpp | 4 ++-- .../components/esp32_camera/esp32_camera.h | 21 +++++++++++++++++-- tests/test4.yaml | 4 ++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 4cbdf7ca5c..ee8a889f4c 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -25,6 +25,11 @@ AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) +ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") +# Triggers +ESP32CameraImageTrigger = esp32_camera_ns.class_( + "ESP32CameraImageTrigger", automation.Trigger.template() +) ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( "ESP32CameraStreamStartTrigger", automation.Trigger.template(), @@ -139,6 +144,7 @@ CONF_IDLE_FRAMERATE = "idle_framerate" # stream trigger CONF_ON_STREAM_START = "on_stream_start" CONF_ON_STREAM_STOP = "on_stream_stop" +CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) @@ -221,6 +227,11 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ), } ), + cv.Optional(CONF_ON_IMAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -289,3 +300,9 @@ async def to_code(config): for conf in config.get(CONF_ON_STREAM_STOP, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_IMAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(ESP32CameraImageData, "image")], conf + ) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index e4020a902e..99cb811fe4 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -335,8 +335,8 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { } /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&f) { - this->new_image_callback_.add(std::move(f)); +void ESP32Camera::add_image_callback(std::function)> &&callback) { + this->new_image_callback_.add(std::move(callback)); } void ESP32Camera::add_stream_start_callback(std::function &&callback) { this->stream_start_callback_.add(std::move(callback)); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 5f88c6fda8..0c25381039 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -86,6 +86,11 @@ class CameraImage { uint8_t requesters_; }; +struct CameraImageData { + uint8_t *data; + size_t length; +}; + /* ---------------- CameraImageReader class ---------------- */ class CameraImageReader { public: @@ -147,12 +152,12 @@ class ESP32Camera : public Component, public EntityBase { void dump_config() override; float get_setup_priority() const override; /* public API (specific) */ - void add_image_callback(std::function)> &&f); void start_stream(CameraRequester requester); void stop_stream(CameraRequester requester); void request_image(CameraRequester requester); void update_camera_parameters(); + void add_image_callback(std::function)> &&callback); void add_stream_start_callback(std::function &&callback); void add_stream_stop_callback(std::function &&callback); @@ -196,7 +201,7 @@ class ESP32Camera : public Component, public EntityBase { uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_; + CallbackManager)> new_image_callback_{}; CallbackManager stream_start_callback_{}; CallbackManager stream_stop_callback_{}; @@ -207,6 +212,18 @@ class ESP32Camera : public Component, public EntityBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; +class ESP32CameraImageTrigger : public Trigger { + public: + explicit ESP32CameraImageTrigger(ESP32Camera *parent) { + parent->add_image_callback([this](const std::shared_ptr &image) { + CameraImageData camera_image_data{}; + camera_image_data.length = image->get_data_length(); + camera_image_data.data = image->get_data_buffer(); + this->trigger(camera_image_data); + }); + } +}; + class ESP32CameraStreamStartTrigger : public Trigger<> { public: explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { diff --git a/tests/test4.yaml b/tests/test4.yaml index 05870d3b8f..9f15b84b12 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -861,6 +861,10 @@ esp32_camera: number: GPIO1 resolution: 640x480 jpeg_quality: 10 + on_image: + then: + - lambda: |- + ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); esp32_camera_web_server: - port: 8080 From 7fb10547edb0ba87b2801c8851a045d62e309ab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:07 +0900 Subject: [PATCH 117/436] Bump actions/stale from 8.0.0 to 9.0.0 (#5899) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a2d3f2f77d..5f510ffe75 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -38,7 +38,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: -1 days-before-pr-close: -1 From 0117de5b78d49ba41f4be3fa07cde8fc85a7bf8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:22 +0900 Subject: [PATCH 118/436] Bump pylint from 3.0.2 to 3.0.3 (#5905) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index cb96f79587..eef69b7515 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==3.0.2 +pylint==3.0.3 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating black==23.11.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating From d0dd0e38db46cbe37de8a6f894a16e4be8120113 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:36 +0900 Subject: [PATCH 119/436] Bump frenck/action-yamllint from 1.4.1 to 1.4.2 (#5716) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/yaml-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml index a77bd2c078..c9f056b18c 100644 --- a/.github/workflows/yaml-lint.yml +++ b/.github/workflows/yaml-lint.yml @@ -19,4 +19,4 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.1 - name: Run yamllint - uses: frenck/action-yamllint@v1.4.1 + uses: frenck/action-yamllint@v1.4.2 From 47665164e80f22500e67994c470bb6fc7627d42c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:52:05 +0900 Subject: [PATCH 120/436] Bump dessant/lock-threads from 4.0.1 to 5.0.1 (#5820) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index b455e3f4ea..e3d75f6d58 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -18,7 +18,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4.0.1 + - uses: dessant/lock-threads@v5.0.1 with: pr-inactive-days: "1" pr-lock-reason: "" From ad79e4fe24fd24267391603f2657ecc92adf5762 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Mon, 11 Dec 2023 22:13:26 -0700 Subject: [PATCH 121/436] Add support for fan preset modes (#5694) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 4 ++ esphome/components/api/api_connection.cpp | 6 ++ esphome/components/api/api_pb2.cpp | 52 +++++++++++++++++ esphome/components/api/api_pb2.h | 6 ++ esphome/components/copy/fan/copy_fan.cpp | 5 ++ esphome/components/fan/__init__.py | 44 ++++++++++++++ esphome/components/fan/automation.h | 18 ++++++ esphome/components/fan/fan.cpp | 58 +++++++++++++++++-- esphome/components/fan/fan.h | 9 +++ esphome/components/fan/fan_traits.h | 10 ++++ esphome/components/hbridge/fan/__init__.py | 7 ++- .../components/hbridge/fan/hbridge_fan.cpp | 11 +++- esphome/components/hbridge/fan/hbridge_fan.h | 7 ++- esphome/components/speed/fan/__init__.py | 9 ++- esphome/components/speed/fan/speed_fan.cpp | 11 +++- esphome/components/speed/fan/speed_fan.h | 7 ++- esphome/const.py | 2 + tests/test1.yaml | 28 ++++++++- 18 files changed, 277 insertions(+), 17 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 2f33750686..34d137981e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -365,6 +365,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10; EntityCategory entity_category = 11; + repeated string supported_preset_modes = 12; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -387,6 +388,7 @@ message FanStateResponse { FanSpeed speed = 4 [deprecated = true]; FanDirection direction = 5; int32 speed_level = 6; + string preset_mode = 7; } message FanCommandRequest { option (id) = 31; @@ -405,6 +407,8 @@ message FanCommandRequest { FanDirection direction = 9; bool has_speed_level = 10; int32 speed_level = 11; + bool has_preset_mode = 12; + string preset_mode = 13; } // ==================== LIGHT ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0389df215f..b8f5b05538 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -293,6 +293,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { } if (traits.supports_direction()) resp.direction = static_cast(fan->direction); + if (traits.supports_preset_modes()) + resp.preset_mode = fan->preset_mode; return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::Fan *fan) { @@ -307,6 +309,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) { msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); + for (auto const &preset : traits.supported_preset_modes()) + msg.supported_preset_modes.push_back(preset); msg.disabled_by_default = fan->is_disabled_by_default(); msg.icon = fan->get_icon(); msg.entity_category = static_cast(fan->get_entity_category()); @@ -328,6 +332,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { } if (msg.has_direction) call.set_direction(static_cast(msg.direction)); + if (msg.has_preset_mode) + call.set_preset_mode(msg.preset_mode); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1e97a57bb1..01fb540e7e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1375,6 +1375,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi this->icon = value.as_string(); return true; } + case 12: { + this->supported_preset_modes.push_back(value.as_string()); + return true; + } default: return false; } @@ -1401,6 +1405,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->disabled_by_default); buffer.encode_string(10, this->icon); buffer.encode_enum(11, this->entity_category); + for (auto &it : this->supported_preset_modes) { + buffer.encode_string(12, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { @@ -1451,6 +1458,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + for (const auto &it : this->supported_preset_modes) { + out.append(" supported_preset_modes: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } out.append("}"); } #endif @@ -1480,6 +1493,16 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } } +bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 7: { + this->preset_mode = value.as_string(); + return true; + } + default: + return false; + } +} bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -1497,6 +1520,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(4, this->speed); buffer.encode_enum(5, this->direction); buffer.encode_int32(6, this->speed_level); + buffer.encode_string(7, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanStateResponse::dump_to(std::string &out) const { @@ -1527,6 +1551,10 @@ void FanStateResponse::dump_to(std::string &out) const { sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -1572,6 +1600,20 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed_level = value.as_int32(); return true; } + case 12: { + this->has_preset_mode = value.as_bool(); + return true; + } + default: + return false; + } +} +bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 13: { + this->preset_mode = value.as_string(); + return true; + } default: return false; } @@ -1598,6 +1640,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(9, this->direction); buffer.encode_bool(10, this->has_speed_level); buffer.encode_int32(11, this->speed_level); + buffer.encode_bool(12, this->has_preset_mode); + buffer.encode_string(13, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanCommandRequest::dump_to(std::string &out) const { @@ -1648,6 +1692,14 @@ void FanCommandRequest::dump_to(std::string &out) const { sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" has_preset_mode: "); + out.append(YESNO(this->has_preset_mode)); + out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a63e90b7b7..fc57348863 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -472,6 +472,7 @@ class ListEntitiesFanResponse : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + std::vector supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -490,6 +491,7 @@ class FanStateResponse : public ProtoMessage { enums::FanSpeed speed{}; enums::FanDirection direction{}; int32_t speed_level{0}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -497,6 +499,7 @@ class FanStateResponse : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class FanCommandRequest : public ProtoMessage { @@ -512,6 +515,8 @@ class FanCommandRequest : public ProtoMessage { enums::FanDirection direction{}; bool has_speed_level{false}; int32_t speed_level{0}; + bool has_preset_mode{false}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -519,6 +524,7 @@ class FanCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesLightResponse : public ProtoMessage { diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 74d9da279f..15a7f5e025 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,6 +12,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); }); @@ -19,6 +20,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); } @@ -33,6 +35,7 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); + traits.set_supported_preset_modes(base.supported_preset_modes()); return traits; } @@ -46,6 +49,8 @@ void CopyFan::control(const fan::FanCall &call) { call2.set_speed(*call.get_speed()); if (call.get_direction().has_value()) call2.set_direction(*call.get_direction()); + if (!call.get_preset_mode().empty()) + call2.set_preset_mode(call.get_preset_mode()); call2.perform(); } diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 23df3c2214..fd0f2f66cb 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_ON_SPEED_SET, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, + CONF_ON_PRESET_SET, CONF_TRIGGER_ID, CONF_DIRECTION, CONF_RESTORE_MODE, @@ -57,6 +58,9 @@ CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) +FanPresetSetTrigger = fan_ns.class_( + "FanPresetSetTrigger", automation.Trigger.template() +) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) @@ -101,9 +105,46 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), } ), + cv.Optional(CONF_ON_PRESET_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanPresetSetTrigger), + } + ), } ) +_PRESET_MODES_SCHEMA = cv.All( + cv.ensure_list(cv.string_strict), + cv.Length(min=1), +) + + +def validate_preset_modes(value): + # Check against defined schema + value = _PRESET_MODES_SCHEMA(value) + + # Ensure preset names are unique + errors = [] + presets = set() + for i, preset in enumerate(value): + # If name does not exist yet add it + if preset not in presets: + presets.add(preset) + continue + + # Otherwise it's an error + errors.append( + cv.Invalid( + f"Found duplicate preset name '{preset}'. Presets must have unique names.", + [i], + ) + ) + + if errors: + raise cv.MultipleInvalid(errors) + + return value + async def setup_fan_core_(var, config): await setup_entity(var, config) @@ -154,6 +195,9 @@ async def setup_fan_core_(var, config): for conf in config.get(CONF_ON_SPEED_SET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PRESET_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_fan(var, config): diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 511acf5682..b5bdeb8a29 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -165,5 +165,23 @@ class FanSpeedSetTrigger : public Trigger<> { int last_speed_; }; +class FanPresetSetTrigger : public Trigger<> { + public: + FanPresetSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto preset_mode = state->preset_mode; + auto should_trigger = preset_mode != this->last_preset_mode_; + this->last_preset_mode_ = preset_mode; + if (should_trigger) { + this->trigger(); + } + }); + this->last_preset_mode_ = state->preset_mode; + } + + protected: + std::string last_preset_mode_; +}; + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 87566bad4a..95e3ae0758 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -32,9 +32,12 @@ void FanCall::perform() { if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } - + if (!this->preset_mode_.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); + } this->parent_.control(*this); } + void FanCall::validate_() { auto traits = this->parent_.get_traits(); @@ -62,6 +65,15 @@ void FanCall::validate_() { ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); this->direction_.reset(); } + + if (!this->preset_mode_.empty()) { + const auto &preset_modes = traits.supported_preset_modes(); + if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { + ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), + this->preset_mode_.c_str()); + this->preset_mode_.clear(); + } + } } FanCall FanRestoreState::to_call(Fan &fan) { @@ -70,6 +82,14 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_oscillating(this->oscillating); call.set_speed(this->speed); call.set_direction(this->direction); + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + } + } return call; } void FanRestoreState::apply(Fan &fan) { @@ -77,6 +97,14 @@ void FanRestoreState::apply(Fan &fan) { fan.oscillating = this->oscillating; fan.speed = this->speed; fan.direction = this->direction; + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + } + } fan.publish_state(); } @@ -100,7 +128,9 @@ void Fan::publish_state() { if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } - + if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); + } this->state_callback_.call(); this->save_state_(); } @@ -143,20 +173,36 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + + if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { + const auto &preset_modes = this->get_traits().supported_preset_modes(); + // Store index of current preset mode + auto preset_iterator = preset_modes.find(this->preset_mode); + if (preset_iterator != preset_modes.end()) + state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + } + this->rtc_.save(&state); } void Fan::dump_traits_(const char *tag, const char *prefix) { - if (this->get_traits().supports_speed()) { + auto traits = this->get_traits(); + + if (traits.supports_speed()) { ESP_LOGCONFIG(tag, "%s Speed: YES", prefix); - ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, this->get_traits().supported_speed_count()); + ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, traits.supported_speed_count()); } - if (this->get_traits().supports_oscillation()) { + if (traits.supports_oscillation()) { ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix); } - if (this->get_traits().supports_direction()) { + if (traits.supports_direction()) { ESP_LOGCONFIG(tag, "%s Direction: YES", prefix); } + if (traits.supports_preset_modes()) { + ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); + for (const std::string &s : traits.supported_preset_modes()) + ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); + } } } // namespace fan diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index f9d317e675..b74187eb4a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -72,6 +72,11 @@ class FanCall { return *this; } optional get_direction() const { return this->direction_; } + FanCall &set_preset_mode(const std::string &preset_mode) { + this->preset_mode_ = preset_mode; + return *this; + } + std::string get_preset_mode() const { return this->preset_mode_; } void perform(); @@ -83,6 +88,7 @@ class FanCall { optional oscillating_; optional speed_; optional direction_{}; + std::string preset_mode_{}; }; struct FanRestoreState { @@ -90,6 +96,7 @@ struct FanRestoreState { int speed; bool oscillating; FanDirection direction; + uint8_t preset_mode; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); @@ -107,6 +114,8 @@ class Fan : public EntityBase { int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; + // The current preset mode of the fan + std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index e69d8e2e53..2ef6f8b7cc 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,3 +1,6 @@ +#include +#include + #pragma once namespace esphome { @@ -25,12 +28,19 @@ class FanTraits { bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } + /// Return the preset modes supported by the fan. + std::set supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan. + void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + /// Return if preset modes are supported + bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; + std::set preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 421883a1ff..424e944290 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( CONF_ID, CONF_DECAY_MODE, @@ -10,6 +11,7 @@ from esphome.const import ( CONF_PIN_A, CONF_PIN_B, CONF_ENABLE_PIN, + CONF_PRESET_MODES, ) from .. import hbridge_ns @@ -28,7 +30,6 @@ DECAY_MODE_OPTIONS = { # Actions BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) - CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( { cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), @@ -39,6 +40,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) @@ -69,3 +71,6 @@ async def to_code(config): if CONF_ENABLE_PIN in config: enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable_pin)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 44cf5ae049..605a9d4ef3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -33,7 +33,12 @@ void HBridgeFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void HBridgeFan::dump_config() { LOG_FAN("", "H-Bridge Fan", this); if (this->decay_mode_ == DECAY_MODE_SLOW) { @@ -42,9 +47,7 @@ void HBridgeFan::dump_config() { ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); } } -fan::FanTraits HBridgeFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); -} + void HBridgeFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -54,10 +57,12 @@ void HBridgeFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void HBridgeFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; if (speed == 0.0f) { // off means idle diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4389b97ccb..4234fccae3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/automation.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -20,10 +22,11 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override; + fan::FanTraits get_traits() override { return this->traits_; } fan::FanCall brake(); @@ -34,6 +37,8 @@ class HBridgeFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; + fan::FanTraits traits_; + std::set preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 978e68d1e9..3acfb005bd 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,14 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( + CONF_PRESET_MODES, + CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, - CONF_DIRECTION_OUTPUT, CONF_OUTPUT_ID, CONF_SPEED, CONF_SPEED_COUNT, ) + from .. import speed_ns SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) @@ -23,6 +26,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( "Configuring individual speeds is deprecated." ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) @@ -40,3 +44,6 @@ async def to_code(config): if CONF_DIRECTION_OUTPUT in config: direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) cg.add(var.set_direction(direction_output)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3a65f2c365..41b222acd6 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -12,11 +12,14 @@ void SpeedFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } -fan::FanTraits SpeedFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); -} + void SpeedFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -26,10 +29,12 @@ void SpeedFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void SpeedFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; this->output_->set_level(speed); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 1fad53813a..ca0fe20e2a 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -15,7 +17,8 @@ class SpeedFan : public Component, public fan::Fan { void dump_config() override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - fan::FanTraits get_traits() override; + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -25,6 +28,8 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; + fan::FanTraits traits_; + std::set preset_modes_{}; }; } // namespace speed diff --git a/esphome/const.py b/esphome/const.py index 2bc088c063..f688cd75f9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -504,6 +504,7 @@ CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" CONF_ON_MULTI_CLICK = "on_multi_click" CONF_ON_OPEN = "on_open" +CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" @@ -601,6 +602,7 @@ CONF_PRESET = "preset" CONF_PRESET_BOOST = "preset_boost" CONF_PRESET_COMMAND_TOPIC = "preset_command_topic" CONF_PRESET_ECO = "preset_eco" +CONF_PRESET_MODES = "preset_modes" CONF_PRESET_SLEEP = "preset_sleep" CONF_PRESET_STATE_TOPIC = "preset_state_topic" CONF_PRESSURE = "pressure" diff --git a/tests/test1.yaml b/tests/test1.yaml index 559b7ab8fc..b77cff7619 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2944,6 +2944,33 @@ fan: on_speed_set: then: - logger.log: Fan speed was changed! + - platform: speed + id: fan_speed_presets + icon: mdi:weather-windy + output: pca_6 + speed_count: 10 + name: Speed Fan w/ Presets + oscillation_output: gpio_19 + direction_output: gpio_26 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! + - platform: hbridge + id: fan_hbridge_presets + icon: mdi:weather-windy + speed_count: 4 + name: H-bridge Fan w/ Presets + pin_a: pca_6 + pin_b: pca_7 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! - platform: bedjet name: My Bedjet fan bedjet_id: my_bedjet_client @@ -4193,4 +4220,3 @@ alarm_control_panel: then: - lambda: !lambda |- ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())); - From cc7d167e8b0996ff24e75ea0feac386ec0ef6f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87etin=20Koca?= <_@cetinkoca.com> Date: Tue, 12 Dec 2023 08:17:56 +0300 Subject: [PATCH 122/436] Fix uninitialized climate target temperature (#5795) --- esphome/components/climate/climate.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index f90db3f52a..1bbb17322d 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -160,6 +160,8 @@ struct ClimateDeviceRestoreState { */ class Climate : public EntityBase { public: + Climate() {} + /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; /// The active state of the climate device. @@ -172,9 +174,9 @@ class Climate : public EntityBase { float target_temperature; struct { /// The minimum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_low; + float target_temperature_low{NAN}; /// The maximum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_high; + float target_temperature_high{NAN}; }; }; From 04720b8440b43c8183df004720323d106e8ff51a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:39:29 -1000 Subject: [PATCH 123/436] Bump aioesphomeapi from 19.3.0 to 20.0.0 (#5911) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 39715232d6..ae8d50f2ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.3.0 +aioesphomeapi==20.0.0 zeroconf==0.128.4 python-magic==0.4.27 From 8e92bb79589701da72120b26966cc139911bf88c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 21:59:10 +0000 Subject: [PATCH 124/436] Bump black from 23.11.0 to 23.12.0 (#5912) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- esphome/writer.py | 4 +++- requirements_test.txt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc22265f1f..36ec1894d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black args: diff --git a/esphome/writer.py b/esphome/writer.py index ad506b6ae6..83e95614a6 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -203,7 +203,9 @@ def write_platformio_project(): write_platformio_ini(content) -DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ +DEFINES_H_FORMAT = ( + ESPHOME_H_FORMAT +) = """\ #pragma once #include "esphome/core/macros.h" {} diff --git a/requirements_test.txt b/requirements_test.txt index eef69b7515..18c6dedf3e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.0.3 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating -black==23.11.0 # also change in .pre-commit-config.yaml when updating +black==23.12.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit From c6dc336c4acfb8a1d6142d342daf8ecccefc7fc5 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Tue, 12 Dec 2023 23:56:01 +0100 Subject: [PATCH 125/436] Updating the touchscreen interface structure (#4596) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: NP v/d Spek Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Gustavo Ambrozio --- CODEOWNERS | 8 +- .../__init__.py} | 8 +- .../ektf2232/{ => touchscreen}/ektf2232.cpp | 63 +----- .../ektf2232/{ => touchscreen}/ektf2232.h | 14 +- esphome/components/ft5x06/__init__.py | 6 + .../components/ft5x06/touchscreen/__init__.py | 26 +++ .../ft5x06/touchscreen/ft5x06_touchscreen.h | 124 +++++++++++ esphome/components/ft63x6/__init__.py | 1 + esphome/components/ft63x6/ft63x6.cpp | 99 +++++++++ esphome/components/ft63x6/ft63x6.h | 41 ++++ esphome/components/ft63x6/touchscreen.py | 44 ++++ .../components/gt911/touchscreen/__init__.py | 35 +-- .../gt911/touchscreen/gt911_touchscreen.cpp | 75 +++---- .../gt911/touchscreen/gt911_touchscreen.h | 15 +- esphome/components/ili9xxx/display.py | 6 +- esphome/components/inkplate6/display.py | 6 +- .../lilygo_t5_47/touchscreen/__init__.py | 8 +- .../touchscreen/lilygo_t5_47_touchscreen.cpp | 108 ++------- .../touchscreen/lilygo_t5_47_touchscreen.h | 16 +- esphome/components/touchscreen/__init__.py | 52 ++++- .../touchscreen_binary_sensor.cpp | 3 +- .../components/touchscreen/touchscreen.cpp | 131 +++++++++-- esphome/components/touchscreen/touchscreen.h | 109 +++++++-- .../tt21100/touchscreen/__init__.py | 8 +- .../tt21100/touchscreen/tt21100.cpp | 52 +---- .../components/tt21100/touchscreen/tt21100.h | 14 +- esphome/components/xpt2046/binary_sensor.py | 3 - esphome/components/xpt2046/touchscreen.py | 116 ---------- .../xpt2046/touchscreen/__init__.py | 93 ++++++++ .../xpt2046/touchscreen/xpt2046.cpp | 113 ++++++++++ .../components/xpt2046/touchscreen/xpt2046.h | 41 ++++ esphome/components/xpt2046/xpt2046.cpp | 207 ------------------ esphome/components/xpt2046/xpt2046.h | 107 --------- tests/test4.yaml | 79 ++++--- tests/test8.yaml | 2 + 35 files changed, 997 insertions(+), 836 deletions(-) rename esphome/components/ektf2232/{touchscreen.py => touchscreen/__init__.py} (89%) rename esphome/components/ektf2232/{ => touchscreen}/ektf2232.cpp (60%) rename esphome/components/ektf2232/{ => touchscreen}/ektf2232.h (67%) create mode 100644 esphome/components/ft5x06/__init__.py create mode 100644 esphome/components/ft5x06/touchscreen/__init__.py create mode 100644 esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h create mode 100644 esphome/components/ft63x6/__init__.py create mode 100644 esphome/components/ft63x6/ft63x6.cpp create mode 100644 esphome/components/ft63x6/ft63x6.h create mode 100644 esphome/components/ft63x6/touchscreen.py delete mode 100644 esphome/components/xpt2046/binary_sensor.py delete mode 100644 esphome/components/xpt2046/touchscreen.py create mode 100644 esphome/components/xpt2046/touchscreen/__init__.py create mode 100644 esphome/components/xpt2046/touchscreen/xpt2046.cpp create mode 100644 esphome/components/xpt2046/touchscreen/xpt2046.h delete mode 100644 esphome/components/xpt2046/xpt2046.cpp delete mode 100644 esphome/components/xpt2046/xpt2046.h diff --git a/CODEOWNERS b/CODEOWNERS index 43b7647fe6..c37eb3581b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,7 +88,7 @@ esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M -esphome/components/ektf2232/* @jesserockz +esphome/components/ektf2232/touchscreen/* @jesserockz esphome/components/emc2101/* @ellull esphome/components/ens160/* @vincentscode esphome/components/ens210/* @itn3rd77 @@ -110,6 +110,8 @@ esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/fs3000/* @kahrendt +esphome/components/ft5x06/* @clydebarrow +esphome/components/ft63x6/* @gpambrozio esphome/components/gcja5/* @gcormier esphome/components/globals/* @esphome/core esphome/components/gp8403/* @jesserockz @@ -331,7 +333,7 @@ esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 -esphome/components/touchscreen/* @jesserockz +esphome/components/touchscreen/* @jesserockz @nielsnl68 esphome/components/tsl2591/* @wjcarpenter esphome/components/tt21100/* @kroimon esphome/components/tuya/binary_sensor/* @jesserockz @@ -364,6 +366,6 @@ esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xl9535/* @mreditor97 -esphome/components/xpt2046/* @nielsnl68 @numo68 +esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/ektf2232/touchscreen.py b/esphome/components/ektf2232/touchscreen/__init__.py similarity index 89% rename from esphome/components/ektf2232/touchscreen.py rename to esphome/components/ektf2232/touchscreen/__init__.py index d937265e7a..c1fefb7f09 100644 --- a/esphome/components/ektf2232/touchscreen.py +++ b/esphome/components/ektf2232/touchscreen/__init__.py @@ -12,7 +12,6 @@ ektf2232_ns = cg.esphome_ns.namespace("ektf2232") EKTF2232Touchscreen = ektf2232_ns.class_( "EKTF2232Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) @@ -28,17 +27,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( ), cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, } - ) - .extend(i2c.i2c_device_schema(0x15)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x15)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/ektf2232/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp similarity index 60% rename from esphome/components/ektf2232/ektf2232.cpp rename to esphome/components/ektf2232/touchscreen/ektf2232.cpp index 80f5f8a8e2..1a2c0389af 100644 --- a/esphome/components/ektf2232/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -15,16 +15,12 @@ static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00}; static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; -void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; } - void EKTF2232Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); this->rts_pin_->setup(); @@ -45,7 +41,7 @@ void EKTF2232Touchscreen::setup() { this->mark_failed(); return; } - this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); + this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->write(GET_Y_RES, 4); if (this->read(received, 4)) { @@ -54,19 +50,14 @@ void EKTF2232Touchscreen::setup() { this->mark_failed(); return; } - this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); - this->store_.touch = false; + this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->set_power_state(true); } -void EKTF2232Touchscreen::loop() { - if (!this->store_.touch) - return; - this->store_.touch = false; - +void EKTF2232Touchscreen::update_touches() { uint8_t touch_count = 0; - std::vector touches; + int16_t x_raw, y_raw; uint8_t raw[8]; this->read(raw, 8); @@ -75,45 +66,15 @@ void EKTF2232Touchscreen::loop() { touch_count++; } - if (touch_count == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - touch_count = std::min(touch_count, 2); ESP_LOGV(TAG, "Touch count: %d", touch_count); for (int i = 0; i < touch_count; i++) { uint8_t *d = raw + 1 + (i * 3); - uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1]; - uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2]; - - raw_x = raw_x * this->display_height_ - 1; - raw_y = raw_y * this->display_width_ - 1; - - TouchPoint tp; - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = raw_x / this->x_resolution_; - tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - case ROTATE_90_DEGREES: - tp.x = raw_x / this->x_resolution_; - tp.y = raw_y / this->y_resolution_; - break; - case ROTATE_180_DEGREES: - tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.x = raw_y / this->y_resolution_; - break; - case ROTATE_270_DEGREES: - tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); + x_raw = (d[0] & 0xF0) << 4 | d[1]; + y_raw = (d[0] & 0x0F) << 8 | d[2]; + this->set_raw_touch_position_(i, x_raw, y_raw); } } @@ -126,7 +87,7 @@ void EKTF2232Touchscreen::set_power_state(bool enable) { bool EKTF2232Touchscreen::get_power_state() { uint8_t received[4]; this->write(GET_POWER_STATE_CMD, 4); - this->store_.touch = false; + this->store_.touched = false; this->read(received, 4); return (received[1] >> 3) & 1; } @@ -145,14 +106,14 @@ bool EKTF2232Touchscreen::soft_reset_() { uint8_t received[4]; uint16_t timeout = 1000; - while (!this->store_.touch && timeout > 0) { + while (!this->store_.touched && timeout > 0) { delay(1); timeout--; } if (timeout > 0) - this->store_.touch = true; + this->store_.touched = true; this->read(received, 4); - this->store_.touch = false; + this->store_.touched = false; return !memcmp(received, HELLO, 4); } diff --git a/esphome/components/ektf2232/ektf2232.h b/esphome/components/ektf2232/touchscreen/ektf2232.h similarity index 67% rename from esphome/components/ektf2232/ektf2232.h rename to esphome/components/ektf2232/touchscreen/ektf2232.h index e880b77f99..e9288d0a27 100644 --- a/esphome/components/ektf2232/ektf2232.h +++ b/esphome/components/ektf2232/touchscreen/ektf2232.h @@ -9,19 +9,11 @@ namespace esphome { namespace ektf2232 { -struct EKTF2232TouchscreenStore { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(EKTF2232TouchscreenStore *store); -}; - using namespace touchscreen; -class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } @@ -33,12 +25,10 @@ class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2 protected: void hard_reset_(); bool soft_reset_(); + void update_touches() override; InternalGPIOPin *interrupt_pin_; GPIOPin *rts_pin_; - EKTF2232TouchscreenStore store_; - uint16_t x_resolution_; - uint16_t y_resolution_; }; } // namespace ektf2232 diff --git a/esphome/components/ft5x06/__init__.py b/esphome/components/ft5x06/__init__.py new file mode 100644 index 0000000000..dceea71dd0 --- /dev/null +++ b/esphome/components/ft5x06/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +ft5x06_ns = cg.esphome_ns.namespace("ft5x06") diff --git a/esphome/components/ft5x06/touchscreen/__init__.py b/esphome/components/ft5x06/touchscreen/__init__.py new file mode 100644 index 0000000000..adeeac0d1a --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID +from .. import ft5x06_ns + +FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener") +FT5x06Touchscreen = ft5x06_ns.class_( + "FT5x06Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FT5x06Touchscreen), + } +).extend(i2c.i2c_device_schema(0x48)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h new file mode 100644 index 0000000000..497d6c906c --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -0,0 +1,124 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ft5x06 { + +static const char *const TAG = "ft5x06.touchscreen"; + +enum VendorId { + FT5X06_ID_UNKNOWN = 0, + FT5X06_ID_1 = 0x51, + FT5X06_ID_2 = 0x11, + FT5X06_ID_3 = 0xCD, +}; + +enum FTCmd : uint8_t { + FT5X06_MODE_REG = 0x00, + FT5X06_ORIGIN_REG = 0x08, + FT5X06_RESOLUTION_REG = 0x0C, + FT5X06_VENDOR_ID_REG = 0xA8, + FT5X06_TD_STATUS = 0x02, + FT5X06_TOUCH_DATA = 0x03, + FT5X06_I_MODE = 0xA4, + FT5X06_TOUCH_MAX = 0x4C, +}; + +enum FTMode : uint8_t { + FT5X06_OP_MODE = 0, + FT5X06_SYSINFO_MODE = 0x10, + FT5X06_TEST_MODE = 0x40, +}; + +static const size_t MAX_TOUCHES = 5; // max number of possible touches reported + +class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override { + esph_log_config(TAG, "Setting up FT5x06 Touchscreen..."); + // wait 200ms after reset. + this->set_timeout(200, [this] { this->continue_setup_(); }); + } + + void continue_setup_(void) { + uint8_t data[4]; + if (!this->set_mode_(FT5X06_OP_MODE)) + return; + + if (!this->err_check_(this->read_register(FT5X06_VENDOR_ID_REG, data, 1), "Read Vendor ID")) + return; + switch (data[0]) { + case FT5X06_ID_1: + case FT5X06_ID_2: + case FT5X06_ID_3: + this->vendor_id_ = (VendorId) data[0]; + esph_log_d(TAG, "Read vendor ID 0x%X", data[0]); + break; + + default: + esph_log_e(TAG, "Unknown vendor ID 0x%X", data[0]); + this->mark_failed(); + return; + } + // reading the chip registers to get max x/y does not seem to work. + this->x_raw_max_ = this->display_->get_width(); + this->y_raw_max_ = this->display_->get_height(); + esph_log_config(TAG, "FT5x06 Touchscreen setup complete"); + } + + void update_touches() override { + uint8_t touch_cnt; + uint8_t data[MAX_TOUCHES][6]; + + if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) { + esph_log_w(TAG, "Failed to read status"); + return; + } + if (touch_cnt == 0) + return; + + if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) { + esph_log_w(TAG, "Failed to read touch data"); + return; + } + for (uint8_t i = 0; i != touch_cnt; i++) { + uint8_t status = data[i][0] >> 6; + uint8_t id = data[i][2] >> 3; + uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); + uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); + + esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + if (status == 0 || status == 2) { + this->set_raw_touch_position_(id, x, y); + } + } + } + + void dump_config() override { + esph_log_config(TAG, "FT5x06 Touchscreen:"); + esph_log_config(TAG, " Address: 0x%02X", this->address_); + esph_log_config(TAG, " Vendor ID: 0x%X", (int) this->vendor_id_); + } + + protected: + bool err_check_(i2c::ErrorCode err, const char *msg) { + if (err != i2c::ERROR_OK) { + this->mark_failed(); + esph_log_e(TAG, "%s failed - err 0x%X", msg, err); + return false; + } + return true; + } + bool set_mode_(FTMode mode) { + return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode"); + } + VendorId vendor_id_{FT5X06_ID_UNKNOWN}; +}; + +} // namespace ft5x06 +} // namespace esphome diff --git a/esphome/components/ft63x6/__init__.py b/esphome/components/ft63x6/__init__.py new file mode 100644 index 0000000000..b6d7d3580e --- /dev/null +++ b/esphome/components/ft63x6/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gpambrozio"] diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp new file mode 100644 index 0000000000..9198954253 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -0,0 +1,99 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#include "ft63x6.h" +#include "esphome/core/log.h" + +// Registers +// Reference: https://focuslcds.com/content/FT6236.pdf +namespace esphome { +namespace ft63x6 { + +static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02; + +static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; +static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; +static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; + +static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; +static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09; +static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B; + +static const char *const TAG = "FT63X6Touchscreen"; + +void FT63X6Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen..."); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + } + + this->hard_reset_(); + + // Get touch resolution + this->x_raw_max_ = 320; + this->y_raw_max_ = 480; +} + +void FT63X6Touchscreen::update_touches() { + int touch_count = this->read_touch_count_(); + if (touch_count == 0) { + return; + } + + uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 + int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); + int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); + this->set_raw_touch_position_(touch_id, x, y); + + if (touch_count >= 2) { + touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01) + x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); + y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); + this->set_raw_touch_position_(touch_id, x, y); + } +} + +void FT63X6Touchscreen::hard_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + } +} + +void FT63X6Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); } + +// Touch functions +uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) { + uint8_t read_buf[2]; + read_buf[0] = this->read_byte_(coordinate); + read_buf[1] = this->read_byte_(coordinate + 1); + return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; +} +uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; } + +uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { + uint8_t byte = 0; + this->read_byte(addr, &byte); + return byte; +} + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/ft63x6.h b/esphome/components/ft63x6/ft63x6.h new file mode 100644 index 0000000000..79b1991041 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.h @@ -0,0 +1,41 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace ft63x6 { + +using namespace touchscreen; + +class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void hard_reset_(); + uint8_t read_byte_(uint8_t addr); + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + + uint8_t read_touch_count_(); + uint16_t read_touch_coordinate_(uint8_t coordinate); + uint8_t read_touch_id_(uint8_t id_address); +}; + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/touchscreen.py b/esphome/components/ft63x6/touchscreen.py new file mode 100644 index 0000000000..d77d9ca287 --- /dev/null +++ b/esphome/components/ft63x6/touchscreen.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +CODEOWNERS = ["@gpambrozio"] +DEPENDENCIES = ["i2c"] + +ft6336u_ns = cg.esphome_ns.namespace("ft63x6") +FT63X6Touchscreen = ft6336u_ns.class_( + "FT63X6Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONF_FT63X6_ID = "ft63x6_id" + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FT63X6Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ).extend(i2c.i2c_device_schema(0x38)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN): + interrupt_pin = await cg.gpio_pin_expression(interrupt_pin_config) + cg.add(var.set_interrupt_pin(interrupt_pin)) + if reset_pin_config := config.get(CONF_RESET_PIN): + reset_pin = await cg.gpio_pin_expression(reset_pin_config) + cg.add(var.set_reset_pin(reset_pin)) diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py index 295e32b1b1..9a0d5cc169 100644 --- a/esphome/components/gt911/touchscreen/__init__.py +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import i2c, touchscreen -from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_ROTATION +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID from .. import gt911_ns @@ -11,36 +11,21 @@ GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") GT911Touchscreen = gt911_ns.class_( "GT911Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) -ROTATIONS = { - 0: touchscreen.TouchRotation.ROTATE_0_DEGREES, - 90: touchscreen.TouchRotation.ROTATE_90_DEGREES, - 180: touchscreen.TouchRotation.ROTATE_180_DEGREES, - 270: touchscreen.TouchRotation.ROTATE_270_DEGREES, -} -CONFIG_SCHEMA = ( - touchscreen.TOUCHSCREEN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(GT911Touchscreen), - cv.Optional(CONF_ROTATION): cv.enum(ROTATIONS), - cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, - } - ) - .extend(i2c.i2c_device_schema(0x5D)) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GT911Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } +).extend(i2c.i2c_device_schema(0x5D)) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) - interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_interrupt_pin(interrupt_pin)) - if CONF_ROTATION in config: - cg.add(var.set_rotation(ROTATIONS[config[CONF_ROTATION]])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 4d3e7e7903..adc577f5da 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -12,6 +12,7 @@ static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; +static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48}; static const size_t MAX_TOUCHES = 5; // max number of possible touches reported #define ERROR_CHECK(err) \ @@ -21,24 +22,35 @@ static const size_t MAX_TOUCHES = 5; // max number of possible touches reported return; \ } -void IRAM_ATTR HOT Store::gpio_intr(Store *store) { store->available = true; } - void GT911Touchscreen::setup() { i2c::ErrorCode err; ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); - // datasheet says NOT to use pullup/down on the int line. - this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); - this->interrupt_pin_->setup(); // check the configuration of the int line. - uint8_t data; + uint8_t data[4]; err = this->write(GET_SWITCHES, 2); if (err == i2c::ERROR_OK) { - err = this->read(&data, 1); + err = this->read(data, 1); if (err == i2c::ERROR_OK) { - ESP_LOGD(TAG, "Read from switches: 0x%02X", data); - this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, - (data & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); + if (this->interrupt_pin_ != nullptr) { + // datasheet says NOT to use pullup/down on the int line. + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, + (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + } + } + } + if (err == i2c::ERROR_OK) { + err = this->write(GET_MAX_VALUES, 2); + if (err == i2c::ERROR_OK) { + err = this->read(data, sizeof(data)); + if (err == i2c::ERROR_OK) { + this->x_raw_max_ = encode_uint16(data[1], data[0]); + this->y_raw_max_ = encode_uint16(data[3], data[2]); + esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_); + } } } if (err != i2c::ERROR_OK) { @@ -46,31 +58,28 @@ void GT911Touchscreen::setup() { this->mark_failed(); return; } + ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); } -void GT911Touchscreen::loop() { +void GT911Touchscreen::update_touches() { i2c::ErrorCode err; - touchscreen::TouchPoint tp; uint8_t touch_state = 0; uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte - if (!this->store_.available) - return; - this->store_.available = false; - err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); ERROR_CHECK(err); err = this->read(&touch_state, 1); ERROR_CHECK(err); this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE)); - - if ((touch_state & 0x80) == 0) - return; uint8_t num_of_touches = touch_state & 0x07; + + if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { + this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet. + return; + } + if (num_of_touches == 0) - this->send_release_(); - if (num_of_touches > MAX_TOUCHES) // should never happen return; err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); @@ -80,29 +89,10 @@ void GT911Touchscreen::loop() { ERROR_CHECK(err); for (uint8_t i = 0; i != num_of_touches; i++) { - tp.id = data[i][0]; + uint16_t id = data[i][0]; uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t y = encode_uint16(data[i][4], data[i][3]); - - switch (this->rotation_) { - case touchscreen::ROTATE_0_DEGREES: - tp.x = x; - tp.y = y; - break; - case touchscreen::ROTATE_90_DEGREES: - tp.x = y; - tp.y = this->display_width_ - x; - break; - case touchscreen::ROTATE_180_DEGREES: - tp.x = this->display_width_ - x; - tp.y = this->display_height_ - y; - break; - case touchscreen::ROTATE_270_DEGREES: - tp.x = this->display_height_ - y; - tp.y = x; - break; - } - this->defer([this, tp]() { this->send_touch_(tp); }); + this->set_raw_touch_position_(id, x, y); } auto keys = data[num_of_touches][0]; for (size_t i = 0; i != 4; i++) { @@ -115,7 +105,6 @@ void GT911Touchscreen::dump_config() { ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); LOG_I2C_DEVICE(this); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); - ESP_LOGCONFIG(TAG, " Rotation: %d", (int) this->rotation_); } } // namespace gt911 diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index dc9248bb4a..44875de5f1 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -8,30 +8,23 @@ namespace esphome { namespace gt911 { -struct Store { - volatile bool available; - - static void gpio_intr(Store *store); -}; - class GT911ButtonListener { public: virtual void update_button(uint8_t index, bool state) = 0; }; -class GT911Touchscreen : public touchscreen::Touchscreen, public Component, public i2c::I2CDevice { +class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; - void set_rotation(touchscreen::TouchRotation rotation) { this->rotation_ = rotation; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } protected: - InternalGPIOPin *interrupt_pin_; - Store store_; + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{}; std::vector button_listeners_; }; diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index f321b2ed63..cd68f1ae27 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -32,7 +32,11 @@ CODEOWNERS = ["@nielsnl68", "@clydebarrow"] ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") ILI9XXXDisplay = ili9xxx_ns.class_( - "ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "ILI9XXXDisplay", + cg.PollingComponent, + spi.SPIDevice, + display.Display, + display.DisplayBuffer, ) ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode") diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a1ecfdd1d6..bcd9580448 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -39,7 +39,11 @@ CONF_VCOM_PIN = "vcom_pin" inkplate6_ns = cg.esphome_ns.namespace("inkplate6") Inkplate6 = inkplate6_ns.class_( - "Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer + "Inkplate6", + cg.PollingComponent, + i2c.I2CDevice, + display.Display, + display.DisplayBuffer, ) InkplateModel = inkplate6_ns.enum("InkplateModel") diff --git a/esphome/components/lilygo_t5_47/touchscreen/__init__.py b/esphome/components/lilygo_t5_47/touchscreen/__init__.py index fe94120644..01b03c807f 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/__init__.py +++ b/esphome/components/lilygo_t5_47/touchscreen/__init__.py @@ -13,7 +13,6 @@ DEPENDENCIES = ["i2c"] LilygoT547Touchscreen = lilygo_t5_47_ns.class_( "LilygoT547Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) @@ -27,17 +26,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( pins.internal_gpio_input_pin_schema ), } - ) - .extend(i2c.i2c_device_schema(0x5A)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x5A)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b89cf2a724..eb61b6f31e 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -23,15 +23,12 @@ static const uint8_t READ_TOUCH[1] = {0x07}; return; \ } -void Store::gpio_intr(Store *store) { store->touch = true; } - void LilygoT547Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up Lilygo T5 4.7 Touchscreen..."); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); if (this->write(nullptr, 0) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Failed to communicate!"); @@ -41,19 +38,14 @@ void LilygoT547Touchscreen::setup() { } this->write_register(POWER_REGISTER, WAKEUP_CMD, 1); + + this->x_raw_max_ = this->get_width_(); + this->y_raw_max_ = this->get_height_(); } -void LilygoT547Touchscreen::loop() { - if (!this->store_.touch) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - this->store_.touch = false; - +void LilygoT547Touchscreen::update_touches() { uint8_t point = 0; uint8_t buffer[40] = {0}; - uint32_t sum_l = 0, sum_h = 0; i2c::ErrorCode err; err = this->write_register(TOUCH_REGISTER, READ_FLAGS, 1); @@ -69,102 +61,30 @@ void LilygoT547Touchscreen::loop() { point = buffer[5] & 0xF; - if (point == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } else if (point == 1) { + if (point == 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 2); ERROR_CHECK(err); - sum_l = buffer[5] << 8 | buffer[6]; } else if (point > 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 5 * (point - 1) + 3); ERROR_CHECK(err); - - sum_l = buffer[5 * point + 1] << 8 | buffer[5 * point + 2]; } this->write_register(TOUCH_REGISTER, CLEAR_FLAGS, 2); - for (int i = 0; i < 5 * point; i++) - sum_h += buffer[i]; + if (point == 0) + point = 1; - if (sum_l != sum_h) - point = 0; - - if (point) { - uint8_t offset; - for (int i = 0; i < point; i++) { - if (i == 0) { - offset = 0; - } else { - offset = 4; - } - - TouchPoint tp; - - tp.id = (buffer[i * 5 + offset] >> 4) & 0x0F; - tp.state = buffer[i * 5 + offset] & 0x0F; - if (tp.state == 0x06) - tp.state = 0x07; - - uint16_t y = (uint16_t) ((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); - } - } else { - TouchPoint tp; - tp.id = (buffer[0] >> 4) & 0x0F; - tp.state = 0x06; - - uint16_t y = (uint16_t) ((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); + uint16_t id, x_raw, y_raw; + for (uint8_t i = 0; i < point; i++) { + id = (buffer[i * 5] >> 4) & 0x0F; + y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F)); + x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F)); + this->set_raw_touch_position_(id, x_raw, y_raw); } this->status_clear_warning(); diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h index 3d00e0b117..6767bf0a71 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h @@ -6,29 +6,25 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace lilygo_t5_47 { -struct Store { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(Store *store); -}; - using namespace touchscreen; -class LilygoT547Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class LilygoT547Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; + void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + void update_touches() override; + InternalGPIOPin *interrupt_pin_; - Store store_; }; } // namespace lilygo_t5_47 diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index a4bdc8cafd..bc09c6364d 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -3,44 +3,84 @@ import esphome.codegen as cg from esphome.components import display from esphome import automation -from esphome.const import CONF_ON_TOUCH +from esphome.const import CONF_ON_TOUCH, CONF_ON_RELEASE from esphome.core import coroutine_with_priority -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] IS_PLATFORM_COMPONENT = True touchscreen_ns = cg.esphome_ns.namespace("touchscreen") -Touchscreen = touchscreen_ns.class_("Touchscreen") +Touchscreen = touchscreen_ns.class_("Touchscreen", cg.PollingComponent) TouchRotation = touchscreen_ns.enum("TouchRotation") TouchPoint = touchscreen_ns.struct("TouchPoint") +TouchPoints_t = cg.std_vector.template(TouchPoint) +TouchPoints_t_const_ref = TouchPoints_t.operator("ref").operator("const") TouchListener = touchscreen_ns.class_("TouchListener") CONF_DISPLAY = "display" CONF_TOUCHSCREEN_ID = "touchscreen_id" +CONF_REPORT_INTERVAL = "report_interval" # not used yet: +CONF_ON_UPDATE = "on_update" + +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" +CONF_SWAP_XY = "swap_xy" +CONF_TRANSFORM = "transform" TOUCHSCREEN_SCHEMA = cv.Schema( { - cv.GenerateID(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), + cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), } -) +).extend(cv.polling_component_schema("50ms")) async def register_touchscreen(var, config): + await cg.register_component(var, config) + disp = await cg.get_variable(config[CONF_DISPLAY]) cg.add(var.set_display(disp)) + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + if CONF_ON_TOUCH in config: await automation.build_automation( var.get_touch_trigger(), - [(TouchPoint, "touch")], + [(TouchPoint, "touch"), (TouchPoints_t_const_ref, "touches")], config[CONF_ON_TOUCH], ) + if CONF_ON_UPDATE in config: + await automation.build_automation( + var.get_update_trigger(), + [(TouchPoints_t_const_ref, "touches")], + config[CONF_ON_UPDATE], + ) + + if CONF_ON_RELEASE in config: + await automation.build_automation( + var.get_release_trigger(), + [], + config[CONF_ON_RELEASE], + ) + @coroutine_with_priority(100.0) async def to_code(config): diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 66df78b62a..6c26ae3626 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -14,11 +14,10 @@ void TouchscreenBinarySensor::touch(TouchPoint tp) { if (this->page_ != nullptr) { touched &= this->page_ == this->parent_->get_display()->get_active_page(); } - if (touched) { this->publish_state(true); } else { - release(); + this->release(); } } diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 20828aad8b..140f46b6f6 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -7,27 +7,128 @@ namespace touchscreen { static const char *const TAG = "touchscreen"; -void Touchscreen::set_display(display::Display *display) { - this->display_ = display; - this->display_width_ = display->get_width(); - this->display_height_ = display->get_height(); - this->rotation_ = static_cast(display->get_rotation()); +void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) { store->touched = true; } - if (this->rotation_ == ROTATE_90_DEGREES || this->rotation_ == ROTATE_270_DEGREES) { - std::swap(this->display_width_, this->display_height_); +void Touchscreen::attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type) { + irq_pin->attach_interrupt(TouchscreenInterrupt::gpio_intr, &this->store_, type); + this->store_.init = true; + this->store_.touched = false; +} + +void Touchscreen::update() { + if (!this->store_.init) { + this->store_.touched = true; + } else { + // no need to poll if we have interrupts. + this->stop_poller(); } } -void Touchscreen::send_release_() { - for (auto *listener : this->touch_listeners_) - listener->release(); +void Touchscreen::loop() { + if (this->store_.touched) { + this->first_touch_ = this->touches_.empty(); + this->need_update_ = false; + this->is_touched_ = false; + this->skip_update_ = false; + for (auto &tp : this->touches_) { + if (tp.second.state == STATE_PRESSED || tp.second.state == STATE_UPDATED) { + tp.second.state = tp.second.state | STATE_RELEASING; + } else { + tp.second.state = STATE_RELEASED; + } + tp.second.x_prev = tp.second.x; + tp.second.y_prev = tp.second.y; + } + this->update_touches(); + if (this->skip_update_) { + for (auto &tp : this->touches_) { + tp.second.state = tp.second.state & -STATE_RELEASING; + } + } else { + this->store_.touched = false; + this->defer([this]() { this->send_touches_(); }); + } + } } -void Touchscreen::send_touch_(TouchPoint tp) { - ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y); - this->touch_trigger_.trigger(tp); - for (auto *listener : this->touch_listeners_) - listener->touch(tp); +void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { + TouchPoint tp; + uint16_t x, y; + if (this->touches_.count(id) == 0) { + tp.state = STATE_PRESSED; + tp.id = id; + } else { + tp = this->touches_[id]; + tp.state = STATE_UPDATED; + } + tp.x_raw = x_raw; + tp.y_raw = y_raw; + tp.z_raw = z_raw; + + x = this->normalize_(x_raw, this->x_raw_min_, this->x_raw_max_, this->invert_x_); + y = this->normalize_(y_raw, this->y_raw_min_, this->y_raw_max_, this->invert_y_); + + if (this->swap_x_y_) { + std::swap(x, y); + } + + tp.x = (uint16_t) ((int) x * this->get_width_() / 0x1000); + tp.y = (uint16_t) ((int) y * this->get_height_() / 0x1000); + + if (tp.state == STATE_PRESSED) { + tp.x_org = tp.x; + tp.y_org = tp.y; + } + + this->touches_[id] = tp; + + this->is_touched_ = true; + if ((tp.x != tp.x_prev) || (tp.y != tp.y_prev)) { + this->need_update_ = true; + } +} + +void Touchscreen::send_touches_() { + if (!this->is_touched_) { + this->release_trigger_.trigger(); + for (auto *listener : this->touch_listeners_) + listener->release(); + this->touches_.clear(); + } else { + TouchPoints_t touches; + for (auto tp : this->touches_) { + touches.push_back(tp.second); + } + if (this->first_touch_) { + TouchPoint tp = this->touches_.begin()->second; + this->touch_trigger_.trigger(tp, touches); + for (auto *listener : this->touch_listeners_) { + listener->touch(tp); + } + } + if (this->need_update_) { + this->update_trigger_.trigger(touches); + for (auto *listener : this->touch_listeners_) { + listener->update(touches); + } + } + } +} + +int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted) { + int16_t ret; + + if (val <= min_val) { + ret = 0; + } else if (val >= max_val) { + ret = 0xfff; + } else { + ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); + } + + ret = (inverted) ? 0xfff - ret : ret; + + return ret; } } // namespace touchscreen diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 6e07bcfea0..1fe304d967 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -1,54 +1,119 @@ #pragma once -#include "esphome/components/display/display_buffer.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display.h" + #include "esphome/core/automation.h" #include "esphome/core/hal.h" #include +#include namespace esphome { namespace touchscreen { +static const uint8_t STATE_RELEASED = 0x00; +static const uint8_t STATE_PRESSED = 0x01; +static const uint8_t STATE_UPDATED = 0x02; +static const uint8_t STATE_RELEASING = 0x04; + struct TouchPoint { - uint16_t x; - uint16_t y; uint8_t id; - uint8_t state; + int16_t x_raw{0}, y_raw{0}, z_raw{0}; + uint16_t x_prev{0}, y_prev{0}; + uint16_t x_org{0}, y_org{0}; + uint16_t x{0}, y{0}; + int8_t state{0}; +}; + +using TouchPoints_t = std::vector; + +struct TouchscreenInterrupt { + volatile bool touched{true}; + bool init{false}; + static void gpio_intr(TouchscreenInterrupt *store); }; class TouchListener { public: - virtual void touch(TouchPoint tp) = 0; + virtual void touch(TouchPoint tp) {} + virtual void update(const TouchPoints_t &tpoints) {} virtual void release() {} }; -enum TouchRotation { - ROTATE_0_DEGREES = 0, - ROTATE_90_DEGREES = 90, - ROTATE_180_DEGREES = 180, - ROTATE_270_DEGREES = 270, -}; - -class Touchscreen { +class Touchscreen : public PollingComponent { public: - void set_display(display::Display *display); + void set_display(display::Display *display) { this->display_ = display; } display::Display *get_display() const { return this->display_; } - Trigger *get_touch_trigger() { return &this->touch_trigger_; } + void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; } + void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; } + void set_swap_xy(bool swap) { this->swap_x_y_ = swap; } + + void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { + this->x_raw_min_ = std::min(x_min, x_max); + this->x_raw_max_ = std::max(x_min, x_max); + this->y_raw_min_ = std::min(y_min, y_max); + this->y_raw_max_ = std::max(y_min, y_max); + if (x_min > x_max) + this->invert_x_ = true; + if (y_min > y_max) + this->invert_y_ = true; + } + + Trigger *get_touch_trigger() { return &this->touch_trigger_; } + Trigger *get_update_trigger() { return &this->update_trigger_; } + Trigger<> *get_release_trigger() { return &this->release_trigger_; } void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } + virtual void update_touches() = 0; + + optional get_touch() { return this->touches_.begin()->second; } + + TouchPoints_t get_touches() { + TouchPoints_t touches; + for (auto i : this->touches_) { + touches.push_back(i.second); + } + return touches; + } + + void update() override; + void loop() override; + protected: /// Call this function to send touch points to the `on_touch` listener and the binary_sensors. - void send_touch_(TouchPoint tp); - void send_release_(); - uint16_t display_width_; - uint16_t display_height_; - display::Display *display_; - TouchRotation rotation_; - Trigger touch_trigger_; + void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type); + + void set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); + + void send_touches_(); + + int16_t normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted = false); + + uint16_t get_width_() { return this->display_->get_width(); } + + uint16_t get_height_() { return this->display_->get_height(); } + + display::Display *display_{nullptr}; + + int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0}; + bool invert_x_{false}, invert_y_{false}, swap_x_y_{false}; + + Trigger touch_trigger_; + Trigger update_trigger_; + Trigger<> release_trigger_; std::vector touch_listeners_; + + std::map touches_; + TouchscreenInterrupt store_; + + bool first_touch_{true}; + bool need_update_{false}; + bool is_touched_{false}; + bool skip_update_{false}; }; } // namespace touchscreen diff --git a/esphome/components/tt21100/touchscreen/__init__.py b/esphome/components/tt21100/touchscreen/__init__.py index d96d389e69..4458ad0974 100644 --- a/esphome/components/tt21100/touchscreen/__init__.py +++ b/esphome/components/tt21100/touchscreen/__init__.py @@ -12,7 +12,6 @@ DEPENDENCIES = ["i2c"] TT21100Touchscreen = tt21100_ns.class_( "TT21100Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) TT21100ButtonListener = tt21100_ns.class_("TT21100ButtonListener") @@ -24,17 +23,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } - ) - .extend(i2c.i2c_device_schema(0x24)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x24)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index 28a8c2d754..ff688fd0b0 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -44,8 +44,6 @@ struct TT21100TouchReport { TT21100TouchRecord touch_record[MAX_TOUCH_POINTS]; } __attribute__((packed)); -void TT21100TouchscreenStore::gpio_intr(TT21100TouchscreenStore *store) { store->touch = true; } - float TT21100Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } void TT21100Touchscreen::setup() { @@ -54,9 +52,8 @@ void TT21100Touchscreen::setup() { // Register interrupt pin this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(TT21100TouchscreenStore::gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); + + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); // Perform reset if necessary if (this->reset_pin_ != nullptr) { @@ -65,19 +62,11 @@ void TT21100Touchscreen::setup() { } // Update display dimensions if they were updated during display setup - this->display_width_ = this->display_->get_width(); - this->display_height_ = this->display_->get_height(); - this->rotation_ = static_cast(this->display_->get_rotation()); - - // Trigger initial read to activate the interrupt - this->store_.touch = true; + this->x_raw_max_ = this->get_width_(); + this->y_raw_max_ = this->get_height_(); } -void TT21100Touchscreen::loop() { - if (!this->store_.touch) - return; - this->store_.touch = false; - +void TT21100Touchscreen::update_touches() { // Read report length uint16_t data_len; this->read((uint8_t *) &data_len, sizeof(data_len)); @@ -111,12 +100,6 @@ void TT21100Touchscreen::loop() { uint8_t touch_count = (data_len - (sizeof(*report) - sizeof(report->touch_record))) / sizeof(TT21100TouchRecord); - if (touch_count == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - for (int i = 0; i < touch_count; i++) { auto *touch = &report->touch_record[i]; @@ -126,30 +109,7 @@ void TT21100Touchscreen::loop() { i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, touch->pressure, touch->major_axis_length, touch->orientation); - TouchPoint tp; - switch (this->rotation_) { - case ROTATE_0_DEGREES: - // Origin is top right, so mirror X by default - tp.x = this->display_width_ - touch->x; - tp.y = touch->y; - break; - case ROTATE_90_DEGREES: - tp.x = touch->y; - tp.y = touch->x; - break; - case ROTATE_180_DEGREES: - tp.x = touch->x; - tp.y = this->display_height_ - touch->y; - break; - case ROTATE_270_DEGREES: - tp.x = this->display_height_ - touch->y; - tp.y = this->display_width_ - touch->x; - break; - } - tp.id = touch->tip; - tp.state = touch->pressure; - - this->defer([this, tp]() { this->send_touch_(tp); }); + this->set_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); } } } diff --git a/esphome/components/tt21100/touchscreen/tt21100.h b/esphome/components/tt21100/touchscreen/tt21100.h index 306360975f..5d1b2efe3c 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.h +++ b/esphome/components/tt21100/touchscreen/tt21100.h @@ -5,27 +5,21 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace tt21100 { using namespace touchscreen; -struct TT21100TouchscreenStore { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(TT21100TouchscreenStore *store); -}; - class TT21100ButtonListener { public: virtual void update_button(uint8_t index, uint16_t state) = 0; }; -class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class TT21100Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; float get_setup_priority() const override; @@ -37,7 +31,7 @@ class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2C protected: void reset_(); - TT21100TouchscreenStore store_; + void update_touches() override; InternalGPIOPin *interrupt_pin_; GPIOPin *reset_pin_{nullptr}; diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py deleted file mode 100644 index 5a6cfe4919..0000000000 --- a/esphome/components/xpt2046/binary_sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("Rename this platform component to Touchscreen.") diff --git a/esphome/components/xpt2046/touchscreen.py b/esphome/components/xpt2046/touchscreen.py deleted file mode 100644 index 150d1cf396..0000000000 --- a/esphome/components/xpt2046/touchscreen.py +++ /dev/null @@ -1,116 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv - -from esphome import pins -from esphome.components import spi, touchscreen -from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_IRQ_PIN, CONF_THRESHOLD - -CODEOWNERS = ["@numo68", "@nielsnl68"] -DEPENDENCIES = ["spi"] - -XPT2046_ns = cg.esphome_ns.namespace("xpt2046") -XPT2046Component = XPT2046_ns.class_( - "XPT2046Component", - touchscreen.Touchscreen, - cg.PollingComponent, - spi.SPIDevice, -) - -CONF_REPORT_INTERVAL = "report_interval" -CONF_CALIBRATION_X_MIN = "calibration_x_min" -CONF_CALIBRATION_X_MAX = "calibration_x_max" -CONF_CALIBRATION_Y_MIN = "calibration_y_min" -CONF_CALIBRATION_Y_MAX = "calibration_y_max" -CONF_SWAP_X_Y = "swap_x_y" - -# obsolete Keys -CONF_DIMENSION_X = "dimension_x" -CONF_DIMENSION_Y = "dimension_y" - - -def validate_xpt2046(config): - if ( - abs( - cv.int_(config[CONF_CALIBRATION_X_MAX]) - - cv.int_(config[CONF_CALIBRATION_X_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration X values difference < 1000") - - if ( - abs( - cv.int_(config[CONF_CALIBRATION_Y_MAX]) - - cv.int_(config[CONF_CALIBRATION_Y_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration Y values difference < 1000") - - return config - - -def report_interval(value): - if value == "never": - return 4294967295 # uint32_t max - return cv.positive_time_period_milliseconds(value) - - -CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(XPT2046Component), - cv.Optional(CONF_INTERRUPT_PIN): cv.All( - pins.internal_gpio_input_pin_schema - ), - cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), - cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, - cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, - # obsolete Keys - cv.Optional(CONF_IRQ_PIN): cv.invalid("Rename IRQ_PIN to INTERUPT_PIN"), - cv.Optional(CONF_DIMENSION_X): cv.invalid( - "This key is now obsolete, please remove it" - ), - cv.Optional(CONF_DIMENSION_Y): cv.invalid( - "This key is now obsolete, please remove it" - ), - }, - ) - .extend(cv.polling_component_schema("50ms")) - .extend(spi.spi_device_schema()), -).add_extra(validate_xpt2046) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await spi.register_spi_device(var, config) - await touchscreen.register_touchscreen(var, config) - - cg.add(var.set_threshold(config[CONF_THRESHOLD])) - cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) - cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) - cg.add( - var.set_calibration( - config[CONF_CALIBRATION_X_MIN], - config[CONF_CALIBRATION_X_MAX], - config[CONF_CALIBRATION_Y_MIN], - config[CONF_CALIBRATION_Y_MAX], - ) - ) - - if CONF_INTERRUPT_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/__init__.py b/esphome/components/xpt2046/touchscreen/__init__.py new file mode 100644 index 0000000000..9f08f38c3f --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/__init__.py @@ -0,0 +1,93 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import spi, touchscreen +from esphome.const import CONF_ID, CONF_THRESHOLD, CONF_INTERRUPT_PIN + +CODEOWNERS = ["@numo68", "@nielsnl68"] +DEPENDENCIES = ["spi"] + +XPT2046_ns = cg.esphome_ns.namespace("xpt2046") +XPT2046Component = XPT2046_ns.class_( + "XPT2046Component", + touchscreen.Touchscreen, + spi.SPIDevice, +) + + +CONF_CALIBRATION_X_MIN = "calibration_x_min" +CONF_CALIBRATION_X_MAX = "calibration_x_max" +CONF_CALIBRATION_Y_MIN = "calibration_y_min" +CONF_CALIBRATION_Y_MAX = "calibration_y_max" + + +def validate_xpt2046(config): + if ( + abs( + cv.int_(config[CONF_CALIBRATION_X_MAX]) + - cv.int_(config[CONF_CALIBRATION_X_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration X values difference < 1000") + + if ( + abs( + cv.int_(config[CONF_CALIBRATION_Y_MAX]) + - cv.int_(config[CONF_CALIBRATION_Y_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration Y values difference < 1000") + + return config + + +CONFIG_SCHEMA = cv.All( + touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XPT2046Component), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), + }, + ) + ).extend(spi.spi_device_schema()), + validate_xpt2046, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await spi.register_spi_device(var, config) + + cg.add(var.set_threshold(config[CONF_THRESHOLD])) + + cg.add( + var.set_calibration( + config[CONF_CALIBRATION_X_MIN], + config[CONF_CALIBRATION_X_MAX], + config[CONF_CALIBRATION_Y_MIN], + config[CONF_CALIBRATION_Y_MAX], + ) + ) + + if CONF_INTERRUPT_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp new file mode 100644 index 0000000000..1a9c202af0 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -0,0 +1,113 @@ +#include "xpt2046.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace xpt2046 { + +static const char *const TAG = "xpt2046"; + +void XPT2046Component::setup() { + if (this->irq_pin_ != nullptr) { + // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state + // while the channels are read and wiring it as an interrupt is not straightforward and would + // need careful masking. A GPIO poll is cheap so we'll just use that. + + this->irq_pin_->setup(); // INPUT + this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->irq_pin_->setup(); + this->attach_interrupt_(this->irq_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + this->spi_setup(); + this->read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin +} + +void XPT2046Component::update_touches() { + int16_t data[6], x_raw, y_raw, z_raw; + bool touch = false; + + enable(); + + int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */); + int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */); + ESP_LOGVV(TAG, "touch_pressure %d, %d", touch_pressure_1, touch_pressure_2); + z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; + + touch = (z_raw >= this->threshold_); + if (touch) { + read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy + data[0] = this->read_adc_(0x91 /* Y */); + data[1] = this->read_adc_(0xD1 /* X */); // make 3 x-y measurements + data[2] = this->read_adc_(0x91 /* Y */); + data[3] = this->read_adc_(0xD1 /* X */); + data[4] = this->read_adc_(0x91 /* Y */); + } + + data[5] = this->read_adc_(0xD0 /* X */); // Last X touch power down + + disable(); + + if (touch) { + x_raw = best_two_avg(data[1], data[3], data[5]); + y_raw = best_two_avg(data[0], data[2], data[4]); + + ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); + + this->set_raw_touch_position_(0, x_raw, y_raw, z_raw); + } +} + +void XPT2046Component::dump_config() { + ESP_LOGCONFIG(TAG, "XPT2046:"); + + LOG_PIN(" IRQ Pin: ", this->irq_pin_); + ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); + ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); + ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); + ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); + + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); + ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); + ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); + + ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); + + LOG_UPDATE_INTERVAL(this); +} + +float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } + +int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) { + int16_t delta_a, delta_b, delta_c; + int16_t reta = 0; + + delta_a = (value1 > value2) ? value1 - value2 : value2 - value1; + delta_b = (value1 > value3) ? value1 - value3 : value3 - value1; + delta_c = (value3 > value2) ? value3 - value2 : value2 - value3; + + if (delta_a <= delta_b && delta_a <= delta_c) { + reta = (value1 + value2) >> 1; + } else if (delta_b <= delta_a && delta_b <= delta_c) { + reta = (value1 + value3) >> 1; + } else { + reta = (value2 + value3) >> 1; + } + + return reta; +} + +int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT + uint8_t data[2]; + + this->write_byte(ctrl); + delay(1); + data[0] = this->read_byte(); + data[1] = this->read_byte(); + + return ((data[0] << 8) | data[1]) >> 3; +} + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.h b/esphome/components/xpt2046/touchscreen/xpt2046.h new file mode 100644 index 0000000000..ff866bc86b --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace xpt2046 { + +using namespace touchscreen; + +class XPT2046Component : public Touchscreen, + public spi::SPIDevice { + public: + /// Set the threshold for the touch detection. + void set_threshold(int16_t threshold) { this->threshold_ = threshold; } + /// Set the pin used to detect the touch. + void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + static int16_t best_two_avg(int16_t value1, int16_t value2, int16_t value3); + + int16_t read_adc_(uint8_t ctrl); + + void update_touches() override; + + int16_t threshold_; + + InternalGPIOPin *irq_pin_{nullptr}; +}; + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp deleted file mode 100644 index 078a1b01e9..0000000000 --- a/esphome/components/xpt2046/xpt2046.cpp +++ /dev/null @@ -1,207 +0,0 @@ -#include "xpt2046.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -#include -#include - -namespace esphome { -namespace xpt2046 { - -static const char *const TAG = "xpt2046"; - -void XPT2046TouchscreenStore::gpio_intr(XPT2046TouchscreenStore *store) { store->touch = true; } - -void XPT2046Component::setup() { - if (this->irq_pin_ != nullptr) { - // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state - // while the channels are read and wiring it as an interrupt is not straightforward and would - // need careful masking. A GPIO poll is cheap so we'll just use that. - - this->irq_pin_->setup(); // INPUT - this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - this->irq_pin_->setup(); - this->irq_pin_->attach_interrupt(XPT2046TouchscreenStore::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); - } - spi_setup(); - read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin -} - -void XPT2046Component::loop() { - if ((this->irq_pin_ != nullptr) && (this->store_.touch || this->touched)) { - this->store_.touch = false; - check_touch_(); - } -} - -void XPT2046Component::update() { - if (this->irq_pin_ == nullptr) - check_touch_(); -} - -void XPT2046Component::check_touch_() { - int16_t data[6]; - bool touch = false; - uint32_t now = millis(); - - enable(); - - int16_t touch_pressure_1 = read_adc_(0xB1 /* touch_pressure_1 */); - int16_t touch_pressure_2 = read_adc_(0xC1 /* touch_pressure_2 */); - - this->z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; - - touch = (this->z_raw >= this->threshold_); - if (touch) { - read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy - data[0] = read_adc_(0x91 /* Y */); - data[1] = read_adc_(0xD1 /* X */); // make 3 x-y measurements - data[2] = read_adc_(0x91 /* Y */); - data[3] = read_adc_(0xD1 /* X */); - data[4] = read_adc_(0x91 /* Y */); - } - - data[5] = read_adc_(0xD0 /* X */); // Last X touch power down - - disable(); - - if (touch) { - this->x_raw = best_two_avg(data[1], data[3], data[5]); - this->y_raw = best_two_avg(data[0], data[2], data[4]); - - ESP_LOGVV(TAG, "Update [x, y] = [%d, %d], z = %d", this->x_raw, this->y_raw, this->z_raw); - - TouchPoint touchpoint; - - touchpoint.x = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); - touchpoint.y = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); - - if (this->swap_x_y_) { - std::swap(touchpoint.x, touchpoint.y); - } - - if (this->invert_x_) { - touchpoint.x = 0xfff - touchpoint.x; - } - - if (this->invert_y_) { - touchpoint.y = 0xfff - touchpoint.y; - } - - switch (static_cast(this->display_->get_rotation())) { - case ROTATE_0_DEGREES: - break; - case ROTATE_90_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_180_DEGREES: - touchpoint.x = 0xfff - touchpoint.x; - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_270_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.x = 0xfff - touchpoint.x; - break; - } - - touchpoint.x = (int16_t) ((int) touchpoint.x * this->display_->get_width() / 0xfff); - touchpoint.y = (int16_t) ((int) touchpoint.y * this->display_->get_height() / 0xfff); - - if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { - ESP_LOGV(TAG, "Touching at [%03X, %03X] => [%3d, %3d]", this->x_raw, this->y_raw, touchpoint.x, touchpoint.y); - - this->defer([this, touchpoint]() { this->send_touch_(touchpoint); }); - - this->x = touchpoint.x; - this->y = touchpoint.y; - this->touched = true; - this->last_pos_ms_ = now; - } - } - - if (!touch && this->touched) { - this->x_raw = this->y_raw = this->z_raw = 0; - ESP_LOGV(TAG, "Released [%d, %d]", this->x, this->y); - this->touched = false; - for (auto *listener : this->touch_listeners_) - listener->release(); - } -} - -void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { // NOLINT - this->x_raw_min_ = std::min(x_min, x_max); - this->x_raw_max_ = std::max(x_min, x_max); - this->y_raw_min_ = std::min(y_min, y_max); - this->y_raw_max_ = std::max(y_min, y_max); - this->invert_x_ = (x_min > x_max); - this->invert_y_ = (y_min > y_max); -} - -void XPT2046Component::dump_config() { - ESP_LOGCONFIG(TAG, "XPT2046:"); - - LOG_PIN(" IRQ Pin: ", this->irq_pin_); - ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); - ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); - ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); - ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); - - ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); - ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); - ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); - - ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); - ESP_LOGCONFIG(TAG, " Report interval: %" PRIu32, this->report_millis_); - - LOG_UPDATE_INTERVAL(this); -} - -float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } - -int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { // NOLINT - int16_t da, db, dc; // NOLINT - int16_t reta = 0; - - da = (x > y) ? x - y : y - x; - db = (x > z) ? x - z : z - x; - dc = (z > y) ? z - y : y - z; - - if (da <= db && da <= dc) { - reta = (x + y) >> 1; - } else if (db <= da && db <= dc) { - reta = (x + z) >> 1; - } else { - reta = (y + z) >> 1; - } - - return reta; -} - -int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_val) { - int16_t ret; - - if (val <= min_val) { - ret = 0; - } else if (val >= max_val) { - ret = 0xfff; - } else { - ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); - } - - return ret; -} - -int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT - uint8_t data[2]; - - write_byte(ctrl); - delay(1); - data[0] = read_byte(); - data[1] = read_byte(); - - return ((data[0] << 8) | data[1]) >> 3; -} - -} // namespace xpt2046 -} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h deleted file mode 100644 index e7d9caba21..0000000000 --- a/esphome/components/xpt2046/xpt2046.h +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/touchscreen/touchscreen.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace xpt2046 { - -using namespace touchscreen; - -struct XPT2046TouchscreenStore { - volatile bool touch; - static void gpio_intr(XPT2046TouchscreenStore *store); -}; - -class XPT2046Component : public Touchscreen, - public PollingComponent, - public spi::SPIDevice { - public: - /// Set the logical touch screen dimensions. - void set_dimensions(int16_t x, int16_t y) { - this->display_width_ = x; - this->display_height_ = y; - } - /// Set the coordinates for the touch screen edges. - void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max); - /// If true the x and y axes will be swapped - void set_swap_x_y(bool val) { this->swap_x_y_ = val; } - - /// Set the interval to report the touch point perodically. - void set_report_interval(uint32_t interval) { this->report_millis_ = interval; } - uint32_t get_report_interval() { return this->report_millis_; } - - /// Set the threshold for the touch detection. - void set_threshold(int16_t threshold) { this->threshold_ = threshold; } - /// Set the pin used to detect the touch. - void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } - - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - - /** Detect the touch if the irq pin is specified. - * - * If the touch is detected and the component does not already know about it - * the update() is called immediately. If the irq pin is not specified - * the loop() is a no-op. - */ - void loop() override; - - /** Read and process the values from the hardware. - * - * Read the raw x, y and touch pressure values from the chip, detect the touch, - * and if touched, transform to the user x and y coordinates. If the state has - * changed or if the value should be reported again due to the - * report interval, run the action and inform the virtual buttons. - */ - void update() override; - - /**@{*/ - /** Coordinates of the touch position. - * - * The values are set immediately before the on_state action with touched == true - * is triggered. The action with touched == false sends the coordinates of the last - * reported touch. - */ - int16_t x{0}, y{0}; - /**@}*/ - - /// True if the component currently detects the touch - bool touched{false}; - - /**@{*/ - /** Raw sensor values of the coordinates and the pressure. - * - * The values are set each time the update() method is called. - */ - int16_t x_raw{0}, y_raw{0}, z_raw{0}; - /**@}*/ - - protected: - static int16_t best_two_avg(int16_t x, int16_t y, int16_t z); - static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val); - - int16_t read_adc_(uint8_t ctrl); - void check_touch_(); - - int16_t threshold_; - int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_; - - bool invert_x_, invert_y_; - bool swap_x_y_; - - uint32_t report_millis_; - uint32_t last_pos_ms_{0}; - - InternalGPIOPin *irq_pin_{nullptr}; - XPT2046TouchscreenStore store_; -}; - -} // namespace xpt2046 -} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 9f15b84b12..fc719abecb 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -464,6 +464,7 @@ binary_sensor: sx1509: sx1509_hub number: 3 + - platform: touchscreen touchscreen_id: lilygo_touchscreen id: touch_key1 @@ -483,6 +484,7 @@ binary_sensor: pin: max6956: max6956_1 number: 4 + mode: input: true pullup: true @@ -506,6 +508,7 @@ binary_sensor: input: true inverted: false + climate: - platform: tuya id: tuya_climate @@ -595,6 +598,8 @@ display: it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + auto touch = id(ft63_touchscreen)->get_touch(); + if (touch) { ESP_LOGD("touch", "%d/%d", touch.value().x, touch.value().y); } rotation: 0° update_interval: 16ms @@ -677,53 +682,53 @@ display: update_interval: 60s display_data_1_pin: - number: 5 + number: GPIO5 allow_other_uses: true display_data_2_pin: - number: 18 + number: GPIO18 allow_other_uses: true display_data_3_pin: - number: 19 + number: GPIO19 allow_other_uses: true display_data_5_pin: - number: 25 + number: GPIO25 allow_other_uses: true display_data_4_pin: - number: 23 + number: GPIO23 allow_other_uses: true display_data_6_pin: - number: 26 + number: GPIO26 allow_other_uses: true display_data_7_pin: - number: 27 + number: GPIO27 allow_other_uses: true ckv_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true sph_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true gmod_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true gpio0_enable_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true oe_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true spv_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true powerup_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true wakeup_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true vcom_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true number: - platform: tuya @@ -816,21 +821,16 @@ esp32_camera: allow_other_uses: true - number: GPIO35 allow_other_uses: true - - - number: GPIO34 - - - number: GPIO5 + - number: GPIO34 + - number: GPIO5 allow_other_uses: true - - - number: GPIO39 - - - number: GPIO18 + - number: GPIO39 allow_other_uses: true - - - number: GPIO36 + - number: GPIO18 allow_other_uses: true - - - number: GPIO19 + - number: GPIO36 + allow_other_uses: true + - number: GPIO19 allow_other_uses: true vsync_pin: allow_other_uses: true @@ -910,18 +910,16 @@ touchscreen: spi_id: spi_id_2 cs_pin: allow_other_uses: true - number: 17 + number: GPIO17 interrupt_pin: - number: 16 + number: GPIO16 display: inkplate_display update_interval: 50ms - report_interval: 1s threshold: 400 calibration_x_min: 3860 calibration_x_max: 280 calibration_y_min: 340 calibration_y_max: 3860 - swap_x_y: false on_touch: - logger.log: format: Touch at (%d, %d) @@ -938,10 +936,25 @@ touchscreen: format: Touch at (%d, %d) args: [touch.x, touch.y] - platform: gt911 - interrupt_pin: GPIO3 + interrupt_pin: + number: GPIO3 display: inkplate_display + - platform: ft63x6 + id: ft63_touchscreen + interrupt_pin: + allow_other_uses: true + number: GPIO39 + reset_pin: + allow_other_uses: true + number: GPIO5 + display: inkplate_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] + i2s_audio: i2s_lrclk_pin: allow_other_uses: true diff --git a/tests/test8.yaml b/tests/test8.yaml index 5e4a41080a..558e86e1f9 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -54,6 +54,7 @@ spi_device: display: - platform: ili9xxx + id: displ8 model: ili9342 cs_pin: GPIO5 dc_pin: GPIO4 @@ -67,6 +68,7 @@ i2c: touchscreen: - platform: tt21100 + display: displ8 interrupt_pin: number: GPIO3 ignore_strapping_warning: true From 259a6d52e1de45ce15aa5229c672e924419da84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=A6mer?= <55437145+TH-Braemer@users.noreply.github.com> Date: Wed, 13 Dec 2023 00:26:08 +0100 Subject: [PATCH 126/436] A02yyuw (#5729) --- CODEOWNERS | 1 + esphome/components/a02yyuw/__init__.py | 1 + esphome/components/a02yyuw/a02yyuw.cpp | 43 ++++++++++++++++++++++++++ esphome/components/a02yyuw/a02yyuw.h | 27 ++++++++++++++++ esphome/components/a02yyuw/sensor.py | 41 ++++++++++++++++++++++++ tests/test4.yaml | 13 ++++++++ 6 files changed, 126 insertions(+) create mode 100644 esphome/components/a02yyuw/__init__.py create mode 100644 esphome/components/a02yyuw/a02yyuw.cpp create mode 100644 esphome/components/a02yyuw/a02yyuw.h create mode 100644 esphome/components/a02yyuw/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c37eb3581b..d509c98433 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -12,6 +12,7 @@ esphome/core/* @esphome/core # Integrations esphome/components/a01nyub/* @MrSuicideParrot +esphome/components/a02yyuw/* @TH-Braemer esphome/components/absolute_humidity/* @DAVe3283 esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core diff --git a/esphome/components/a02yyuw/__init__.py b/esphome/components/a02yyuw/__init__.py new file mode 100644 index 0000000000..6724dbb970 --- /dev/null +++ b/esphome/components/a02yyuw/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@TH-Braemer"] diff --git a/esphome/components/a02yyuw/a02yyuw.cpp b/esphome/components/a02yyuw/a02yyuw.cpp new file mode 100644 index 0000000000..ee378c3283 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.cpp @@ -0,0 +1,43 @@ +// Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311 + +#include "a02yyuw.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace a02yyuw { + +static const char *const TAG = "a02yyuw.sensor"; + +void A02yyuwComponent::loop() { + uint8_t data; + while (this->available() > 0) { + this->read_byte(&data); + if (this->buffer_.empty() && (data != 0xff)) + continue; + buffer_.push_back(data); + if (this->buffer_.size() == 4) + this->check_buffer_(); + } +} + +void A02yyuwComponent::check_buffer_() { + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; + if (this->buffer_[3] == checksum) { + float distance = (this->buffer_[1] << 8) + this->buffer_[2]; + if (distance > 30) { + ESP_LOGV(TAG, "Distance from sensor: %f mm", distance); + this->publish_state(distance); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + } + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); + } + this->buffer_.clear(); +} + +void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); } + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/a02yyuw.h b/esphome/components/a02yyuw/a02yyuw.h new file mode 100644 index 0000000000..6ff370fdc3 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace a02yyuw { + +class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + + std::vector buffer_; +}; + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/sensor.py b/esphome/components/a02yyuw/sensor.py new file mode 100644 index 0000000000..5232b04546 --- /dev/null +++ b/esphome/components/a02yyuw/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + ICON_ARROW_EXPAND_VERTICAL, + DEVICE_CLASS_DISTANCE, +) + +CODEOWNERS = ["@TH-Braemer"] +DEPENDENCIES = ["uart"] +UNIT_MILLIMETERS = "mm" + +a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw") +A02yyuwComponent = a02yyuw_ns.class_( + "A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = sensor.sensor_schema( + A02yyuwComponent, + unit_of_measurement=UNIT_MILLIMETERS, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_DISTANCE, +).extend(uart.UART_DEVICE_SCHEMA) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "a02yyuw", + baud_rate=9600, + require_tx=False, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/test4.yaml b/tests/test4.yaml index fc719abecb..089caf073b 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -84,6 +84,14 @@ uart: allow_other_uses: true number: GPIO26 baud_rate: 9600 + - id: uart_a02yyuw + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 + baud_rate: 9600 - id: uart_he60r tx_pin: number: GPIO18 @@ -357,6 +365,11 @@ sensor: name: "a01nyub Distance" uart_id: uart9600 state_topic: "esphome/sensor/a01nyub_sensor/state" + - platform: a02yyuw + id: a02yyuw_sensor + name: "a02yyuw Distance" + uart_id: uart_a02yyuw + state_topic: "esphome/sensor/a02yyuw_sensor/state" # # platform sensor.apds9960 requires component apds9960 From 03baaa94a8d506212ff3fdf9ada258887e717cc7 Mon Sep 17 00:00:00 2001 From: Theo Hussey Date: Tue, 12 Dec 2023 23:28:59 +0000 Subject: [PATCH 127/436] Fix AHT10 / AHT20 communication (#5198) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/aht10/aht10.cpp | 55 ++++++++++++++++++------------ esphome/components/aht10/aht10.h | 6 ++++ esphome/components/aht10/sensor.py | 11 ++++++ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 1ca06b458a..4d69a67487 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -21,36 +21,49 @@ namespace esphome { namespace aht10 { static const char *const TAG = "aht10"; -static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; +static const size_t SIZE_CALIBRATE_CMD = 3; +static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00}; +static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms +static const uint8_t AHT10_CAL_ATTEMPTS = 10; +static const uint8_t AHT10_STATUS_BUSY = 0x80; void AHT10Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up AHT10..."); + const uint8_t *calibrate_cmd; + switch (this->variant_) { + case AHT10Variant::AHT20: + calibrate_cmd = AHT20_CALIBRATE_CMD; + ESP_LOGCONFIG(TAG, "Setting up AHT20"); + break; + case AHT10Variant::AHT10: + default: + calibrate_cmd = AHT10_CALIBRATE_CMD; + ESP_LOGCONFIG(TAG, "Setting up AHT10"); + } - if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { + if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->mark_failed(); return; } - uint8_t data = 0; - if (this->write(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - delay(AHT10_DEFAULT_DELAY); - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; + uint8_t data = AHT10_STATUS_BUSY; + int cal_attempts = 0; + while (data & AHT10_STATUS_BUSY) { + delay(AHT10_DEFAULT_DELAY); + if (this->read(&data, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + ++cal_attempts; + if (cal_attempts > AHT10_CAL_ATTEMPTS) { + ESP_LOGE(TAG, "AHT10 calibration timed out!"); + this->mark_failed(); + return; + } } if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED ESP_LOGE(TAG, "AHT10 calibration failed!"); @@ -62,7 +75,7 @@ void AHT10Component::setup() { } void AHT10Component::update() { - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->status_set_warning(); return; @@ -89,7 +102,7 @@ void AHT10Component::update() { break; } else { ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->status_set_warning(); return; diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index 4d0eaa5919..3840609d56 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" @@ -7,12 +9,15 @@ namespace esphome { namespace aht10 { +enum AHT10Variant { AHT10, AHT20 }; + class AHT10Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void update() override; void dump_config() override; float get_setup_priority() const override; + void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -20,6 +25,7 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { protected: sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + AHT10Variant variant_{}; }; } // namespace aht10 diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py index a52773b6d7..31b07c0e73 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, + CONF_VARIANT, ) DEPENDENCIES = ["i2c"] @@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"] aht10_ns = cg.esphome_ns.namespace("aht10") AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) +AHT10Variant = aht10_ns.enum("AHT10Variant") +AHT10_VARIANTS = { + "AHT10": AHT10Variant.AHT10, + "AHT20": AHT10Variant.AHT20, +} + CONFIG_SCHEMA = ( cv.Schema( { @@ -33,6 +40,9 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum( + AHT10_VARIANTS, upper=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -44,6 +54,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + cg.add(var.set_variant(config[CONF_VARIANT])) if temperature := config.get(CONF_TEMPERATURE): sens = await sensor.new_sensor(temperature) From 29002c8f45f69a9caf0887d3de07b9db67149f0d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:38:37 +1100 Subject: [PATCH 128/436] Fix crash when handling pin_check error (#5915) --- esphome/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/config.py b/esphome/config.py index 3461223490..745883c2ef 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -111,7 +111,12 @@ class Config(OrderedDict, fv.FinalValidateConfig): last_root = max( i for i, v in enumerate(error.path) if v is cv.ROOT_CONFIG_PATH ) - error.path = error.path[last_root + 1 :] + # can't change the path so re-create the error + error = vol.Invalid( + message=error.error_message, + path=error.path[last_root + 1 :], + error_type=error.error_type, + ) self.errors.append(error) def add_validation_step(self, step: "ConfigValidationStep"): From 8789925fe89d6f9e98083111335530ae64a3d9ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 13:48:14 -1000 Subject: [PATCH 129/436] Bump aioesphomeapi to 20.1.0 (#5914) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae8d50f2ea..0d08da2b45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==20.0.0 +aioesphomeapi==20.1.0 zeroconf==0.128.4 python-magic==0.4.27 From 69026f759958b6203fd021e255d42ba7664e2730 Mon Sep 17 00:00:00 2001 From: Mathias Pihl Date: Wed, 13 Dec 2023 00:50:55 +0100 Subject: [PATCH 130/436] Remove setpoint-change from error when calculating derivative in pid controller (#4737) Co-authored-by: Mathias Pihl --- esphome/components/pid/pid_controller.cpp | 11 ++++++++--- esphome/components/pid/pid_controller.h | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 30f6038325..1a16f14542 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -16,7 +16,7 @@ float PIDController::update(float setpoint, float process_value) { calculate_proportional_term_(); calculate_integral_term_(); - calculate_derivative_term_(); + calculate_derivative_term_(setpoint); // u(t) := p(t) + i(t) + d(t) float output = proportional_term_ + integral_term_ + derivative_term_; @@ -69,13 +69,18 @@ void PIDController::calculate_integral_term_() { integral_term_ = accumulated_integral_; } -void PIDController::calculate_derivative_term_() { +void PIDController::calculate_derivative_term_(float setpoint) { // derivative_term_ // d(t) := K_d * de(t)/dt float derivative = 0.0f; - if (dt_ != 0.0f) + if (dt_ != 0.0f) { + // remove changes to setpoint from error + if (!std::isnan(previous_setpoint_) && previous_setpoint_ != setpoint) + previous_error_ -= previous_setpoint_ - setpoint; derivative = (error_ - previous_error_) / dt_; + } previous_error_ = error_; + previous_setpoint_ = setpoint; // smooth the derivative samples derivative = weighted_average_(derivative_list_, derivative, derivative_samples_); diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 05ce5f9224..e2a7030b57 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -49,12 +49,13 @@ struct PIDController { void calculate_proportional_term_(); void calculate_integral_term_(); - void calculate_derivative_term_(); + void calculate_derivative_term_(float setpoint); float weighted_average_(std::deque &list, float new_value, int samples); float calculate_relative_time_(); /// Error from previous update used for derivative term float previous_error_ = 0; + float previous_setpoint_ = NAN; /// Accumulated integral value float accumulated_integral_ = 0; uint32_t last_time_ = 0; From d0bcba3b3fef78a394c6937ba20d2ca88bc30150 Mon Sep 17 00:00:00 2001 From: Nicolas Gilles Date: Wed, 13 Dec 2023 00:55:20 +0100 Subject: [PATCH 131/436] ir_transmitter: add support for repeated commands in NEC protocol (#4995) Co-authored-by: Nicolas Gilles --- esphome/components/remote_base/__init__.py | 5 +++ .../components/remote_base/nec_protocol.cpp | 42 +++++++++++++++---- esphome/components/remote_base/nec_protocol.h | 3 ++ esphome/const.py | 1 + 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a2411b1b12..3accd5038c 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import binary_sensor from esphome.const import ( + CONF_COMMAND_REPEATS, CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, @@ -638,6 +639,7 @@ NEC_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint16_t, cv.Required(CONF_COMMAND): cv.hex_uint16_t, + cv.Optional(CONF_COMMAND_REPEATS, default=1): cv.uint16_t, } ) @@ -650,6 +652,7 @@ def nec_binary_sensor(var, config): NECData, ("address", config[CONF_ADDRESS]), ("command", config[CONF_COMMAND]), + ("command_repeats", config[CONF_COMMAND_REPEATS]), ) ) ) @@ -671,6 +674,8 @@ async def nec_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint16) cg.add(var.set_command(template_)) + template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint16) + cg.add(var.set_command_repeats(template_)) # Pioneer diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index d5c68784ee..6ea9a8583c 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -13,10 +13,14 @@ static const uint32_t BIT_ONE_LOW_US = 1690; static const uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { - dst->reserve(68); + ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); + + dst->reserve(2 + 32 + 32 * data.command_repeats + 2); dst->set_carrier_frequency(38000); dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint16_t mask = 1; mask; mask <<= 1) { if (data.address & mask) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); @@ -25,11 +29,13 @@ void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { } } - for (uint16_t mask = 1; mask; mask <<= 1) { - if (data.command & mask) { - dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); - } else { - dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + for (uint16_t repeats = 0; repeats < data.command_repeats; repeats++) { + for (uint16_t mask = 1; mask; mask <<= 1) { + if (data.command & mask) { + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } } } @@ -39,6 +45,7 @@ optional NECProtocol::decode(RemoteReceiveData src) { NECData data{ .address = 0, .command = 0, + .command_repeats = 1, }; if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) return {}; @@ -63,11 +70,32 @@ optional NECProtocol::decode(RemoteReceiveData src) { } } + while (src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US) || src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + uint16_t command = 0; + for (uint16_t mask = 1; mask; mask <<= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + command |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + command &= ~mask; + } else { + return {}; + } + } + + // Make sure the extra/repeated data matches original command + if (command != data.command) { + return {}; + } + + data.command_repeats += 1; + } + src.expect_mark(BIT_HIGH_US); return data; } void NECProtocol::dump(const NECData &data) { - ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X", data.address, data.command); + ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); } } // namespace remote_base diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 593a3efe17..71e1bccba8 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -8,6 +8,7 @@ namespace remote_base { struct NECData { uint16_t address; uint16_t command; + uint16_t command_repeats; bool operator==(const NECData &rhs) const { return address == rhs.address && command == rhs.command; } }; @@ -25,11 +26,13 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); + data.command_repeats = this->command_repeats_.value(x...); NECProtocol().encode(dst, data); } }; diff --git a/esphome/const.py b/esphome/const.py index f688cd75f9..1aabe81db3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -131,6 +131,7 @@ CONF_COLOR_PALETTE = "color_palette" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" +CONF_COMMAND_REPEATS = "command_repeats" CONF_COMMAND_RETAIN = "command_retain" CONF_COMMAND_TOPIC = "command_topic" CONF_COMMENT = "comment" From a72725f4b40266646aa46886d042428d76504ae3 Mon Sep 17 00:00:00 2001 From: Cossid <83468485+Cossid@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:04:17 -0600 Subject: [PATCH 132/436] BP1658CJ - Fix timing for all platforms, now consistent with other drivers (#5799) --- esphome/components/bp1658cj/bp1658cj.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index 05c3f790c2..4b74cc85f5 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -90,40 +90,41 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) { void BP1658CJ::write_bit_(bool value) { this->data_pin_->digital_write(value); - this->clock_pin_->digital_write(true); - delayMicroseconds(BP1658CJ_DELAY); - + this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); } void BP1658CJ::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); - delayMicroseconds(BP1658CJ_DELAY); } // ack bit this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); - delayMicroseconds(BP1658CJ_DELAY); - this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); - delayMicroseconds(BP1658CJ_DELAY); } this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); } } // namespace bp1658cj From 6c7a133faa722f0a7d9c808094aba0ef1321bd70 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Wed, 13 Dec 2023 02:23:02 +0100 Subject: [PATCH 133/436] Add humidity support to climate (#5732) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 8 ++ esphome/components/api/api_connection.cpp | 10 +++ esphome/components/api/api_pb2.cpp | 79 ++++++++++++++++++- esphome/components/api/api_pb2.h | 8 ++ .../bang_bang/bang_bang_climate.cpp | 13 +++ .../components/bang_bang/bang_bang_climate.h | 4 + esphome/components/bang_bang/climate.py | 6 ++ esphome/components/climate/__init__.py | 43 ++++++++++ esphome/components/climate/automation.h | 2 + esphome/components/climate/climate.cpp | 54 ++++++++++++- esphome/components/climate/climate.h | 19 +++++ esphome/components/climate/climate_traits.h | 17 ++++ esphome/components/mqtt/mqtt_climate.cpp | 42 +++++++++- esphome/components/mqtt/mqtt_climate.h | 3 + esphome/components/mqtt/mqtt_const.h | 4 + esphome/components/pid/climate.py | 7 +- esphome/components/pid/pid_climate.cpp | 13 +++ esphome/components/pid/pid_climate.h | 3 + esphome/components/thermostat/climate.py | 6 ++ .../thermostat/thermostat_climate.cpp | 15 ++++ .../thermostat/thermostat_climate.h | 3 + esphome/const.py | 4 + tests/test3.yaml | 3 + 23 files changed, 359 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 34d137981e..04db649aef 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -859,6 +859,10 @@ message ListEntitiesClimateResponse { string icon = 19; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; + bool supports_current_humidity = 22; + bool supports_target_humidity = 23; + float visual_min_humidity = 24; + float visual_max_humidity = 25; } message ClimateStateResponse { option (id) = 47; @@ -879,6 +883,8 @@ message ClimateStateResponse { string custom_fan_mode = 11; ClimatePreset preset = 12; string custom_preset = 13; + float current_humidity = 14; + float target_humidity = 15; } message ClimateCommandRequest { option (id) = 48; @@ -907,6 +913,8 @@ message ClimateCommandRequest { ClimatePreset preset = 19; bool has_custom_preset = 20; string custom_preset = 21; + bool has_target_humidity = 22; + float target_humidity = 23; } // ==================== NUMBER ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b8f5b05538..d5ab00a822 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -560,6 +560,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { resp.custom_preset = climate->custom_preset.value(); if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); + if (traits.get_supports_current_humidity()) + resp.current_humidity = climate->current_humidity; + if (traits.get_supports_target_humidity()) + resp.target_humidity = climate->target_humidity; return this->send_climate_state_response(resp); } bool APIConnection::send_climate_info(climate::Climate *climate) { @@ -576,7 +580,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.entity_category = static_cast(climate->get_entity_category()); msg.supports_current_temperature = traits.get_supports_current_temperature(); + msg.supports_current_humidity = traits.get_supports_current_humidity(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); + msg.supports_target_humidity = traits.get_supports_target_humidity(); for (auto mode : traits.get_supported_modes()) msg.supported_modes.push_back(static_cast(mode)); @@ -585,6 +591,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); + msg.visual_min_humidity = traits.get_visual_min_humidity(); + msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); @@ -615,6 +623,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_target_temperature_high) call.set_target_temperature_high(msg.target_temperature_high); + if (msg.has_target_humidity) + call.set_target_humidity(msg.target_humidity); if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 01fb540e7e..8dd34e7ef1 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3611,6 +3611,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->entity_category = value.as_enum(); return true; } + case 22: { + this->supports_current_humidity = value.as_bool(); + return true; + } + case 23: { + this->supports_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -3667,6 +3675,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val this->visual_current_temperature_step = value.as_float(); return true; } + case 24: { + this->visual_min_humidity = value.as_float(); + return true; + } + case 25: { + this->visual_max_humidity = value.as_float(); + return true; + } default: return false; } @@ -3705,6 +3721,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(19, this->icon); buffer.encode_enum(20, this->entity_category); buffer.encode_float(21, this->visual_current_temperature_step); + buffer.encode_bool(22, this->supports_current_humidity); + buffer.encode_bool(23, this->supports_target_humidity); + buffer.encode_float(24, this->visual_min_humidity); + buffer.encode_float(25, this->visual_max_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -3810,7 +3830,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->visual_current_temperature_step); out.append(buffer); out.append("\n"); - out.append("}"); + + out.append(" supports_current_humidity: "); + out.append(YESNO(this->supports_current_humidity)); + out.append("\n"); + + out.append(" supports_target_humidity: "); + out.append(YESNO(this->supports_target_humidity)); + out.append("\n"); + + out.append(" visual_min_humidity: "); + sprintf(buffer, "%g", this->visual_min_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" visual_max_humidity: "); + sprintf(buffer, "%g", this->visual_max_humidity); + out.append(buffer); + out.append("\n"); } #endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -3879,6 +3916,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 14: { + this->current_humidity = value.as_float(); + return true; + } + case 15: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -3897,6 +3942,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->custom_fan_mode); buffer.encode_enum(12, this->preset); buffer.encode_string(13, this->custom_preset); + buffer.encode_float(14, this->current_humidity); + buffer.encode_float(15, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateStateResponse::dump_to(std::string &out) const { @@ -3958,7 +4005,16 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); - out.append("}"); + + out.append(" current_humidity: "); + sprintf(buffer, "%g", this->current_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); } #endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -4023,6 +4079,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->has_custom_preset = value.as_bool(); return true; } + case 22: { + this->has_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -4059,6 +4119,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 23: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -4085,6 +4149,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(19, this->preset); buffer.encode_bool(20, this->has_custom_preset); buffer.encode_string(21, this->custom_preset); + buffer.encode_bool(22, this->has_target_humidity); + buffer.encode_float(23, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateCommandRequest::dump_to(std::string &out) const { @@ -4177,6 +4243,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); + + out.append(" has_target_humidity: "); + out.append(YESNO(this->has_target_humidity)); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index fc57348863..02fc7b88f8 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -985,6 +985,10 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::string icon{}; enums::EntityCategory entity_category{}; float visual_current_temperature_step{0.0f}; + bool supports_current_humidity{false}; + bool supports_target_humidity{false}; + float visual_min_humidity{0.0f}; + float visual_max_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1010,6 +1014,8 @@ class ClimateStateResponse : public ProtoMessage { std::string custom_fan_mode{}; enums::ClimatePreset preset{}; std::string custom_preset{}; + float current_humidity{0.0f}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1043,6 +1049,8 @@ class ClimateCommandRequest : public ProtoMessage { enums::ClimatePreset preset{}; bool has_custom_preset{false}; std::string custom_preset{}; + bool has_target_humidity{false}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 20cb87025a..13b0cd2a09 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -15,6 +15,16 @@ void BangBangClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, }); @@ -171,6 +183,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa BangBangClimate::BangBangClimate() : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 84bcd51f34..96368af34c 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); Trigger<> *get_idle_trigger() const; Trigger<> *get_cool_trigger() const; void set_supports_cool(bool supports_cool); @@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /** The trigger to call when the controller should switch to idle mode. * * In idle mode, the controller is assumed to have both heating and cooling disabled. diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index ac0c328000..9dde0ae1ac 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR, @@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), @@ -47,6 +49,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + normal_config = BangBangClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 85242eb344..c9c3900a0c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY, CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, + CONF_CURRENT_HUMIDITY_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -28,6 +29,8 @@ from esphome.const import ( CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, + CONF_TARGET_HUMIDITY_COMMAND_TOPIC, + CONF_TARGET_HUMIDITY_STATE_TOPIC, CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_STATE_TOPIC, @@ -106,6 +109,9 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) CONF_CURRENT_TEMPERATURE = "current_temperature" +CONF_MIN_HUMIDITY = "min_humidity" +CONF_MAX_HUMIDITY = "max_humidity" +CONF_TARGET_HUMIDITY = "target_humidity" visual_temperature = cv.float_with_unit( "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" @@ -153,6 +159,8 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, + cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int, + cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int, } ), cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( @@ -167,6 +175,9 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -209,6 +220,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_ON_CONTROL): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), @@ -238,6 +255,10 @@ async def setup_climate_core_(var, config): visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], ) ) + if CONF_MIN_HUMIDITY in visual: + cg.add(var.set_visual_min_humidity_override(visual[CONF_MIN_HUMIDITY])) + if CONF_MAX_HUMIDITY in visual: + cg.add(var.set_visual_max_humidity_override(visual[CONF_MAX_HUMIDITY])) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) @@ -255,6 +276,12 @@ async def setup_climate_core_(var, config): config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] ) ) + if CONF_CURRENT_HUMIDITY_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_current_humidity_state_topic( + config[CONF_CURRENT_HUMIDITY_STATE_TOPIC] + ) + ) if CONF_FAN_MODE_COMMAND_TOPIC in config: cg.add( mqtt_.set_custom_fan_mode_command_topic( @@ -323,6 +350,18 @@ async def setup_climate_core_(var, config): config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] ) ) + if CONF_TARGET_HUMIDITY_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_target_humidity_command_topic( + config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC] + ) + ) + if CONF_TARGET_HUMIDITY_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_target_humidity_state_topic( + config[CONF_TARGET_HUMIDITY_STATE_TOPIC] + ) + ) for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -351,6 +390,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode @@ -387,6 +427,9 @@ async def climate_control_to_code(config, action_id, template_arg, args): config[CONF_TARGET_TEMPERATURE_HIGH], args, float ) cg.add(var.set_target_temperature_high(template_)) + if CONF_TARGET_HUMIDITY in config: + template_ = await cg.templatable(config[CONF_TARGET_HUMIDITY], args, float) + cg.add(var.set_target_humidity(template_)) if CONF_FAN_MODE in config: template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 382871e1e7..a4d13ade58 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -14,6 +14,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_high) + TEMPLATABLE_VALUE(float, target_humidity) TEMPLATABLE_VALUE(bool, away) TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(std::string, custom_fan_mode) @@ -27,6 +28,7 @@ template class ControlAction : public Action { call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); + call.set_target_humidity(this->target_humidity_.optional_value(x...)); if (away_.has_value()) { call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ea24cab954..1822707152 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -45,6 +45,9 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } + if (this->target_humidity_.has_value()) { + ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + } this->parent_->control(*this); } void ClimateCall::validate_() { @@ -262,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h this->target_temperature_high_ = target_temperature_high; return *this; } +ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} + const optional &ClimateCall::get_mode() const { return this->mode_; } const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } +const optional &ClimateCall::get_target_humidity() const { return this->target_humidity_; } const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } @@ -283,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional target_temperat this->target_temperature_ = target_temperature; return *this; } +ClimateCall &ClimateCall::set_target_humidity(optional target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; @@ -343,6 +356,9 @@ void Climate::save_state_() { } else { state.target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + state.target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && fan_mode.has_value()) { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); @@ -408,6 +424,12 @@ void Climate::publish_state() { } else { ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); } + if (traits.get_supports_current_humidity()) { + ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + } + if (traits.get_supports_target_humidity()) { + ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + } // Send state to frontend this->state_callback_.call(*this); @@ -427,6 +449,12 @@ ClimateTraits Climate::get_traits() { traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); } + if (this->visual_min_humidity_override_.has_value()) { + traits.set_visual_min_humidity(*this->visual_min_humidity_override_); + } + if (this->visual_max_humidity_override_.has_value()) { + traits.set_visual_max_humidity(*this->visual_max_humidity_override_); + } return traits; } @@ -441,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current) this->visual_target_temperature_step_override_ = target; this->visual_current_temperature_step_override_ = current; } +void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { + this->visual_min_humidity_override_ = visual_min_humidity_override; +} +void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { + this->visual_max_humidity_override_ = visual_max_humidity_override; +} ClimateCall Climate::make_call() { return ClimateCall(this); } @@ -454,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { } else { call.set_target_temperature(this->target_temperature); } + if (traits.get_supports_target_humidity()) { + call.set_target_humidity(this->target_humidity); + } if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { call.set_fan_mode(this->fan_mode); } @@ -474,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { } else { climate->target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + climate->target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { climate->fan_mode = this->fan_mode; } @@ -530,17 +570,25 @@ void Climate::dump_traits_(const char *tag) { auto traits = this->get_traits(); ESP_LOGCONFIG(tag, "ClimateTraits:"); ESP_LOGCONFIG(tag, " [x] Visual settings:"); - ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); - ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); - ESP_LOGCONFIG(tag, " - Step:"); + ESP_LOGCONFIG(tag, " - Min temperature: %.1f", traits.get_visual_min_temperature()); + ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature()); + ESP_LOGCONFIG(tag, " - Temperature step:"); ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); + ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity()); + ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity()); if (traits.get_supports_current_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports current temperature"); } + if (traits.get_supports_current_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports current humidity"); + } if (traits.get_supports_two_point_target_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); } + if (traits.get_supports_target_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports target humidity"); + } if (traits.get_supports_action()) { ESP_LOGCONFIG(tag, " [x] Supports action"); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 1bbb17322d..7c2a0b1ed3 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,6 +64,10 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(float target_humidity); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(optional target_humidity); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); /// Set the fan mode of the climate device. @@ -93,6 +97,7 @@ class ClimateCall { const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; + const optional &get_target_humidity() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_custom_fan_mode() const; @@ -107,6 +112,7 @@ class ClimateCall { optional target_temperature_; optional target_temperature_low_; optional target_temperature_high_; + optional target_humidity_; optional fan_mode_; optional swing_mode_; optional custom_fan_mode_; @@ -136,6 +142,7 @@ struct ClimateDeviceRestoreState { float target_temperature_high; }; }; + float target_humidity; /// Convert this struct to a climate call that can be performed. ClimateCall to_call(Climate *climate); @@ -164,11 +171,16 @@ class Climate : public EntityBase { /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. ClimateAction action{CLIMATE_ACTION_OFF}; + /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; + /// The current humidity of the climate device, as reported from the integration. + float current_humidity{NAN}; + union { /// The target temperature of the climate device. float target_temperature; @@ -180,6 +192,9 @@ class Climate : public EntityBase { }; }; + /// The target humidity of the climate device. + float target_humidity; + /// The active fan mode of the climate device. optional fan_mode; @@ -233,6 +248,8 @@ class Climate : public EntityBase { void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); void set_visual_temperature_step_override(float target, float current); + void set_visual_min_humidity_override(float visual_min_humidity_override); + void set_visual_max_humidity_override(float visual_max_humidity_override); protected: friend ClimateCall; @@ -282,6 +299,8 @@ class Climate : public EntityBase { optional visual_max_temperature_override_{}; optional visual_target_temperature_step_override_{}; optional visual_current_temperature_step_override_{}; + optional visual_min_humidity_override_{}; + optional visual_max_humidity_override_{}; }; } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index e8c2db6c06..fd5b025a03 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -44,10 +44,18 @@ class ClimateTraits { void set_supports_current_temperature(bool supports_current_temperature) { supports_current_temperature_ = supports_current_temperature; } + bool get_supports_current_humidity() const { return supports_current_humidity_; } + void set_supports_current_humidity(bool supports_current_humidity) { + supports_current_humidity_ = supports_current_humidity; + } bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { supports_two_point_target_temperature_ = supports_two_point_target_temperature; } + bool get_supports_target_humidity() const { return supports_target_humidity_; } + void set_supports_target_humidity(bool supports_target_humidity) { + supports_target_humidity_ = supports_target_humidity; + } void set_supported_modes(std::set modes) { supported_modes_ = std::move(modes); } void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") @@ -153,6 +161,11 @@ class ClimateTraits { int8_t get_target_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const; + float get_visual_min_humidity() const { return visual_min_humidity_; } + void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; } + float get_visual_max_humidity() const { return visual_max_humidity_; } + void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; } + protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { @@ -177,7 +190,9 @@ class ClimateTraits { } bool supports_current_temperature_{false}; + bool supports_current_humidity_{false}; bool supports_two_point_target_temperature_{false}; + bool supports_target_humidity_{false}; std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; bool supports_action_{false}; std::set supported_fan_modes_; @@ -190,6 +205,8 @@ class ClimateTraits { float visual_max_temperature_{30}; float visual_target_temperature_step_{0.1}; float visual_current_temperature_step_{0.1}; + float visual_min_humidity_{30}; + float visual_max_humidity_{99}; }; } // namespace climate diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 44c490c308..49a8f06734 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -17,9 +17,12 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { - // current_temperature_topic root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); } + // current_humidity_topic + if (traits.get_supports_current_humidity()) { + root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic(); + } // mode_command_topic root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic(); // mode_state_topic @@ -57,6 +60,13 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); } + if (traits.get_supports_target_humidity()) { + // target_humidity_command_topic + root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic(); + // target_humidity_state_topic + root[MQTT_TARGET_HUMIDITY_STATE_TOPIC] = this->get_target_humidity_state_topic(); + } + // min_temp root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); // max_temp @@ -66,6 +76,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; + // min_humidity + root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity(); + // max_humidity + root[MQTT_MAX_HUMIDITY] = traits.get_visual_max_humidity(); + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { // preset_mode_command_topic root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic(); @@ -192,6 +207,20 @@ void MQTTClimateComponent::setup() { }); } + if (traits.get_supports_target_humidity()) { + this->subscribe(this->get_target_humidity_command_topic(), + [this](const std::string &topic, const std::string &payload) { + auto val = parse_number(payload); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); + return; + } + auto call = this->device_->make_call(); + call.set_target_humidity(*val); + call.perform(); + }); + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) { auto call = this->device_->make_call(); @@ -273,6 +302,17 @@ bool MQTTClimateComponent::publish_state_() { success = false; } + if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0); + if (!this->publish(this->get_current_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0); + if (!this->publish(this->get_target_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { std::string payload; if (this->device_->preset.has_value()) { diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index a93070fe66..4e54230e68 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -20,6 +20,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { void setup() override; MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state) + MQTT_COMPONENT_CUSTOM_TOPIC(current_humidity, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, state) @@ -28,6 +29,8 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, state) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, state) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, command) MQTT_COMPONENT_CUSTOM_TOPIC(away, state) MQTT_COMPONENT_CUSTOM_TOPIC(away, command) MQTT_COMPONENT_CUSTOM_TOPIC(action, state) diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 7f74197ab4..3d9e0b4c00 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -51,6 +51,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "cod_arm_req"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "curr_hum_t"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "curr_hum_tpl"; constexpr const char *const MQTT_DEVICE = "dev"; constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla"; constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; @@ -305,6 +307,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "code_arm_required"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"; constexpr const char *const MQTT_DEVICE = "device"; constexpr const char *const MQTT_DEVICE_CLASS = "device_class"; constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 7cd414f912..2c4ef688a5 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor, output -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.const import CONF_HUMIDITY_SENSOR, CONF_ID, CONF_SENSOR pid_ns = cg.esphome_ns.namespace("pid") PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component) @@ -45,6 +45,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PIDClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), @@ -86,6 +87,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + if CONF_COOL_OUTPUT in config: out = await cg.get_variable(config[CONF_COOL_OUTPUT]) cg.add(var.set_cool_output(out)) diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index dab4502d40..93b6999a00 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -14,6 +14,16 @@ void PIDClimate::setup() { this->update_pid_(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,9 @@ climate::ClimateTraits PIDClimate::traits() { traits.set_supports_current_temperature(true); traits.set_supports_two_point_target_temperature(false); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); if (supports_cool_()) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index da57209a7e..5ae97ee10b 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -19,6 +19,7 @@ class PIDClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { humidity_sensor_ = sensor; } void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; } void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; } void set_kp(float kp) { controller_.kp_ = kp; } @@ -85,6 +86,8 @@ class PIDClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; output::FloatOutput *cool_output_{nullptr}; output::FloatOutput *heat_output_{nullptr}; PIDController controller_; diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index cca46609db..89d6b13376 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -35,6 +35,7 @@ from esphome.const import ( CONF_HEAT_DEADBAND, CONF_HEAT_MODE, CONF_HEAT_OVERRUN, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_MAX_COOLING_RUN_TIME, @@ -519,6 +520,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ThermostatClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), cv.Optional( @@ -658,6 +660,10 @@ async def to_code(config): ) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 73b061b07c..40a29295f1 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -27,6 +27,15 @@ void ThermostatClimate::setup() { }); this->current_temperature = this->sensor_->state; + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + auto use_default_preset = true; if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { @@ -217,6 +226,9 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + if (supports_auto_) traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); if (supports_heat_cool_) @@ -1169,6 +1181,9 @@ void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { + this->humidity_sensor_ = humidity_sensor; +} void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 677b4ad324..559812a94f 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -81,6 +81,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_heating_minimum_run_time_in_sec(uint32_t time); void set_idle_minimum_time_in_sec(uint32_t time); void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); @@ -238,6 +239,8 @@ class ThermostatClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// diff --git a/esphome/const.py b/esphome/const.py index 1aabe81db3..101a690db5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -157,6 +157,7 @@ CONF_CS_PIN = "cs_pin" CONF_CSS_INCLUDE = "css_include" CONF_CSS_URL = "css_url" CONF_CURRENT = "current" +CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" @@ -324,6 +325,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference" CONF_HOUR = "hour" CONF_HOURS = "hours" CONF_HUMIDITY = "humidity" +CONF_HUMIDITY_SENSOR = "humidity_sensor" CONF_HYSTERESIS = "hysteresis" CONF_I2C = "i2c" CONF_I2C_ID = "i2c_id" @@ -758,6 +760,8 @@ CONF_SYNC = "sync" CONF_TABLET = "tablet" CONF_TAG = "tag" CONF_TARGET = "target" +CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" CONF_TARGET_TEMPERATURE = "target_temperature" CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" CONF_TARGET_TEMPERATURE_COMMAND_TOPIC = "target_temperature_command_topic" diff --git a/tests/test3.yaml b/tests/test3.yaml index 6b1757c2ad..ab7f38d07f 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -899,6 +899,7 @@ climate: - platform: bang_bang name: Bang Bang Climate sensor: ha_hello_world + humidity_sensor: ha_hello_world default_target_temperature_low: 18°C default_target_temperature_high: 24°C idle_action: @@ -913,6 +914,7 @@ climate: - platform: thermostat name: Thermostat Climate sensor: ha_hello_world + humidity_sensor: ha_hello_world preset: - name: Default Preset default_target_temperature_low: 18°C @@ -1000,6 +1002,7 @@ climate: id: pid_climate name: PID Climate Controller sensor: ha_hello_world + humidity_sensor: ha_hello_world default_target_temperature: 21°C heat_output: my_slow_pwm control_parameters: From 2fcc5b3ef212c6be62a299b50274d8a48d169ae7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:46:11 +0900 Subject: [PATCH 134/436] Remove lingering note (#5916) --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec23656763..1c92d91159 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,5 +10,3 @@ Things to note when contributing: for more information. - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files which checks if your new feature compiles correctly. - - Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping - me after some time. From f1f8689462db94fb42e1440795d9bb68b7c2ceac Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:47:46 +0900 Subject: [PATCH 135/436] Fix typo added in esp32 post_build filename (#5918) --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fd5e9377dd..307a3e76e5 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -462,7 +462,7 @@ async def to_code(config): add_extra_script( "post", - "post_build2.py", + "post_build.py", os.path.join(os.path.dirname(__file__), "post_build.py.script"), ) From 058c43e95319b2dcc544c807c786d859ed8384f7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:51:07 +0900 Subject: [PATCH 136/436] Copy esp32 custom partition files to build folder (#5919) --- esphome/components/esp32/__init__.py | 33 +++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 307a3e76e5..5d17633975 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -497,10 +497,11 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: - cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) - else: - cg.add_platformio_option("board_build.partitions", "partitions.csv") + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -639,20 +640,22 @@ def _write_sdkconfig(): # Called by writer.py def copy_files(): if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) if CORE.using_esp_idf: _write_sdkconfig() - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_idf_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_idf_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. From 9daaadb3b68aaa1fabe1ecf67acd394053da883e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 13 Dec 2023 08:29:05 +0100 Subject: [PATCH 137/436] UART change at runtime (#5909) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../uart/uart_component_esp32_arduino.cpp | 9 +++++++++ .../uart/uart_component_esp32_arduino.h | 14 ++++++++++++++ .../components/uart/uart_component_esp_idf.cpp | 16 ++++++++++++++-- esphome/components/uart/uart_component_esp_idf.h | 14 ++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 7306dd2f31..75b67bf5c2 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -109,6 +109,11 @@ void ESP32ArduinoUARTComponent::setup() { this->number_ = next_uart_num; this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory) } + + this->load_settings(false); +} + +void ESP32ArduinoUARTComponent::load_settings(bool dump_config) { int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; bool invert = false; @@ -118,6 +123,10 @@ void ESP32ArduinoUARTComponent::setup() { invert = true; this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); + if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->number_); + this->dump_config(); + } } void ESP32ArduinoUARTComponent::dump_config() { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index 02dfd0531e..ec4953a598 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -32,6 +32,20 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { HardwareSerial *get_hw_serial() { return this->hw_serial_; } uint8_t get_hw_serial_number() { return this->number_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config = true); + protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index c78626fa26..2dd6ab105f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -122,9 +122,21 @@ void IDFUARTComponent::setup() { xSemaphoreGive(this->lock_); } +void IDFUARTComponent::load_settings(bool dump_config) { + uart_config_t uart_config = this->get_config_(); + esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } else if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); + this->dump_config(); + } +} + void IDFUARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus:"); - ESP_LOGCONFIG(TAG, " Number: %u", this->uart_num_); + ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); LOG_PIN(" TX Pin: ", tx_pin_); LOG_PIN(" RX Pin: ", rx_pin_); if (this->rx_pin_ != nullptr) { diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index fdaa4da9a7..068ebd32dc 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -26,6 +26,20 @@ class IDFUARTComponent : public UARTComponent, public Component { uint8_t get_hw_serial_number() { return this->uart_num_; } QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config = true); + protected: void check_logger_conflict() override; uart_port_t uart_num_; From f355972c9d8f43b3a64849d698da1163d41325dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:57:50 -1000 Subject: [PATCH 138/436] Bump aioesphomeapi from 20.1.0 to 21.0.0 (#5922) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0d08da2b45..f330ecbf3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==20.1.0 +aioesphomeapi==21.0.0 zeroconf==0.128.4 python-magic==0.4.27 From 9a8bc9484d99c47d207d1092d43ce81121c6e107 Mon Sep 17 00:00:00 2001 From: Jacob Masen-Smith Date: Wed, 13 Dec 2023 12:33:24 -0800 Subject: [PATCH 139/436] Fix the initial run of lambda light effects (#5921) The timer used for `millis()` is a monotonic timer based on the last start time of the device. If, for some reason, you pick a long `update_interval` and try to apply it as soon as you start the device, nothing happens because the device hasn't been on for longer than the `update_interval` --- esphome/components/light/addressable_light_effect.h | 2 +- esphome/components/light/base_light_effects.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0482cf53b9..c2109b2d23 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -57,7 +57,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index d126e4960c..c62ca43ca1 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -118,7 +118,7 @@ class LambdaLightEffect : public LightEffect { void start() override { this->initial_run_ = true; } void apply() override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(this->initial_run_); this->initial_run_ = false; From 81aa48a5f346f8127b6a50563e045ebd9af90eb3 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:45:08 +0100 Subject: [PATCH 140/436] Exposes `load_settings` to `UARTComponent` class (#5920) --- esphome/components/uart/uart_component.h | 4 ++++ esphome/components/uart/uart_component_esp32_arduino.h | 3 ++- esphome/components/uart/uart_component_esp_idf.h | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 42702cf5b8..34bda42bb5 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -62,6 +62,10 @@ class UARTComponent { UARTParityOptions get_parity() const { return this->parity_; } void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } uint32_t get_baud_rate() const { return baud_rate_; } +#ifdef USE_ESP32 + virtual void load_settings() = 0; + virtual void load_settings(bool dump_config) = 0; +#endif // USE_ESP32 #ifdef USE_UART_DEBUGGER void add_debug_callback(std::function &&callback) { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index ec4953a598..de17d9718b 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -44,7 +44,8 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - void load_settings(bool dump_config = true); + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 068ebd32dc..215641ebe2 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -38,7 +38,8 @@ class IDFUARTComponent : public UARTComponent, public Component { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - void load_settings(bool dump_config = true); + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } protected: void check_logger_conflict() override; From 6fd239362d768e4e9ec533e59ef453bc3cab1988 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 13 Dec 2023 16:54:55 -0600 Subject: [PATCH 141/436] Add support for PN7160 (#5486) --- CODEOWNERS | 3 + esphome/components/nfc/nci_core.h | 144 ++ esphome/components/nfc/nci_message.cpp | 166 +++ esphome/components/nfc/nci_message.h | 50 + esphome/components/nfc/nfc.cpp | 2 +- esphome/components/nfc/nfc_helpers.cpp | 47 + esphome/components/nfc/nfc_helpers.h | 17 + esphome/components/pn7160/__init__.py | 227 ++++ esphome/components/pn7160/automation.h | 82 ++ esphome/components/pn7160/pn7160.cpp | 1161 +++++++++++++++++ esphome/components/pn7160/pn7160.h | 315 +++++ .../pn7160/pn7160_mifare_classic.cpp | 322 +++++ .../pn7160/pn7160_mifare_ultralight.cpp | 186 +++ esphome/components/pn7160_i2c/__init__.py | 25 + esphome/components/pn7160_i2c/pn7160_i2c.cpp | 49 + esphome/components/pn7160_i2c/pn7160_i2c.h | 22 + esphome/components/pn7160_spi/__init__.py | 26 + esphome/components/pn7160_spi/pn7160_spi.cpp | 54 + esphome/components/pn7160_spi/pn7160_spi.h | 30 + tests/test1.yaml | 37 + 20 files changed, 2964 insertions(+), 1 deletion(-) create mode 100644 esphome/components/nfc/nci_core.h create mode 100644 esphome/components/nfc/nci_message.cpp create mode 100644 esphome/components/nfc/nci_message.h create mode 100644 esphome/components/nfc/nfc_helpers.cpp create mode 100644 esphome/components/nfc/nfc_helpers.h create mode 100644 esphome/components/pn7160/__init__.py create mode 100644 esphome/components/pn7160/automation.h create mode 100644 esphome/components/pn7160/pn7160.cpp create mode 100644 esphome/components/pn7160/pn7160.h create mode 100644 esphome/components/pn7160/pn7160_mifare_classic.cpp create mode 100644 esphome/components/pn7160/pn7160_mifare_ultralight.cpp create mode 100644 esphome/components/pn7160_i2c/__init__.py create mode 100644 esphome/components/pn7160_i2c/pn7160_i2c.cpp create mode 100644 esphome/components/pn7160_i2c/pn7160_i2c.h create mode 100644 esphome/components/pn7160_spi/__init__.py create mode 100644 esphome/components/pn7160_spi/pn7160_spi.cpp create mode 100644 esphome/components/pn7160_spi/pn7160_spi.h diff --git a/CODEOWNERS b/CODEOWNERS index d509c98433..2269d580e4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,9 @@ esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn7160/* @jesserockz @kbx81 +esphome/components/pn7160_i2c/* @jesserockz @kbx81 +esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h new file mode 100644 index 0000000000..fdaf6d0cc5 --- /dev/null +++ b/esphome/components/nfc/nci_core.h @@ -0,0 +1,144 @@ +#pragma once + +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +// Header info +static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +// Important masks +static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static const uint8_t NCI_PKT_GID_MASK = 0x0F; +static const uint8_t NCI_PKT_OID_MASK = 0x3F; +// Message types +static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +// GIDs +static const uint8_t NCI_CORE_GID = 0x0; +static const uint8_t RF_GID = 0x1; +static const uint8_t NFCEE_GID = 0x1; +static const uint8_t NCI_PROPRIETARY_GID = 0xF; +// OIDs +static const uint8_t NCI_CORE_RESET_OID = 0x00; +static const uint8_t NCI_CORE_INIT_OID = 0x01; +static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; + +static const uint8_t RF_DISCOVER_MAP_OID = 0x00; +static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static const uint8_t RF_DISCOVER_OID = 0x03; +static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static const uint8_t RF_DEACTIVATE_OID = 0x06; +static const uint8_t RF_FIELD_INFO_OID = 0x07; +static const uint8_t RF_T3T_POLLING_OID = 0x08; +static const uint8_t RF_NFCEE_ACTION_OID = 0x09; +static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; + +static const uint8_t NFCEE_DISCOVER_OID = 0x00; +static const uint8_t NFCEE_MODE_SET_OID = 0x01; +// Interfaces +static const uint8_t INTF_NFCEE_DIRECT = 0x00; +static const uint8_t INTF_FRAME = 0x01; +static const uint8_t INTF_ISODEP = 0x02; +static const uint8_t INTF_NFCDEP = 0x03; +static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +// Bit rates +static const uint8_t NFC_BIT_RATE_106 = 0x00; +static const uint8_t NFC_BIT_RATE_212 = 0x01; +static const uint8_t NFC_BIT_RATE_424 = 0x02; +static const uint8_t NFC_BIT_RATE_848 = 0x03; +static const uint8_t NFC_BIT_RATE_1695 = 0x04; +static const uint8_t NFC_BIT_RATE_3390 = 0x05; +static const uint8_t NFC_BIT_RATE_6780 = 0x06; +// Protocols +static const uint8_t PROT_UNDETERMINED = 0x00; +static const uint8_t PROT_T1T = 0x01; +static const uint8_t PROT_T2T = 0x02; +static const uint8_t PROT_T3T = 0x03; +static const uint8_t PROT_ISODEP = 0x04; +static const uint8_t PROT_NFCDEP = 0x05; +static const uint8_t PROT_T5T = 0x06; +static const uint8_t PROT_MIFARE = 0x80; +// RF Technologies +static const uint8_t NFC_RF_TECH_A = 0x00; +static const uint8_t NFC_RF_TECH_B = 0x01; +static const uint8_t NFC_RF_TECH_F = 0x02; +static const uint8_t NFC_RF_TECH_15693 = 0x03; +// RF Technology & Modes +static const uint8_t MODE_MASK = 0xF0; +static const uint8_t MODE_LISTEN_MASK = 0x80; +static const uint8_t MODE_POLL = 0x00; + +static const uint8_t TECH_PASSIVE_NFCA = 0x00; +static const uint8_t TECH_PASSIVE_NFCB = 0x01; +static const uint8_t TECH_PASSIVE_NFCF = 0x02; +static const uint8_t TECH_ACTIVE_NFCA = 0x03; +static const uint8_t TECH_ACTIVE_NFCF = 0x05; +static const uint8_t TECH_PASSIVE_15693 = 0x06; +// Status codes +static const uint8_t STATUS_OK = 0x00; +static const uint8_t STATUS_REJECTED = 0x01; +static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static const uint8_t STATUS_FAILED = 0x03; +static const uint8_t STATUS_NOT_INITIALIZED = 0x04; +static const uint8_t STATUS_SYNTAX_ERROR = 0x05; +static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static const uint8_t STATUS_INVALID_PARAM = 0x09; +static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static const uint8_t RF_PROTOCOL_ERROR = 0xB1; +static const uint8_t RF_TIMEOUT_ERROR = 0xB2; +static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +// Deactivation types/reasons +static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +// RF discover map modes +static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +// RF discover notification types +static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +// Important message offsets +static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.cpp b/esphome/components/nfc/nci_message.cpp new file mode 100644 index 0000000000..c6b21f6ae0 --- /dev/null +++ b/esphome/components/nfc/nci_message.cpp @@ -0,0 +1,166 @@ +#include "nci_core.h" +#include "nci_message.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace nfc { + +static const char *const TAG = "NciMessage"; + +NciMessage::NciMessage(const uint8_t message_type, const std::vector &payload) { + this->set_message(message_type, payload); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + this->set_header(message_type, gid, oid); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->set_message(message_type, gid, oid, payload); +} + +NciMessage::NciMessage(const std::vector &raw_packet) { this->nci_message_ = raw_packet; }; + +std::vector NciMessage::encode() { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + std::vector message = this->nci_message_; + return message; +} + +void NciMessage::reset() { this->nci_message_ = {0, 0, 0}; } + +uint8_t NciMessage::get_message_type() const { + return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK; +} + +uint8_t NciMessage::get_gid() const { return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK; } + +uint8_t NciMessage::get_oid() const { return this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK; } + +uint8_t NciMessage::get_payload_size(const bool recompute) { + if (!this->nci_message_.empty()) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return 0; +} + +uint8_t NciMessage::get_simple_status_response() const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return STATUS_FAILED; +} + +uint8_t NciMessage::get_message_byte(const uint8_t offset) const { + if (this->nci_message_.size() > offset) { + return this->nci_message_[offset]; + } + return 0; +} + +std::vector &NciMessage::get_message() { return this->nci_message_; } + +bool NciMessage::has_payload() const { return this->nci_message_.size() > nfc::NCI_PKT_HEADER_SIZE; } + +bool NciMessage::message_type_is(const uint8_t message_type) const { + if (!this->nci_message_.empty()) { + return message_type == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK); + } + return false; +} + +bool NciMessage::message_length_is(const uint8_t message_length, const bool recompute) { + if (this->nci_message_.size() > nfc::NCI_PKT_LENGTH_OFFSET) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return message_length == this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return false; +} + +bool NciMessage::gid_is(const uint8_t gid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_MT_GID_OFFSET) { + return gid == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK); + } + return false; +} + +bool NciMessage::oid_is(const uint8_t oid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_OID_OFFSET) { + return oid == (this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK); + } + return false; +} + +bool NciMessage::simple_status_response_is(const uint8_t response) const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return response == this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return false; +} + +void NciMessage::set_header(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_message(const uint8_t message_type, const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message_type(const uint8_t message_type) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto mt_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_MT_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = mt_masked | (message_type & nfc::NCI_PKT_MT_MASK); +} + +void NciMessage::set_gid(const uint8_t gid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto gid_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_GID_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = gid_masked | (gid & nfc::NCI_PKT_GID_MASK); +} + +void NciMessage::set_oid(const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_payload(const std::vector &payload) { + std::vector message(this->nci_message_.begin(), this->nci_message_.begin() + nfc::NCI_PKT_HEADER_SIZE); + + message.insert(message.end(), payload.begin(), payload.end()); + message[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_ = message; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.h b/esphome/components/nfc/nci_message.h new file mode 100644 index 0000000000..c6b8537402 --- /dev/null +++ b/esphome/components/nfc/nci_message.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +class NciMessage { + public: + NciMessage() {} + NciMessage(uint8_t message_type, const std::vector &payload); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + NciMessage(const std::vector &raw_packet); + + std::vector encode(); + void reset(); + + uint8_t get_message_type() const; + uint8_t get_gid() const; + uint8_t get_oid() const; + uint8_t get_payload_size(bool recompute = false); + uint8_t get_simple_status_response() const; + uint8_t get_message_byte(uint8_t offset) const; + std::vector &get_message(); + + bool has_payload() const; + bool message_type_is(uint8_t message_type) const; + bool message_length_is(uint8_t message_length, bool recompute = false); + bool gid_is(uint8_t gid) const; + bool oid_is(uint8_t oid) const; + bool simple_status_response_is(uint8_t response) const; + + void set_header(uint8_t message_type, uint8_t gid, uint8_t oid); + void set_message(uint8_t message_type, const std::vector &payload); + void set_message(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + void set_message_type(uint8_t message_type); + void set_gid(uint8_t gid); + void set_oid(uint8_t oid); + void set_payload(const std::vector &payload); + + protected: + std::vector nci_message_{0, 0, 0}; // three bytes, MT/PBF/GID, OID, payload length/size +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 7225e373b3..cf5a7f5ef1 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -53,7 +53,7 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { } bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { - uint8_t i = get_mifare_classic_ndef_start_index(data); + auto i = get_mifare_classic_ndef_start_index(data); if (data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp new file mode 100644 index 0000000000..bfaed6e486 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -0,0 +1,47 @@ +#include "nfc_helpers.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.helpers"; + +bool has_ha_tag_ndef(NfcTag &tag) { return !get_ha_tag_ndef(tag).empty(); } + +std::string get_ha_tag_ndef(NfcTag &tag) { + if (!tag.has_ndef_message()) { + return std::string(); + } + auto message = tag.get_ndef_message(); + auto records = message->get_records(); + for (const auto &record : records) { + std::string payload = record->get_payload(); + size_t pos = payload.find(HA_TAG_ID_PREFIX); + if (pos != std::string::npos) { + return payload.substr(pos + sizeof(HA_TAG_ID_PREFIX) - 1); + } + } + return std::string(); +} + +std::string get_random_ha_tag_ndef() { + static const char ALPHANUM[] = "0123456789abcdef"; + std::string uri = HA_TAG_ID_PREFIX; + for (int i = 0; i < 8; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 4; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + } + for (int i = 0; i < 12; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + ESP_LOGD("pn7160", "Payload to be written: %s", uri.c_str()); + return uri; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_helpers.h b/esphome/components/nfc/nfc_helpers.h new file mode 100644 index 0000000000..74f5beba13 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.h @@ -0,0 +1,17 @@ +#pragma once + +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const char HA_TAG_ID_EXT_RECORD_TYPE[] = "android.com:pkg"; +static const char HA_TAG_ID_EXT_RECORD_PAYLOAD[] = "io.homeassistant.companion.android"; +static const char HA_TAG_ID_PREFIX[] = "https://www.home-assistant.io/tag/"; + +std::string get_ha_tag_ndef(NfcTag &tag); +std::string get_random_ha_tag_ndef(); +bool has_ha_tag_ndef(NfcTag &tag); + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py new file mode 100644 index 0000000000..c91ca78b03 --- /dev/null +++ b/esphome/components/pn7160/__init__.py @@ -0,0 +1,227 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_DWL_REQ_PIN = "dwl_req_pin" +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_MESSAGE = "message" +CONF_ON_FINISHED_WRITE = "on_finished_write" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7160_ID = "pn7160_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" +CONF_WKUP_REQ_PIN = "wkup_req_pin" + +pn7160_ns = cg.esphome_ns.namespace("pn7160") +PN7160 = pn7160_ns.class_("PN7160", cg.Component) + +EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7160_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7160_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7160_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7160_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7160_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7160_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action) + + +PN7160OnEmulatedTagScanTrigger = pn7160_ns.class_( + "PN7160OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7160OnFinishedWriteTrigger = pn7160_ns.class_( + "PN7160OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7160IsWritingCondition = pn7160_ns.class_( + "PN7160IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7160), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7160_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7160), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_DWL_REQ_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_WKUP_REQ_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7160_set_message_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7160_simple_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def setup_pn7160(var, config): + await cg.register_component(var, config) + + if dwl_req_pin_config := config.get(CONF_DWL_REQ_PIN): + pin = await cg.gpio_pin_expression(dwl_req_pin_config) + cg.add(var.set_dwl_req_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if wakeup_req_pin_config := config.get(CONF_WKUP_REQ_PIN): + pin = await cg.gpio_pin_expression(wakeup_req_pin_config) + cg.add(var.set_wkup_req_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7160.is_writing", + PN7160IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + } + ), +) +async def pn7160_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h new file mode 100644 index 0000000000..854fb11684 --- /dev/null +++ b/esphome/components/pn7160/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" + +namespace esphome { +namespace pn7160 { + +class PN7160OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7160OnEmulatedTagScanTrigger(PN7160 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7160OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7160OnFinishedWriteTrigger(PN7160 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7160IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp new file mode 100644 index 0000000000..ce5374d1d1 --- /dev/null +++ b/esphome/components/pn7160/pn7160.cpp @@ -0,0 +1,1161 @@ +#include + +#include "automation.h" +#include "pn7160.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160"; + +void PN7160::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->setup(); + } + if (this->wkup_req_pin_ != nullptr) { + this->wkup_req_pin_->setup(); + } + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7160::dump_config() { + ESP_LOGCONFIG(TAG, "PN7160:"); + if (this->dwl_req_pin_ != nullptr) { + LOG_PIN(" DWL_REQ pin: ", this->dwl_req_pin_); + } + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); + if (this->wkup_req_pin_ != nullptr) { + LOG_PIN(" WKUP_REQ pin: ", this->wkup_req_pin_); + } +} + +void PN7160::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7160::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7160::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7160::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7160::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7160::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7160::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7160::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7160::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7160::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7160::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7160::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7160::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + } + + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // read reset notification + if (this->read_nfcc(rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Reset notification was not received"); + return nfc::STATUS_FAILED; + } + // verify reset notification + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.message_length_is(9)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET] != 0x02) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset notification was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Configuration %s", rx.get_message()[4] ? "reset" : "retained"); + ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[5] == 0x20 ? "2.0" : "1.0"); + ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", rx.get_message()[6]); + rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); + ESP_LOGD(TAG, "Manufacturer info: %s", nfc::format_bytes(rx.get_message()).c_str()); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t hw_version = rx.get_message()[17 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[18 + rx.get_message()[8]]; + uint8_t flash_major_version = rx.get_message()[19 + rx.get_message()[8]]; + uint8_t flash_minor_version = rx.get_message()[20 + rx.get_message()[8]]; + std::vector features(rx.get_message().begin() + 4, rx.get_message().begin() + 8); + + ESP_LOGD(TAG, "Hardware version: %u", hw_version); + ESP_LOGD(TAG, "ROM code version: %u", rom_code_version); + ESP_LOGD(TAG, "FLASH major version: %u", flash_major_version); + ESP_LOGD(TAG, "FLASH minor version: %u", flash_minor_version); + ESP_LOGD(TAG, "Features: %s", nfc::format_bytes(features).c_str()); + + return rx.get_simple_status_response(); +} + +uint8_t PN7160::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7160::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7160::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7160::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7160::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7160::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7160::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7160::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7160::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7160::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7160::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7160::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7160::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7160::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7160::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7160::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7160::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h new file mode 100644 index 0000000000..2b3cb99453 --- /dev/null +++ b/esphome/components/pn7160/pn7160.h @@ -0,0 +1,315 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7160 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 11, // length + 0x11, // IRQ Enable: PVDD + temp sensor IRQs + 0x01, // RFU + 0x01, // Power and Clock Configuration, device on (CFG1) + 0x01, // Power and Clock Configuration, device off (CFG1) + 0x00, // RFU + 0x00, // DC-DC 0 + 0x00, // DC-DC 1 + // 0x14, // TXLDO (3.3V / 4.75V) + // 0xBB, // TXLDO (4.7V / 4.7V) + 0xFF, // TXLDO (5.0V / 5.0V) + 0x00, // RFU + 0xD0, // TXLDO check + 0x0C, // RFU +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7160 : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + void set_wkup_req_pin(GPIOPin *wkup_req_pin) { this->wkup_req_pin_ = wkup_req_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *dwl_req_pin_{nullptr}; + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + GPIOPin *wkup_req_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp new file mode 100644 index 0000000000..fa63cc00d5 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_classic"; + +uint8_t PN7160::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7160::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7160::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7160::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7160::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp new file mode 100644 index 0000000000..a74f23d4f2 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_ultralight"; + +uint8_t PN7160::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7160::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7160::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/__init__.py b/esphome/components/pn7160_i2c/__init__.py new file mode 100644 index 0000000000..87c4719ca8 --- /dev/null +++ b/esphome/components/pn7160_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7160_i2c_ns = cg.esphome_ns.namespace("pn7160_i2c") +PN7160I2C = pn7160_i2c_ns.class_("PN7160I2C", pn7160.PN7160, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp new file mode 100644 index 0000000000..7c6da9dd06 --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7160_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7160_i2c { + +static const char *const TAG = "pn7160_i2c"; + +uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7160I2C::dump_config() { + PN7160::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.h b/esphome/components/pn7160_i2c/pn7160_i2c.h new file mode 100644 index 0000000000..eb253085eb --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7160_i2c { + +class PN7160I2C : public pn7160::PN7160, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_spi/__init__.py b/esphome/components/pn7160_spi/__init__.py new file mode 100644 index 0000000000..ae1235655a --- /dev/null +++ b/esphome/components/pn7160_spi/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +pn7160_spi_ns = cg.esphome_ns.namespace("pn7160_spi") +PN7160Spi = pn7160_spi_ns.class_("PN7160Spi", pn7160.PN7160, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160Spi), + } + ).extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/pn7160_spi/pn7160_spi.cpp b/esphome/components/pn7160_spi/pn7160_spi.cpp new file mode 100644 index 0000000000..09f673f700 --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.cpp @@ -0,0 +1,54 @@ +#include "pn7160_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160_spi { + +static const char *const TAG = "pn7160_spi"; + +void PN7160Spi::setup() { + this->spi_setup(); + this->cs_->digital_write(false); + PN7160::setup(); +} + +uint8_t PN7160Spi::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + this->enable(); + this->write_byte(TDD_SPI_READ); // send "transfer direction detector" + this->read_array(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE); + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + this->read_array(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length); + } + this->disable(); + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160Spi::write_nfcc(nfc::NciMessage &tx) { + this->enable(); + this->write_byte(TDD_SPI_WRITE); // send "transfer direction detector" + this->write_array(tx.encode().data(), tx.encode().size()); + this->disable(); + return nfc::STATUS_OK; +} + +void PN7160Spi::dump_config() { + PN7160::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +} // namespace pn7160_spi +} // namespace esphome diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h new file mode 100644 index 0000000000..7d4460a76d --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/spi/spi.h" + +#include + +namespace esphome { +namespace pn7160_spi { + +static const uint8_t TDD_SPI_READ = 0xFF; +static const uint8_t TDD_SPI_WRITE = 0x0A; + +class PN7160Spi : public pn7160::PN7160, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_spi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index b77cff7619..59818bbde5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3388,6 +3388,43 @@ pn532_spi: pn532_i2c: i2c_id: i2c_bus +pn7160_i2c: + id: nfcc_pn7160_i2c + i2c_id: i2c_bus + dwl_req_pin: + allow_other_uses: true + number: GPIO17 + irq_pin: + allow_other_uses: true + number: GPIO35 + ven_pin: + allow_other_uses: true + number: GPIO16 + wkup_req_pin: + allow_other_uses: true + number: GPIO21 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + +pn7160_spi: + id: nfcc_pn7160_spi + cs_pin: + number: GPIO15 + dwl_req_pin: + allow_other_uses: true + number: GPIO17 + irq_pin: + allow_other_uses: true + number: GPIO35 + ven_pin: + allow_other_uses: true + number: GPIO16 + wkup_req_pin: + allow_other_uses: true + number: GPIO21 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + rdm6300: uart_id: uart_0 From 76a6e288b6e7667f8620b85ea1160f9a89307c20 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 13 Dec 2023 17:27:35 -0600 Subject: [PATCH 142/436] Add support for PN7150 (#5487) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/pn7150/__init__.py | 215 ++++ esphome/components/pn7150/automation.h | 82 ++ esphome/components/pn7150/pn7150.cpp | 1137 +++++++++++++++++ esphome/components/pn7150/pn7150.h | 296 +++++ .../pn7150/pn7150_mifare_classic.cpp | 322 +++++ .../pn7150/pn7150_mifare_ultralight.cpp | 186 +++ esphome/components/pn7150_i2c/__init__.py | 25 + esphome/components/pn7150_i2c/pn7150_i2c.cpp | 49 + esphome/components/pn7150_i2c/pn7150_i2c.h | 22 + tests/test1.yaml | 9 + 11 files changed, 2345 insertions(+) create mode 100644 esphome/components/pn7150/__init__.py create mode 100644 esphome/components/pn7150/automation.h create mode 100644 esphome/components/pn7150/pn7150.cpp create mode 100644 esphome/components/pn7150/pn7150.h create mode 100644 esphome/components/pn7150/pn7150_mifare_classic.cpp create mode 100644 esphome/components/pn7150/pn7150_mifare_ultralight.cpp create mode 100644 esphome/components/pn7150_i2c/__init__.py create mode 100644 esphome/components/pn7150_i2c/pn7150_i2c.cpp create mode 100644 esphome/components/pn7150_i2c/pn7150_i2c.h diff --git a/CODEOWNERS b/CODEOWNERS index 2269d580e4..320a23ffaa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,8 @@ esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn7150/* @jesserockz @kbx81 +esphome/components/pn7150_i2c/* @jesserockz @kbx81 esphome/components/pn7160/* @jesserockz @kbx81 esphome/components/pn7160_i2c/* @jesserockz @kbx81 esphome/components/pn7160_spi/* @jesserockz @kbx81 diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py new file mode 100644 index 0000000000..3b80b574e9 --- /dev/null +++ b/esphome/components/pn7150/__init__.py @@ -0,0 +1,215 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_MESSAGE = "message" +CONF_ON_FINISHED_WRITE = "on_finished_write" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7150_ID = "pn7150_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" + +pn7150_ns = cg.esphome_ns.namespace("pn7150") +PN7150 = pn7150_ns.class_("PN7150", cg.Component) + +EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7150_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7150_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7150_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7150_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7150_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7150_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action) + + +PN7150OnEmulatedTagScanTrigger = pn7150_ns.class_( + "PN7150OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7150OnFinishedWriteTrigger = pn7150_ns.class_( + "PN7150OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7150IsWritingCondition = pn7150_ns.class_( + "PN7150IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7150), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7150_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7150), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7150_set_message_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7150_simple_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def setup_pn7150(var, config): + await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7150.is_writing", + PN7150IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + } + ), +) +async def pn7150_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h new file mode 100644 index 0000000000..aebb1b7573 --- /dev/null +++ b/esphome/components/pn7150/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" + +namespace esphome { +namespace pn7150 { + +class PN7150OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7150OnEmulatedTagScanTrigger(PN7150 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7150OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7150OnFinishedWriteTrigger(PN7150 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7150IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp new file mode 100644 index 0000000000..6703ab6a12 --- /dev/null +++ b/esphome/components/pn7150/pn7150.cpp @@ -0,0 +1,1137 @@ +#include "automation.h" +#include "pn7150.h" + +#include + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150"; + +void PN7150::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7150::dump_config() { + ESP_LOGCONFIG(TAG, "PN7150:"); + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); +} + +void PN7150::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7150::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7150::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7150::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7150::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7150::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7150::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7150::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7150::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7150::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7150::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7150::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7150::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // verify reset response + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_RESPONSE)) || (!rx.message_length_is(3)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != 0x11) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset response was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Configuration %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained"); + ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t manf_id = rx.get_message()[15 + rx.get_message()[8]]; + uint8_t hw_version = rx.get_message()[16 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[17 + rx.get_message()[8]]; + uint8_t flash_major_version = rx.get_message()[18 + rx.get_message()[8]]; + uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; + + ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", manf_id); + ESP_LOGD(TAG, "Hardware version: 0x%02X", hw_version); + ESP_LOGD(TAG, "ROM code version: 0x%02X", rom_code_version); + ESP_LOGD(TAG, "FLASH major version: 0x%02X", flash_major_version); + ESP_LOGD(TAG, "FLASH minor version: 0x%02X", flash_minor_version); + + return rx.get_simple_status_response(); +} + +uint8_t PN7150::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7150::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7150::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7150::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7150::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7150::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7150::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7150::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7150::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::TEST: + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7150::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7150::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7150::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7150::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7150::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7150::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7150::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7150::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h new file mode 100644 index 0000000000..4aad4e1720 --- /dev/null +++ b/esphome/components/pn7150/pn7150.h @@ -0,0 +1,296 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7150 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 3, // length + 0x06, // VBAT1 connected to 5V (CFG2) + 0x64, // TVDD monitoring threshold = 5.0V; TxLDO voltage = 4.7V (in reader & card modes) + 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7150 : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp new file mode 100644 index 0000000000..0443929f69 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_classic"; + +uint8_t PN7150::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7150::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7150::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7150::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7150::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp new file mode 100644 index 0000000000..791b0634d6 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_ultralight"; + +uint8_t PN7150::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7150::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7150::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/__init__.py b/esphome/components/pn7150_i2c/__init__.py new file mode 100644 index 0000000000..5f48a0f3cb --- /dev/null +++ b/esphome/components/pn7150_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7150 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7150"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7150_i2c_ns = cg.esphome_ns.namespace("pn7150_i2c") +PN7150I2C = pn7150_i2c_ns.class_("PN7150I2C", pn7150.PN7150, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7150.PN7150_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7150I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7150.setup_pn7150(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp new file mode 100644 index 0000000000..38b3102b37 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7150_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7150_i2c { + +static const char *const TAG = "pn7150_i2c"; + +uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7150::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7150I2C::dump_config() { + PN7150::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.h b/esphome/components/pn7150_i2c/pn7150_i2c.h new file mode 100644 index 0000000000..9308dddd26 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7150_i2c { + +class PN7150I2C : public pn7150::PN7150, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 59818bbde5..f7b433cce2 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3388,6 +3388,15 @@ pn532_spi: pn532_i2c: i2c_id: i2c_bus +pn7150_i2c: + i2c_id: i2c_bus + irq_pin: + allow_other_uses: true + number: GPIO32 + ven_pin: + allow_other_uses: true + number: GPIO16 + pn7160_i2c: id: nfcc_pn7160_i2c i2c_id: i2c_bus From 8cc44766e6acc236bf2c7e70e24a4c5531a9ffba Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 08:30:45 +0900 Subject: [PATCH 143/436] Bump version to 2023.12.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 101a690db5..b8495a3a7c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0-dev" +__version__ = "2023.12.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 9f27eadaee108cde1410c9de65fa5a82bd555e55 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 08:30:45 +0900 Subject: [PATCH 144/436] Bump version to 2024.1.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 101a690db5..5de34b86cd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0-dev" +__version__ = "2024.1.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 777cdb1c21ec3b467f47cf71ffdc53ee9112b69a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:24:16 +0900 Subject: [PATCH 145/436] Allow use of CDC/JTAG loggers on esp32 variants with Arduino (#4658) * Allow use of CDC/JTAG loggers on esp32 variants with Arduino * Only on s2/s3 * Separate C3 from S2/S3 * C code builds & runs correctly, still needs work though * Works on S2 * It works! * Remove unnecessary header --------- Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 2 -- esphome/components/logger/logger.cpp | 32 ++++++++++++++++++++++++--- esphome/components/logger/logger.h | 2 -- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e431997276..11a7f996f0 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -124,8 +124,6 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: - if value.upper() in ESP_IDF_UARTS and not CORE.using_esp_idf: - raise cv.Invalid(f"Only esp-idf framework supports {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2d2524b5f4..e0ca0806cb 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -236,8 +236,13 @@ void Logger::pre_setup() { this->hw_serial_ = &Serial1; Serial1.begin(this->baud_rate_); #else +#if ARDUINO_USB_CDC_ON_BOOT this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif #endif #ifdef USE_ESP8266 if (this->uart_ == UART_SELECTION_UART0_SWAP) { @@ -265,12 +270,35 @@ void Logger::pre_setup() { Serial2.begin(this->baud_rate_); break; #endif +#if defined(USE_ESP32) && \ + (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3)) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_CDC: +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_SERIAL_JTAG: +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 +#ifdef USE_ESP32_VARIANT_ESP32C3 + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // USE_ESP32_VARIANT_ESP32C3 +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // ARDUINO_USB_CDC_ON_BOOT +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 + break; +#endif // USE_ESP32 && (USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3) #ifdef USE_RP2040 case UART_SELECTION_USB_CDC: this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); break; -#endif +#endif // USE_RP2040 } #endif // USE_ARDUINO #ifdef USE_ESP_IDF @@ -393,14 +421,12 @@ const char *const UART_SELECTIONS[] = { "UART2", #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARINT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#if defined(USE_ESP_IDF) #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_CDC", #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_SERIAL_JTAG", #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 -#endif // USE_ESP_IDF }; #endif // USE_ESP32 #ifdef USE_ESP8266 diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 3816b1dd14..68efc056df 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -45,7 +45,6 @@ enum UARTSelection { UART_SELECTION_UART2, #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#ifdef USE_ESP_IDF #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) UART_SELECTION_USB_CDC, #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 @@ -54,7 +53,6 @@ enum UARTSelection { UART_SELECTION_USB_SERIAL_JTAG, #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || // USE_ESP32_VARIANT_ESP32H2 -#endif // USE_ESP_IDF #endif // USE_ESP32 #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, From 8c37066ed90a5eba7cf7ca95783c6ce4c09ace7b Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 14 Dec 2023 05:47:31 +0100 Subject: [PATCH 146/436] [Logger] ESP32 S3 serial logger (#4853) * Add support for ESP32 S3 logger. * fix default * Remove cpp & h changes to combine with PR #4658 * Not enough attention to details. * Add build flag * Validation fix * Fix validation for real this time --------- Co-authored-by: Your Name Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 10 ++++- esphome/config_validation.py | 53 +++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 11a7f996f0..be302bd489 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -97,7 +97,7 @@ UART_SELECTION_LIBRETINY = { COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } -ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] +ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] @@ -124,6 +124,8 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: + if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS: + raise cv.Invalid(f"Arduino framework does not support {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) @@ -169,6 +171,8 @@ CONFIG_SCHEMA = cv.All( CONF_HARDWARE_UART, esp8266=UART0, esp32=UART0, + esp32_s2=USB_CDC, + esp32_s3=USB_CDC, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, @@ -256,6 +260,10 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") + if CORE.using_arduino: + if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ad2ee11512..fdbe7d6320 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1528,6 +1528,12 @@ class SplitDefault(Optional): esp32=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, + esp32_s2=vol.UNDEFINED, + esp32_s2_arduino=vol.UNDEFINED, + esp32_s2_idf=vol.UNDEFINED, + esp32_s3=vol.UNDEFINED, + esp32_s3_arduino=vol.UNDEFINED, + esp32_s3_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1541,6 +1547,26 @@ class SplitDefault(Optional): self._esp32_idf_default = vol.default_factory( esp32_idf if esp32 is vol.UNDEFINED else esp32 ) + self._esp32_s2_arduino_default = vol.default_factory( + (esp32_s2_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s2_idf_default = vol.default_factory( + (esp32_s2_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s3_arduino_default = vol.default_factory( + (esp32_s3_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) + self._esp32_s3_idf_default = vol.default_factory( + (esp32_s3_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) self._rtl87xx_default = vol.default_factory(rtl87xx) @@ -1550,10 +1576,29 @@ class SplitDefault(Optional): def default(self): if CORE.is_esp8266: return self._esp8266_default - if CORE.is_esp32 and CORE.using_arduino: - return self._esp32_arduino_default - if CORE.is_esp32 and CORE.using_esp_idf: - return self._esp32_idf_default + if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import ( + VARIANT_ESP32S2, + VARIANT_ESP32S3, + ) + + variant = get_esp32_variant() + if variant == VARIANT_ESP32S2: + if CORE.using_arduino: + return self._esp32_s2_arduino_default + if CORE.using_esp_idf: + return self._esp32_s2_idf_default + elif variant == VARIANT_ESP32S3: + if CORE.using_arduino: + return self._esp32_s3_arduino_default + if CORE.using_esp_idf: + return self._esp32_s3_idf_default + else: + if CORE.using_arduino: + return self._esp32_arduino_default + if CORE.using_esp_idf: + return self._esp32_idf_default if CORE.is_rp2040: return self._rp2040_default if CORE.is_bk72xx: From a3cc5518565c56239808b34e3881090c5385632a Mon Sep 17 00:00:00 2001 From: jochenvg Date: Thu, 14 Dec 2023 05:01:01 +0000 Subject: [PATCH 147/436] Support toggle action for template cover (#5917) --- esphome/components/template/cover/__init__.py | 7 +++++++ esphome/components/template/cover/template_cover.cpp | 10 ++++++++++ esphome/components/template/cover/template_cover.h | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 8844ddd6ab..43d0be99b4 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -31,6 +31,7 @@ RESTORE_MODES = { } CONF_HAS_POSITION = "has_position" +CONF_TOGGLE_ACTION = "toggle_action" CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( { @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( RESTORE_MODES, upper=True @@ -74,6 +76,11 @@ async def to_code(config): var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) cg.add(var.set_has_stop(True)) + if CONF_TOGGLE_ACTION in config: + await automation.build_automation( + var.get_toggle_trigger(), [], config[CONF_TOGGLE_ACTION] + ) + cg.add(var.set_has_toggle(True)) if CONF_TILT_ACTION in config: await automation.build_automation( var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index b16e439943..2d6c3087ae 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -12,6 +12,7 @@ TemplateCover::TemplateCover() : open_trigger_(new Trigger<>()), close_trigger_(new Trigger<>), stop_trigger_(new Trigger<>()), + toggle_trigger_(new Trigger<>()), position_trigger_(new Trigger()), tilt_trigger_(new Trigger()) {} void TemplateCover::setup() { @@ -68,6 +69,7 @@ float TemplateCover::get_setup_priority() const { return setup_priority::HARDWAR Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } +Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::control(const CoverCall &call) { if (call.get_stop()) { @@ -76,6 +78,12 @@ void TemplateCover::control(const CoverCall &call) { this->prev_command_trigger_ = this->stop_trigger_; this->publish_state(); } + if (call.get_toggle().has_value()) { + this->stop_prev_trigger_(); + this->toggle_trigger_->trigger(); + this->prev_command_trigger_ = this->toggle_trigger_; + this->publish_state(); + } if (call.get_position().has_value()) { auto pos = *call.get_position(); this->stop_prev_trigger_(); @@ -110,6 +118,7 @@ CoverTraits TemplateCover::get_traits() { auto traits = CoverTraits(); traits.set_is_assumed_state(this->assumed_state_); traits.set_supports_stop(this->has_stop_); + traits.set_supports_toggle(this->has_toggle_); traits.set_supports_position(this->has_position_); traits.set_supports_tilt(this->has_tilt_); return traits; @@ -118,6 +127,7 @@ Trigger *TemplateCover::get_position_trigger() const { return this->posit Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } +void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 4ff5caf1db..958c94b0a6 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -21,6 +21,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; + Trigger<> *get_toggle_trigger() const; Trigger *get_position_trigger() const; Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); @@ -29,6 +30,7 @@ class TemplateCover : public cover::Cover, public Component { void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); + void set_has_toggle(bool has_toggle); void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } void setup() override; @@ -50,7 +52,9 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *open_trigger_; Trigger<> *close_trigger_; bool has_stop_{false}; + bool has_toggle_{false}; Trigger<> *stop_trigger_; + Trigger<> *toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; Trigger *position_trigger_; bool has_position_{false}; From 0a188ad9d22a2b5a8060ec7ad3318cb9dba509e2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:33:04 +0900 Subject: [PATCH 148/436] Fix SplitDefault with variants (#5928) --- esphome/config_validation.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index fdbe7d6320..7b94608509 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1518,6 +1518,13 @@ class GenerateID(Optional): super().__init__(key, default=lambda: None) +def _get_priority_default(*args): + for arg in args: + if arg is not vol.UNDEFINED: + return arg + return vol.UNDEFINED + + class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" @@ -1542,30 +1549,22 @@ class SplitDefault(Optional): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_arduino_default = vol.default_factory( - esp32_arduino if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32, esp32_arduino) ) self._esp32_idf_default = vol.default_factory( - esp32_idf if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32, esp32_idf) ) self._esp32_s2_arduino_default = vol.default_factory( - (esp32_s2_arduino if esp32 is vol.UNDEFINED else esp32) - if esp32_s2 is vol.UNDEFINED - else esp32_s2 + _get_priority_default(esp32_s2, esp32, esp32_s2_arduino, esp32_arduino) ) self._esp32_s2_idf_default = vol.default_factory( - (esp32_s2_idf if esp32 is vol.UNDEFINED else esp32) - if esp32_s2 is vol.UNDEFINED - else esp32_s2 + _get_priority_default(esp32_s2, esp32, esp32_s2_idf, esp32_idf) ) self._esp32_s3_arduino_default = vol.default_factory( - (esp32_s3_arduino if esp32 is vol.UNDEFINED else esp32) - if esp32_s3 is vol.UNDEFINED - else esp32_s3 + _get_priority_default(esp32_s3, esp32, esp32_s3_arduino, esp32_arduino) ) self._esp32_s3_idf_default = vol.default_factory( - (esp32_s3_idf if esp32 is vol.UNDEFINED else esp32) - if esp32_s3 is vol.UNDEFINED - else esp32_s3 + _get_priority_default(esp32_s3, esp32, esp32_s3_idf, esp32_idf) ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) From 300343ae2495ef66407d95254308517b3857ac40 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:24:52 +1100 Subject: [PATCH 149/436] ESP32-S3 and ESP-IDF don't play well with USB_CDC and need USB_SERIAL_JTAG (#5929) Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 4 +- esphome/config_validation.py | 27 +++++++--- tests/test8.1.yaml | 78 +++++++++++++++++++++++++++ tests/test8.2.yaml | 75 ++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 tests/test8.1.yaml create mode 100644 tests/test8.2.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index be302bd489..6cad783db9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -172,7 +172,9 @@ CONFIG_SCHEMA = cv.All( esp8266=UART0, esp32=UART0, esp32_s2=USB_CDC, - esp32_s3=USB_CDC, + esp32_s3_idf=USB_SERIAL_JTAG, + esp32_c3_idf=USB_SERIAL_JTAG, + esp32_s3_arduino=USB_CDC, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 7b94608509..8f2e080b46 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1541,6 +1541,9 @@ class SplitDefault(Optional): esp32_s3=vol.UNDEFINED, esp32_s3_arduino=vol.UNDEFINED, esp32_s3_idf=vol.UNDEFINED, + esp32_c3=vol.UNDEFINED, + esp32_c3_arduino=vol.UNDEFINED, + esp32_c3_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1549,22 +1552,28 @@ class SplitDefault(Optional): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_arduino_default = vol.default_factory( - _get_priority_default(esp32, esp32_arduino) + _get_priority_default(esp32_arduino, esp32) ) self._esp32_idf_default = vol.default_factory( - _get_priority_default(esp32, esp32_idf) + _get_priority_default(esp32_idf, esp32) ) self._esp32_s2_arduino_default = vol.default_factory( - _get_priority_default(esp32_s2, esp32, esp32_s2_arduino, esp32_arduino) + _get_priority_default(esp32_s2_arduino, esp32_s2, esp32_arduino, esp32) ) self._esp32_s2_idf_default = vol.default_factory( - _get_priority_default(esp32_s2, esp32, esp32_s2_idf, esp32_idf) + _get_priority_default(esp32_s2_idf, esp32_s2, esp32_idf, esp32) ) self._esp32_s3_arduino_default = vol.default_factory( - _get_priority_default(esp32_s3, esp32, esp32_s3_arduino, esp32_arduino) + _get_priority_default(esp32_s3_arduino, esp32_s3, esp32_arduino, esp32) ) self._esp32_s3_idf_default = vol.default_factory( - _get_priority_default(esp32_s3, esp32, esp32_s3_idf, esp32_idf) + _get_priority_default(esp32_s3_idf, esp32_s3, esp32_idf, esp32) + ) + self._esp32_c3_arduino_default = vol.default_factory( + _get_priority_default(esp32_c3_arduino, esp32_c3, esp32_arduino, esp32) + ) + self._esp32_c3_idf_default = vol.default_factory( + _get_priority_default(esp32_c3_idf, esp32_c3, esp32_idf, esp32) ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) @@ -1580,6 +1589,7 @@ class SplitDefault(Optional): from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, + VARIANT_ESP32C3, ) variant = get_esp32_variant() @@ -1593,6 +1603,11 @@ class SplitDefault(Optional): return self._esp32_s3_arduino_default if CORE.using_esp_idf: return self._esp32_s3_idf_default + elif variant == VARIANT_ESP32C3: + if CORE.using_arduino: + return self._esp32_c3_arduino_default + if CORE.using_esp_idf: + return self._esp32_c3_idf_default else: if CORE.using_arduino: return self._esp32_arduino_default diff --git a/tests/test8.1.yaml b/tests/test8.1.yaml new file mode 100644 index 0000000000..bc1d2e22a4 --- /dev/null +++ b/tests/test8.1.yaml @@ -0,0 +1,78 @@ +# Tests for ESP32-S3 boards - IDf +--- +wifi: + ssid: "ssid" + +network: + enable_ipv6: true + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: esp-idf + +esphome: + name: esp32-s3-test + +logger: + +debug: + +psram: + +spi: + - id: spi_id_1 + clk_pin: + number: GPIO7 + allow_other_uses: false + mosi_pin: GPIO6 + interface: any + +spi_device: + id: spidev + data_rate: 2MHz + spi_id: spi_id_1 + mode: 3 + bit_order: lsb_first + +display: + - platform: ili9xxx + id: displ8 + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: + number: GPIO48 + allow_other_uses: true + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: tt21100 + display: displ8 + interrupt_pin: + number: GPIO3 + ignore_strapping_warning: true + allow_other_uses: false + reset_pin: + number: GPIO48 + allow_other_uses: true + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 + +sensor: + - platform: debug + free: + name: "Heap Free" + block: + name: "Max Block Free" + loop_time: + name: "Loop Time" + psram: + name: "PSRAM Free" diff --git a/tests/test8.2.yaml b/tests/test8.2.yaml new file mode 100644 index 0000000000..69525b333b --- /dev/null +++ b/tests/test8.2.yaml @@ -0,0 +1,75 @@ +# Tests for ESP32-C3 boards - IDf +--- +wifi: + ssid: "ssid" + +network: + enable_ipv6: true + +esp32: + board: lolin_c3_mini + variant: ESP32C3 + framework: + type: esp-idf + +esphome: + name: esp32-c3-test + +logger: + +debug: + +psram: + +spi: + - id: spi_id_1 + clk_pin: + number: GPIO7 + allow_other_uses: false + mosi_pin: GPIO6 + interface: any + +spi_device: + id: spidev + data_rate: 2MHz + spi_id: spi_id_1 + mode: 3 + bit_order: lsb_first + +display: + - platform: ili9xxx + id: displ8 + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: + number: GPIO21 + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: tt21100 + display: displ8 + interrupt_pin: + number: GPIO3 + allow_other_uses: false + reset_pin: + number: GPIO20 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 + +sensor: + - platform: debug + free: + name: "Heap Free" + block: + name: "Max Block Free" + loop_time: + name: "Loop Time" + psram: + name: "PSRAM Free" From 836a3db163b130a4aecf3a632868dc3a90c609c8 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:39:05 +1000 Subject: [PATCH 150/436] Update ENS160 TVOC device_class and AQI units to match required by HA (#5939) --- esphome/components/ens160/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/ens160/sensor.py b/esphome/components/ens160/sensor.py index 55f0ff7b6f..393b63bae1 100644 --- a/esphome/components/ens160/sensor.py +++ b/esphome/components/ens160/sensor.py @@ -9,7 +9,7 @@ from esphome.const import ( CONF_TVOC, DEVICE_CLASS_AQI, DEVICE_CLASS_CARBON_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, ICON_CHEMICAL_WEAPON, ICON_MOLECULE_CO2, ICON_RADIATOR, @@ -45,11 +45,10 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_AQI): sensor.sensor_schema( - unit_of_measurement=UNIT_INDEX, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, From 3c3ac920380dd07c38fa11860da21e944f2a1f01 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:24:16 +0900 Subject: [PATCH 151/436] Allow use of CDC/JTAG loggers on esp32 variants with Arduino (#4658) * Allow use of CDC/JTAG loggers on esp32 variants with Arduino * Only on s2/s3 * Separate C3 from S2/S3 * C code builds & runs correctly, still needs work though * Works on S2 * It works! * Remove unnecessary header --------- Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 2 -- esphome/components/logger/logger.cpp | 32 ++++++++++++++++++++++++--- esphome/components/logger/logger.h | 2 -- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e431997276..11a7f996f0 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -124,8 +124,6 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: - if value.upper() in ESP_IDF_UARTS and not CORE.using_esp_idf: - raise cv.Invalid(f"Only esp-idf framework supports {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2d2524b5f4..e0ca0806cb 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -236,8 +236,13 @@ void Logger::pre_setup() { this->hw_serial_ = &Serial1; Serial1.begin(this->baud_rate_); #else +#if ARDUINO_USB_CDC_ON_BOOT this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif #endif #ifdef USE_ESP8266 if (this->uart_ == UART_SELECTION_UART0_SWAP) { @@ -265,12 +270,35 @@ void Logger::pre_setup() { Serial2.begin(this->baud_rate_); break; #endif +#if defined(USE_ESP32) && \ + (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3)) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_CDC: +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_SERIAL_JTAG: +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 +#ifdef USE_ESP32_VARIANT_ESP32C3 + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // USE_ESP32_VARIANT_ESP32C3 +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // ARDUINO_USB_CDC_ON_BOOT +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 + break; +#endif // USE_ESP32 && (USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3) #ifdef USE_RP2040 case UART_SELECTION_USB_CDC: this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); break; -#endif +#endif // USE_RP2040 } #endif // USE_ARDUINO #ifdef USE_ESP_IDF @@ -393,14 +421,12 @@ const char *const UART_SELECTIONS[] = { "UART2", #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARINT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#if defined(USE_ESP_IDF) #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_CDC", #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_SERIAL_JTAG", #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 -#endif // USE_ESP_IDF }; #endif // USE_ESP32 #ifdef USE_ESP8266 diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 3816b1dd14..68efc056df 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -45,7 +45,6 @@ enum UARTSelection { UART_SELECTION_UART2, #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#ifdef USE_ESP_IDF #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) UART_SELECTION_USB_CDC, #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 @@ -54,7 +53,6 @@ enum UARTSelection { UART_SELECTION_USB_SERIAL_JTAG, #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || // USE_ESP32_VARIANT_ESP32H2 -#endif // USE_ESP_IDF #endif // USE_ESP32 #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, From 3e475c21ff9664c95b347375fe43b5647afdad03 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 14 Dec 2023 05:47:31 +0100 Subject: [PATCH 152/436] [Logger] ESP32 S3 serial logger (#4853) * Add support for ESP32 S3 logger. * fix default * Remove cpp & h changes to combine with PR #4658 * Not enough attention to details. * Add build flag * Validation fix * Fix validation for real this time --------- Co-authored-by: Your Name Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 10 ++++- esphome/config_validation.py | 53 +++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 11a7f996f0..be302bd489 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -97,7 +97,7 @@ UART_SELECTION_LIBRETINY = { COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } -ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] +ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] @@ -124,6 +124,8 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: + if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS: + raise cv.Invalid(f"Arduino framework does not support {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) @@ -169,6 +171,8 @@ CONFIG_SCHEMA = cv.All( CONF_HARDWARE_UART, esp8266=UART0, esp32=UART0, + esp32_s2=USB_CDC, + esp32_s3=USB_CDC, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, @@ -256,6 +260,10 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") + if CORE.using_arduino: + if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ad2ee11512..fdbe7d6320 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1528,6 +1528,12 @@ class SplitDefault(Optional): esp32=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, + esp32_s2=vol.UNDEFINED, + esp32_s2_arduino=vol.UNDEFINED, + esp32_s2_idf=vol.UNDEFINED, + esp32_s3=vol.UNDEFINED, + esp32_s3_arduino=vol.UNDEFINED, + esp32_s3_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1541,6 +1547,26 @@ class SplitDefault(Optional): self._esp32_idf_default = vol.default_factory( esp32_idf if esp32 is vol.UNDEFINED else esp32 ) + self._esp32_s2_arduino_default = vol.default_factory( + (esp32_s2_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s2_idf_default = vol.default_factory( + (esp32_s2_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s3_arduino_default = vol.default_factory( + (esp32_s3_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) + self._esp32_s3_idf_default = vol.default_factory( + (esp32_s3_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) self._rtl87xx_default = vol.default_factory(rtl87xx) @@ -1550,10 +1576,29 @@ class SplitDefault(Optional): def default(self): if CORE.is_esp8266: return self._esp8266_default - if CORE.is_esp32 and CORE.using_arduino: - return self._esp32_arduino_default - if CORE.is_esp32 and CORE.using_esp_idf: - return self._esp32_idf_default + if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import ( + VARIANT_ESP32S2, + VARIANT_ESP32S3, + ) + + variant = get_esp32_variant() + if variant == VARIANT_ESP32S2: + if CORE.using_arduino: + return self._esp32_s2_arduino_default + if CORE.using_esp_idf: + return self._esp32_s2_idf_default + elif variant == VARIANT_ESP32S3: + if CORE.using_arduino: + return self._esp32_s3_arduino_default + if CORE.using_esp_idf: + return self._esp32_s3_idf_default + else: + if CORE.using_arduino: + return self._esp32_arduino_default + if CORE.using_esp_idf: + return self._esp32_idf_default if CORE.is_rp2040: return self._rp2040_default if CORE.is_bk72xx: From f28cf9348e486b3aab5c2b3e6a04f372c4ad4364 Mon Sep 17 00:00:00 2001 From: jochenvg Date: Thu, 14 Dec 2023 05:01:01 +0000 Subject: [PATCH 153/436] Support toggle action for template cover (#5917) --- esphome/components/template/cover/__init__.py | 7 +++++++ esphome/components/template/cover/template_cover.cpp | 10 ++++++++++ esphome/components/template/cover/template_cover.h | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 8844ddd6ab..43d0be99b4 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -31,6 +31,7 @@ RESTORE_MODES = { } CONF_HAS_POSITION = "has_position" +CONF_TOGGLE_ACTION = "toggle_action" CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( { @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( RESTORE_MODES, upper=True @@ -74,6 +76,11 @@ async def to_code(config): var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) cg.add(var.set_has_stop(True)) + if CONF_TOGGLE_ACTION in config: + await automation.build_automation( + var.get_toggle_trigger(), [], config[CONF_TOGGLE_ACTION] + ) + cg.add(var.set_has_toggle(True)) if CONF_TILT_ACTION in config: await automation.build_automation( var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index b16e439943..2d6c3087ae 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -12,6 +12,7 @@ TemplateCover::TemplateCover() : open_trigger_(new Trigger<>()), close_trigger_(new Trigger<>), stop_trigger_(new Trigger<>()), + toggle_trigger_(new Trigger<>()), position_trigger_(new Trigger()), tilt_trigger_(new Trigger()) {} void TemplateCover::setup() { @@ -68,6 +69,7 @@ float TemplateCover::get_setup_priority() const { return setup_priority::HARDWAR Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } +Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::control(const CoverCall &call) { if (call.get_stop()) { @@ -76,6 +78,12 @@ void TemplateCover::control(const CoverCall &call) { this->prev_command_trigger_ = this->stop_trigger_; this->publish_state(); } + if (call.get_toggle().has_value()) { + this->stop_prev_trigger_(); + this->toggle_trigger_->trigger(); + this->prev_command_trigger_ = this->toggle_trigger_; + this->publish_state(); + } if (call.get_position().has_value()) { auto pos = *call.get_position(); this->stop_prev_trigger_(); @@ -110,6 +118,7 @@ CoverTraits TemplateCover::get_traits() { auto traits = CoverTraits(); traits.set_is_assumed_state(this->assumed_state_); traits.set_supports_stop(this->has_stop_); + traits.set_supports_toggle(this->has_toggle_); traits.set_supports_position(this->has_position_); traits.set_supports_tilt(this->has_tilt_); return traits; @@ -118,6 +127,7 @@ Trigger *TemplateCover::get_position_trigger() const { return this->posit Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } +void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 4ff5caf1db..958c94b0a6 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -21,6 +21,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; + Trigger<> *get_toggle_trigger() const; Trigger *get_position_trigger() const; Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); @@ -29,6 +30,7 @@ class TemplateCover : public cover::Cover, public Component { void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); + void set_has_toggle(bool has_toggle); void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } void setup() override; @@ -50,7 +52,9 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *open_trigger_; Trigger<> *close_trigger_; bool has_stop_{false}; + bool has_toggle_{false}; Trigger<> *stop_trigger_; + Trigger<> *toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; Trigger *position_trigger_; bool has_position_{false}; From 6b5eb7e656440ea03eaa6784172f662b1194bc3b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:33:04 +0900 Subject: [PATCH 154/436] Fix SplitDefault with variants (#5928) --- esphome/config_validation.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index fdbe7d6320..7b94608509 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1518,6 +1518,13 @@ class GenerateID(Optional): super().__init__(key, default=lambda: None) +def _get_priority_default(*args): + for arg in args: + if arg is not vol.UNDEFINED: + return arg + return vol.UNDEFINED + + class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" @@ -1542,30 +1549,22 @@ class SplitDefault(Optional): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_arduino_default = vol.default_factory( - esp32_arduino if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32, esp32_arduino) ) self._esp32_idf_default = vol.default_factory( - esp32_idf if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32, esp32_idf) ) self._esp32_s2_arduino_default = vol.default_factory( - (esp32_s2_arduino if esp32 is vol.UNDEFINED else esp32) - if esp32_s2 is vol.UNDEFINED - else esp32_s2 + _get_priority_default(esp32_s2, esp32, esp32_s2_arduino, esp32_arduino) ) self._esp32_s2_idf_default = vol.default_factory( - (esp32_s2_idf if esp32 is vol.UNDEFINED else esp32) - if esp32_s2 is vol.UNDEFINED - else esp32_s2 + _get_priority_default(esp32_s2, esp32, esp32_s2_idf, esp32_idf) ) self._esp32_s3_arduino_default = vol.default_factory( - (esp32_s3_arduino if esp32 is vol.UNDEFINED else esp32) - if esp32_s3 is vol.UNDEFINED - else esp32_s3 + _get_priority_default(esp32_s3, esp32, esp32_s3_arduino, esp32_arduino) ) self._esp32_s3_idf_default = vol.default_factory( - (esp32_s3_idf if esp32 is vol.UNDEFINED else esp32) - if esp32_s3 is vol.UNDEFINED - else esp32_s3 + _get_priority_default(esp32_s3, esp32, esp32_s3_idf, esp32_idf) ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) From e030c0fc45c1105cc21e2ddb12db9c0c90766a2b Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:39:05 +1000 Subject: [PATCH 155/436] Update ENS160 TVOC device_class and AQI units to match required by HA (#5939) --- esphome/components/ens160/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/ens160/sensor.py b/esphome/components/ens160/sensor.py index 55f0ff7b6f..393b63bae1 100644 --- a/esphome/components/ens160/sensor.py +++ b/esphome/components/ens160/sensor.py @@ -9,7 +9,7 @@ from esphome.const import ( CONF_TVOC, DEVICE_CLASS_AQI, DEVICE_CLASS_CARBON_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, ICON_CHEMICAL_WEAPON, ICON_MOLECULE_CO2, ICON_RADIATOR, @@ -45,11 +45,10 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_AQI): sensor.sensor_schema( - unit_of_measurement=UNIT_INDEX, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, From 514db8b26e8614902b0e8c849802d22709df018b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:02:47 +0900 Subject: [PATCH 156/436] Bump version to 2023.12.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index b8495a3a7c..6c2a520331 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0b1" +__version__ = "2023.12.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ea4e618f2af48b25c813ca9f54eddae530077c29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 16:27:08 +0000 Subject: [PATCH 157/436] Bump zeroconf from 0.128.4 to 0.130.0 (#5950) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f330ecbf3e..20a5514e71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==21.0.0 -zeroconf==0.128.4 +zeroconf==0.130.0 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From 94904f44f9d3d4f596f93faa1f7f7ec0d98129d5 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 18 Dec 2023 00:19:30 +0100 Subject: [PATCH 158/436] UARTComponent inline doc (#5930) --- esphome/components/uart/uart_component.h | 88 +++++++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 34bda42bb5..e03784fdd8 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -31,40 +31,122 @@ const LogString *parity_to_str(UARTParityOptions parity); class UARTComponent { public: + // Writes an array of bytes to the UART bus. + // @param data A vector of bytes to be written. void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } + + // Writes a single byte to the UART bus. + // @param data The byte to be written. void write_byte(uint8_t data) { this->write_array(&data, 1); }; + + // Writes a null-terminated string to the UART bus. + // @param str Pointer to the null-terminated string. void write_str(const char *str) { const auto *data = reinterpret_cast(str); this->write_array(data, strlen(str)); }; + // Pure virtual method to write an array of bytes to the UART bus. + // @param data Pointer to the array of bytes. + // @param len Length of the array. virtual void write_array(const uint8_t *data, size_t len) = 0; + // Reads a single byte from the UART bus. + // @param data Pointer to the byte where the read data will be stored. + // @return True if a byte was successfully read, false otherwise. bool read_byte(uint8_t *data) { return this->read_array(data, 1); }; + + // Pure virtual method to peek the next byte in the UART buffer without removing it. + // @param data Pointer to the byte where the peeked data will be stored. + // @return True if a byte is available to peek, false otherwise. virtual bool peek_byte(uint8_t *data) = 0; + + // Pure virtual method to read an array of bytes from the UART bus. + // @param data Pointer to the array where the read data will be stored. + // @param len Number of bytes to read. + // @return True if the specified number of bytes were successfully read, false otherwise. virtual bool read_array(uint8_t *data, size_t len) = 0; - /// Return available number of bytes. + // Pure virtual method to return the number of bytes available for reading. + // @return Number of available bytes. virtual int available() = 0; - /// Block until all bytes have been written to the UART bus. + + // Pure virtual method to block until all bytes have been written to the UART bus. virtual void flush() = 0; + // Sets the TX (transmit) pin for the UART bus. + // @param tx_pin Pointer to the internal GPIO pin used for transmission. void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; } + + // Sets the RX (receive) pin for the UART bus. + // @param rx_pin Pointer to the internal GPIO pin used for reception. void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } + + // Sets the size of the RX buffer. + // @param rx_buffer_size Size of the RX buffer in bytes. void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } + + // Gets the size of the RX buffer. + // @return Size of the RX buffer in bytes. size_t get_rx_buffer_size() { return this->rx_buffer_size_; } + // Sets the number of stop bits used in UART communication. + // @param stop_bits Number of stop bits. void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } + + // Gets the number of stop bits used in UART communication. + // @return Number of stop bits. uint8_t get_stop_bits() const { return this->stop_bits_; } + + // Set the number of data bits used in UART communication. + // @param data_bits Number of data bits. void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } + + // Get the number of data bits used in UART communication. + // @return Number of data bits. uint8_t get_data_bits() const { return this->data_bits_; } + + // Set the parity used in UART communication. + // @param parity Parity option. void set_parity(UARTParityOptions parity) { this->parity_ = parity; } + + // Get the parity used in UART communication. + // @return Parity option. UARTParityOptions get_parity() const { return this->parity_; } + + // Set the baud rate for UART communication. + // @param baud_rate Baud rate in bits per second. void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } + + // Get the baud rate for UART communication. + // @return Baud rate in bits per second. uint32_t get_baud_rate() const { return baud_rate_; } + #ifdef USE_ESP32 - virtual void load_settings() = 0; + /** + * Load the UART settings. + * @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly. + * + * Example: + * ```cpp + * id(uart1).load_settings(false); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ virtual void load_settings(bool dump_config) = 0; + + /** + * Load the UART settings. + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + virtual void load_settings() = 0; #endif // USE_ESP32 #ifdef USE_UART_DEBUGGER From 003d8b0cf573ce5a6397fe00c398d6a789012033 Mon Sep 17 00:00:00 2001 From: Grant Le Roux Date: Mon, 18 Dec 2023 07:28:48 +0800 Subject: [PATCH 159/436] Fix - Tuya Fan - Allow integer speed datapoint (#5948) Co-authored-by: Cram42 <5396871+cram42@users.noreply.github.com> --- esphome/components/tuya/fan/tuya_fan.cpp | 23 +++++++++++++++++------ esphome/components/tuya/fan/tuya_fan.h | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 1b03ea50fa..481c931f2e 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -9,13 +9,20 @@ static const char *const TAG = "tuya.fan"; void TuyaFan::setup() { if (this->speed_id_.has_value()) { this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) { - ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); - if (datapoint.value_enum >= this->speed_count_) { - ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum); - } else { - this->speed = datapoint.value_enum + 1; + if (datapoint.type == TuyaDatapointType::ENUM) { + ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); + if (datapoint.value_enum >= this->speed_count_) { + ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum); + } else { + this->speed = datapoint.value_enum + 1; + this->publish_state(); + } + } else if (datapoint.type == TuyaDatapointType::INTEGER) { + ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_int); + this->speed = datapoint.value_int; this->publish_state(); } + this->speed_type_ = datapoint.type; }); } if (this->switch_id_.has_value()) { @@ -80,7 +87,11 @@ void TuyaFan::control(const fan::FanCall &call) { this->parent_->set_enum_datapoint_value(*this->direction_id_, enable); } if (this->speed_id_.has_value() && call.get_speed().has_value()) { - this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); + if (this->speed_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); + } else if (this->speed_type_ == TuyaDatapointType::INTEGER) { + this->parent_->set_integer_datapoint_value(*this->speed_id_, *call.get_speed()); + } } } diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 4aba1e1c07..77b2cc6383 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -28,6 +28,7 @@ class TuyaFan : public Component, public fan::Fan { optional oscillation_id_{}; optional direction_id_{}; int speed_count_{}; + TuyaDatapointType speed_type_{}; }; } // namespace tuya From 8a23b7e0c864fecc27a82f8f03c9af9afd9ea568 Mon Sep 17 00:00:00 2001 From: Alex Hermann Date: Mon, 18 Dec 2023 00:58:13 +0100 Subject: [PATCH 160/436] i2s_audio: Set player_task's prio to 1 (#5945) --- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index e729cdf954..95e63035fe 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -29,7 +29,7 @@ void I2SAudioSpeaker::start_() { } this->state_ = speaker::STATE_RUNNING; - xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_); + xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 1, &this->player_task_handle_); } void I2SAudioSpeaker::player_task(void *params) { From 8653972cb8d7ca4a896d260fbc753e30418449d9 Mon Sep 17 00:00:00 2001 From: Alex Hermann Date: Mon, 18 Dec 2023 01:00:42 +0100 Subject: [PATCH 161/436] esp32_camera: Set framebuffer task prio to 1 (#5943) --- esphome/components/esp32_camera/esp32_camera.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 99cb811fe4..555f6ca5f1 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -37,7 +37,7 @@ void ESP32Camera::setup() { "framebuffer_task", // name 1024, // stack size nullptr, // task pv params - 0, // priority + 1, // priority nullptr, // handle 1 // core ); From 29fb2a53609e1c0e2ce234926d6402c0c0d86a9a Mon Sep 17 00:00:00 2001 From: dentra Date: Mon, 18 Dec 2023 03:01:21 +0300 Subject: [PATCH 162/436] web_server_idf: fix call with hardcoded http code (#5942) --- esphome/components/web_server_idf/web_server_idf.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index bc64e5231e..2fbc5cd2e9 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -117,7 +117,7 @@ class AsyncWebServerRequest { // NOLINTNEXTLINE(readability-identifier-naming) AsyncWebServerResponse *beginResponse(int code, const char *content_type) { auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) - this->init_response_(res, 200, content_type); + this->init_response_(res, code, content_type); return res; } // NOLINTNEXTLINE(readability-identifier-naming) From 1d37edb63c95e9ccff69b13b849b499952bd3a34 Mon Sep 17 00:00:00 2001 From: Jean Louis-Guerin Date: Mon, 18 Dec 2023 01:03:01 +0100 Subject: [PATCH 163/436] Revert pure virtual functions in UART component from #5920 (#5932) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/uart/uart_component.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index e03784fdd8..6f27f36bcb 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -134,7 +134,7 @@ class UARTComponent { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - virtual void load_settings(bool dump_config) = 0; + virtual void load_settings(bool dump_config){}; /** * Load the UART settings. @@ -146,7 +146,7 @@ class UARTComponent { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - virtual void load_settings() = 0; + virtual void load_settings(){}; #endif // USE_ESP32 #ifdef USE_UART_DEBUGGER From 2060d1ac89bdd4f593eb06ac90cfbfa0f8e06b6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:30:27 +0900 Subject: [PATCH 164/436] Bump esptool from 4.6.2 to 4.7.0 (#5935) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 20a5514e71..e545a8023a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ tzlocal==5.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.11 # When updating platformio, also update Dockerfile -esptool==4.6.2 +esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==21.0.0 From 323f8c9bdb134a801d8439e882eb533ab51464c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:30:50 +0900 Subject: [PATCH 165/436] Bump actions/download-artifact from 3.0.2 to 4.0.0 (#5936) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3.0.2 to 4.0.0. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3.0.2...v4.0.0) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 625a8c8ecb..2972ae7fac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -158,7 +158,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Download digests - uses: actions/download-artifact@v3.0.2 + uses: actions/download-artifact@v4.0.0 with: name: digests-${{ matrix.image.target }}-${{ matrix.registry }} path: /tmp/digests From 0f4d7dadb38a1d5cffbe5f7aa5cdbd2302bee916 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Dec 2023 13:32:18 +0900 Subject: [PATCH 166/436] Bump build-image action versions (#5954) --- .github/actions/build-image/action.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 4c98a47ecd..cc7909d724 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -36,7 +36,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v5.0.0 + uses: docker/build-push-action@v5.1.0 with: context: . file: ./docker/Dockerfile @@ -58,7 +58,7 @@ runs: touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" - name: Upload ghcr digest - uses: actions/upload-artifact@v3.1.3 + uses: actions/upload-artifact@v4.0.0 with: name: digests-${{ inputs.target }}-ghcr path: /tmp/digests/${{ inputs.target }}/ghcr/* @@ -67,7 +67,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v5.0.0 + uses: docker/build-push-action@v5.1.0 with: context: . file: ./docker/Dockerfile @@ -89,7 +89,7 @@ runs: touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" - name: Upload dockerhub digest - uses: actions/upload-artifact@v3.1.3 + uses: actions/upload-artifact@v4.0.0 with: name: digests-${{ inputs.target }}-dockerhub path: /tmp/digests/${{ inputs.target }}/dockerhub/* From 89c6f3d45dce54dd2bae8904e67422ea7ff60fe1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:27:26 +0900 Subject: [PATCH 167/436] Revert "Bump build-image action versions" (#5955) --- .github/actions/build-image/action.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index cc7909d724..4c98a47ecd 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -36,7 +36,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v5.0.0 with: context: . file: ./docker/Dockerfile @@ -58,7 +58,7 @@ runs: touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" - name: Upload ghcr digest - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v3.1.3 with: name: digests-${{ inputs.target }}-ghcr path: /tmp/digests/${{ inputs.target }}/ghcr/* @@ -67,7 +67,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v5.0.0 with: context: . file: ./docker/Dockerfile @@ -89,7 +89,7 @@ runs: touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" - name: Upload dockerhub digest - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v3.1.3 with: name: digests-${{ inputs.target }}-dockerhub path: /tmp/digests/${{ inputs.target }}/dockerhub/* From bf258230cd4ddffa38e8da633069ee2921c05ca3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:29:24 +0900 Subject: [PATCH 168/436] Revert "Bump actions/download-artifact from 3.0.2 to 4.0.0" (#5956) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2972ae7fac..625a8c8ecb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -158,7 +158,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Download digests - uses: actions/download-artifact@v4.0.0 + uses: actions/download-artifact@v3.0.2 with: name: digests-${{ matrix.image.target }}-${{ matrix.registry }} path: /tmp/digests From d99598bba62baeb01861250da243437685f091cf Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 18 Dec 2023 01:33:12 -0600 Subject: [PATCH 169/436] Use the correct UART/`Serial` when CDC is enabled (#5957) --- esphome/components/logger/logger.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index e0ca0806cb..05b97a5f64 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -237,8 +237,8 @@ void Logger::pre_setup() { Serial1.begin(this->baud_rate_); #else #if ARDUINO_USB_CDC_ON_BOOT - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); + this->hw_serial_ = &Serial0; + Serial0.begin(this->baud_rate_); #else this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); From 70dac541139a7514dd06281152f233fc2efd3382 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 16:27:08 +0000 Subject: [PATCH 170/436] Bump zeroconf from 0.128.4 to 0.130.0 (#5950) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f330ecbf3e..20a5514e71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==21.0.0 -zeroconf==0.128.4 +zeroconf==0.130.0 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From 917e0f93edd561b13fd25da0b5ee4beb58e10e1d Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 18 Dec 2023 00:19:30 +0100 Subject: [PATCH 171/436] UARTComponent inline doc (#5930) --- esphome/components/uart/uart_component.h | 88 +++++++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 34bda42bb5..e03784fdd8 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -31,40 +31,122 @@ const LogString *parity_to_str(UARTParityOptions parity); class UARTComponent { public: + // Writes an array of bytes to the UART bus. + // @param data A vector of bytes to be written. void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } + + // Writes a single byte to the UART bus. + // @param data The byte to be written. void write_byte(uint8_t data) { this->write_array(&data, 1); }; + + // Writes a null-terminated string to the UART bus. + // @param str Pointer to the null-terminated string. void write_str(const char *str) { const auto *data = reinterpret_cast(str); this->write_array(data, strlen(str)); }; + // Pure virtual method to write an array of bytes to the UART bus. + // @param data Pointer to the array of bytes. + // @param len Length of the array. virtual void write_array(const uint8_t *data, size_t len) = 0; + // Reads a single byte from the UART bus. + // @param data Pointer to the byte where the read data will be stored. + // @return True if a byte was successfully read, false otherwise. bool read_byte(uint8_t *data) { return this->read_array(data, 1); }; + + // Pure virtual method to peek the next byte in the UART buffer without removing it. + // @param data Pointer to the byte where the peeked data will be stored. + // @return True if a byte is available to peek, false otherwise. virtual bool peek_byte(uint8_t *data) = 0; + + // Pure virtual method to read an array of bytes from the UART bus. + // @param data Pointer to the array where the read data will be stored. + // @param len Number of bytes to read. + // @return True if the specified number of bytes were successfully read, false otherwise. virtual bool read_array(uint8_t *data, size_t len) = 0; - /// Return available number of bytes. + // Pure virtual method to return the number of bytes available for reading. + // @return Number of available bytes. virtual int available() = 0; - /// Block until all bytes have been written to the UART bus. + + // Pure virtual method to block until all bytes have been written to the UART bus. virtual void flush() = 0; + // Sets the TX (transmit) pin for the UART bus. + // @param tx_pin Pointer to the internal GPIO pin used for transmission. void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; } + + // Sets the RX (receive) pin for the UART bus. + // @param rx_pin Pointer to the internal GPIO pin used for reception. void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } + + // Sets the size of the RX buffer. + // @param rx_buffer_size Size of the RX buffer in bytes. void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } + + // Gets the size of the RX buffer. + // @return Size of the RX buffer in bytes. size_t get_rx_buffer_size() { return this->rx_buffer_size_; } + // Sets the number of stop bits used in UART communication. + // @param stop_bits Number of stop bits. void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } + + // Gets the number of stop bits used in UART communication. + // @return Number of stop bits. uint8_t get_stop_bits() const { return this->stop_bits_; } + + // Set the number of data bits used in UART communication. + // @param data_bits Number of data bits. void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } + + // Get the number of data bits used in UART communication. + // @return Number of data bits. uint8_t get_data_bits() const { return this->data_bits_; } + + // Set the parity used in UART communication. + // @param parity Parity option. void set_parity(UARTParityOptions parity) { this->parity_ = parity; } + + // Get the parity used in UART communication. + // @return Parity option. UARTParityOptions get_parity() const { return this->parity_; } + + // Set the baud rate for UART communication. + // @param baud_rate Baud rate in bits per second. void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } + + // Get the baud rate for UART communication. + // @return Baud rate in bits per second. uint32_t get_baud_rate() const { return baud_rate_; } + #ifdef USE_ESP32 - virtual void load_settings() = 0; + /** + * Load the UART settings. + * @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly. + * + * Example: + * ```cpp + * id(uart1).load_settings(false); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ virtual void load_settings(bool dump_config) = 0; + + /** + * Load the UART settings. + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + virtual void load_settings() = 0; #endif // USE_ESP32 #ifdef USE_UART_DEBUGGER From 2309f15ce06b0fe633420b8061293427e62f0051 Mon Sep 17 00:00:00 2001 From: Grant Le Roux Date: Mon, 18 Dec 2023 07:28:48 +0800 Subject: [PATCH 172/436] Fix - Tuya Fan - Allow integer speed datapoint (#5948) Co-authored-by: Cram42 <5396871+cram42@users.noreply.github.com> --- esphome/components/tuya/fan/tuya_fan.cpp | 23 +++++++++++++++++------ esphome/components/tuya/fan/tuya_fan.h | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 1b03ea50fa..481c931f2e 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -9,13 +9,20 @@ static const char *const TAG = "tuya.fan"; void TuyaFan::setup() { if (this->speed_id_.has_value()) { this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) { - ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); - if (datapoint.value_enum >= this->speed_count_) { - ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum); - } else { - this->speed = datapoint.value_enum + 1; + if (datapoint.type == TuyaDatapointType::ENUM) { + ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); + if (datapoint.value_enum >= this->speed_count_) { + ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum); + } else { + this->speed = datapoint.value_enum + 1; + this->publish_state(); + } + } else if (datapoint.type == TuyaDatapointType::INTEGER) { + ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_int); + this->speed = datapoint.value_int; this->publish_state(); } + this->speed_type_ = datapoint.type; }); } if (this->switch_id_.has_value()) { @@ -80,7 +87,11 @@ void TuyaFan::control(const fan::FanCall &call) { this->parent_->set_enum_datapoint_value(*this->direction_id_, enable); } if (this->speed_id_.has_value() && call.get_speed().has_value()) { - this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); + if (this->speed_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); + } else if (this->speed_type_ == TuyaDatapointType::INTEGER) { + this->parent_->set_integer_datapoint_value(*this->speed_id_, *call.get_speed()); + } } } diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 4aba1e1c07..77b2cc6383 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -28,6 +28,7 @@ class TuyaFan : public Component, public fan::Fan { optional oscillation_id_{}; optional direction_id_{}; int speed_count_{}; + TuyaDatapointType speed_type_{}; }; } // namespace tuya From 168e70413030060eedb3d723939ea7191767a8a0 Mon Sep 17 00:00:00 2001 From: Alex Hermann Date: Mon, 18 Dec 2023 00:58:13 +0100 Subject: [PATCH 173/436] i2s_audio: Set player_task's prio to 1 (#5945) --- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index e729cdf954..95e63035fe 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -29,7 +29,7 @@ void I2SAudioSpeaker::start_() { } this->state_ = speaker::STATE_RUNNING; - xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_); + xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 1, &this->player_task_handle_); } void I2SAudioSpeaker::player_task(void *params) { From e8ce780482c914f45df13bff31448352d5ee5e76 Mon Sep 17 00:00:00 2001 From: Alex Hermann Date: Mon, 18 Dec 2023 01:00:42 +0100 Subject: [PATCH 174/436] esp32_camera: Set framebuffer task prio to 1 (#5943) --- esphome/components/esp32_camera/esp32_camera.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 99cb811fe4..555f6ca5f1 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -37,7 +37,7 @@ void ESP32Camera::setup() { "framebuffer_task", // name 1024, // stack size nullptr, // task pv params - 0, // priority + 1, // priority nullptr, // handle 1 // core ); From d0df73769d6e51f0efffab59011298e9f836e4f6 Mon Sep 17 00:00:00 2001 From: dentra Date: Mon, 18 Dec 2023 03:01:21 +0300 Subject: [PATCH 175/436] web_server_idf: fix call with hardcoded http code (#5942) --- esphome/components/web_server_idf/web_server_idf.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index bc64e5231e..2fbc5cd2e9 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -117,7 +117,7 @@ class AsyncWebServerRequest { // NOLINTNEXTLINE(readability-identifier-naming) AsyncWebServerResponse *beginResponse(int code, const char *content_type) { auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) - this->init_response_(res, 200, content_type); + this->init_response_(res, code, content_type); return res; } // NOLINTNEXTLINE(readability-identifier-naming) From eefa1cd3aba5ff2fcf93328b35117cdbf70594b7 Mon Sep 17 00:00:00 2001 From: Jean Louis-Guerin Date: Mon, 18 Dec 2023 01:03:01 +0100 Subject: [PATCH 176/436] Revert pure virtual functions in UART component from #5920 (#5932) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/uart/uart_component.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index e03784fdd8..6f27f36bcb 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -134,7 +134,7 @@ class UARTComponent { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - virtual void load_settings(bool dump_config) = 0; + virtual void load_settings(bool dump_config){}; /** * Load the UART settings. @@ -146,7 +146,7 @@ class UARTComponent { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - virtual void load_settings() = 0; + virtual void load_settings(){}; #endif // USE_ESP32 #ifdef USE_UART_DEBUGGER From ab22a3da349d3d60b687cbf70dd5a00ec44b67e2 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 18 Dec 2023 01:33:12 -0600 Subject: [PATCH 177/436] Use the correct UART/`Serial` when CDC is enabled (#5957) --- esphome/components/logger/logger.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index e0ca0806cb..05b97a5f64 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -237,8 +237,8 @@ void Logger::pre_setup() { Serial1.begin(this->baud_rate_); #else #if ARDUINO_USB_CDC_ON_BOOT - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); + this->hw_serial_ = &Serial0; + Serial0.begin(this->baud_rate_); #else this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); From dbfa77cb4bac5f5fc6120a79320e52a142c01812 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:51:11 +0900 Subject: [PATCH 178/436] Bump version to 2023.12.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 6c2a520331..4153646aea 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0b2" +__version__ = "2023.12.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 8961e8ab32cd398405b59fb6e806db91c9150439 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 18 Dec 2023 20:23:22 +0100 Subject: [PATCH 179/436] rename set_raw_touch_position_ to add_raw_touch_position_ (#5962) --- esphome/components/ektf2232/touchscreen/ektf2232.cpp | 2 +- esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h | 2 +- esphome/components/ft63x6/ft63x6.cpp | 4 ++-- esphome/components/gt911/touchscreen/gt911_touchscreen.cpp | 2 +- .../lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp | 2 +- esphome/components/touchscreen/touchscreen.cpp | 2 +- esphome/components/touchscreen/touchscreen.h | 2 +- esphome/components/tt21100/touchscreen/tt21100.cpp | 2 +- esphome/components/xpt2046/touchscreen/xpt2046.cpp | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp index 1a2c0389af..00e00bc7e6 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -74,7 +74,7 @@ void EKTF2232Touchscreen::update_touches() { uint8_t *d = raw + 1 + (i * 3); x_raw = (d[0] & 0xF0) << 4 | d[1]; y_raw = (d[0] & 0x0F) << 8 | d[2]; - this->set_raw_touch_position_(i, x_raw, y_raw); + this->add_raw_touch_position_(i, x_raw, y_raw); } } diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h index 497d6c906c..0b3a2c1b86 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -94,7 +94,7 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); if (status == 0 || status == 2) { - this->set_raw_touch_position_(id, x, y); + this->add_raw_touch_position_(id, x, y); } } } diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp index 9198954253..b674ded22c 100644 --- a/esphome/components/ft63x6/ft63x6.cpp +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -53,13 +53,13 @@ void FT63X6Touchscreen::update_touches() { uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); - this->set_raw_touch_position_(touch_id, x, y); + this->add_raw_touch_position_(touch_id, x, y); if (touch_count >= 2) { touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01) x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); - this->set_raw_touch_position_(touch_id, x, y); + this->add_raw_touch_position_(touch_id, x, y); } } diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index adc577f5da..84854d5b0d 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -92,7 +92,7 @@ void GT911Touchscreen::update_touches() { uint16_t id = data[i][0]; uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t y = encode_uint16(data[i][4], data[i][3]); - this->set_raw_touch_position_(id, x, y); + this->add_raw_touch_position_(id, x, y); } auto keys = data[num_of_touches][0]; for (size_t i = 0; i != 4; i++) { diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index eb61b6f31e..64cc7ad4d1 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -84,7 +84,7 @@ void LilygoT547Touchscreen::update_touches() { id = (buffer[i * 5] >> 4) & 0x0F; y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F)); x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F)); - this->set_raw_touch_position_(id, x_raw, y_raw); + this->add_raw_touch_position_(id, x_raw, y_raw); } this->status_clear_warning(); diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 140f46b6f6..f095c2af8c 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -51,7 +51,7 @@ void Touchscreen::loop() { } } -void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { +void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { TouchPoint tp; uint16_t x, y; if (this->touches_.count(id) == 0) { diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 1fe304d967..74747c589c 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -87,7 +87,7 @@ class Touchscreen : public PollingComponent { void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type); - void set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); + void add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); void send_touches_(); diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index ff688fd0b0..6b5cba74cd 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -109,7 +109,7 @@ void TT21100Touchscreen::update_touches() { i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, touch->pressure, touch->major_axis_length, touch->orientation); - this->set_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); + this->add_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); } } } diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp index 1a9c202af0..a268da06dd 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.cpp +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -55,7 +55,7 @@ void XPT2046Component::update_touches() { ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); - this->set_raw_touch_position_(0, x_raw, y_raw, z_raw); + this->add_raw_touch_position_(0, x_raw, y_raw, z_raw); } } From 3ea5054cf2e1ad91175b0eafb7ac82e859bb5bc6 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 18 Dec 2023 17:11:07 -0600 Subject: [PATCH 180/436] Fix build issue with UART component when building with Arduino and CDC (#5964) --- esphome/components/uart/uart_component_esp32_arduino.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 75b67bf5c2..f77783e20e 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -88,7 +88,11 @@ void ESP32ArduinoUARTComponent::setup() { #endif static uint8_t next_uart_num = 0; if (is_default_tx && is_default_rx && next_uart_num == 0) { +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial0; +#else this->hw_serial_ = &Serial; +#endif next_uart_num++; } else { #ifdef USE_LOGGER From 0a117eb562204a28d1955aa8eb6d2963c625d95a Mon Sep 17 00:00:00 2001 From: Jean Louis-Guerin Date: Tue, 19 Dec 2023 00:14:42 +0100 Subject: [PATCH 181/436] Fix I2CBus::write() bug and add i2c documentation (#5947) --- esphome/components/i2c/i2c.h | 153 +++++++++++++++++++++++++++++-- esphome/components/i2c/i2c_bus.h | 79 ++++++++++++---- 2 files changed, 205 insertions(+), 27 deletions(-) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index eb5d463b65..8d8e139c61 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -11,43 +11,116 @@ namespace i2c { #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); -class I2CDevice; +class I2CDevice; // forward declaration + +/// @brief This class is used to create I2CRegister objects that act as proxies to read/write internal registers on an +/// I2C device. +/// @details +/// @n typical usage: +/// @code +/// constexpr uint8_t ADDR_REGISTER_1 = 0x12; +/// i2c::I2CRegister reg_1 = this->reg(ADDR_REGISTER_1); // declare +/// reg_1 |= 0x01; // set bit +/// reg_1 &= ~0x01; // reset bit +/// reg_1 = 10; // Set value +/// uint val = reg_1.get(); // get value +/// @endcode +/// @details The I²C protocol specifies how to read/write in sets of 8-bits followed by an Acknowledgement (ACK/NACK) +/// from the device receiving the data. How the device interprets the bits read/written can vary greatly from +/// device to device. However most of the devices follow the same protocol for reading/writing 8 bit registers using as +/// implemented in the I2CRegister: after sending the device address, the controller sends one byte with the internal +/// register address and then read or write the specified register content. class I2CRegister { public: + /// @brief overloads the = operator. This allows to set the value of an i2c register + /// @param value value to be set in the register + /// @return pointer to current object I2CRegister &operator=(uint8_t value); + + /// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister &operator&=(uint8_t value); + + /// @brief overloads the compound |= operator. This allows to set specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister &operator|=(uint8_t value); + /// @brief overloads the uint8_t() cast operator to return the I²C register value + /// @return pointer to current object explicit operator uint8_t() const { return get(); } + /// @brief returns the register value + /// @return the register value uint8_t get() const; protected: friend class I2CDevice; + /// @brief protected constructor that stores the owning object and the register address. Note as only friends can + /// create an I2CRegister @see I2CDevice::reg() + /// @param parent our parent + /// @param a_register address of the i2c register I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} - I2CDevice *parent_; - uint8_t register_; + I2CDevice *parent_; ///< I2CDevice object pointer + uint8_t register_; ///< the internal address of the register }; +/// @brief This class is used to create I2CRegister16 objects that act as proxies to read/write internal registers +/// (specified with a 16 bit address) on an I2C device. +/// @details +/// @n typical usage: +/// @code +/// constexpr uint16_t X16_BIT_ADDR_REGISTER_1 = 0x1234; +/// i2c::I2CRegister16 reg_1 = this->reg16(X16_BIT_ADDR_REGISTER_1); // declare +/// reg_1 |= 0x01; // set bit +/// reg_1 &= ~0x01; // reset bit +/// reg_1 = 10; // Set value +/// uint val = reg_1.get(); // get value +/// @endcode +/// @details The I²C protocol specification, reads/writes in sets of 8-bits followed by an Acknowledgement (ACK/NACK) +/// from the device receiving the data. How the device interprets the bits read/written to it can vary greatly from +/// device to device. This class can be used to access in the device 8 bits registers that uses a 16 bits internal +/// address. After sending the device address, the controller sends the internal register address (using two consecutive +/// bytes following the big indian convention) and then read or write the register content. class I2CRegister16 { public: + /// @brief overloads the = operator. This allows to set the value of an I²C register + /// @param value value to be set in the register + /// @return pointer to current object I2CRegister16 &operator=(uint8_t value); + + /// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister16 &operator&=(uint8_t value); + + /// @brief overloads the compound |= operator. This allows to set bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister16 &operator|=(uint8_t value); + /// @brief overloads the uint8_t() cast operator to return the I²C register value + /// @return the register value explicit operator uint8_t() const { return get(); } + /// @brief returns the register value + /// @return the register value uint8_t get() const; protected: friend class I2CDevice; + /// @brief protected constructor that store the owning object and the register address. Only friends can create an + /// I2CRegister16 @see I2CDevice::reg16() + /// @param parent our parent + /// @param a_register 16 bits address of the i2c register I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {} - I2CDevice *parent_; - uint16_t register_; + I2CDevice *parent_; ///< I2CDevice object pointer + uint16_t register_; ///< the internal 16 bits address of the register }; // like ntohs/htons but without including networking headers. @@ -55,29 +128,91 @@ class I2CRegister16 { inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(hostshort); } +/// @brief This Class provides the methods to read/write bytes from/to an i2c device. +/// Objects keep a list of devices found on bus as well as a pointer to the I2CBus in use. class I2CDevice { public: + /// @brief we use the C++ default constructor I2CDevice() = default; + /// @brief We store the address of the device on the bus + /// @param address of the device void set_i2c_address(uint8_t address) { address_ = address; } + + /// @brief we store the pointer to the I2CBus to use + /// @param bus pointer to the I2CBus object void set_i2c_bus(I2CBus *bus) { bus_ = bus; } + /// @brief calls the I2CRegister constructor + /// @param a_register address of the I²C register + /// @return an I2CRegister proxy object I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + + /// @brief calls the I2CRegister16 constructor + /// @param a_register 16 bits address of the I²C register + /// @return an I2CRegister16 proxy object I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; } + /// @brief reads an array of bytes from the device using an I2CBus + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } + + /// @brief reads an array of bytes from a specific register in the I²C device + /// @param a_register an 8 bits internal address of the I²C register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + + /// @brief reads an array of bytes from a specific register in the I²C device + /// @param a_register the 16 bits internal address of the I²C register to read from + /// @param data pointer to an array of bytes to store the information + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); - ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + /// @brief writes an array of bytes to a device using an I2CBus + /// @param data pointer to an array that contains the bytes to send + /// @param len length of the buffer = number of bytes to write + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + ErrorCode write(const uint8_t *data, size_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + + /// @brief writes an array of bytes to a specific register in the I²C device + /// @param a_register the internal address of the register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + + /// @brief write an array of bytes to a specific register in the I²C device + /// @param a_register the 16 bits internal address of the register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); - // Compat APIs + /// + /// Compat APIs + /// All methods below have been added for compatibility reasons. They do not bring any functionality and therefore on + /// new code it is not recommend to use them. + /// bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len) { return read_register(a_register, data, len) == ERROR_OK; } + bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; } template optional> read_bytes(uint8_t a_register) { @@ -131,8 +266,8 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } protected: - uint8_t address_{0x00}; - I2CBus *bus_{nullptr}; + uint8_t address_{0x00}; ///< store the address of the device on the bus + I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance }; } // namespace i2c diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 2633a7adf6..fbfc88323e 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -7,50 +7,93 @@ namespace esphome { namespace i2c { +/// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { - ERROR_OK = 0, - ERROR_INVALID_ARGUMENT = 1, - ERROR_NOT_ACKNOWLEDGED = 2, - ERROR_TIMEOUT = 3, - ERROR_NOT_INITIALIZED = 4, - ERROR_TOO_LARGE = 5, - ERROR_UNKNOWN = 6, - ERROR_CRC = 7, + NO_ERROR = 0, ///< No error found during execution of method + ERROR_OK = 0, ///< No error found during execution of method + ERROR_INVALID_ARGUMENT = 1, ///< method called invalid argument(s) + ERROR_NOT_ACKNOWLEDGED = 2, ///< I2C bus acknowledgment not received + ERROR_TIMEOUT = 3, ///< timeout while waiting to receive bytes + ERROR_NOT_INITIALIZED = 4, ///< call method to a not initialized bus + ERROR_TOO_LARGE = 5, ///< requested a transfer larger than buffers can hold + ERROR_UNKNOWN = 6, ///< miscellaneous I2C error during execution + ERROR_CRC = 7, ///< bytes received with a CRC error }; +/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length struct ReadBuffer { - uint8_t *data; - size_t len; -}; -struct WriteBuffer { - const uint8_t *data; - size_t len; + uint8_t *data; ///< pointer to the read buffer + size_t len; ///< length of the buffer }; +/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length +struct WriteBuffer { + const uint8_t *data; ///< pointer to the write buffer + size_t len; ///< length of the buffer +}; + +/// @brief This Class provides the methods to read and write bytes from an I2CBus. +/// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required +/// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and +/// user manual can be found here https://www.nxp.com/docs/en/user-guide/UM10204.pdf and an interesting I²C Application +/// note https://www.nxp.com/docs/en/application-note/AN10216.pdf class I2CBus { public: + /// @brief Creates a ReadBuffer and calls the virtual readv() method to read bytes into this buffer + /// @param address address of the I²C component on the i2c bus + /// @param buffer pointer to an array of bytes that will be used to store the data received + /// @param len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { ReadBuffer buf; buf.data = buffer; buf.len = len; return readv(address, &buf, 1); } - virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0; + + /// @brief This virtual method reads bytes from an I2CBus into an array of ReadBuffer. + /// @param address address of the I²C component on the i2c bus + /// @param buffers pointer to an array of ReadBuffer + /// @param count number of ReadBuffer to read + /// @return an i2c::ErrorCode + /// @details This is a pure virtual method that must be implemented in a subclass. + virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t count) = 0; + virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { return write(address, buffer, len, true); } + + /// @brief Creates a WriteBuffer and calls the writev() method to send the bytes from this buffer + /// @param address address of the I²C component on the i2c bus + /// @param buffer pointer to an array of bytes that contains the data to be sent + /// @param len length of the buffer = number of bytes to write + /// @param stop true or false: True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { WriteBuffer buf; buf.data = buffer; buf.len = len; return writev(address, &buf, 1, stop); } + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { return writev(address, buffers, cnt, true); } - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0; + + /// @brief This virtual method writes bytes to an I2CBus from an array of WriteBuffer. + /// @param address address of the I²C component on the i2c bus + /// @param buffers pointer to an array of WriteBuffer + /// @param count number of WriteBuffer to write + /// @param stop true or false: True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + /// @details This is a pure virtual method that must be implemented in the subclass. + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t count, bool stop) = 0; protected: + /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair + /// that contains the address and the corresponding bool presence flag. void i2c_scan_() { for (uint8_t address = 8; address < 120; address++) { auto err = writev(address, nullptr, 0); @@ -61,8 +104,8 @@ class I2CBus { } } } - std::vector> scan_results_; - bool scan_{false}; + std::vector> scan_results_; ///< array containing scan results + bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; } // namespace i2c From 52b9668170f49e836c8f3dacb39dbd70c738fb50 Mon Sep 17 00:00:00 2001 From: mathieu-mp Date: Tue, 19 Dec 2023 04:29:00 +0100 Subject: [PATCH 182/436] Add deep sleep between updates for waveshare epaper 1.54in and 1.54inv2 (#5961) --- .../waveshare_epaper/waveshare_epaper.cpp | 33 +++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 25 ++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 53bfa57f4f..8fdb9a3ac0 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -167,6 +167,25 @@ void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); } // ======================================================== void WaveshareEPaperTypeA::initialize() { + // Achieve display intialization + this->init_display_(); + // If a reset pin is configured, eligible displays can be set to deep sleep + // between updates, as recommended by the hardware provider + if (this->reset_pin_ != nullptr) { + switch (this->model_) { + // More models can be added here to enable deep sleep if eligible + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: + this->deep_sleep_between_updates_ = true; + ESP_LOGI(TAG, "Set the display to deep sleep"); + this->deep_sleep(); + break; + default: + break; + } + } +} +void WaveshareEPaperTypeA::init_display_() { if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { this->reset_pin_->digital_write(false); delay(10); @@ -261,6 +280,13 @@ void HOT WaveshareEPaperTypeA::display() { bool full_update = this->at_update_ == 0; bool prev_full_update = this->at_update_ == 1; + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Wake up the display"); + this->reset_(); + this->wait_until_idle_(); + this->init_display_(); + } + if (!this->wait_until_idle_()) { this->status_set_warning(); return; @@ -384,6 +410,11 @@ void HOT WaveshareEPaperTypeA::display() { this->command(0xFF); this->status_clear_warning(); + + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Set the display back to deep sleep"); + this->deep_sleep(); + } } int WaveshareEPaperTypeA::get_width_internal() { switch (this->model_) { @@ -445,6 +476,8 @@ void WaveshareEPaperTypeA::set_full_update_every(uint32_t full_update_every) { uint32_t WaveshareEPaperTypeA::idle_timeout_() { switch (this->model_) { + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: case TTGO_EPAPER_2_13_IN_B1: return 2500; default: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index f6ccf90861..42e8a16829 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -92,13 +92,20 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { void display() override; void deep_sleep() override { - if (this->model_ == WAVESHARE_EPAPER_2_9_IN_V2 || this->model_ == WAVESHARE_EPAPER_1_54_IN_V2) { - // COMMAND DEEP SLEEP MODE - this->command(0x10); - this->data(0x01); - } else { - // COMMAND DEEP SLEEP MODE - this->command(0x10); + switch (this->model_) { + // Models with specific deep sleep command and data + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: + case WAVESHARE_EPAPER_2_9_IN_V2: + // COMMAND DEEP SLEEP MODE + this->command(0x10); + this->data(0x01); + break; + // Other models default to simple deep sleep command + default: + // COMMAND DEEP SLEEP + this->command(0x10); + break; } this->wait_until_idle_(); } @@ -108,6 +115,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { protected: void write_lut_(const uint8_t *lut, uint8_t size); + void init_display_(); + int get_width_internal() override; int get_height_internal() override; @@ -118,6 +127,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { uint32_t at_update_{0}; WaveshareEPaperTypeAModel model_; uint32_t idle_timeout_() override; + + bool deep_sleep_between_updates_{false}; }; enum WaveshareEPaperTypeBModel { From cd06dc77ee6693595356a72032731701c66c91ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Dec 2023 19:24:48 -1000 Subject: [PATCH 183/436] Speed up writing protobuf strings/bytes (#5828) --- esphome/components/api/proto.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index fea219ecb9..ccc6c0d52c 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -160,8 +160,7 @@ class ProtoWriteBuffer { this->encode_field_raw(field_id, 2); this->encode_varint_raw(len); auto *data = reinterpret_cast(string); - for (size_t i = 0; i < len; i++) - this->write(data[i]); + this->buffer_->insert(this->buffer_->end(), data, data + len); } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size()); From efda2033f7cdc61f3895d0e324bb1285a8417371 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 20:16:10 +0000 Subject: [PATCH 184/436] Bump zeroconf from 0.130.0 to 0.131.0 (#5967) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e545a8023a..fffe011c5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==21.0.0 -zeroconf==0.130.0 +zeroconf==0.131.0 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From 16798bbfb461bf4b79799b715c2575654755548e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Dec 2023 13:41:26 -1000 Subject: [PATCH 185/436] Bump aioesphomeapi to 21.0.1 (#5969) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fffe011c5a..115f85de3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==21.0.0 +aioesphomeapi==21.0.1 zeroconf==0.131.0 python-magic==0.4.27 From 7807f0d89250a890aaec97a524ad1d68310d07c3 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 18 Dec 2023 17:11:07 -0600 Subject: [PATCH 186/436] Fix build issue with UART component when building with Arduino and CDC (#5964) --- esphome/components/uart/uart_component_esp32_arduino.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 75b67bf5c2..f77783e20e 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -88,7 +88,11 @@ void ESP32ArduinoUARTComponent::setup() { #endif static uint8_t next_uart_num = 0; if (is_default_tx && is_default_rx && next_uart_num == 0) { +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial0; +#else this->hw_serial_ = &Serial; +#endif next_uart_num++; } else { #ifdef USE_LOGGER From 820f3282480d282914a6a0682908443ad3b5755a Mon Sep 17 00:00:00 2001 From: Jean Louis-Guerin Date: Tue, 19 Dec 2023 00:14:42 +0100 Subject: [PATCH 187/436] Fix I2CBus::write() bug and add i2c documentation (#5947) --- esphome/components/i2c/i2c.h | 153 +++++++++++++++++++++++++++++-- esphome/components/i2c/i2c_bus.h | 79 ++++++++++++---- 2 files changed, 205 insertions(+), 27 deletions(-) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index eb5d463b65..8d8e139c61 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -11,43 +11,116 @@ namespace i2c { #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); -class I2CDevice; +class I2CDevice; // forward declaration + +/// @brief This class is used to create I2CRegister objects that act as proxies to read/write internal registers on an +/// I2C device. +/// @details +/// @n typical usage: +/// @code +/// constexpr uint8_t ADDR_REGISTER_1 = 0x12; +/// i2c::I2CRegister reg_1 = this->reg(ADDR_REGISTER_1); // declare +/// reg_1 |= 0x01; // set bit +/// reg_1 &= ~0x01; // reset bit +/// reg_1 = 10; // Set value +/// uint val = reg_1.get(); // get value +/// @endcode +/// @details The I²C protocol specifies how to read/write in sets of 8-bits followed by an Acknowledgement (ACK/NACK) +/// from the device receiving the data. How the device interprets the bits read/written can vary greatly from +/// device to device. However most of the devices follow the same protocol for reading/writing 8 bit registers using as +/// implemented in the I2CRegister: after sending the device address, the controller sends one byte with the internal +/// register address and then read or write the specified register content. class I2CRegister { public: + /// @brief overloads the = operator. This allows to set the value of an i2c register + /// @param value value to be set in the register + /// @return pointer to current object I2CRegister &operator=(uint8_t value); + + /// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister &operator&=(uint8_t value); + + /// @brief overloads the compound |= operator. This allows to set specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister &operator|=(uint8_t value); + /// @brief overloads the uint8_t() cast operator to return the I²C register value + /// @return pointer to current object explicit operator uint8_t() const { return get(); } + /// @brief returns the register value + /// @return the register value uint8_t get() const; protected: friend class I2CDevice; + /// @brief protected constructor that stores the owning object and the register address. Note as only friends can + /// create an I2CRegister @see I2CDevice::reg() + /// @param parent our parent + /// @param a_register address of the i2c register I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} - I2CDevice *parent_; - uint8_t register_; + I2CDevice *parent_; ///< I2CDevice object pointer + uint8_t register_; ///< the internal address of the register }; +/// @brief This class is used to create I2CRegister16 objects that act as proxies to read/write internal registers +/// (specified with a 16 bit address) on an I2C device. +/// @details +/// @n typical usage: +/// @code +/// constexpr uint16_t X16_BIT_ADDR_REGISTER_1 = 0x1234; +/// i2c::I2CRegister16 reg_1 = this->reg16(X16_BIT_ADDR_REGISTER_1); // declare +/// reg_1 |= 0x01; // set bit +/// reg_1 &= ~0x01; // reset bit +/// reg_1 = 10; // Set value +/// uint val = reg_1.get(); // get value +/// @endcode +/// @details The I²C protocol specification, reads/writes in sets of 8-bits followed by an Acknowledgement (ACK/NACK) +/// from the device receiving the data. How the device interprets the bits read/written to it can vary greatly from +/// device to device. This class can be used to access in the device 8 bits registers that uses a 16 bits internal +/// address. After sending the device address, the controller sends the internal register address (using two consecutive +/// bytes following the big indian convention) and then read or write the register content. class I2CRegister16 { public: + /// @brief overloads the = operator. This allows to set the value of an I²C register + /// @param value value to be set in the register + /// @return pointer to current object I2CRegister16 &operator=(uint8_t value); + + /// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister16 &operator&=(uint8_t value); + + /// @brief overloads the compound |= operator. This allows to set bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister16 &operator|=(uint8_t value); + /// @brief overloads the uint8_t() cast operator to return the I²C register value + /// @return the register value explicit operator uint8_t() const { return get(); } + /// @brief returns the register value + /// @return the register value uint8_t get() const; protected: friend class I2CDevice; + /// @brief protected constructor that store the owning object and the register address. Only friends can create an + /// I2CRegister16 @see I2CDevice::reg16() + /// @param parent our parent + /// @param a_register 16 bits address of the i2c register I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {} - I2CDevice *parent_; - uint16_t register_; + I2CDevice *parent_; ///< I2CDevice object pointer + uint16_t register_; ///< the internal 16 bits address of the register }; // like ntohs/htons but without including networking headers. @@ -55,29 +128,91 @@ class I2CRegister16 { inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(hostshort); } +/// @brief This Class provides the methods to read/write bytes from/to an i2c device. +/// Objects keep a list of devices found on bus as well as a pointer to the I2CBus in use. class I2CDevice { public: + /// @brief we use the C++ default constructor I2CDevice() = default; + /// @brief We store the address of the device on the bus + /// @param address of the device void set_i2c_address(uint8_t address) { address_ = address; } + + /// @brief we store the pointer to the I2CBus to use + /// @param bus pointer to the I2CBus object void set_i2c_bus(I2CBus *bus) { bus_ = bus; } + /// @brief calls the I2CRegister constructor + /// @param a_register address of the I²C register + /// @return an I2CRegister proxy object I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + + /// @brief calls the I2CRegister16 constructor + /// @param a_register 16 bits address of the I²C register + /// @return an I2CRegister16 proxy object I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; } + /// @brief reads an array of bytes from the device using an I2CBus + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } + + /// @brief reads an array of bytes from a specific register in the I²C device + /// @param a_register an 8 bits internal address of the I²C register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + + /// @brief reads an array of bytes from a specific register in the I²C device + /// @param a_register the 16 bits internal address of the I²C register to read from + /// @param data pointer to an array of bytes to store the information + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); - ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + /// @brief writes an array of bytes to a device using an I2CBus + /// @param data pointer to an array that contains the bytes to send + /// @param len length of the buffer = number of bytes to write + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + ErrorCode write(const uint8_t *data, size_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + + /// @brief writes an array of bytes to a specific register in the I²C device + /// @param a_register the internal address of the register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + + /// @brief write an array of bytes to a specific register in the I²C device + /// @param a_register the 16 bits internal address of the register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); - // Compat APIs + /// + /// Compat APIs + /// All methods below have been added for compatibility reasons. They do not bring any functionality and therefore on + /// new code it is not recommend to use them. + /// bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len) { return read_register(a_register, data, len) == ERROR_OK; } + bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; } template optional> read_bytes(uint8_t a_register) { @@ -131,8 +266,8 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } protected: - uint8_t address_{0x00}; - I2CBus *bus_{nullptr}; + uint8_t address_{0x00}; ///< store the address of the device on the bus + I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance }; } // namespace i2c diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 2633a7adf6..fbfc88323e 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -7,50 +7,93 @@ namespace esphome { namespace i2c { +/// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { - ERROR_OK = 0, - ERROR_INVALID_ARGUMENT = 1, - ERROR_NOT_ACKNOWLEDGED = 2, - ERROR_TIMEOUT = 3, - ERROR_NOT_INITIALIZED = 4, - ERROR_TOO_LARGE = 5, - ERROR_UNKNOWN = 6, - ERROR_CRC = 7, + NO_ERROR = 0, ///< No error found during execution of method + ERROR_OK = 0, ///< No error found during execution of method + ERROR_INVALID_ARGUMENT = 1, ///< method called invalid argument(s) + ERROR_NOT_ACKNOWLEDGED = 2, ///< I2C bus acknowledgment not received + ERROR_TIMEOUT = 3, ///< timeout while waiting to receive bytes + ERROR_NOT_INITIALIZED = 4, ///< call method to a not initialized bus + ERROR_TOO_LARGE = 5, ///< requested a transfer larger than buffers can hold + ERROR_UNKNOWN = 6, ///< miscellaneous I2C error during execution + ERROR_CRC = 7, ///< bytes received with a CRC error }; +/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length struct ReadBuffer { - uint8_t *data; - size_t len; -}; -struct WriteBuffer { - const uint8_t *data; - size_t len; + uint8_t *data; ///< pointer to the read buffer + size_t len; ///< length of the buffer }; +/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length +struct WriteBuffer { + const uint8_t *data; ///< pointer to the write buffer + size_t len; ///< length of the buffer +}; + +/// @brief This Class provides the methods to read and write bytes from an I2CBus. +/// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required +/// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and +/// user manual can be found here https://www.nxp.com/docs/en/user-guide/UM10204.pdf and an interesting I²C Application +/// note https://www.nxp.com/docs/en/application-note/AN10216.pdf class I2CBus { public: + /// @brief Creates a ReadBuffer and calls the virtual readv() method to read bytes into this buffer + /// @param address address of the I²C component on the i2c bus + /// @param buffer pointer to an array of bytes that will be used to store the data received + /// @param len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { ReadBuffer buf; buf.data = buffer; buf.len = len; return readv(address, &buf, 1); } - virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0; + + /// @brief This virtual method reads bytes from an I2CBus into an array of ReadBuffer. + /// @param address address of the I²C component on the i2c bus + /// @param buffers pointer to an array of ReadBuffer + /// @param count number of ReadBuffer to read + /// @return an i2c::ErrorCode + /// @details This is a pure virtual method that must be implemented in a subclass. + virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t count) = 0; + virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { return write(address, buffer, len, true); } + + /// @brief Creates a WriteBuffer and calls the writev() method to send the bytes from this buffer + /// @param address address of the I²C component on the i2c bus + /// @param buffer pointer to an array of bytes that contains the data to be sent + /// @param len length of the buffer = number of bytes to write + /// @param stop true or false: True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { WriteBuffer buf; buf.data = buffer; buf.len = len; return writev(address, &buf, 1, stop); } + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { return writev(address, buffers, cnt, true); } - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0; + + /// @brief This virtual method writes bytes to an I2CBus from an array of WriteBuffer. + /// @param address address of the I²C component on the i2c bus + /// @param buffers pointer to an array of WriteBuffer + /// @param count number of WriteBuffer to write + /// @param stop true or false: True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + /// @details This is a pure virtual method that must be implemented in the subclass. + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t count, bool stop) = 0; protected: + /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair + /// that contains the address and the corresponding bool presence flag. void i2c_scan_() { for (uint8_t address = 8; address < 120; address++) { auto err = writev(address, nullptr, 0); @@ -61,8 +104,8 @@ class I2CBus { } } } - std::vector> scan_results_; - bool scan_{false}; + std::vector> scan_results_; ///< array containing scan results + bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; } // namespace i2c From 977e0184a72880f917dbaa8cfe3d48b2cc862b52 Mon Sep 17 00:00:00 2001 From: mathieu-mp Date: Tue, 19 Dec 2023 04:29:00 +0100 Subject: [PATCH 188/436] Add deep sleep between updates for waveshare epaper 1.54in and 1.54inv2 (#5961) --- .../waveshare_epaper/waveshare_epaper.cpp | 33 +++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 25 ++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 53bfa57f4f..8fdb9a3ac0 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -167,6 +167,25 @@ void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); } // ======================================================== void WaveshareEPaperTypeA::initialize() { + // Achieve display intialization + this->init_display_(); + // If a reset pin is configured, eligible displays can be set to deep sleep + // between updates, as recommended by the hardware provider + if (this->reset_pin_ != nullptr) { + switch (this->model_) { + // More models can be added here to enable deep sleep if eligible + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: + this->deep_sleep_between_updates_ = true; + ESP_LOGI(TAG, "Set the display to deep sleep"); + this->deep_sleep(); + break; + default: + break; + } + } +} +void WaveshareEPaperTypeA::init_display_() { if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { this->reset_pin_->digital_write(false); delay(10); @@ -261,6 +280,13 @@ void HOT WaveshareEPaperTypeA::display() { bool full_update = this->at_update_ == 0; bool prev_full_update = this->at_update_ == 1; + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Wake up the display"); + this->reset_(); + this->wait_until_idle_(); + this->init_display_(); + } + if (!this->wait_until_idle_()) { this->status_set_warning(); return; @@ -384,6 +410,11 @@ void HOT WaveshareEPaperTypeA::display() { this->command(0xFF); this->status_clear_warning(); + + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Set the display back to deep sleep"); + this->deep_sleep(); + } } int WaveshareEPaperTypeA::get_width_internal() { switch (this->model_) { @@ -445,6 +476,8 @@ void WaveshareEPaperTypeA::set_full_update_every(uint32_t full_update_every) { uint32_t WaveshareEPaperTypeA::idle_timeout_() { switch (this->model_) { + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: case TTGO_EPAPER_2_13_IN_B1: return 2500; default: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index f6ccf90861..42e8a16829 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -92,13 +92,20 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { void display() override; void deep_sleep() override { - if (this->model_ == WAVESHARE_EPAPER_2_9_IN_V2 || this->model_ == WAVESHARE_EPAPER_1_54_IN_V2) { - // COMMAND DEEP SLEEP MODE - this->command(0x10); - this->data(0x01); - } else { - // COMMAND DEEP SLEEP MODE - this->command(0x10); + switch (this->model_) { + // Models with specific deep sleep command and data + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: + case WAVESHARE_EPAPER_2_9_IN_V2: + // COMMAND DEEP SLEEP MODE + this->command(0x10); + this->data(0x01); + break; + // Other models default to simple deep sleep command + default: + // COMMAND DEEP SLEEP + this->command(0x10); + break; } this->wait_until_idle_(); } @@ -108,6 +115,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { protected: void write_lut_(const uint8_t *lut, uint8_t size); + void init_display_(); + int get_width_internal() override; int get_height_internal() override; @@ -118,6 +127,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { uint32_t at_update_{0}; WaveshareEPaperTypeAModel model_; uint32_t idle_timeout_() override; + + bool deep_sleep_between_updates_{false}; }; enum WaveshareEPaperTypeBModel { From e5414d70f5c9f3b6aab84b14a198611df3f0c34e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Dec 2023 19:24:48 -1000 Subject: [PATCH 189/436] Speed up writing protobuf strings/bytes (#5828) --- esphome/components/api/proto.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index fea219ecb9..ccc6c0d52c 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -160,8 +160,7 @@ class ProtoWriteBuffer { this->encode_field_raw(field_id, 2); this->encode_varint_raw(len); auto *data = reinterpret_cast(string); - for (size_t i = 0; i < len; i++) - this->write(data[i]); + this->buffer_->insert(this->buffer_->end(), data, data + len); } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size()); From 1b3068a4092e72263dc252b43848ae959f9028cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Dec 2023 13:41:26 -1000 Subject: [PATCH 190/436] Bump aioesphomeapi to 21.0.1 (#5969) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 20a5514e71..b9bcd5dae0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==21.0.0 +aioesphomeapi==21.0.1 zeroconf==0.130.0 python-magic==0.4.27 From e2a00f66b8c2150f49538af8282b80d6fd8aae30 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:59:58 +0900 Subject: [PATCH 191/436] Bump version to 2023.12.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 4153646aea..20218ab61d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0b3" +__version__ = "2023.12.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fbf3d03a33289aa8f47a664b11a724621f499142 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 18 Dec 2023 20:23:22 +0100 Subject: [PATCH 192/436] rename set_raw_touch_position_ to add_raw_touch_position_ (#5962) --- esphome/components/ektf2232/touchscreen/ektf2232.cpp | 2 +- esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h | 2 +- esphome/components/ft63x6/ft63x6.cpp | 4 ++-- esphome/components/gt911/touchscreen/gt911_touchscreen.cpp | 2 +- .../lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp | 2 +- esphome/components/touchscreen/touchscreen.cpp | 2 +- esphome/components/touchscreen/touchscreen.h | 2 +- esphome/components/tt21100/touchscreen/tt21100.cpp | 2 +- esphome/components/xpt2046/touchscreen/xpt2046.cpp | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp index 1a2c0389af..00e00bc7e6 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -74,7 +74,7 @@ void EKTF2232Touchscreen::update_touches() { uint8_t *d = raw + 1 + (i * 3); x_raw = (d[0] & 0xF0) << 4 | d[1]; y_raw = (d[0] & 0x0F) << 8 | d[2]; - this->set_raw_touch_position_(i, x_raw, y_raw); + this->add_raw_touch_position_(i, x_raw, y_raw); } } diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h index 497d6c906c..0b3a2c1b86 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -94,7 +94,7 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); if (status == 0 || status == 2) { - this->set_raw_touch_position_(id, x, y); + this->add_raw_touch_position_(id, x, y); } } } diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp index 9198954253..b674ded22c 100644 --- a/esphome/components/ft63x6/ft63x6.cpp +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -53,13 +53,13 @@ void FT63X6Touchscreen::update_touches() { uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); - this->set_raw_touch_position_(touch_id, x, y); + this->add_raw_touch_position_(touch_id, x, y); if (touch_count >= 2) { touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01) x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); - this->set_raw_touch_position_(touch_id, x, y); + this->add_raw_touch_position_(touch_id, x, y); } } diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index adc577f5da..84854d5b0d 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -92,7 +92,7 @@ void GT911Touchscreen::update_touches() { uint16_t id = data[i][0]; uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t y = encode_uint16(data[i][4], data[i][3]); - this->set_raw_touch_position_(id, x, y); + this->add_raw_touch_position_(id, x, y); } auto keys = data[num_of_touches][0]; for (size_t i = 0; i != 4; i++) { diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index eb61b6f31e..64cc7ad4d1 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -84,7 +84,7 @@ void LilygoT547Touchscreen::update_touches() { id = (buffer[i * 5] >> 4) & 0x0F; y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F)); x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F)); - this->set_raw_touch_position_(id, x_raw, y_raw); + this->add_raw_touch_position_(id, x_raw, y_raw); } this->status_clear_warning(); diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 140f46b6f6..f095c2af8c 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -51,7 +51,7 @@ void Touchscreen::loop() { } } -void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { +void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { TouchPoint tp; uint16_t x, y; if (this->touches_.count(id) == 0) { diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 1fe304d967..74747c589c 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -87,7 +87,7 @@ class Touchscreen : public PollingComponent { void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type); - void set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); + void add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); void send_touches_(); diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index ff688fd0b0..6b5cba74cd 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -109,7 +109,7 @@ void TT21100Touchscreen::update_touches() { i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, touch->pressure, touch->major_axis_length, touch->orientation); - this->set_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); + this->add_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); } } } diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp index 1a9c202af0..a268da06dd 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.cpp +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -55,7 +55,7 @@ void XPT2046Component::update_touches() { ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); - this->set_raw_touch_position_(0, x_raw, y_raw, z_raw); + this->add_raw_touch_position_(0, x_raw, y_raw, z_raw); } } From a64e96e7adbca4879c5d3b239272f8052e5d91ff Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:43:43 +0900 Subject: [PATCH 193/436] Bump version to 2023.12.0b5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 20218ab61d..0df9c27233 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0b4" +__version__ = "2023.12.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From d582cfa30a0ec8406a7d55cbe35f342c36e438e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Wed, 20 Dec 2023 11:33:05 +0100 Subject: [PATCH 194/436] image: allow the image to by auto-loaded by animation (#5139) --- esphome/__main__.py | 4 ++-- esphome/components/image/__init__.py | 1 + esphome/config.py | 20 ++++++++++++++++++-- esphome/loader.py | 4 ++++ esphome/writer.py | 8 ++++---- tests/test8.yaml | 10 ++++++++++ 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 0796dead43..baa5ecde47 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -12,7 +12,7 @@ import argcomplete from esphome import const, writer, yaml_util import esphome.codegen as cg -from esphome.config import iter_components, read_config, strip_default_ids +from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, CONF_BAUD_RATE, @@ -196,7 +196,7 @@ def write_cpp(config): def generate_cpp_contents(config): _LOGGER.info("Generating C++ source...") - for name, component, conf in iter_components(CORE.config): + for name, component, conf in iter_component_configs(CORE.config): if component.to_code is not None: coro = wrap_to_code(name, component) CORE.add_job(coro, conf) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index c11021fc9c..73dc73aa45 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "image" DEPENDENCIES = ["display"] MULTI_CONF = True +MULTI_CONF_NO_DEFAULT = True image_ns = cg.esphome_ns.namespace("image") diff --git a/esphome/config.py b/esphome/config.py index 745883c2ef..e9433d537e 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -39,6 +39,17 @@ _LOGGER = logging.getLogger(__name__) def iter_components(config): + for domain, conf in config.items(): + component = get_component(domain) + yield domain, component + if component.is_platform_component: + for p_config in conf: + p_name = f"{domain}.{p_config[CONF_PLATFORM]}" + platform = get_platform(domain, p_config[CONF_PLATFORM]) + yield p_name, platform + + +def iter_component_configs(config): for domain, conf in config.items(): component = get_component(domain) if component.multi_conf: @@ -303,8 +314,10 @@ class LoadValidationStep(ConfigValidationStep): # Ignore top-level keys starting with a dot return result.add_output_path([self.domain], self.domain) - result[self.domain] = self.conf component = get_component(self.domain) + if component.multi_conf_no_default and isinstance(self.conf, core.AutoLoad): + self.conf = [] + result[self.domain] = self.conf path = [self.domain] if component is None: result.add_str_error(f"Component not found: {self.domain}", path) @@ -424,7 +437,10 @@ class MetadataValidationStep(ConfigValidationStep): def run(self, result: Config) -> None: if self.conf is None: - result[self.domain] = self.conf = {} + if self.comp.multi_conf and self.comp.multi_conf_no_default: + result[self.domain] = self.conf = [] + else: + result[self.domain] = self.conf = {} success = True for dependency in self.comp.dependencies: diff --git a/esphome/loader.py b/esphome/loader.py index cd21e5a509..40a38d0a14 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -57,6 +57,10 @@ class ComponentManifest: def multi_conf(self) -> bool: return getattr(self.module, "MULTI_CONF", False) + @property + def multi_conf_no_default(self) -> bool: + return getattr(self.module, "MULTI_CONF_NO_DEFAULT", False) + @property def to_code(self) -> Optional[Callable[[Any], None]]: return getattr(self.module, "to_code", None) diff --git a/esphome/writer.py b/esphome/writer.py index 83e95614a6..3ad0e60d31 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -4,7 +4,7 @@ import re from pathlib import Path from typing import Union -from esphome.config import iter_components +from esphome.config import iter_components, iter_component_configs from esphome.const import ( HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, @@ -70,14 +70,14 @@ UPLOAD_SPEED_OVERRIDE = { def get_flags(key): flags = set() - for _, component, conf in iter_components(CORE.config): + for _, component, conf in iter_component_configs(CORE.config): flags |= getattr(component, key)(conf) return flags def get_include_text(): include_text = '#include "esphome.h"\nusing namespace esphome;\n' - for _, component, conf in iter_components(CORE.config): + for _, component, conf in iter_component_configs(CORE.config): if not hasattr(component, "includes"): continue includes = component.includes @@ -232,7 +232,7 @@ the custom_components folder or the external_components feature. def copy_src_tree(): source_files: list[loader.FileResource] = [] - for _, component, _ in iter_components(CORE.config): + for _, component in iter_components(CORE.config): source_files += component.resources source_files_map = { Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files diff --git a/tests/test8.yaml b/tests/test8.yaml index 558e86e1f9..fafdb76e12 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -92,3 +92,13 @@ sensor: name: "Loop Time" psram: name: "PSRAM Free" + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: pnglogo.png + type: RGB565 + use_transparency: no From 84174aeb8031db638bfa91b73ed8ec93e63ce518 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Dec 2023 19:42:27 +0900 Subject: [PATCH 195/436] Fix pin reuse error with pin expanders (#5973) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/pins.py | 20 +++++++++++--------- tests/test1.yaml | 4 ---- tests/test3.1.yaml | 2 -- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/esphome/pins.py b/esphome/pins.py index e2fd8e98e2..87f7084d4f 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,7 +1,7 @@ import operator from functools import reduce import esphome.config_validation as cv -from esphome.core import CORE, ID +from esphome.core import CORE from esphome.const import ( CONF_INPUT, @@ -25,15 +25,16 @@ class PinRegistry(dict): def reset(self): self.pins_used = {} - def get_count(self, key, number): + def get_count(self, key, id, number): """ Get the number of places a given pin is used. - :param key: The ID of the defining component + :param key: The key of the registered pin schema. + :param id: The ID of the defining component :param number: The pin number :return: The number of places the pin is used. """ - pin_key = (key, number) - return self.pins_used[pin_key] if pin_key in self.pins_used else 0 + pin_key = (key, id, number) + return len(self.pins_used[pin_key]) if pin_key in self.pins_used else 0 def register(self, name, schema, final_validate=None): """ @@ -65,9 +66,10 @@ class PinRegistry(dict): result = self[key][1](conf) if CONF_NUMBER in result: # key maps to the pin schema - if isinstance(key, ID): - key = key.id - pin_key = (key, result[CONF_NUMBER]) + if key != CORE.target_platform: + pin_key = (key, conf[key], result[CONF_NUMBER]) + else: + pin_key = (key, key, result[CONF_NUMBER]) if pin_key not in self.pins_used: self.pins_used[pin_key] = [] # client_id identifies the instance of the providing component @@ -101,7 +103,7 @@ class PinRegistry(dict): Run the final validation for all pins, and check for reuse :param fconf: The full config """ - for (key, _), pin_list in self.pins_used.items(): + for (key, _, _), pin_list in self.pins_used.items(): count = len(pin_list) # number of places same pin used. final_val_fun = self[key][2] # final validation function for pin_path, client_id, pin_config in pin_list: diff --git a/tests/test1.yaml b/tests/test1.yaml index f7b433cce2..3f8ae0151f 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1667,7 +1667,6 @@ binary_sensor: mcp23xxx: mcp23s08_hub # Use pin number 1 number: 1 - allow_other_uses: true # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false @@ -2149,7 +2148,6 @@ output: pin: mcp23xxx: mcp23017_hub number: 0 - allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2157,7 +2155,6 @@ output: pin: mcp23xxx: mcp23008_hub number: 0 - allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2597,7 +2594,6 @@ switch: mcp23xxx: mcp23s08_hub # Use pin number 0 number: 0 - allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 63ef4e8ce0..b5428abbfa 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -401,7 +401,6 @@ switch: pin: mcp23xxx: mcp23017_hub number: 0 - allow_other_uses: true mode: OUTPUT interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - platform: gpio @@ -409,7 +408,6 @@ switch: pin: mcp23xxx: mcp23008_hub number: 0 - allow_other_uses: true mode: OUTPUT interlock: *interlock - platform: gpio From 23ceddafed185ab38a8326782994b9c7e1f07f3c Mon Sep 17 00:00:00 2001 From: Yorick Smilda Date: Wed, 20 Dec 2023 11:52:46 +0100 Subject: [PATCH 196/436] Add ability to lock to set mode (#5924) --- esphome/components/hlw8012/hlw8012.cpp | 2 +- esphome/components/hlw8012/sensor.py | 11 ++++++++--- tests/test1.yaml | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 1a9f47faaf..14e83f60e1 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -96,7 +96,7 @@ void HLW8012Component::update() { this->energy_sensor_->publish_state(energy); } - if (this->change_mode_at_++ == this->change_mode_every_) { + if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) { this->current_mode_ = !this->current_mode_; ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE"); this->change_mode_at_ = 0; diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 033cccc3d4..2687edaca2 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -79,8 +79,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True), - cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All( - cv.uint32_t, cv.Range(min=1) + cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.Any( + "never", + cv.All(cv.uint32_t, cv.Range(min=1)), ), cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of( *INITIAL_MODES, lower=True @@ -114,6 +115,10 @@ async def to_code(config): cg.add(var.set_energy_sensor(sens)) cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) - cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY])) cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]])) cg.add(var.set_sensor_model(config[CONF_MODEL])) + + interval = config[CONF_CHANGE_MODE_EVERY] + if interval == "never": + interval = 0 + cg.add(var.set_change_mode_every(interval)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 3f8ae0151f..aaad10ff0a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -794,7 +794,7 @@ sensor: update_interval: 15s current_resistor: 0.001 ohm voltage_divider: 2351 - change_mode_every: 16 + change_mode_every: "never" initial_mode: VOLTAGE model: hlw8012 - platform: total_daily_energy From ab25e32509798e8564bad1b556f8ad552f13880f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Wed, 20 Dec 2023 11:33:05 +0100 Subject: [PATCH 197/436] image: allow the image to by auto-loaded by animation (#5139) --- esphome/__main__.py | 4 ++-- esphome/components/image/__init__.py | 1 + esphome/config.py | 20 ++++++++++++++++++-- esphome/loader.py | 4 ++++ esphome/writer.py | 8 ++++---- tests/test8.yaml | 10 ++++++++++ 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 0796dead43..baa5ecde47 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -12,7 +12,7 @@ import argcomplete from esphome import const, writer, yaml_util import esphome.codegen as cg -from esphome.config import iter_components, read_config, strip_default_ids +from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, CONF_BAUD_RATE, @@ -196,7 +196,7 @@ def write_cpp(config): def generate_cpp_contents(config): _LOGGER.info("Generating C++ source...") - for name, component, conf in iter_components(CORE.config): + for name, component, conf in iter_component_configs(CORE.config): if component.to_code is not None: coro = wrap_to_code(name, component) CORE.add_job(coro, conf) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index c11021fc9c..73dc73aa45 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "image" DEPENDENCIES = ["display"] MULTI_CONF = True +MULTI_CONF_NO_DEFAULT = True image_ns = cg.esphome_ns.namespace("image") diff --git a/esphome/config.py b/esphome/config.py index 745883c2ef..e9433d537e 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -39,6 +39,17 @@ _LOGGER = logging.getLogger(__name__) def iter_components(config): + for domain, conf in config.items(): + component = get_component(domain) + yield domain, component + if component.is_platform_component: + for p_config in conf: + p_name = f"{domain}.{p_config[CONF_PLATFORM]}" + platform = get_platform(domain, p_config[CONF_PLATFORM]) + yield p_name, platform + + +def iter_component_configs(config): for domain, conf in config.items(): component = get_component(domain) if component.multi_conf: @@ -303,8 +314,10 @@ class LoadValidationStep(ConfigValidationStep): # Ignore top-level keys starting with a dot return result.add_output_path([self.domain], self.domain) - result[self.domain] = self.conf component = get_component(self.domain) + if component.multi_conf_no_default and isinstance(self.conf, core.AutoLoad): + self.conf = [] + result[self.domain] = self.conf path = [self.domain] if component is None: result.add_str_error(f"Component not found: {self.domain}", path) @@ -424,7 +437,10 @@ class MetadataValidationStep(ConfigValidationStep): def run(self, result: Config) -> None: if self.conf is None: - result[self.domain] = self.conf = {} + if self.comp.multi_conf and self.comp.multi_conf_no_default: + result[self.domain] = self.conf = [] + else: + result[self.domain] = self.conf = {} success = True for dependency in self.comp.dependencies: diff --git a/esphome/loader.py b/esphome/loader.py index cd21e5a509..40a38d0a14 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -57,6 +57,10 @@ class ComponentManifest: def multi_conf(self) -> bool: return getattr(self.module, "MULTI_CONF", False) + @property + def multi_conf_no_default(self) -> bool: + return getattr(self.module, "MULTI_CONF_NO_DEFAULT", False) + @property def to_code(self) -> Optional[Callable[[Any], None]]: return getattr(self.module, "to_code", None) diff --git a/esphome/writer.py b/esphome/writer.py index 83e95614a6..3ad0e60d31 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -4,7 +4,7 @@ import re from pathlib import Path from typing import Union -from esphome.config import iter_components +from esphome.config import iter_components, iter_component_configs from esphome.const import ( HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, @@ -70,14 +70,14 @@ UPLOAD_SPEED_OVERRIDE = { def get_flags(key): flags = set() - for _, component, conf in iter_components(CORE.config): + for _, component, conf in iter_component_configs(CORE.config): flags |= getattr(component, key)(conf) return flags def get_include_text(): include_text = '#include "esphome.h"\nusing namespace esphome;\n' - for _, component, conf in iter_components(CORE.config): + for _, component, conf in iter_component_configs(CORE.config): if not hasattr(component, "includes"): continue includes = component.includes @@ -232,7 +232,7 @@ the custom_components folder or the external_components feature. def copy_src_tree(): source_files: list[loader.FileResource] = [] - for _, component, _ in iter_components(CORE.config): + for _, component in iter_components(CORE.config): source_files += component.resources source_files_map = { Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files diff --git a/tests/test8.yaml b/tests/test8.yaml index 558e86e1f9..fafdb76e12 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -92,3 +92,13 @@ sensor: name: "Loop Time" psram: name: "PSRAM Free" + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: pnglogo.png + type: RGB565 + use_transparency: no From 6a9e85438f95b14be54bdf9a961897bdd78224b9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Dec 2023 19:42:27 +0900 Subject: [PATCH 198/436] Fix pin reuse error with pin expanders (#5973) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/pins.py | 20 +++++++++++--------- tests/test1.yaml | 4 ---- tests/test3.1.yaml | 2 -- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/esphome/pins.py b/esphome/pins.py index e2fd8e98e2..87f7084d4f 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,7 +1,7 @@ import operator from functools import reduce import esphome.config_validation as cv -from esphome.core import CORE, ID +from esphome.core import CORE from esphome.const import ( CONF_INPUT, @@ -25,15 +25,16 @@ class PinRegistry(dict): def reset(self): self.pins_used = {} - def get_count(self, key, number): + def get_count(self, key, id, number): """ Get the number of places a given pin is used. - :param key: The ID of the defining component + :param key: The key of the registered pin schema. + :param id: The ID of the defining component :param number: The pin number :return: The number of places the pin is used. """ - pin_key = (key, number) - return self.pins_used[pin_key] if pin_key in self.pins_used else 0 + pin_key = (key, id, number) + return len(self.pins_used[pin_key]) if pin_key in self.pins_used else 0 def register(self, name, schema, final_validate=None): """ @@ -65,9 +66,10 @@ class PinRegistry(dict): result = self[key][1](conf) if CONF_NUMBER in result: # key maps to the pin schema - if isinstance(key, ID): - key = key.id - pin_key = (key, result[CONF_NUMBER]) + if key != CORE.target_platform: + pin_key = (key, conf[key], result[CONF_NUMBER]) + else: + pin_key = (key, key, result[CONF_NUMBER]) if pin_key not in self.pins_used: self.pins_used[pin_key] = [] # client_id identifies the instance of the providing component @@ -101,7 +103,7 @@ class PinRegistry(dict): Run the final validation for all pins, and check for reuse :param fconf: The full config """ - for (key, _), pin_list in self.pins_used.items(): + for (key, _, _), pin_list in self.pins_used.items(): count = len(pin_list) # number of places same pin used. final_val_fun = self[key][2] # final validation function for pin_path, client_id, pin_config in pin_list: diff --git a/tests/test1.yaml b/tests/test1.yaml index f7b433cce2..3f8ae0151f 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1667,7 +1667,6 @@ binary_sensor: mcp23xxx: mcp23s08_hub # Use pin number 1 number: 1 - allow_other_uses: true # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false @@ -2149,7 +2148,6 @@ output: pin: mcp23xxx: mcp23017_hub number: 0 - allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2157,7 +2155,6 @@ output: pin: mcp23xxx: mcp23008_hub number: 0 - allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2597,7 +2594,6 @@ switch: mcp23xxx: mcp23s08_hub # Use pin number 0 number: 0 - allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 63ef4e8ce0..b5428abbfa 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -401,7 +401,6 @@ switch: pin: mcp23xxx: mcp23017_hub number: 0 - allow_other_uses: true mode: OUTPUT interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - platform: gpio @@ -409,7 +408,6 @@ switch: pin: mcp23xxx: mcp23008_hub number: 0 - allow_other_uses: true mode: OUTPUT interlock: *interlock - platform: gpio From bec1ad9396806086f61eae699ca6d3d8210682a7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Dec 2023 20:59:46 +0900 Subject: [PATCH 199/436] Bump version to 2023.12.0b6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0df9c27233..475f28c35e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0b5" +__version__ = "2023.12.0b6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 867595561417deb035fcde921a33db72d1b8c3fa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Dec 2023 08:09:10 +0900 Subject: [PATCH 200/436] Bump version to 2023.12.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 475f28c35e..55be91a4f9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0b6" +__version__ = "2023.12.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 991880d53fa2d21512dfeaf84ee1380a4bd32bbe Mon Sep 17 00:00:00 2001 From: Branden Cash <203336+ammmze@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:07:40 -0700 Subject: [PATCH 201/436] feat: add AS5600 component/sensor (#5174) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/as5600/__init__.py | 228 ++++++++++++++++++ esphome/components/as5600/as5600.cpp | 138 +++++++++++ esphome/components/as5600/as5600.h | 105 ++++++++ esphome/components/as5600/sensor/__init__.py | 119 +++++++++ .../as5600/sensor/as5600_sensor.cpp | 98 ++++++++ .../components/as5600/sensor/as5600_sensor.h | 43 ++++ tests/test1.yaml | 22 ++ 8 files changed, 755 insertions(+) create mode 100644 esphome/components/as5600/__init__.py create mode 100644 esphome/components/as5600/as5600.cpp create mode 100644 esphome/components/as5600/as5600.h create mode 100644 esphome/components/as5600/sensor/__init__.py create mode 100644 esphome/components/as5600/sensor/as5600_sensor.cpp create mode 100644 esphome/components/as5600/sensor/as5600_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 320a23ffaa..fc14b2088e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -34,6 +34,8 @@ esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/api/* @OttoWinter +esphome/components/as5600/* @ammmze +esphome/components/as5600/sensor/* @ammmze esphome/components/as7341/* @mrgnr esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py new file mode 100644 index 0000000000..1840b22768 --- /dev/null +++ b/esphome/components/as5600/__init__.py @@ -0,0 +1,228 @@ +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_DIR_PIN, + CONF_DIRECTION, + CONF_HYSTERESIS, + CONF_RANGE, +) + +CODEOWNERS = ["@ammmze"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +as5600_ns = cg.esphome_ns.namespace("as5600") +AS5600Component = as5600_ns.class_("AS5600Component", cg.Component, i2c.I2CDevice) + +DIRECTION = { + "CLOCKWISE": 0, + "COUNTERCLOCKWISE": 1, +} + +POWER_MODE = { + "NOMINAL": 0, + "LOW1": 1, + "LOW2": 2, + "LOW3": 3, +} + +HYSTERESIS = { + "NONE": 0, + "LSB1": 1, + "LSB2": 2, + "LSB3": 3, +} + +SLOW_FILTER = { + "16X": 0, + "8X": 1, + "4X": 2, + "2X": 3, +} + +FAST_FILTER = { + "NONE": 0, + "LSB6": 1, + "LSB7": 2, + "LSB9": 3, + "LSB18": 4, + "LSB21": 5, + "LSB24": 6, + "LSB10": 7, +} + +CONF_ANGLE = "angle" +CONF_RAW_ANGLE = "raw_angle" +CONF_RAW_POSITION = "raw_position" +CONF_WATCHDOG = "watchdog" +CONF_POWER_MODE = "power_mode" +CONF_SLOW_FILTER = "slow_filter" +CONF_FAST_FILTER = "fast_filter" +CONF_START_POSITION = "start_position" +CONF_END_POSITION = "end_position" + + +RESOLUTION = 4096 +MAX_POSITION = RESOLUTION - 1 +ANGLE_TO_POSITION = RESOLUTION / 360 +POSITION_TO_ANGLE = 360 / RESOLUTION +# validate min range of 18deg (per datasheet) ... though i seem to get valid values down to a range of 192steps (16.875deg) +MIN_RANGE = round(18 * ANGLE_TO_POSITION) + + +def angle(min=-360, max=360): + return cv.All( + cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max) + ) + + +def angle_to_position(value, min=-360, max=360): + try: + value = angle(min=min, max=max)(value) + return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION + except cv.Invalid as e: + raise cv.Invalid(f"When using angle, {e.error_message}") + + +def percent_to_position(value): + value = cv.possibly_negative_percentage(value) + return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION + + +def position(min=-MAX_POSITION, max=MAX_POSITION): + """Validate that the config option is a position. + Accepts integers, degrees, or percentage (of 360 degrees). + """ + + def validator(value): + if isinstance(value, str) and value.endswith("%"): + value = percent_to_position(value) + + if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")): + return angle_to_position( + value, + min=round(min * POSITION_TO_ANGLE), + max=round(max * POSITION_TO_ANGLE), + ) + + return cv.int_range(min=min, max=max)(value) + + return validator + + +def position_range(): + """Validate that value given is a valid range for the device. + A valid range is one of the following: + - a value of 0 (meaning full range) + - 18 thru 360 degrees + - negative 360 thru negative 18 degrees (notes: these are normalized to their positive values, accepting negatives is for convenience) + """ + zero_validator = position(min=0, max=0) + negative_validator = cv.Any( + position(min=-MAX_POSITION, max=-MIN_RANGE), + zero_validator, + ) + positive_validator = cv.Any( + position(min=MIN_RANGE, max=MAX_POSITION), + zero_validator, + ) + + def validator(value): + is_negative_str = isinstance(value, str) and value.startswith("-") + is_negative_num = isinstance(value, (float, int)) and value < 0 + if is_negative_str or is_negative_num: + return negative_validator(value) + return positive_validator(value) + + return validator + + +def has_valid_range_config(): + """Validate that that the config start + end position results in a valid + positional range, which must be >= 18degrees + """ + range_validator = position_range() + + def validator(config): + # if we don't have an end position, then there is nothing to do + if CONF_END_POSITION not in config: + return config + + # determine the range by taking the difference from the end and start + range = config[CONF_END_POSITION] - config[CONF_START_POSITION] + + # but need to account for start position being greater than end position + # where the range rolls back around the 0 position + if config[CONF_END_POSITION] < config[CONF_START_POSITION]: + range = RESOLUTION + config[CONF_END_POSITION] - config[CONF_START_POSITION] + + try: + range_validator(range) + return config + except cv.Invalid as e: + raise cv.Invalid( + f"The range between start and end position is invalid. It was was {range} but {e.error_message}" + ) + + return validator + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AS5600Component), + cv.Optional(CONF_DIR_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_DIRECTION, default="CLOCKWISE"): cv.enum( + DIRECTION, upper=True + ), + cv.Optional(CONF_WATCHDOG, default=False): cv.boolean, + cv.Optional(CONF_POWER_MODE, default="NOMINAL"): cv.enum( + POWER_MODE, upper=True, space="" + ), + cv.Optional(CONF_HYSTERESIS, default="NONE"): cv.enum( + HYSTERESIS, upper=True, space="" + ), + cv.Optional(CONF_SLOW_FILTER, default="16X"): cv.enum( + SLOW_FILTER, upper=True, space="" + ), + cv.Optional(CONF_FAST_FILTER, default="NONE"): cv.enum( + FAST_FILTER, upper=True, space="" + ), + cv.Optional(CONF_START_POSITION, default=0): position(), + cv.Optional(CONF_END_POSITION): position(), + cv.Optional(CONF_RANGE): position_range(), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x36)), + # ensure end_position and range are mutually exclusive + cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE), + has_valid_range_config(), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_direction(config[CONF_DIRECTION])) + cg.add(var.set_watchdog(config[CONF_WATCHDOG])) + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + cg.add(var.set_slow_filter(config[CONF_SLOW_FILTER])) + cg.add(var.set_fast_filter(config[CONF_FAST_FILTER])) + cg.add(var.set_start_position(config[CONF_START_POSITION])) + + if dir_pin_config := config.get(CONF_DIR_PIN): + pin = await cg.gpio_pin_expression(dir_pin_config) + cg.add(var.set_dir_pin(pin)) + + if (end_position_config := config.get(CONF_END_POSITION, None)) is not None: + cg.add(var.set_end_position(end_position_config)) + + if (range_config := config.get(CONF_RANGE, None)) is not None: + cg.add(var.set_range(range_config)) diff --git a/esphome/components/as5600/as5600.cpp b/esphome/components/as5600/as5600.cpp new file mode 100644 index 0000000000..3fe7eab58d --- /dev/null +++ b/esphome/components/as5600/as5600.cpp @@ -0,0 +1,138 @@ +#include "as5600.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as5600 { + +static const char *const TAG = "as5600"; + +// Configuration registers +static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R +static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW +static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW +static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW +static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW + +// Output registers +static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R +static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R + +// Status registers +static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R +static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R +static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R + +void AS5600Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up AS5600..."); + + if (!this->read_byte(REGISTER_STATUS).has_value()) { + this->mark_failed(); + return; + } + + // configuration direction pin, if given + // the dir pin on the chip should be low for clockwise + // and high for counterclockwise. If the pin is left floating + // the reported positions will be erratic. + if (this->dir_pin_ != nullptr) { + this->dir_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->dir_pin_->digital_write(this->direction_ == 1); + } + + // build config register + // take the value, shift it left, and add mask to it to ensure we + // are only changing the bits appropriate for that setting in the + // off chance we somehow have bad value in there and it makes for + // a nice visual for the bit positions. + uint16_t config = 0; + // clang-format off + config |= (this->watchdog_ << 13) & 0b0010000000000000; + config |= (this->fast_filter_ << 10) & 0b0001110000000000; + config |= (this->slow_filter_ << 8) & 0b0000001100000000; + config |= (this->pwm_frequency_ << 6) & 0b0000000011000000; + config |= (this->output_mode_ << 4) & 0b0000000000110000; + config |= (this->hysteresis_ << 2) & 0b0000000000001100; + config |= (this->power_mode_ << 0) & 0b0000000000000011; + // clang-format on + + // write config to config register + if (!this->write_byte_16(REGISTER_CONF, config)) { + this->mark_failed(); + return; + } + + // configure the start position + this->write_byte_16(REGISTER_ZPOS, this->start_position_); + + // configure either end position or max angle + if (this->end_mode_ == END_MODE_POSITION) { + this->write_byte_16(REGISTER_MPOS, this->end_position_); + } else { + this->write_byte_16(REGISTER_MANG, this->end_position_); + } + + // calculate the raw max from end position or start + range + this->raw_max_ = this->end_mode_ == END_MODE_POSITION ? this->end_position_ & 4095 + : (this->start_position_ + this->end_position_) & 4095; + + // calculate allowed range of motion by taking the start from the end + // but only if the end is greater than the start. If the start is greater + // than the end position, then that means we take the start all the way to + // reset point (i.e. 0 deg raw) and then we that with the end position + uint16_t range = this->raw_max_ > this->start_position_ ? this->raw_max_ - this->start_position_ + : (4095 - this->start_position_) + this->raw_max_; + + // range scale is ratio of actual allowed range to the full range + this->range_scale_ = range / 4095.0f; +} + +void AS5600Component::dump_config() { + ESP_LOGCONFIG(TAG, "AS5600:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AS5600 failed!"); + return; + } + + ESP_LOGCONFIG(TAG, " Watchdog: %d", this->watchdog_); + ESP_LOGCONFIG(TAG, " Fast Filter: %d", this->fast_filter_); + ESP_LOGCONFIG(TAG, " Slow Filter: %d", this->slow_filter_); + ESP_LOGCONFIG(TAG, " Hysteresis: %d", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Start Position: %d", this->start_position_); + if (this->end_mode_ == END_MODE_POSITION) { + ESP_LOGCONFIG(TAG, " End Position: %d", this->end_position_); + } else { + ESP_LOGCONFIG(TAG, " Range: %d", this->end_position_); + } +} + +bool AS5600Component::in_range(uint16_t raw_position) { + return this->raw_max_ > this->start_position_ + ? raw_position >= this->start_position_ && raw_position <= this->raw_max_ + : raw_position >= this->start_position_ || raw_position <= this->raw_max_; +} + +AS5600MagnetStatus AS5600Component::read_magnet_status() { + uint8_t status = this->reg(REGISTER_STATUS).get() >> 3 & 0b000111; + return static_cast(status); +} + +optional AS5600Component::read_position() { + uint16_t pos = 0; + if (!this->read_byte_16(REGISTER_ANGLE, &pos)) { + return {}; + } + return pos; +} + +optional AS5600Component::read_raw_position() { + uint16_t pos = 0; + if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) { + return {}; + } + return pos; +} + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h new file mode 100644 index 0000000000..fbfd18db40 --- /dev/null +++ b/esphome/components/as5600/as5600.h @@ -0,0 +1,105 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/preferences.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace as5600 { + +static const uint16_t POSITION_COUNT = 4096; +static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT; +static const float DEGREES_TO_RAW = POSITION_COUNT / 360.0; + +enum EndPositionMode : uint8_t { + // In this mode, the end position is calculated by taking the start position + // and adding the range/positions. For example, you could say start at 90deg, + // and have a range of 180deg and effectively the sensor will report values + // from the physical 90deg thru 270deg. + END_MODE_RANGE, + // In this mode, the end position is explicitly set, and changing the start + // position will NOT change the end position. + END_MODE_POSITION, +}; + +enum OutRangeMode : uint8_t { + // In this mode, the AS5600 chip itself actually reports these values, but + // effectively it splits the out-of-range values in half, and when positioned + // over the half closest to the min/start position, it will report 0 and when + // positioned over the half closes to the max/end position, it will report the + // max/end value. + OUT_RANGE_MODE_MIN_MAX, + // In this mode, when the magnet is positioned outside the configured + // range, the sensor will report NAN, which translates to "Unknown" + // in Home Assistant. + OUT_RANGE_MODE_NAN, +}; + +enum AS5600MagnetStatus : uint8_t { + MAGNET_GONE = 2, // 0b010 / magnet not detected + MAGNET_OK = 4, // 0b100 / magnet just right + MAGNET_STRONG = 5, // 0b101 / magnet too strong + MAGNET_WEAK = 6, // 0b110 / magnet too weak +}; + +class AS5600Component : public Component, public i2c::I2CDevice { + public: + /// Set up the internal sensor array. + void setup() override; + void dump_config() override; + /// HARDWARE_LATE setup priority + float get_setup_priority() const override { return setup_priority::DATA; } + + // configuration setters + void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; } + void set_direction(uint8_t direction) { this->direction_ = direction; } + void set_fast_filter(uint8_t fast_filter) { this->fast_filter_ = fast_filter; } + void set_hysteresis(uint8_t hysteresis) { this->hysteresis_ = hysteresis; } + void set_power_mode(uint8_t power_mode) { this->power_mode_ = power_mode; } + void set_slow_filter(uint8_t slow_filter) { this->slow_filter_ = slow_filter; } + void set_watchdog(bool watchdog) { this->watchdog_ = watchdog; } + bool get_watchdog() { return this->watchdog_; } + void set_start_position(uint16_t start_position) { this->start_position_ = start_position % POSITION_COUNT; } + void set_end_position(uint16_t end_position) { + this->end_position_ = end_position % POSITION_COUNT; + this->end_mode_ = END_MODE_POSITION; + } + void set_range(uint16_t range) { + this->end_position_ = range % POSITION_COUNT; + this->end_mode_ = END_MODE_RANGE; + } + + // Gets the scale value for the configured range. + // For example, if configured to start at 0deg and end at 180deg, the + // range is 50% of the native/raw range, so the range scale would be 0.5. + // If configured to use the full 360deg, the range scale would be 1.0. + float get_range_scale() { return this->range_scale_; } + + // Indicates whether the given *raw* position is within the configured range + bool in_range(uint16_t raw_position); + + AS5600MagnetStatus read_magnet_status(); + optional read_position(); + optional read_raw_position(); + + protected: + InternalGPIOPin *dir_pin_{nullptr}; + uint8_t direction_; + uint8_t fast_filter_; + uint8_t hysteresis_; + uint8_t power_mode_; + uint8_t slow_filter_; + uint8_t pwm_frequency_{0}; + uint8_t output_mode_{0}; + bool watchdog_; + uint16_t start_position_; + uint16_t end_position_{0}; + uint16_t raw_max_; + EndPositionMode end_mode_{END_MODE_RANGE}; + float range_scale_{1.0}; +}; + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py new file mode 100644 index 0000000000..589a66950a --- /dev/null +++ b/esphome/components/as5600/sensor/__init__.py @@ -0,0 +1,119 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + ICON_MAGNET, + ICON_ROTATE_RIGHT, + CONF_GAIN, + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_MAGNITUDE, + CONF_STATUS, + CONF_POSITION, +) +from .. import as5600_ns, AS5600Component + +CODEOWNERS = ["@ammmze"] +DEPENDENCIES = ["as5600"] + +AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent) + +CONF_ANGLE = "angle" +CONF_RAW_ANGLE = "raw_angle" +CONF_RAW_POSITION = "raw_position" +CONF_WATCHDOG = "watchdog" +CONF_POWER_MODE = "power_mode" +CONF_SLOW_FILTER = "slow_filter" +CONF_FAST_FILTER = "fast_filter" +CONF_PWM_FREQUENCY = "pwm_frequency" +CONF_BURN_COUNT = "burn_count" +CONF_START_POSITION = "start_position" +CONF_END_POSITION = "end_position" +CONF_OUT_OF_RANGE_MODE = "out_of_range_mode" + +OutOfRangeMode = as5600_ns.enum("OutRangeMode") +OUT_OF_RANGE_MODES = { + "MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX, + "NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN, +} + + +CONF_AS5600_ID = "as5600_id" +CONFIG_SCHEMA = ( + sensor.sensor_schema( + AS5600Sensor, + accuracy_decimals=0, + icon=ICON_ROTATE_RIGHT, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_AS5600_ID): cv.use_id(AS5600Component), + cv.Optional(CONF_OUT_OF_RANGE_MODE): cv.enum( + OUT_OF_RANGE_MODES, upper=True, space="_" + ), + cv.Optional(CONF_RAW_POSITION): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_ROTATE_RIGHT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_GAIN): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_MAGNITUDE): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_MAGNET, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_MAGNET, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_AS5600_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE): + cg.add(var.set_out_of_range_mode(out_of_range_mode_config)) + + if angle_config := config.get(CONF_ANGLE): + sens = await sensor.new_sensor(angle_config) + cg.add(var.set_angle_sensor(sens)) + + if raw_angle_config := config.get(CONF_RAW_ANGLE): + sens = await sensor.new_sensor(raw_angle_config) + cg.add(var.set_raw_angle_sensor(sens)) + + if position_config := config.get(CONF_POSITION): + sens = await sensor.new_sensor(position_config) + cg.add(var.set_position_sensor(sens)) + + if raw_position_config := config.get(CONF_RAW_POSITION): + sens = await sensor.new_sensor(raw_position_config) + cg.add(var.set_raw_position_sensor(sens)) + + if gain_config := config.get(CONF_GAIN): + sens = await sensor.new_sensor(gain_config) + cg.add(var.set_gain_sensor(sens)) + + if magnitude_config := config.get(CONF_MAGNITUDE): + sens = await sensor.new_sensor(magnitude_config) + cg.add(var.set_magnitude_sensor(sens)) + + if status_config := config.get(CONF_STATUS): + sens = await sensor.new_sensor(status_config) + cg.add(var.set_status_sensor(sens)) diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp new file mode 100644 index 0000000000..feb8f6cebf --- /dev/null +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -0,0 +1,98 @@ +#include "as5600_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as5600 { + +static const char *const TAG = "as5600.sensor"; + +// Configuration registers +static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R +static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW +static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW +static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW +static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW + +// Output registers +static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R +static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R + +// Status registers +static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R +static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R +static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R + +float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void AS5600Sensor::dump_config() { + LOG_SENSOR("", "AS5600 Sensor", this); + ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); + if (this->angle_sensor_ != nullptr) { + LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_); + } + if (this->raw_angle_sensor_ != nullptr) { + LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_); + } + if (this->position_sensor_ != nullptr) { + LOG_SENSOR(" ", "Position Sensor", this->position_sensor_); + } + if (this->raw_position_sensor_ != nullptr) { + LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); + } + if (this->gain_sensor_ != nullptr) { + LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); + } + if (this->magnitude_sensor_ != nullptr) { + LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); + } + if (this->status_sensor_ != nullptr) { + LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); + } + LOG_UPDATE_INTERVAL(this); +} + +void AS5600Sensor::update() { + if (this->gain_sensor_ != nullptr) { + this->gain_sensor_->publish_state(this->parent_->reg(REGISTER_AGC).get()); + } + + if (this->magnitude_sensor_ != nullptr) { + uint16_t value = 0; + this->parent_->read_byte_16(REGISTER_MAGNITUDE, &value); + this->magnitude_sensor_->publish_state(value); + } + + // 2 = magnet not detected + // 4 = magnet just right + // 5 = magnet too strong + // 6 = magnet too weak + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(this->parent_->read_magnet_status()); + } + + auto pos = this->parent_->read_position(); + if (!pos.has_value()) { + this->status_set_warning(); + return; + } + + auto raw = this->parent_->read_raw_position(); + if (!raw.has_value()) { + this->status_set_warning(); + return; + } + + if (this->out_of_range_mode_ == OUT_RANGE_MODE_NAN) { + this->publish_state(this->parent_->in_range(raw.value()) ? pos.value() : NAN); + } else { + this->publish_state(pos.value()); + } + + if (this->raw_position_sensor_ != nullptr) { + this->raw_position_sensor_->publish_state(raw.value()); + } + this->status_clear_warning(); +} + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h new file mode 100644 index 0000000000..0af9b01ae5 --- /dev/null +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/preferences.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/as5600/as5600.h" + +namespace esphome { +namespace as5600 { + +class AS5600Sensor : public PollingComponent, public Parented, public sensor::Sensor { + public: + void update() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } + void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } + void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; } + void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) { + this->raw_position_sensor_ = raw_position_sensor; + } + void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; } + void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; } + OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; } + + protected: + sensor::Sensor *angle_sensor_{nullptr}; + sensor::Sensor *raw_angle_sensor_{nullptr}; + sensor::Sensor *position_sensor_{nullptr}; + sensor::Sensor *raw_position_sensor_{nullptr}; + sensor::Sensor *gain_sensor_{nullptr}; + sensor::Sensor *magnitude_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX}; +}; + +} // namespace as5600 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index aaad10ff0a..2748a09c59 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -322,6 +322,18 @@ ads1115: address: 0x48 i2c_id: i2c_bus +as5600: + i2c_id: i2c_bus + dir_pin: GPIO27 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + dallas: pin: allow_other_uses: true @@ -555,6 +567,16 @@ sensor: state_topic: hi/me retain: false availability: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status - platform: as7341 update_interval: 15s gain: X8 From 3c2383e26119c3c05341786aa335ea7653c0d120 Mon Sep 17 00:00:00 2001 From: mknjc Date: Thu, 21 Dec 2023 01:08:13 +0100 Subject: [PATCH 202/436] Add default substitutions for package includes (#5752) --- esphome/const.py | 1 + esphome/yaml_util.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5de34b86cd..58c6c47e3a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -185,6 +185,7 @@ CONF_DEFAULT_MODE = "default_mode" CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high" CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low" CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" +CONF_DEFAULTS = "defaults" CONF_DELAY = "delay" CONF_DELIMITER = "delimiter" CONF_DELTA = "delta" diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index f0f755dd61..aa9fe45ebb 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -282,7 +282,7 @@ class ESPHomeLoader(FastestAvailableSafeLoader): return file, vars def substitute_vars(config, vars): - from esphome.const import CONF_SUBSTITUTIONS + from esphome.const import CONF_SUBSTITUTIONS, CONF_DEFAULTS from esphome.components import substitutions org_subs = None @@ -294,7 +294,15 @@ class ESPHomeLoader(FastestAvailableSafeLoader): elif CONF_SUBSTITUTIONS in result: org_subs = result.pop(CONF_SUBSTITUTIONS) + defaults = {} + if CONF_DEFAULTS in result: + defaults = result.pop(CONF_DEFAULTS) + result[CONF_SUBSTITUTIONS] = vars + for k, v in defaults.items(): + if k not in result[CONF_SUBSTITUTIONS]: + result[CONF_SUBSTITUTIONS][k] = v + # Ignore missing vars that refer to the top level substitutions substitutions.do_substitution_pass(result, None, ignore_missing=True) result.pop(CONF_SUBSTITUTIONS) From c6a37da9da4061e5a9f663b800d694ca27b478f0 Mon Sep 17 00:00:00 2001 From: Matthew Campbell Date: Wed, 20 Dec 2023 16:08:44 -0800 Subject: [PATCH 203/436] Add gradient option to addressable color wipe effect (#5689) --- esphome/components/light/addressable_light_effect.h | 12 ++++++++++-- esphome/components/light/effects.py | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index c2109b2d23..73083a58b7 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -100,6 +100,7 @@ struct AddressableColorWipeEffectColor { uint8_t r, g, b, w; bool random; size_t num_leds; + bool gradient; }; class AddressableColorWipeEffect : public AddressableLightEffect { @@ -117,8 +118,15 @@ class AddressableColorWipeEffect : public AddressableLightEffect { it.shift_left(1); else it.shift_right(1); - const AddressableColorWipeEffectColor color = this->colors_[this->at_color_]; - const Color esp_color = Color(color.r, color.g, color.b, color.w); + const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_]; + Color esp_color = Color(color.r, color.g, color.b, color.w); + if (color.gradient) { + size_t next_color_index = (this->at_color_ + 1) % this->colors_.size(); + const AddressableColorWipeEffectColor &next_color = this->colors_[next_color_index]; + const Color next_esp_color = Color(next_color.r, next_color.g, next_color.b, next_color.w); + uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds); + esp_color = esp_color.gradient(next_esp_color, gradient); + } if (this->reverse_) it[-1] = esp_color; else diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 5212e90938..5093ace949 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -58,6 +58,7 @@ from .types import ( CONF_ADD_LED_INTERVAL = "add_led_interval" CONF_REVERSE = "reverse" +CONF_GRADIENT = "gradient" CONF_MOVE_INTERVAL = "move_interval" CONF_SCAN_WIDTH = "scan_width" CONF_TWINKLE_PROBABILITY = "twinkle_probability" @@ -386,6 +387,7 @@ async def addressable_rainbow_effect_to_code(config, effect_id): cv.Optional(CONF_WHITE, default=1.0): cv.percentage, cv.Optional(CONF_RANDOM, default=False): cv.boolean, cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)), + cv.Optional(CONF_GRADIENT, default=False): cv.boolean, } ), cv.Optional( @@ -409,6 +411,7 @@ async def addressable_color_wipe_effect_to_code(config, effect_id): ("w", int(round(color[CONF_WHITE] * 255))), ("random", color[CONF_RANDOM]), ("num_leds", color[CONF_NUM_LEDS]), + ("gradient", color[CONF_GRADIENT]), ) ) cg.add(var.set_colors(colors)) From b5932940eebb085317e67de24c3f93ade537419f Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Thu, 21 Dec 2023 01:10:46 +0100 Subject: [PATCH 204/436] Added alarm processing for Haier component (hOn protocol) (#5965) --- esphome/components/haier/climate.py | 51 ++++++-- esphome/components/haier/haier_base.cpp | 3 +- esphome/components/haier/haier_base.h | 3 +- esphome/components/haier/hon_climate.cpp | 117 +++++++++++++++++- esphome/components/haier/hon_climate.h | 27 ++++ esphome/components/haier/hon_packet.h | 56 +++++++++ .../components/haier/smartair2_climate.cpp | 19 ++- tests/test3.yaml | 23 +++- 8 files changed, 282 insertions(+), 17 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 49d42a231f..c6998ce0c5 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_SUPPORTED_SWING_MODES, CONF_TARGET_TEMPERATURE, CONF_TEMPERATURE_STEP, + CONF_TRIGGER_ID, CONF_VISUAL, CONF_WIFI, DEVICE_CLASS_TEMPERATURE, @@ -49,6 +50,8 @@ CONF_CONTROL_METHOD = "control_method" CONF_CONTROL_PACKET_SIZE = "control_packet_size" CONF_DISPLAY = "display" CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" +CONF_ON_ALARM_START = "on_alarm_start" +CONF_ON_ALARM_END = "on_alarm_end" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_VERTICAL_AIRFLOW = "vertical_airflow" CONF_WIFI_SIGNAL = "wifi_signal" @@ -85,8 +88,8 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = { } SUPPORTED_SWING_MODES_OPTIONS = { - "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available - "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available + "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, } @@ -101,13 +104,15 @@ SUPPORTED_CLIMATE_MODES_OPTIONS = { } SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = { + "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, } SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { - "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, } @@ -118,6 +123,16 @@ SUPPORTED_HON_CONTROL_METHODS = { "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, } +HaierAlarmStartTrigger = haier_ns.class_( + "HaierAlarmStartTrigger", + automation.Trigger.template(cg.uint8, cg.const_char_ptr), +) + +HaierAlarmEndTrigger = haier_ns.class_( + "HaierAlarmEndTrigger", + automation.Trigger.template(cg.uint8, cg.const_char_ptr), +) + def validate_visual(config): if CONF_VISUAL in config: @@ -200,9 +215,7 @@ CONFIG_SCHEMA = cv.All( ): cv.boolean, cv.Optional( CONF_SUPPORTED_PRESETS, - default=list( - SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys() - ), + default=list(["BOOST", "COMFORT"]), # No AWAY by default ): cv.ensure_list( cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True) ), @@ -222,7 +235,7 @@ CONFIG_SCHEMA = cv.All( ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50), cv.Optional( CONF_SUPPORTED_PRESETS, - default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), + default=list(["BOOST", "ECO", "SLEEP"]), # No AWAY by default ): cv.ensure_list( cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) ), @@ -233,6 +246,20 @@ CONFIG_SCHEMA = cv.All( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ON_ALARM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + HaierAlarmStartTrigger + ), + } + ), + cv.Optional(CONF_ON_ALARM_END): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + HaierAlarmEndTrigger + ), + } + ), } ), }, @@ -457,5 +484,15 @@ async def to_code(config): config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE ) ) + for conf in config.get(CONF_ON_ALARM_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + ) + for conf in config.get(CONF_ON_ALARM_END, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + ) # https://github.com/paveldn/HaierProtocol cg.add_library("pavlodn/HaierProtocol", "0.9.24") diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 6943fc7d9c..a3f68bb081 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -25,13 +25,14 @@ const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { "SENDING_INIT_1", "SENDING_INIT_2", "SENDING_FIRST_STATUS_REQUEST", - "SENDING_ALARM_STATUS_REQUEST", + "SENDING_FIRST_ALARM_STATUS_REQUEST", "IDLE", "SENDING_STATUS_REQUEST", "SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_SIGNAL_LEVEL", "SENDING_CONTROL", "SENDING_ACTION_COMMAND", + "SENDING_ALARM_STATUS_REQUEST", "UNKNOWN" // Should be the last! }; static_assert( diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 75abbc20fb..504c841e5f 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -64,7 +64,7 @@ class HaierClimateBase : public esphome::Component, SENDING_INIT_1 = 0, SENDING_INIT_2, SENDING_FIRST_STATUS_REQUEST, - SENDING_ALARM_STATUS_REQUEST, + SENDING_FIRST_ALARM_STATUS_REQUEST, // FUNCTIONAL STATE IDLE, SENDING_STATUS_REQUEST, @@ -72,6 +72,7 @@ class HaierClimateBase : public esphome::Component, SENDING_SIGNAL_LEVEL, SENDING_CONTROL, SENDING_ACTION_COMMAND, + SENDING_ALARM_STATUS_REQUEST, NUM_PROTOCOL_PHASES }; const char *phase_to_string_(ProtocolPhases phase); diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 09f90fffa8..e5aa88e2c9 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -16,6 +16,7 @@ constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); +constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000; hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { switch (direction) { @@ -110,6 +111,14 @@ void HonClimate::start_steri_cleaning() { } } +void HonClimate::add_alarm_start_callback(std::function &&callback) { + this->alarm_start_callback_.add(std::move(callback)); +} + +void HonClimate::add_alarm_end_callback(std::function &&callback) { + this->alarm_end_callback_.add(std::move(callback)); +} + haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { @@ -194,7 +203,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy switch (this->protocol_phase_) { case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: ESP_LOGI(TAG, "First HVAC status received"); - this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + this->set_phase(ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST); break; case ProtocolPhases::SENDING_ACTION_COMMAND: // Do nothing, phase will be changed in process_phase @@ -251,12 +260,15 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_ this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; } - if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) { + if ((this->protocol_phase_ != ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) && + (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)) { // Don't expect this answer now this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; } - memcpy(this->active_alarms_, data + 2, 8); + if (data_size < sizeof(active_alarms_) + 2) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + this->process_alarm_message_(data, data_size, this->protocol_phase_ >= ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::HANDLER_OK; } else { @@ -265,6 +277,19 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_ } } +haier_protocol::HandlerError HonClimate::alarm_status_message_handler_(haier_protocol::FrameType type, + const uint8_t *buffer, size_t size) { + haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; + if (size < sizeof(this->active_alarms_) + 2) { + // Log error but confirm anyway to avoid to many messages + result = haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + } + this->process_alarm_message_(buffer, size, true); + this->haier_protocol_.send_answer(haier_protocol::HaierMessage(haier_protocol::FrameType::CONFIRM)); + this->last_alarm_request_ = std::chrono::steady_clock::now(); + return result; +} + void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( @@ -291,6 +316,10 @@ void HonClimate::set_handlers() { haier_protocol::FrameType::REPORT_NETWORK_STATUS, std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_message_handler( + haier_protocol::FrameType::ALARM_STATUS, + std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3)); } void HonClimate::dump_config() { @@ -363,10 +392,12 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { this->set_phase(ProtocolPhases::IDLE); break; #endif + case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); + this->last_alarm_request_ = now; } break; case ProtocolPhases::SENDING_CONTROL: @@ -417,12 +448,16 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->forced_request_status_ = false; + } else if (std::chrono::duration_cast(now - this->last_alarm_request_).count() > + ALARM_STATUS_REQUEST_INTERVAL_MS) { + this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); } #ifdef USE_WIFI else if (this->send_wifi_signal_ && (std::chrono::duration_cast(now - this->last_signal_request_).count() > - SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) { this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); + } #endif } break; default: @@ -452,6 +487,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + control_out_buffer[4] = 0; // This byte should be cleared before setting values bool has_hvac_settings = false; if (this->current_hvac_settings_.valid) { has_hvac_settings = true; @@ -552,31 +588,41 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; + out_data->ten_degree = 0; break; case CLIMATE_PRESET_ECO: // Eco is not supported in Fan only mode out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; + out_data->ten_degree = 0; break; case CLIMATE_PRESET_BOOST: out_data->quiet_mode = 0; // Boost is not supported in Fan only mode out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->sleep_mode = 0; + out_data->ten_degree = 0; break; case CLIMATE_PRESET_AWAY: out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; + // 10 degrees allowed only in heat mode + out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0; break; case CLIMATE_PRESET_SLEEP: out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 1; + out_data->ten_degree = 0; break; default: ESP_LOGE("Control", "Unsupported preset"); + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + out_data->ten_degree = 0; break; } } @@ -595,6 +641,50 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); } +void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new) { + constexpr size_t active_alarms_size = sizeof(this->active_alarms_); + if (size >= active_alarms_size + 2) { + if (check_new) { + size_t alarm_code = 0; + for (int i = active_alarms_size - 1; i >= 0; i--) { + if (packet[2 + i] != active_alarms_[i]) { + uint8_t alarm_bit = 1; + for (int b = 0; b < 8; b++) { + if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) { + bool alarm_status = (packet[2 + i] & alarm_bit) != 0; + int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO; + const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT + ? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str() + : "Unknown"; + esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated", + alarm_code, alarm_message); + if (alarm_status) { + this->alarm_start_callback_.call(alarm_code, alarm_message); + this->active_alarm_count_ += 1.0f; + } else { + this->alarm_end_callback_.call(alarm_code, alarm_message); + this->active_alarm_count_ -= 1.0f; + } + } + alarm_bit <<= 1; + alarm_code++; + } + active_alarms_[i] = packet[2 + i]; + } else + alarm_code += 8; + } + } else { + float alarm_count = 0.0f; + static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}; + for (size_t i = 0; i < sizeof(this->active_alarms_); i++) { + alarm_count += (float) (nibble_bits_count[packet[2 + i] & 0x0F] + nibble_bits_count[packet[2 + i] >> 4]); + } + this->active_alarm_count_ = alarm_count; + memcpy(this->active_alarms_, packet + 2, sizeof(this->active_alarms_)); + } + } +} + haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_) return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; @@ -626,6 +716,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * this->preset = CLIMATE_PRESET_BOOST; } else if (packet.control.sleep_mode != 0) { this->preset = CLIMATE_PRESET_SLEEP; + } else if (packet.control.ten_degree != 0) { + this->preset = CLIMATE_PRESET_AWAY; } else { this->preset = CLIMATE_PRESET_NONE; } @@ -882,25 +974,35 @@ void HonClimate::fill_control_messages_queue_() { // CLimate preset { uint8_t fast_mode_buf[] = {0x00, 0xFF}; + uint8_t away_mode_buf[] = {0x00, 0xFF}; if (!new_power) { // If AC is off - no presets allowed quiet_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00; + away_mode_buf[1] = 0x00; } else if (climate_control.preset.has_value()) { switch (climate_control.preset.value()) { case CLIMATE_PRESET_NONE: quiet_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00; + away_mode_buf[1] = 0x00; break; case CLIMATE_PRESET_ECO: // Eco is not supported in Fan only mode quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; fast_mode_buf[1] = 0x00; + away_mode_buf[1] = 0x00; break; case CLIMATE_PRESET_BOOST: quiet_mode_buf[1] = 0x00; // Boost is not supported in Fan only mode fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; + away_mode_buf[1] = 0x00; + break; + case CLIMATE_PRESET_AWAY: + quiet_mode_buf[1] = 0x00; + fast_mode_buf[1] = 0x00; + away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00; break; default: ESP_LOGE("Control", "Unsupported preset"); @@ -921,6 +1023,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2)); } + if (away_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, + away_mode_buf, 2)); + } } // Target temperature if (climate_control.target_temperature.has_value()) { diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 1ba6a8e041..9c05e59b87 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -2,6 +2,7 @@ #include #include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" #include "haier_base.h" namespace esphome { @@ -52,6 +53,9 @@ class HonClimate : public HaierClimateBase { void start_steri_cleaning(); void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; }; void set_control_method(HonControlMethod method) { this->control_method_ = method; }; + void add_alarm_start_callback(std::function &&callback); + void add_alarm_end_callback(std::function &&callback); + float get_active_alarm_count() const { return this->active_alarm_count_; } protected: void set_handlers() override; @@ -77,8 +81,11 @@ class HonClimate : public HaierClimateBase { haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); + haier_protocol::HandlerError alarm_status_message_handler_(haier_protocol::FrameType type, const uint8_t *buffer, + size_t size); // Helper functions haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + void process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new); void fill_control_messages_queue_(); void clear_control_messages_queue_(); @@ -101,6 +108,26 @@ class HonClimate : public HaierClimateBase { HonControlMethod control_method_; esphome::sensor::Sensor *outdoor_sensor_; std::queue control_messages_queue_; + CallbackManager alarm_start_callback_{}; + CallbackManager alarm_end_callback_{}; + float active_alarm_count_{NAN}; + std::chrono::steady_clock::time_point last_alarm_request_; +}; + +class HaierAlarmStartTrigger : public Trigger { + public: + explicit HaierAlarmStartTrigger(HonClimate *parent) { + parent->add_alarm_start_callback( + [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); + } +}; + +class HaierAlarmEndTrigger : public Trigger { + public: + explicit HaierAlarmEndTrigger(HonClimate *parent) { + parent->add_alarm_end_callback( + [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); + } }; } // namespace haier diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h index 7724b43854..be1b0ae51c 100644 --- a/esphome/components/haier/hon_packet.h +++ b/esphome/components/haier/hon_packet.h @@ -163,6 +163,62 @@ enum class SubcommandsControl : uint16_t { // content: all values like in status packet) }; +const std::string HON_ALARM_MESSAGES[] = { + "Outdoor module failure", + "Outdoor defrost sensor failure", + "Outdoor compressor exhaust sensor failure", + "Outdoor EEPROM abnormality", + "Indoor coil sensor failure", + "Indoor-outdoor communication failure", + "Power supply overvoltage protection", + "Communication failure between panel and indoor unit", + "Outdoor compressor overheat protection", + "Outdoor environmental sensor abnormality", + "Full water protection", + "Indoor EEPROM failure", + "Outdoor out air sensor failure", + "CBD and module communication failure", + "Indoor DC fan failure", + "Outdoor DC fan failure", + "Door switch failure", + "Dust filter needs cleaning reminder", + "Water shortage protection", + "Humidity sensor failure", + "Indoor temperature sensor failure", + "Manipulator limit failure", + "Indoor PM2.5 sensor failure", + "Outdoor PM2.5 sensor failure", + "Indoor heating overload/high load alarm", + "Outdoor AC current protection", + "Outdoor compressor operation abnormality", + "Outdoor DC current protection", + "Outdoor no-load failure", + "CT current abnormality", + "Indoor cooling freeze protection", + "High and low pressure protection", + "Compressor out air temperature is too high", + "Outdoor evaporator sensor failure", + "Outdoor cooling overload", + "Water pump drainage failure", + "Three-phase power supply failure", + "Four-way valve failure", + "External alarm/scraper flow switch failure", + "Temperature cutoff protection alarm", + "Different mode operation failure", + "Electronic expansion valve failure", + "Dual heat source sensor Tw failure", + "Communication failure with the wired controller", + "Indoor unit address duplication failure", + "50Hz zero crossing failure", + "Outdoor unit failure", + "Formaldehyde sensor failure", + "VOC sensor failure", + "CO2 sensor failure", + "Firewall failure", +}; + +constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]); + } // namespace hon_protocol } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index c2326883f7..00590694d5 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -95,7 +95,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type, phase_to_string_(this->protocol_phase_)); ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); - if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) + if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) new_phase = ProtocolPhases::SENDING_INIT_1; this->set_phase(new_phase); return haier_protocol::HandlerError::HANDLER_OK; @@ -170,9 +170,12 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); break; - case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST: this->set_phase(ProtocolPhases::SENDING_INIT_1); break; + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; case ProtocolPhases::SENDING_CONTROL: if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { ESP_LOGI(TAG, "Sending control packet"); @@ -343,19 +346,29 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } else if (climate_control.preset.has_value()) { switch (climate_control.preset.value()) { case CLIMATE_PRESET_NONE: + out_data->ten_degree = 0; out_data->turbo_mode = 0; out_data->quiet_mode = 0; break; case CLIMATE_PRESET_BOOST: + out_data->ten_degree = 0; out_data->turbo_mode = 1; out_data->quiet_mode = 0; break; case CLIMATE_PRESET_COMFORT: + out_data->ten_degree = 0; out_data->turbo_mode = 0; out_data->quiet_mode = 1; break; + case CLIMATE_PRESET_AWAY: + // Only allowed in heat mode + out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0; + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; default: ESP_LOGE("Control", "Unsupported preset"); + out_data->ten_degree = 0; out_data->turbo_mode = 0; out_data->quiet_mode = 0; break; @@ -381,6 +394,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin this->preset = CLIMATE_PRESET_BOOST; } else if (packet.control.quiet_mode != 0) { this->preset = CLIMATE_PRESET_COMFORT; + } else if (packet.control.ten_degree != 0) { + this->preset = CLIMATE_PRESET_AWAY; } else { this->preset = CLIMATE_PRESET_NONE; } diff --git a/tests/test3.yaml b/tests/test3.yaml index ab7f38d07f..2c7a7a81f7 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1026,11 +1026,13 @@ climate: wifi_signal: true beeper: true outdoor_temperature: - name: Haier AC outdoor temperature + name: Haier AC outdoor temperature visual: min_temperature: 16 °C max_temperature: 30 °C - temperature_step: 1 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 supported_modes: - 'OFF' - HEAT_COOL @@ -1043,6 +1045,23 @@ climate: - VERTICAL - HORIZONTAL - BOTH + supported_presets: + - AWAY + - BOOST + - ECO + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [ code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [ code, message] sprinkler: - id: yard_sprinkler_ctrlr From 937a9c96ce87a5f3cb871d9d147022497696e2e1 Mon Sep 17 00:00:00 2001 From: Chris AtLee Date: Wed, 20 Dec 2023 19:11:32 -0500 Subject: [PATCH 205/436] Allow haier remote protocol to use lambdas (#5898) --- esphome/components/remote_base/haier_protocol.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/remote_base/haier_protocol.h b/esphome/components/remote_base/haier_protocol.h index 6a1c4bea72..7a4ee640e8 100644 --- a/esphome/components/remote_base/haier_protocol.h +++ b/esphome/components/remote_base/haier_protocol.h @@ -26,12 +26,11 @@ DECLARE_REMOTE_PROTOCOL(Haier) template class HaierAction : public RemoteTransmitterActionBase { public: - TEMPLATABLE_VALUE(std::vector, data) + TEMPLATABLE_VALUE(std::vector, code) - void set_code(const std::vector &code) { data_ = code; } void encode(RemoteTransmitData *dst, Ts... x) override { HaierData data{}; - data.data = this->data_.value(x...); + data.data = this->code_.value(x...); HaierProtocol().encode(dst, data); } }; From 2a1d16f17b90544e70dedffb33ba4f7097115e51 Mon Sep 17 00:00:00 2001 From: William Heimbigner Date: Wed, 20 Dec 2023 21:55:34 -0600 Subject: [PATCH 206/436] PMSx003 add relevant device and state classes to default config (#5633) --- esphome/components/pmsx003/sensor.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index eefcb529f2..08ccd6096e 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -92,66 +92,78 @@ CONFIG_SCHEMA = ( icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, From 784dff7574ee39554759eb7681e8486bcc4219e6 Mon Sep 17 00:00:00 2001 From: Fabian Pflug Date: Thu, 21 Dec 2023 05:30:10 +0100 Subject: [PATCH 207/436] Add waveshare 2.7in V2 model (#5903) --- .../components/waveshare_epaper/display.py | 4 ++ .../waveshare_epaper/waveshare_epaper.cpp | 53 +++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 16 ++++++ 3 files changed, 73 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 519b07fca2..432c7b6119 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -26,6 +26,9 @@ WaveshareEPaperTypeA = waveshare_epaper_ns.class_( WaveshareEPaper2P7In = waveshare_epaper_ns.class_( "WaveshareEPaper2P7In", WaveshareEPaper ) +WaveshareEPaper2P7InV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P7InV2", WaveshareEPaper +) WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InB", WaveshareEPaper ) @@ -83,6 +86,7 @@ MODELS = { "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), "gdey029t94": ("c", GDEY029T94), "2.70in": ("b", WaveshareEPaper2P7In), + "2.70inv2": ("b", WaveshareEPaper2P7InV2), "2.90in-b": ("b", WaveshareEPaper2P9InB), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 8fdb9a3ac0..4cd5e3366c 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -634,6 +634,59 @@ void WaveshareEPaper2P7In::dump_config() { LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper2P7InV2::initialize() { + this->reset_(); + this->wait_until_idle_(); + + this->command(0x12); // SWRESET + this->wait_until_idle_(); + + // SET WINDOWS + // XRAM_START_AND_END_POSITION + this->command(0x44); + this->data(0x00); + this->data(((get_width_internal() - 1) >> 3) & 0xFF); + // YRAM_START_AND_END_POSITION + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data((get_height_internal() - 1) & 0xFF); + this->data(((get_height_internal() - 1) >> 8) & 0xFF); + + // SET CURSOR + // XRAM_ADDRESS + this->command(0x4E); + this->data(0x00); + // YRAM_ADDRESS + this->command(0x4F); + this->data(0x00); + this->data(0x00); + + this->command(0x11); // data entry mode + this->data(0x03); +} +void HOT WaveshareEPaper2P7InV2::display() { + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // COMMAND DISPLAY REFRESH + this->command(0x22); + this->data(0xF7); + this->command(0x20); +} +int WaveshareEPaper2P7InV2::get_width_internal() { return 176; } +int WaveshareEPaper2P7InV2::get_height_internal() { return 264; } +void WaveshareEPaper2P7InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.7in V2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + // ======================================================== // 2.90in Type B (LUT from OTP) // Datasheet: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 42e8a16829..bc5dbea11d 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -160,6 +160,22 @@ class WaveshareEPaper2P7In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P7InV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { ; } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class GDEY029T94 : public WaveshareEPaper { public: void initialize() override; From c305f61020ecd390600763d930fcb47328842f01 Mon Sep 17 00:00:00 2001 From: Fabian Pflug Date: Thu, 21 Dec 2023 05:36:43 +0100 Subject: [PATCH 208/436] Add support for waveshare 2.9in B V3 version (#5902) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 4 ++ .../waveshare_epaper/waveshare_epaper.cpp | 69 +++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 20 ++++++ 3 files changed, 93 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 432c7b6119..1dd4b7fc54 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -32,6 +32,9 @@ WaveshareEPaper2P7InV2 = waveshare_epaper_ns.class_( WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InB", WaveshareEPaper ) +WaveshareEPaper2P9InBV3 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InBV3", WaveshareEPaper +) GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper @@ -88,6 +91,7 @@ MODELS = { "2.70in": ("b", WaveshareEPaper2P7In), "2.70inv2": ("b", WaveshareEPaper2P7InV2), "2.90in-b": ("b", WaveshareEPaper2P9InB), + "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 4cd5e3366c..0e9b129988 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -766,6 +766,75 @@ void WaveshareEPaper2P9InB::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 2.90in Type B (LUT from OTP) +// Datasheet: +// - https://files.waveshare.com/upload/a/af/2.9inch-e-paper-b-v3-specification.pdf +// ======================================================== + +void WaveshareEPaper2P9InBV3::initialize() { + // from https://github.com/waveshareteam/e-Paper/blob/master/Arduino/epd2in9b_V3/epd2in9b_V3.cpp + this->reset_(); + + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x0F); + this->data(0x89); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x80); + this->data(0x01); + this->data(0x28); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x77); +} +void HOT WaveshareEPaper2P9InBV3::display() { + // COMMAND DATA START TRANSMISSION 1 (B/W data) + this->command(0x10); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + this->command(0x92); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED data) + this->command(0x13); + delay(2); + this->start_data_(); + for (size_t i = 0; i < this->get_buffer_length_(); i++) + this->write_byte(0xFF); + this->end_data_(); + this->command(0x92); + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(2); + this->wait_until_idle_(); + + // COMMAND POWER OFF + // NOTE: power off < deep sleep + this->command(0x02); +} +int WaveshareEPaper2P9InBV3::get_width_internal() { return 128; } +int WaveshareEPaper2P9InBV3::get_height_internal() { return 296; } +void WaveshareEPaper2P9InBV3::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in (B) V3"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + // ======================================================== // Good Display 2.9in black/white/grey // Datasheet: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index bc5dbea11d..ee9443e8be 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -256,6 +256,26 @@ class WaveshareEPaper2P9InB : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P9InBV3 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; From c92715e403b766061669bd19658f0e1dafb37640 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:37:02 +0900 Subject: [PATCH 209/436] Fix pin reuse in test1 (#5978) --- tests/test1.yaml | 64 +++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/tests/test1.yaml b/tests/test1.yaml index 2748a09c59..eb72f90b21 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -324,7 +324,9 @@ ads1115: as5600: i2c_id: i2c_bus - dir_pin: GPIO27 + dir_pin: + number: 27 + allow_other_uses: true direction: clockwise start_position: 90deg range: 180deg @@ -570,13 +572,13 @@ sensor: - platform: as5600 name: AS5600 Position raw_position: - name: AS5600 Raw Position + name: AS5600 Raw Position gain: - name: AS5600 Gain + name: AS5600 Gain magnitude: - name: AS5600 Magnitude + name: AS5600 Magnitude status: - name: AS5600 Status + name: AS5600 Status - platform: as7341 update_interval: 15s gain: X8 @@ -2037,21 +2039,21 @@ my9231: sm2235: data_pin: - allow_other_uses: true - number: GPIO4 + allow_other_uses: true + number: GPIO4 clock_pin: - allow_other_uses: true - number: GPIO5 + allow_other_uses: true + number: GPIO5 max_power_color_channels: 9 max_power_white_channels: 9 sm2335: data_pin: - allow_other_uses: true - number: GPIO4 + allow_other_uses: true + number: GPIO4 clock_pin: - allow_other_uses: true - number: GPIO5 + allow_other_uses: true + number: GPIO5 max_power_color_channels: 9 max_power_white_channels: 9 @@ -3040,17 +3042,13 @@ display: id: my_lcd_gpio dimensions: 18x4 data_pins: - - - allow_other_uses: true + - allow_other_uses: true number: GPIO19 - - - allow_other_uses: true + - allow_other_uses: true number: GPIO21 - - - allow_other_uses: true + - allow_other_uses: true number: GPIO22 - - - allow_other_uses: true + - allow_other_uses: true number: GPIO23 enable_pin: allow_other_uses: true @@ -4201,25 +4199,25 @@ graphical_display_menu: lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' items: - type: back - text: 'Back' + text: "Back" - type: label - type: menu - text: 'Submenu 1' + text: "Submenu 1" items: - type: back - text: 'Back' + text: "Back" - type: menu - text: 'Submenu 21' + text: "Submenu 21" items: - type: back - text: 'Back' + text: "Back" - type: command - text: 'Show Main' + text: "Show Main" on_value: then: - display_menu.show_main: test_graphical_display_menu - type: select - text: 'Enum Item' + text: "Enum Item" immediate_edit: true select: test_select on_enter: @@ -4232,7 +4230,7 @@ graphical_display_menu: then: lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - type: number - text: 'Number' + text: "Number" number: test_number on_enter: then: @@ -4244,15 +4242,15 @@ graphical_display_menu: then: lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - type: command - text: 'Hide' + text: "Hide" on_value: then: - display_menu.hide: test_graphical_display_menu - type: switch - text: 'Switch' + text: "Switch" switch: my_switch - on_text: 'Bright' - off_text: 'Dark' + on_text: "Bright" + off_text: "Dark" immediate_edit: false on_value: then: From a784f1e69127daafc6457f0be897bf51bb1cb5e6 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:38:11 +1000 Subject: [PATCH 210/436] Add Waveshare 1.47in 172x320 to ST7789v component (#5884) --- esphome/components/st7789v/display.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 41970afd26..5b2b5a126c 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -97,6 +97,19 @@ MODELS = { CONF_BACKLIGHT_PIN: "GPIO15", } ), + "WAVESHARE_1.47IN_172X320": model_spec( + presets={ + CONF_HEIGHT: 320, + CONF_WIDTH: 172, + CONF_OFFSET_HEIGHT: 34, + CONF_OFFSET_WIDTH: 0, + CONF_ROTATION: 90, + CONF_CS_PIN: "GPIO21", + CONF_DC_PIN: "GPIO22", + CONF_RESET_PIN: "GPIO23", + CONF_BACKLIGHT_PIN: "GPIO4", + } + ), "CUSTOM": model_spec(), } From 04b354799251808f85a3fe12c2a2579977de5770 Mon Sep 17 00:00:00 2001 From: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com> Date: Thu, 21 Dec 2023 05:39:55 +0100 Subject: [PATCH 211/436] (fingerprint_grow) Added on_finger_scan_invalid automation. (#5885) --- esphome/components/fingerprint_grow/__init__.py | 16 ++++++++++++++++ .../fingerprint_grow/fingerprint_grow.cpp | 4 ++++ .../fingerprint_grow/fingerprint_grow.h | 11 +++++++++++ esphome/const.py | 1 + tests/test3.yaml | 3 +++ 5 files changed, 35 insertions(+) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index ecbbc3d477..5249107f17 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_ON_ENROLLMENT_SCAN, CONF_ON_FINGER_SCAN_MATCHED, CONF_ON_FINGER_SCAN_UNMATCHED, + CONF_ON_FINGER_SCAN_INVALID, CONF_PASSWORD, CONF_SENSING_PIN, CONF_SPEED, @@ -42,6 +43,10 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( "FingerScanUnmatchedTrigger", automation.Trigger.template() ) +FingerScanInvalidTrigger = fingerprint_grow_ns.class_( + "FingerScanInvalidTrigger", automation.Trigger.template() +) + EnrollmentScanTrigger = fingerprint_grow_ns.class_( "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) ) @@ -108,6 +113,13 @@ CONFIG_SCHEMA = ( ), } ), + cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanInvalidTrigger + ), + } + ), cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -162,6 +174,10 @@ async def to_code(config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 8729e0f4bf..2486e02964 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -134,12 +134,14 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { case NO_FINGER: if (this->sensing_pin_ != nullptr) { ESP_LOGD(TAG, "No finger"); + this->finger_scan_invalid_callback_.call(); } else { ESP_LOGV(TAG, "No finger"); } return this->data_[0]; case IMAGE_FAIL: ESP_LOGE(TAG, "Imaging error"); + this->finger_scan_invalid_callback_.call(); default: return this->data_[0]; } @@ -152,10 +154,12 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { break; case IMAGE_MESS: ESP_LOGE(TAG, "Image too messy"); + this->finger_scan_invalid_callback_.call(); break; case FEATURE_FAIL: case INVALID_IMAGE: ESP_LOGE(TAG, "Could not find fingerprint features"); + this->finger_scan_invalid_callback_.call(); break; } return this->data_[0]; diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index f414146e64..9aad94fc2a 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -124,6 +124,9 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic void add_on_finger_scan_unmatched_callback(std::function callback) { this->finger_scan_unmatched_callback_.add(std::move(callback)); } + void add_on_finger_scan_invalid_callback(std::function callback) { + this->finger_scan_invalid_callback_.add(std::move(callback)); + } void add_on_enrollment_scan_callback(std::function callback) { this->enrollment_scan_callback_.add(std::move(callback)); } @@ -172,6 +175,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic sensor::Sensor *last_finger_id_sensor_{nullptr}; sensor::Sensor *last_confidence_sensor_{nullptr}; binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + CallbackManager finger_scan_invalid_callback_; CallbackManager finger_scan_matched_callback_; CallbackManager finger_scan_unmatched_callback_; CallbackManager enrollment_scan_callback_; @@ -194,6 +198,13 @@ class FingerScanUnmatchedTrigger : public Trigger<> { } }; +class FingerScanInvalidTrigger : public Trigger<> { + public: + explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); }); + } +}; + class EnrollmentScanTrigger : public Trigger { public: explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { diff --git a/esphome/const.py b/esphome/const.py index 58c6c47e3a..4a4a59f659 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -500,6 +500,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_FINGER_SCAN_INVALID = "on_finger_scan_invalid" CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched" CONF_ON_JSON_MESSAGE = "on_json_message" diff --git a/tests/test3.yaml b/tests/test3.yaml index 2c7a7a81f7..e39e711ab3 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1257,6 +1257,9 @@ fingerprint_grow: number: 4 password: 0x12FE37DC new_password: 0xA65B9840 + on_finger_scan_invalid: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_finger_scan_invalid on_finger_scan_matched: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_finger_scan_matched From 223e6e8f137ae442cce15125e80889ec76c85b65 Mon Sep 17 00:00:00 2001 From: Steve Rodgers Date: Wed, 20 Dec 2023 21:10:47 -0800 Subject: [PATCH 212/436] Alarm panel: Add changes to support enhanced features (#5671) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 4 +- .../alarm_control_panel/__init__.py | 53 +++++++- .../alarm_control_panel.cpp | 8 ++ .../alarm_control_panel/alarm_control_panel.h | 16 +++ .../alarm_control_panel/automation.h | 14 +++ .../template/alarm_control_panel/__init__.py | 25 +++- .../template_alarm_control_panel.cpp | 116 ++++++++++++++---- .../template_alarm_control_panel.h | 32 ++++- 8 files changed, 237 insertions(+), 31 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fc14b2088e..b8c870f20f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -25,7 +25,7 @@ esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban -esphome/components/alarm_control_panel/* @grahambrown11 +esphome/components/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/alpha3/* @jan-hofmeier esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix @@ -327,7 +327,7 @@ esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax -esphome/components/template/alarm_control_panel/* @grahambrown11 +esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index d9cafb4f30..35d239c267 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -11,7 +11,7 @@ from esphome.const import ( ) from esphome.cpp_helpers import setup_entity -CODEOWNERS = ["@grahambrown11"] +CODEOWNERS = ["@grahambrown11", "@hwstar"] IS_PLATFORM_COMPONENT = True CONF_ON_TRIGGERED = "on_triggered" @@ -22,6 +22,8 @@ CONF_ON_ARMED_HOME = "on_armed_home" CONF_ON_ARMED_NIGHT = "on_armed_night" CONF_ON_ARMED_AWAY = "on_armed_away" CONF_ON_DISARMED = "on_disarmed" +CONF_ON_CHIME = "on_chime" +CONF_ON_READY = "on_ready" alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) @@ -53,12 +55,22 @@ ArmedAwayTrigger = alarm_control_panel_ns.class_( DisarmedTrigger = alarm_control_panel_ns.class_( "DisarmedTrigger", automation.Trigger.template() ) +ChimeTrigger = alarm_control_panel_ns.class_( + "ChimeTrigger", automation.Trigger.template() +) +ReadyTrigger = alarm_control_panel_ns.class_( + "ReadyTrigger", automation.Trigger.template() +) + ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action) DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action) PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action) TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action) +ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action) +ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action) + AlarmControlPanelCondition = alarm_control_panel_ns.class_( "AlarmControlPanelCondition", automation.Condition ) @@ -111,6 +123,16 @@ ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( 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), + } + ), } ) @@ -157,6 +179,12 @@ async def setup_alarm_control_panel_core_(var, config): for conf in config.get(CONF_ON_CLEARED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CHIME, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_READY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_alarm_control_panel(var, config): @@ -232,6 +260,29 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_chime_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) + return var + + +@automation.register_action( + "alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +@automation.register_condition( + "alarm_control_panel.ready", + AlarmControlPanelCondition, + ALARM_CONTROL_PANEL_CONDITION_SCHEMA, +) +async def alarm_action_ready_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) + return var + + @automation.register_condition( "alarm_control_panel.is_armed", AlarmControlPanelCondition, diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 9dc083c004..9f1485ee90 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -96,6 +96,14 @@ void AlarmControlPanel::add_on_cleared_callback(std::function &&callback this->cleared_callback_.add(std::move(callback)); } +void AlarmControlPanel::add_on_chime_callback(std::function &&callback) { + this->chime_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_ready_callback(std::function &&callback) { + this->ready_callback_.add(std::move(callback)); +} + void AlarmControlPanel::arm_away(optional code) { auto call = this->make_call(); call.arm_away(); diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index dc0b92df76..85c2b2148e 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -89,6 +89,18 @@ class AlarmControlPanel : public EntityBase { */ void add_on_cleared_callback(std::function &&callback); + /** Add a callback for when a chime zone goes from closed to open + * + * @param callback The callback function + */ + void add_on_chime_callback(std::function &&callback); + + /** Add a callback for when a ready state changes + * + * @param callback The callback function + */ + void add_on_ready_callback(std::function &&callback); + /** A numeric representation of the supported features as per HomeAssistant * */ @@ -178,6 +190,10 @@ class AlarmControlPanel : public EntityBase { CallbackManager disarmed_callback_{}; // clear callback CallbackManager cleared_callback_{}; + // chime callback + CallbackManager chime_callback_{}; + // ready callback + CallbackManager ready_callback_{}; }; } // namespace alarm_control_panel diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 8538020c53..2177fb710f 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -69,6 +69,20 @@ class ClearedTrigger : public Trigger<> { } }; +class ChimeTrigger : public Trigger<> { + public: + explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); }); + } +}; + +class ReadyTrigger : public Trigger<> { + public: + explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); }); + } +}; + template class ArmAwayAction : public Action { public: explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 27b7e92b4f..3555f2fafd 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -12,11 +12,13 @@ from esphome.const import ( ) from .. import template_ns -CODEOWNERS = ["@grahambrown11"] +CODEOWNERS = ["@grahambrown11", "@hwstar"] CONF_CODES = "codes" CONF_BYPASS_ARMED_HOME = "bypass_armed_home" CONF_BYPASS_ARMED_NIGHT = "bypass_armed_night" +CONF_CHIME = "chime" +CONF_TRIGGER_MODE = "trigger_mode" CONF_REQUIRES_CODE_TO_ARM = "requires_code_to_arm" CONF_ARMING_HOME_TIME = "arming_home_time" CONF_ARMING_NIGHT_TIME = "arming_night_time" @@ -24,16 +26,20 @@ CONF_ARMING_AWAY_TIME = "arming_away_time" CONF_PENDING_TIME = "pending_time" CONF_TRIGGER_TIME = "trigger_time" + FLAG_NORMAL = "normal" FLAG_BYPASS_ARMED_HOME = "bypass_armed_home" FLAG_BYPASS_ARMED_NIGHT = "bypass_armed_night" +FLAG_CHIME = "chime" BinarySensorFlags = { FLAG_NORMAL: 1 << 0, FLAG_BYPASS_ARMED_HOME: 1 << 1, FLAG_BYPASS_ARMED_NIGHT: 1 << 2, + FLAG_CHIME: 1 << 3, } + TemplateAlarmControlPanel = template_ns.class_( "TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component ) @@ -46,6 +52,14 @@ RESTORE_MODES = { "RESTORE_DEFAULT_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, } +AlarmSensorType = template_ns.enum("AlarmSensorType") + +ALARM_SENSOR_TYPES = { + "DELAYED": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED, + "INSTANT": AlarmSensorType.ALARM_SENSOR_TYPE_INSTANT, + "DELAYED_FOLLOWER": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED_FOLLOWER, +} + def validate_config(config): if config.get(CONF_REQUIRES_CODE_TO_ARM, False) and not config.get(CONF_CODES, []): @@ -60,6 +74,10 @@ TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA = cv.maybe_simple_value( cv.Required(CONF_INPUT): cv.use_id(binary_sensor.BinarySensor), cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean, cv.Optional(CONF_BYPASS_ARMED_NIGHT, default=False): cv.boolean, + cv.Optional(CONF_CHIME, default=False): cv.boolean, + cv.Optional(CONF_TRIGGER_MODE, default="DELAYED"): cv.enum( + ALARM_SENSOR_TYPES, upper=True, space="_" + ), }, key=CONF_INPUT, ) @@ -123,6 +141,7 @@ async def to_code(config): for sensor in config.get(CONF_BINARY_SENSORS, []): bs = await cg.get_variable(sensor[CONF_INPUT]) + flags = BinarySensorFlags[FLAG_NORMAL] if sensor[CONF_BYPASS_ARMED_HOME]: flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME] @@ -130,7 +149,9 @@ async def to_code(config): if sensor[CONF_BYPASS_ARMED_NIGHT]: flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_NIGHT] supports_arm_night = True - cg.add(var.add_sensor(bs, flags)) + if sensor[CONF_CHIME]: + flags |= BinarySensorFlags[FLAG_CHIME] + cg.add(var.add_sensor(bs, flags, sensor[CONF_TRIGGER_MODE])) cg.add(var.set_supports_arm_home(supports_arm_home)) cg.add(var.set_supports_arm_night(supports_arm_night)) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index b39b587811..99843417fa 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -1,3 +1,4 @@ + #include "template_alarm_control_panel.h" #include #include "esphome/components/alarm_control_panel/alarm_control_panel.h" @@ -15,8 +16,14 @@ static const char *const TAG = "template.alarm_control_panel"; TemplateAlarmControlPanel::TemplateAlarmControlPanel(){}; #ifdef USE_BINARY_SENSOR -void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags) { - this->sensor_map_[sensor] = flags; +void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) { + // Save the flags and type. Assign a store index for the per sensor data type. + SensorDataStore sd; + sd.last_chime_state = false; + this->sensor_map_[sensor].flags = flags; + this->sensor_map_[sensor].type = type; + this->sensor_data_.push_back(sd); + this->sensor_map_[sensor].store_index = this->next_store_index_++; }; #endif @@ -35,13 +42,27 @@ void TemplateAlarmControlPanel::dump_config() { ESP_LOGCONFIG(TAG, " Trigger Time: %" PRIu32 "s", (this->trigger_time_ / 1000)); ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->get_supported_features()); #ifdef USE_BINARY_SENSOR - for (auto sensor_pair : this->sensor_map_) { - ESP_LOGCONFIG(TAG, " Binary Sesnsor:"); - ESP_LOGCONFIG(TAG, " Name: %s", sensor_pair.first->get_name().c_str()); + for (auto sensor_info : this->sensor_map_) { + ESP_LOGCONFIG(TAG, " Binary Sensor:"); + ESP_LOGCONFIG(TAG, " Name: %s", sensor_info.first->get_name().c_str()); ESP_LOGCONFIG(TAG, " Armed home bypass: %s", - TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)); + TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)); ESP_LOGCONFIG(TAG, " Armed night bypass: %s", - TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)); + TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)); + ESP_LOGCONFIG(TAG, " Chime mode: %s", TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)); + const char *sensor_type; + switch (sensor_info.second.type) { + case ALARM_SENSOR_TYPE_INSTANT: + sensor_type = "instant"; + break; + case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: + sensor_type = "delayed_follower"; + break; + case ALARM_SENSOR_TYPE_DELAYED: + default: + sensor_type = "delayed"; + } + ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type); } #endif } @@ -92,31 +113,80 @@ void TemplateAlarmControlPanel::loop() { (millis() - this->last_update_) > this->trigger_time_) { future_state = this->desired_state_; } - bool trigger = false; + + bool delayed_sensor_not_ready = false; + bool instant_sensor_not_ready = false; + #ifdef USE_BINARY_SENSOR - if (this->is_state_armed(future_state)) { - // TODO might be better to register change for each sensor in setup... - for (auto sensor_pair : this->sensor_map_) { - if (sensor_pair.first->state) { - if (this->current_state_ == ACP_STATE_ARMED_HOME && - (sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { - continue; + // Test all of the sensors in the list regardless of the alarm panel state + for (auto sensor_info : this->sensor_map_) { + // Check for chime zones + if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) { + // Look for the transition from closed to open + if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) { + // Must be disarmed to chime + if (this->current_state_ == ACP_STATE_DISARMED) { + this->chime_callback_.call(); } - if (this->current_state_ == ACP_STATE_ARMED_NIGHT && - (sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { - continue; + } + // Record the sensor state change + this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state; + } + // Check for triggered sensors + if (sensor_info.first->state) { // Sensor triggered? + // Skip if bypass armed home + if (this->current_state_ == ACP_STATE_ARMED_HOME && + (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { + continue; + } + // Skip if bypass armed night + if (this->current_state_ == ACP_STATE_ARMED_NIGHT && + (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { + continue; + } + + // If sensor type is of type instant + if (sensor_info.second.type == ALARM_SENSOR_TYPE_INSTANT) { + instant_sensor_not_ready = true; + break; + } + // If sensor type is of type interior follower + if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED_FOLLOWER) { + // Look to see if we are in the pending state + if (this->current_state_ == ACP_STATE_PENDING) { + delayed_sensor_not_ready = true; + } else { + instant_sensor_not_ready = true; } - trigger = true; + } + // If sensor type is of type delayed + if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED) { + delayed_sensor_not_ready = true; break; } } } + // Update all sensors not ready flag + this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready)); + + // Call the ready state change callback if there was a change + if (this->sensors_ready_ != this->sensors_ready_last_) { + this->ready_callback_.call(); + this->sensors_ready_last_ = this->sensors_ready_; + } + #endif - if (trigger) { - if (this->pending_time_ > 0 && this->current_state_ != ACP_STATE_TRIGGERED) { - this->publish_state(ACP_STATE_PENDING); - } else { + if (this->is_state_armed(future_state) && (!this->sensors_ready_)) { + // Instant sensors + if (instant_sensor_not_ready) { this->publish_state(ACP_STATE_TRIGGERED); + } else if (delayed_sensor_not_ready) { + // Delayed sensors + if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) { + this->publish_state(ACP_STATE_PENDING); + } else { + this->publish_state(ACP_STATE_TRIGGERED); + } } } else if (future_state != this->current_state_) { this->publish_state(future_state); diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 9582ed157c..9ae69a0422 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -21,7 +21,15 @@ enum BinarySensorFlags : uint16_t { BINARY_SENSOR_MODE_NORMAL = 1 << 0, BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2, + BINARY_SENSOR_MODE_CHIME = 1 << 3, }; + +enum AlarmSensorType : uint16_t { + ALARM_SENSOR_TYPE_DELAYED = 0, + ALARM_SENSOR_TYPE_INSTANT, + ALARM_SENSOR_TYPE_DELAYED_FOLLOWER +}; + #endif enum TemplateAlarmControlPanelRestoreMode { @@ -29,6 +37,16 @@ enum TemplateAlarmControlPanelRestoreMode { ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, }; +struct SensorDataStore { + bool last_chime_state; +}; + +struct SensorInfo { + uint16_t flags; + AlarmSensorType type; + uint8_t store_index; +}; + class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); @@ -38,6 +56,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, uint32_t get_supported_features() const override; bool get_requires_code() const override; bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } + bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } #ifdef USE_BINARY_SENSOR @@ -46,7 +65,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, * @param sensor The BinarySensor instance. * @param ignore_when_home if this should be ignored when armed_home mode */ - void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0); + void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, + AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); #endif /** add a code @@ -98,8 +118,9 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, protected: void control(const alarm_control_panel::AlarmControlPanelCall &call) override; #ifdef USE_BINARY_SENSOR - // the map of binary sensors that the alarm_panel monitors with their modes - std::map sensor_map_; + // This maps a binary sensor to its type and attribute bits + std::map sensor_map_; + #endif TemplateAlarmControlPanelRestoreMode restore_mode_{}; @@ -115,10 +136,15 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, uint32_t trigger_time_; // a list of codes std::vector codes_; + // Per sensor data store + std::vector sensor_data_; // requires a code to arm bool requires_code_to_arm_ = false; bool supports_arm_home_ = false; bool supports_arm_night_ = false; + bool sensors_ready_ = false; + bool sensors_ready_last_ = false; + uint8_t next_store_index_ = 0; // check if the code is valid bool is_code_valid_(optional code); From f096f107e238236e75cb7f6b68b9f90433bb2e59 Mon Sep 17 00:00:00 2001 From: sbrudenell Date: Wed, 20 Dec 2023 21:13:54 -0900 Subject: [PATCH 213/436] support default pins for adafruit esp32 feather v2 (#5482) --- esphome/components/esp32/boards.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index e6c23c4d96..d361ca9917 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -42,6 +42,34 @@ ESP32_BASE_PINS = { } ESP32_BOARD_PINS = { + "adafruit_feather_esp32_v2": { + "A0": 26, + "A1": 25, + "A2": 34, + "A3": 39, + "A4": 36, + "A5": 4, + "SCK": 5, + "MOSI": 19, + "MISO": 21, + "RX": 7, + "TX": 8, + "D37": 37, + "LED": 13, + "LED_BUILTIN": 13, + "D12": 12, + "D27": 27, + "D33": 33, + "D15": 15, + "D32": 32, + "D14": 14, + "SCL": 20, + "SDA": 22, + "BUTTON": 38, + "NEOPIXEL": 0, + "PIN_NEOPIXEL": 0, + "NEOPIXEL_POWER": 2, + }, "adafruit_feather_esp32s2_tft": { "BUTTON": 0, "A0": 18, From d73ad39aedebf0bb4941aca26e4fce789f637a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Bana=C5=9B?= Date: Thu, 21 Dec 2023 08:03:57 +0100 Subject: [PATCH 214/436] Bug: Unwanted change resistance in x9c component (#5483) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/x9c/x9c.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp index ff7777e71f..1b283a68e5 100644 --- a/esphome/components/x9c/x9c.cpp +++ b/esphome/components/x9c/x9c.cpp @@ -7,6 +7,10 @@ namespace x9c { static const char *const TAG = "x9c.output"; void X9cOutput::trim_value(int change_amount) { + if (change_amount == 0) { + return; + } + if (change_amount > 0) { // Set change direction this->ud_pin_->digital_write(true); } else { From 222bb9b495ceeac459c65bff08fd5187d0b4a797 Mon Sep 17 00:00:00 2001 From: marshn Date: Thu, 21 Dec 2023 07:17:01 +0000 Subject: [PATCH 215/436] Improvements to RF receiver for Drayton Digistat heating controller (#5504) --- .../remote_base/drayton_protocol.cpp | 142 ++++++++++-------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index fb5f37b470..6c617f56c8 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -13,7 +13,8 @@ static const uint8_t NBITS_SYNC = 4; static const uint8_t NBITS_ADDRESS = 16; static const uint8_t NBITS_CHANNEL = 5; static const uint8_t NBITS_COMMAND = 7; -static const uint8_t NBITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static const uint8_t MIN_RX_SRC = (NDATABITS * 2 + NBITS_SYNC / 2); static const uint8_t CMD_ON = 0x41; static const uint8_t CMD_OFF = 0x02; @@ -116,7 +117,7 @@ void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { ESP_LOGV(TAG, "Send Drayton: out_data %08" PRIx32, out_data); - for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { + for (uint32_t mask = 1UL << (NDATABITS - 1); mask != 0; mask >>= 1) { if (out_data & mask) { dst->mark(BIT_TIME_US); dst->space(BIT_TIME_US); @@ -134,79 +135,96 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { .command = 0, }; - if (src.size() < 45) { - return {}; - } + while (src.size() - src.get_index() > MIN_RX_SRC) { + ESP_LOGVV(TAG, + "Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "", + src.size() - src.get_index(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), + src.peek(5), src.peek(6), src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), + src.peek(13), src.peek(14), src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); - ESP_LOGVV(TAG, - "Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 - " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 - " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "", - src.size(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), - src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), - src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + // If first preamble item is a space, skip it + if (src.peek_space_at_least(1)) { + src.advance(1); + } - // If first preamble item is a space, skip it - if (src.peek_space_at_least(1)) { - src.advance(1); - } + // Look for sync pulse, after. If sucessful index points to space of sync symbol + while (src.size() - src.get_index() >= NDATABITS) { + ESP_LOGVV(TAG, "Decode Drayton: sync search %d, %" PRId32 " %" PRId32, src.size() - src.get_index(), src.peek(), + src.peek(1)); + if (src.peek_mark(2 * BIT_TIME_US) && + (src.peek_space(2 * BIT_TIME_US, 1) || src.peek_space(3 * BIT_TIME_US, 1))) { + src.advance(1); + ESP_LOGVV(TAG, "Decode Drayton: Found SYNC, - %d", src.get_index()); + break; + } else { + src.advance(2); + } + } - // Look for sync pulse, after. If sucessful index points to space of sync symbol - for (uint16_t preamble = 0; preamble <= NBITS_PREAMBLE * 2; preamble += 2) { - ESP_LOGVV(TAG, "Decode Drayton: preamble %d %" PRId32 " %" PRId32, preamble, src.peek(preamble), - src.peek(preamble + 1)); - if (src.peek_mark(2 * BIT_TIME_US, preamble) && - (src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) { - src.advance(preamble + 1); + // No point continuing if not enough samples remaining to complete a packet + if (src.size() - src.get_index() < NDATABITS) { + ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index()); break; } - } - // Read data. Index points to space of sync symbol - // Extract first bit - // Checks next bit to leave index pointing correctly - uint32_t out_data = 0; - uint8_t bit = NBITS_ADDRESS + NBITS_COMMAND + NBITS_CHANNEL - 1; - if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { - out_data |= 0 << bit; - } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && - (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { - out_data |= 1 << bit; - } else { - ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index()); - return {}; - } - - // Before/after each bit is read the index points to the transition at the start of the bit period or, - // if there is no transition at the start of the bit period, then the transition in the middle of - // the previous bit period. - while (--bit >= 1) { - ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data); - if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && - (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + // Read data. Index points to space of sync symbol + // Extract first bit + // Checks next bit to leave index pointing correctly + uint32_t out_data = 0; + uint8_t bit = NDATABITS - 1; + ESP_LOGVV(TAG, "Decode Drayton: first bit %d %" PRId32 ", %" PRId32, src.peek(0), src.peek(1), src.peek(2)); + if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { out_data |= 0 << bit; - } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { out_data |= 1 << bit; } else { - ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08" PRIx32, bit, out_data); - return {}; + ESP_LOGV(TAG, "Decode Drayton: Fail 2, - %d %d %d", src.peek(-1), src.peek(0), src.peek(1)); + continue; } - } - if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { - out_data |= 0; - } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { - out_data |= 1; - } - ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data); - out.channel = (uint8_t) (out_data & 0x1F); - out_data >>= NBITS_CHANNEL; - out.command = (uint8_t) (out_data & 0x7F); - out_data >>= NBITS_COMMAND; - out.address = (uint16_t) (out_data & 0xFFFF); + // Before/after each bit is read the index points to the transition at the start of the bit period or, + // if there is no transition at the start of the bit period, then the transition in the middle of + // the previous bit period. + while (--bit >= 1) { + ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data); + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + break; + } + } - return out; + if (bit > 0) { + ESP_LOGVV(TAG, "Decode Drayton: Fail 3, %d %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), src.peek(1)); + continue; + } + + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } else { + continue; + } + + ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); + + out.channel = (uint8_t) (out_data & 0x1F); + out_data >>= NBITS_CHANNEL; + out.command = (uint8_t) (out_data & 0x7F); + out_data >>= NBITS_COMMAND; + out.address = (uint16_t) (out_data & 0xFFFF); + + return out; + } + return {}; } void DraytonProtocol::dump(const DraytonData &data) { ESP_LOGI(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, From 74281b93c4491f80bfd02db9e56696a86081083c Mon Sep 17 00:00:00 2001 From: kahrendt Date: Thu, 21 Dec 2023 02:19:15 -0500 Subject: [PATCH 216/436] Reduce memory usage with StringRef in MQTT Components (#5719) --- esphome/components/mqtt/mqtt_binary_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 12 ++++++------ esphome/components/mqtt/mqtt_component.h | 13 ++++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 79e6989a8f..6d12e88391 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -25,7 +25,7 @@ void MQTTBinarySensorComponent::dump_config() { MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor) : binary_sensor_(binary_sensor) { if (this->binary_sensor_->is_status_binary_sensor()) { - this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic); + this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic.c_str()); } } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index af4d6f13a5..8855b4d8e3 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -34,13 +34,13 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con std::string MQTTComponent::get_state_topic_() const { if (this->has_custom_state_topic_) - return this->custom_state_topic_; + return this->custom_state_topic_.str(); return this->get_default_topic_for_("state"); } std::string MQTTComponent::get_command_topic_() const { if (this->has_custom_command_topic_) - return this->custom_command_topic_; + return this->custom_command_topic_.str(); return this->get_default_topic_for_("command"); } @@ -180,12 +180,12 @@ MQTTComponent::MQTTComponent() = default; float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; } -void MQTTComponent::set_custom_state_topic(const std::string &custom_state_topic) { - this->custom_state_topic_ = custom_state_topic; +void MQTTComponent::set_custom_state_topic(const char *custom_state_topic) { + this->custom_state_topic_ = StringRef(custom_state_topic); this->has_custom_state_topic_ = true; } -void MQTTComponent::set_custom_command_topic(const std::string &custom_command_topic) { - this->custom_command_topic_ = custom_command_topic; +void MQTTComponent::set_custom_command_topic(const char *custom_command_topic) { + this->custom_command_topic_ = StringRef(custom_command_topic); this->has_custom_command_topic_ = true; } void MQTTComponent::set_command_retain(bool command_retain) { this->command_retain_ = command_retain; } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index aacfe8891f..9fc8c795d3 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -8,6 +8,7 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/string_ref.h" #include "mqtt_client.h" namespace esphome { @@ -88,9 +89,9 @@ class MQTTComponent : public Component { virtual std::string component_type() const = 0; /// Set a custom state topic. Set to "" for default behavior. - void set_custom_state_topic(const std::string &custom_state_topic); + void set_custom_state_topic(const char *custom_state_topic); /// Set a custom command topic. Set to "" for default behavior. - void set_custom_command_topic(const std::string &custom_command_topic); + void set_custom_command_topic(const char *custom_command_topic); /// Set whether command message should be retained. void set_command_retain(bool command_retain); @@ -188,15 +189,17 @@ class MQTTComponent : public Component { /// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name. std::string get_default_object_id_() const; - std::string custom_state_topic_{}; - std::string custom_command_topic_{}; + StringRef custom_state_topic_{}; + StringRef custom_command_topic_{}; + + std::unique_ptr availability_; + bool has_custom_state_topic_{false}; bool has_custom_command_topic_{false}; bool command_retain_{false}; bool retain_{true}; bool discovery_enabled_{true}; - std::unique_ptr availability_; bool resend_state_{false}; }; From 5e2df0b6a27821bc978a8beb6e8d8c5e7e7c91b9 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:34:33 +0100 Subject: [PATCH 217/436] Nextion allow underscore on names (#5979) --- esphome/components/nextion/base_component.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 5bd6643cb8..784da35371 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -33,14 +33,14 @@ CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" def NextionName(value): - valid_chars = f"{ascii_letters + digits}." + valid_chars = f"{ascii_letters + digits + '_'}." if not isinstance(value, str) or len(value) > 29: raise cv.Invalid("Must be a string less than 29 characters") for char in value: if char not in valid_chars: raise cv.Invalid( - f"Must only consist of upper/lowercase characters, numbers and the period '.'. The character '{char}' cannot be used." + f"Must only consist of upper/lowercase characters, numbers, the underscore '_', and the period '.'. The character '{char}' cannot be used." ) return value From 442820deafd1b17d0da003ad4577f80c98d6c513 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 22 Dec 2023 03:28:25 +1300 Subject: [PATCH 218/436] Fix replaced - in allowed characters during object_id sanitizing (#5983) --- esphome/helpers.py | 2 +- tests/unit_tests/test_helpers.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 00416b591f..254c950b5d 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -357,7 +357,7 @@ def snake_case(value): return value.replace(" ", "_").lower() -_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9_]") +_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9-_]") def sanitize(value): diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 79d39901f0..fc6bdbcdec 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -261,6 +261,7 @@ def test_snake_case(text, expected): ('!"§$%&/()=?foo_bar', "___________foo_bar"), ('foo_!"§$%&/()=?bar', "foo____________bar"), ('foo_bar!"§$%&/()=?', "foo_bar___________"), + ('foo-bar!"§$%&/()=?', "foo-bar___________"), ), ) def test_sanitize(text, expected): From 0a779a9299edf1b8af2813a245e6dfb51814e394 Mon Sep 17 00:00:00 2001 From: CVan Date: Thu, 21 Dec 2023 17:55:10 -0500 Subject: [PATCH 219/436] Update libtiff6 (#5985) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ee7c70bb0f..7c162fc316 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -50,7 +50,7 @@ RUN \ libssl-dev=3.0.11-1~deb12u2 \ libffi-dev=3.4.4-1 \ libopenjp2-7=2.5.0-2 \ - libtiff6=4.5.0-6 \ + libtiff6=4.5.0-6+deb12u1 \ cargo=0.66.0+ds1-1 \ pkg-config=1.8.1-1 \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \ From 31448a4fcd3f12e0d486e8197eddf7e022a8ac25 Mon Sep 17 00:00:00 2001 From: davidmonro Date: Fri, 22 Dec 2023 09:57:12 +1100 Subject: [PATCH 220/436] Override GPIOs 12 and 13 on the airm2m (LuatOS) board (#5982) Co-authored-by: David Monro --- esphome/components/esp32/boards.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index d361ca9917..cd85f3da97 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -161,6 +161,10 @@ ESP32_BOARD_PINS = { "BUTTON": 0, "SWITCH": 0, }, + "airm2m_core_esp32c3": { + "LED1_BUILTIN": 12, + "LED2_BUILTIN": 13, + }, "alksesp32": { "A0": 32, "A1": 33, From 513a02ce11f8879e869afcba8bd118febf823cce Mon Sep 17 00:00:00 2001 From: marshn Date: Fri, 22 Dec 2023 00:30:23 +0000 Subject: [PATCH 221/436] Add Keeloq RF protocol (#5511) --- esphome/components/remote_base/__init__.py | 56 ++++++ .../remote_base/keeloq_protocol.cpp | 188 ++++++++++++++++++ .../components/remote_base/keeloq_protocol.h | 53 +++++ 3 files changed, 297 insertions(+) create mode 100644 esphome/components/remote_base/keeloq_protocol.cpp create mode 100644 esphome/components/remote_base/keeloq_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 3accd5038c..1d8c6e0967 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -633,6 +633,62 @@ async def magiquest_action(var, config, args): cg.add(var.set_magnitude(template_)) +# Microchip HCS301 KeeLoq OOK +( + KeeloqData, + KeeloqBinarySensor, + KeeloqTrigger, + KeeloqAction, + KeeloqDumper, +) = declare_protocol("Keeloq") +KEELOQ_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFF)), + cv.Required(CONF_CODE): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFFF)), + cv.Optional(CONF_COMMAND, default=0x10): cv.All( + cv.hex_int, + cv.Range(min=0, max=0x10), + ), + cv.Optional(CONF_LEVEL, default=False): cv.boolean, + } +) + + +@register_binary_sensor("keeloq", KeeloqBinarySensor, KEELOQ_SCHEMA) +def Keeloq_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + KeeloqData, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("keeloq", KeeloqTrigger, KeeloqData) +def keeloq_trigger(var, config): + pass + + +@register_dumper("keeloq", KeeloqDumper) +def keeloq_dumper(var, config): + pass + + +@register_action("keeloq", KeeloqAction, KEELOQ_SCHEMA) +async def keeloq_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint32) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32) + cg.add(var.set_encrypted(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + template_ = await cg.templatable(config[CONF_LEVEL], args, bool) + cg.add(var.set_vlow(template_)) + + # NEC NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC") NEC_SCHEMA = cv.Schema( diff --git a/esphome/components/remote_base/keeloq_protocol.cpp b/esphome/components/remote_base/keeloq_protocol.cpp new file mode 100644 index 0000000000..77a2f9be6c --- /dev/null +++ b/esphome/components/remote_base/keeloq_protocol.cpp @@ -0,0 +1,188 @@ +#include "keeloq_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.keeloq"; + +static const uint32_t BIT_TIME_US = 380; +static const uint8_t NBITS_PREAMBLE = 12; +static const uint8_t NBITS_REPEAT = 1; +static const uint8_t NBITS_VLOW = 1; +static const uint8_t NBITS_SERIAL = 28; +static const uint8_t NBITS_BUTTONS = 4; +static const uint8_t NBITS_DISC = 12; +static const uint8_t NBITS_SYNC_CNT = 16; + +static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; +static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; +static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; + +/* +KeeLoq Protocol + +Coded using information from datasheet for Microchip HCS301 KeeLow Code Hopping Encoder + +Encoder - Hopping code is generated at random. + +Decoder - Hopping code is ignored and not checked when received. Serial number of +transmitter and nutton command is decoded. + +*/ + +void KeeloqProtocol::encode(RemoteTransmitData *dst, const KeeloqData &data) { + uint32_t out_data = 0x0; + + ESP_LOGD(TAG, "Send Keeloq: address=%07x command=%03x encrypted=%08x", data.address, data.command, data.encrypted); + ESP_LOGV(TAG, "Send Keeloq: data bits (%d + %d)", NBITS_ENCRYPTED_DATA, NBITS_FIXED_DATA); + + // Preamble = '01' x 12 + for (uint8_t cnt = NBITS_PREAMBLE; cnt; cnt--) { + dst->space(BIT_TIME_US); + dst->mark(BIT_TIME_US); + } + + // Header = 10 bit space + dst->space(10 * BIT_TIME_US); + + // Encrypted field + out_data = data.encrypted; + + ESP_LOGV(TAG, "Send Keeloq: Encrypted data %04x", out_data); + + for (uint32_t mask = 1, cnt = 0; cnt < NBITS_ENCRYPTED_DATA; cnt++, mask <<= 1) { + if (out_data & mask) { + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + } else { + dst->mark(2 * BIT_TIME_US); + dst->space(1 * BIT_TIME_US); + } + } + + // first 32 bits of fixed portion + out_data = (data.command & 0x0f); + out_data <<= NBITS_SERIAL; + out_data |= data.address; + ESP_LOGV(TAG, "Send Keeloq: Fixed data %04x", out_data); + + for (uint32_t mask = 1, cnt = 0; cnt < (NBITS_FIXED_DATA - 2); cnt++, mask <<= 1) { + if (out_data & mask) { + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + } else { + dst->mark(2 * BIT_TIME_US); + dst->space(1 * BIT_TIME_US); + } + } + + // low battery flag + if (data.vlow) { + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + } else { + dst->mark(2 * BIT_TIME_US); + dst->space(1 * BIT_TIME_US); + } + + // repeat flag - always sent as a '1' + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + + // Guard time at end of packet + dst->space(39 * BIT_TIME_US); +} + +optional KeeloqProtocol::decode(RemoteReceiveData src) { + KeeloqData out{ + .encrypted = 0, + .address = 0, + .command = 0, + .repeat = false, + .vlow = false, + + }; + + if (src.size() != (NBITS_PREAMBLE + NBITS_DATA) * 2) { + return {}; + } + + ESP_LOGVV(TAG, "%2d: %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", src.size(), src.peek(0), + src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), src.peek(7), src.peek(8), + src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), src.peek(15), + src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + + // Check preamble bits + int8_t bit = NBITS_PREAMBLE - 1; + while (--bit >= 0) { + if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US)) { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %d", bit + 1, src.peek()); + return {}; + } + } + if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(10 * BIT_TIME_US)) { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %d", bit + 1, src.peek()); + return {}; + } + + // Read encrypted bits + uint32_t out_data = 0; + for (bit = 0; bit < NBITS_ENCRYPTED_DATA; bit++) { + if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + out_data |= 0 << bit; + } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) { + out_data |= 1 << bit; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 2, %d %d", src.get_index(), src.peek()); + return {}; + } + } + ESP_LOGVV(TAG, "Decode KeeLoq: Data, %d %08x", bit, out_data); + out.encrypted = out_data; + + // Read Serial Number and Button Status + out_data = 0; + for (bit = 0; bit < NBITS_SERIAL + NBITS_BUTTONS; bit++) { + if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + out_data |= 0 << bit; + } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) { + out_data |= 1 << bit; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 3, %d %d", src.get_index(), src.peek()); + return {}; + } + } + ESP_LOGVV(TAG, "Decode KeeLoq: Data, %2d %08x", bit, out_data); + out.command = (out_data >> 28) & 0xf; + out.address = out_data & 0xfffffff; + + // Read Vlow bit + if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + out.vlow = false; + } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) { + out.vlow = true; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 4, %08x", src.peek()); + return {}; + } + + // Read Repeat bit + if (src.expect_mark(2 * BIT_TIME_US) && src.peek_space_at_least(BIT_TIME_US)) { + out.repeat = false; + } else if (src.expect_mark(BIT_TIME_US) && src.peek_space_at_least(2 * BIT_TIME_US)) { + out.repeat = true; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 5, %08x", src.peek()); + return {}; + } + + return out; +} + +void KeeloqProtocol::dump(const KeeloqData &data) { + ESP_LOGD(TAG, "Received Keeloq: address=0x%08X, command=0x%02x", data.address, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/keeloq_protocol.h b/esphome/components/remote_base/keeloq_protocol.h new file mode 100644 index 0000000000..47125c151b --- /dev/null +++ b/esphome/components/remote_base/keeloq_protocol.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct KeeloqData { + uint32_t encrypted; // 32 bit encrypted field + uint32_t address; // 28 bit serial number + uint8_t command; // Button Status S2-S1-S0-S3 + bool repeat; // Repeated command bit + bool vlow; // Battery status bit + + bool operator==(const KeeloqData &rhs) const { + // Treat 0x10 as a special, wildcard button press + // This allows us to match on just the address if wanted. + if (address != rhs.address) { + return false; + } + return (rhs.command == 0x10 || command == rhs.command); + } +}; + +class KeeloqProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const KeeloqData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const KeeloqData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Keeloq) + +template class KeeloqAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, address) + TEMPLATABLE_VALUE(uint32_t, encrypted) + TEMPLATABLE_VALUE(uint8_t, command) + TEMPLATABLE_VALUE(bool, vlow) + + void encode(RemoteTransmitData *dst, Ts... x) override { + KeeloqData data{}; + data.address = this->address_.value(x...); + data.encrypted = this->encrypted_.value(x...); + data.command = this->command_.value(x...); + data.vlow = this->vlow_.value(x...); + KeeloqProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome From 059e4cee58def2e433eb5fe02451981b45a6731f Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 21 Dec 2023 19:42:12 -0600 Subject: [PATCH 222/436] Add workaround for crash in Arduino 2.0.9 when CDC is configured (#5987) --- esphome/components/logger/logger.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 05b97a5f64..e0f7e77d2c 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -285,6 +285,7 @@ void Logger::pre_setup() { #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #if ARDUINO_USB_CDC_ON_BOOT this->hw_serial_ = &Serial; + Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection Serial.begin(this->baud_rate_); #else this->hw_serial_ = &Serial; From 70fdc3c3f8a1250fb5476d26d24f0ad21985b491 Mon Sep 17 00:00:00 2001 From: Jessica Hamilton Date: Fri, 22 Dec 2023 14:58:30 +1300 Subject: [PATCH 223/436] web_server.py: return empty content when file doesn't exist (#5980) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/dashboard/web_server.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 4552aebf7b..c4b84d3fe3 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -792,13 +792,22 @@ class EditRequestHandler(BaseHandler): """Get the content of a file.""" loop = asyncio.get_running_loop() filename = settings.rel_path(configuration) - content = await loop.run_in_executor(None, self._read_file, filename) - self.write(content) + content = await loop.run_in_executor( + None, self._read_file, filename, configuration + ) + if content is not None: + self.write(content) - def _read_file(self, filename: str) -> bytes: + def _read_file(self, filename: str, configuration: str) -> bytes | None: """Read a file and return the content as bytes.""" - with open(file=filename, encoding="utf-8") as f: - return f.read() + try: + with open(file=filename, encoding="utf-8") as f: + return f.read() + except FileNotFoundError: + if configuration in const.SECRETS_FILES: + return "" + self.set_status(404) + return None def _write_file(self, filename: str, content: bytes) -> None: """Write a file with the given content.""" From 3de5b26d7726128de941dbfa8c32ea168e497dff Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Thu, 21 Dec 2023 20:33:29 -0600 Subject: [PATCH 224/436] Add a Binary Sensor Filter for state settling (#5900) --- esphome/components/binary_sensor/__init__.py | 14 ++++++++++++++ esphome/components/binary_sensor/filter.cpp | 17 +++++++++++++++++ esphome/components/binary_sensor/filter.h | 13 +++++++++++++ tests/test1.yaml | 2 ++ 4 files changed, 46 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index babe46e082..2f788d7103 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -141,6 +141,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) +SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) FILTER_REGISTRY = Registry() validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) @@ -259,6 +260,19 @@ async def lambda_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, lambda_) +@register_filter( + "settle", + SettleFilter, + cv.templatable(cv.positive_time_period_milliseconds), +) +async def settle_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id) + await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_delay(template_)) + return var + + MULTI_CLICK_TIMING_SCHEMA = cv.Schema( { cv.Optional(CONF_STATE): cv.boolean, diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 46957383c3..8f94b108ac 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -111,6 +111,23 @@ LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } +optional SettleFilter::new_value(bool value, bool is_initial) { + if (!this->steady_) { + this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { + this->steady_ = true; + this->output(value, is_initial); + }); + return {}; + } else { + this->steady_ = false; + this->output(value, is_initial); + this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); + return value; + } +} + +float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 9514cb3fe2..f7342db2fb 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -108,6 +108,19 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; +class SettleFilter : public Filter, public Component { + public: + optional new_value(bool value, bool is_initial) override; + + float get_setup_priority() const override; + + template void set_delay(T delay) { this->delay_ = delay; } + + protected: + TemplatableValue delay_{}; + bool steady_{true}; +}; + } // namespace binary_sensor } // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index eb72f90b21..720d4e8e82 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1732,6 +1732,8 @@ binary_sensor: - delayed_on_off: !lambda "return 10;" - delayed_on: !lambda "return 1000;" - delayed_off: !lambda "return 0;" + - settle: 40ms + - settle: !lambda "return 10;" on_press: then: - lambda: >- From 4fb7e945f86c143e69bb95e77213d45b2e278950 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Dec 2023 16:59:24 -1000 Subject: [PATCH 225/436] Fix unexpected disconnects when outgoing buffer is full during keepalive (#5988) --- esphome/components/api/api_connection.cpp | 24 +++++++++++++++++++---- esphome/components/api/api_connection.h | 3 +++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d5ab00a822..4ebfb7582e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -118,7 +118,9 @@ void APIConnection::loop() { this->list_entities_iterator_.advance(); this->initial_state_iterator_.advance(); - const uint32_t keepalive = 60000; + static uint32_t keepalive = 60000; + static uint8_t max_ping_retries = 60; + static uint16_t ping_retry_interval = 1000; const uint32_t now = millis(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive @@ -126,10 +128,24 @@ void APIConnection::loop() { on_fatal_error(); ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str()); } - } else if (now - this->last_traffic_ > keepalive) { + } else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) { ESP_LOGVV(TAG, "Sending keepalive PING..."); - this->sent_ping_ = true; - this->send_ping_request(PingRequest()); + this->sent_ping_ = this->send_ping_request(PingRequest()); + if (!this->sent_ping_) { + this->next_ping_retry_ = now + ping_retry_interval; + this->ping_retries_++; + if (this->ping_retries_ >= max_ping_retries) { + on_fatal_error(); + ESP_LOGE(TAG, "%s: Sending keepalive failed %d time(s). Disconnecting...", this->client_combined_info_.c_str(), + this->ping_retries_); + } else if (this->ping_retries_ >= 10) { + ESP_LOGW(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms", + this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval); + } else { + ESP_LOGD(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms", + this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval); + } + } } #ifdef USE_ESP32_CAMERA diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 09b595bb71..9d01468807 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -140,6 +140,7 @@ class APIConnection : public APIServerConnection { void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping + this->ping_retries_ = 0; this->sent_ping_ = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; @@ -217,6 +218,8 @@ class APIConnection : public APIServerConnection { bool state_subscription_{false}; int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; uint32_t last_traffic_; + uint32_t next_ping_retry_{0}; + uint8_t ping_retries_{0}; bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; From bd6fa29f7765c0cc5b6f9ee1705c89f11b965179 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 22 Dec 2023 18:29:10 +1300 Subject: [PATCH 226/436] Regenerate api_pb2 after manual changes were added incorrectly in #5732 (#5990) --- esphome/components/api/api_pb2.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8dd34e7ef1..f81bf04e99 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3848,6 +3848,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->visual_max_humidity); out.append(buffer); out.append("\n"); + out.append("}"); } #endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -4015,6 +4016,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->target_humidity); out.append(buffer); out.append("\n"); + out.append("}"); } #endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { From 676ae6b26e1d759f809373d60b047a6f929266d2 Mon Sep 17 00:00:00 2001 From: matzman666 Date: Fri, 22 Dec 2023 07:58:17 +0100 Subject: [PATCH 227/436] Improved sensor readings in htu21d component. (#5839) --- esphome/components/htu21d/htu21d.cpp | 67 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index a8133ae32e..d0dbb15a43 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -39,45 +39,54 @@ void HTU21DComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_); } void HTU21DComponent::update() { - uint16_t raw_temperature; if (this->write(&HTU21D_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } - delay(50); // NOLINT - if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_temperature = i2c::i2ctohs(raw_temperature); - float temperature = (float(raw_temperature & 0xFFFC)) * 175.72f / 65536.0f - 46.85f; + // According to the datasheet sht21 temperature readings can take up to 85ms + this->set_timeout(85, [this]() { + uint16_t raw_temperature; + if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_temperature = i2c::i2ctohs(raw_temperature); - uint16_t raw_humidity; - if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - delay(50); // NOLINT - if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_humidity = i2c::i2ctohs(raw_humidity); + float temperature = (float(raw_temperature & 0xFFFC)) * 175.72f / 65536.0f - 46.85f; - float humidity = (float(raw_humidity & 0xFFFC)) * 125.0f / 65536.0f - 6.0f; + ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); - int8_t heater_level = this->get_heater_level(); + if (this->temperature_ != nullptr) + this->temperature_->publish_state(temperature); + this->status_clear_warning(); - ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%% Heater Level=%d", temperature, humidity, heater_level); + if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } - if (this->temperature_ != nullptr) - this->temperature_->publish_state(temperature); - if (this->humidity_ != nullptr) - this->humidity_->publish_state(humidity); - if (this->heater_ != nullptr) - this->heater_->publish_state(heater_level); - this->status_clear_warning(); + this->set_timeout(50, [this]() { + uint16_t raw_humidity; + if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_humidity = i2c::i2ctohs(raw_humidity); + + float humidity = (float(raw_humidity & 0xFFFC)) * 125.0f / 65536.0f - 6.0f; + + int8_t heater_level = this->get_heater_level(); + + ESP_LOGD(TAG, "Got Humidity=%.1f%% Heater Level=%d", humidity, heater_level); + + if (this->humidity_ != nullptr) + this->humidity_->publish_state(humidity); + if (this->heater_ != nullptr) + this->heater_->publish_state(heater_level); + this->status_clear_warning(); + }); + }); } bool HTU21DComponent::is_heater_enabled() { From d2d005838694774d9a6e4d0f1721a89d7588e79f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 22 Dec 2023 20:03:47 +1300 Subject: [PATCH 228/436] Lint the script folder files (#5991) --- script/api_protobuf/api_protobuf.py | 478 ++++++++++++++-------------- script/build_language_schema.py | 29 +- script/bump-version.py | 5 +- script/ci-custom.py | 233 +++++++------- script/helpers.py | 23 +- script/sync-device_class.py | 7 +- 6 files changed, 398 insertions(+), 377 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index b1292095d8..a2bc3abf64 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -17,28 +17,22 @@ then run this script with python3 and the files will be generated, they still need to be formatted """ -import re import os +import re +import sys +from abc import ABC, abstractmethod from pathlib import Path -from textwrap import dedent from subprocess import call +from textwrap import dedent # Generate with # protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto - import aioesphomeapi.api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor -file_header = "// This file was automatically generated with a tool.\n" -file_header += "// See scripts/api_protobuf/api_protobuf.py\n" - -cwd = Path(__file__).resolve().parent -root = cwd.parent.parent / "esphome" / "components" / "api" -prot = root / "api.protoc" -call(["protoc", "-o", str(prot), "-I", str(root), "api.proto"]) -content = prot.read_bytes() - -d = descriptor.FileDescriptorSet.FromString(content) +FILE_HEADER = """// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py +""" def indent_list(text, padding=" "): @@ -64,7 +58,7 @@ def camel_to_snake(name): return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() -class TypeInfo: +class TypeInfo(ABC): def __init__(self, field): self._field = field @@ -186,10 +180,12 @@ class TypeInfo: def dump_content(self): o = f'out.append(" {self.name}: ");\n' o += self.dump(f"this->{self.field_name}") + "\n" - o += f'out.append("\\n");\n' + o += 'out.append("\\n");\n' return o - dump = None + @abstractmethod + def dump(self, name: str): + pass TYPE_INFO = {} @@ -212,7 +208,7 @@ class DoubleType(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%g", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -225,7 +221,7 @@ class FloatType(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%g", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -238,7 +234,7 @@ class Int64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%lld", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -251,7 +247,7 @@ class UInt64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%llu", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -264,7 +260,7 @@ class Int32Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%" PRId32, {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -277,7 +273,7 @@ class Fixed64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%llu", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -290,7 +286,7 @@ class Fixed32Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%" PRIu32, {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -372,7 +368,7 @@ class UInt32Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%" PRIu32, {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -406,7 +402,7 @@ class SFixed32Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%" PRId32, {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -419,7 +415,7 @@ class SFixed64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%lld", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -432,7 +428,7 @@ class SInt32Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%" PRId32, {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -445,7 +441,7 @@ class SInt64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%lld", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -527,7 +523,7 @@ class RepeatedTypeInfo(TypeInfo): def encode_content(self): o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" - o += f"}}" + o += "}" return o @property @@ -535,10 +531,13 @@ class RepeatedTypeInfo(TypeInfo): o = f'for (const auto {"" if self._ti_is_bool else "&"}it : this->{self.field_name}) {{\n' o += f' out.append(" {self.name}: ");\n' o += indent(self._ti.dump("it")) + "\n" - o += f' out.append("\\n");\n' - o += f"}}\n" + o += ' out.append("\\n");\n' + o += "}\n" return o + def dump(self, _: str): + pass + def build_enum_type(desc): name = desc.name @@ -547,17 +546,17 @@ def build_enum_type(desc): out += f" {v.name} = {v.number},\n" out += "};\n" - cpp = f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + cpp = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" cpp += f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n" - cpp += f" switch (value) {{\n" + cpp += " switch (value) {\n" for v in desc.value: cpp += f" case enums::{v.name}:\n" cpp += f' return "{v.name}";\n' - cpp += f" default:\n" - cpp += f' return "UNKNOWN";\n' - cpp += f" }}\n" - cpp += f"}}\n" - cpp += f"#endif\n" + cpp += " default:\n" + cpp += ' return "UNKNOWN";\n' + cpp += " }\n" + cpp += "}\n" + cpp += "#endif\n" return out, cpp @@ -652,10 +651,10 @@ def build_message_type(desc): o += f" {dump[0]} " else: o += "\n" - o += f" __attribute__((unused)) char buffer[64];\n" + o += " __attribute__((unused)) char buffer[64];\n" o += f' out.append("{desc.name} {{\\n");\n' o += indent("\n".join(dump)) + "\n" - o += f' out.append("}}");\n' + o += ' out.append("}");\n' else: o2 = f'out.append("{desc.name} {{}}");' if len(o) + len(o2) + 3 < 120: @@ -664,9 +663,9 @@ def build_message_type(desc): o += "\n" o += f" {o2}\n" o += "}\n" - cpp += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" cpp += o - cpp += f"#endif\n" + cpp += "#endif\n" prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" prot += "void dump_to(std::string &out) const override;\n" prot += "#endif\n" @@ -684,71 +683,12 @@ def build_message_type(desc): return out, cpp -file = d.file[0] -content = file_header -content += """\ -#pragma once - -#include "proto.h" - -namespace esphome { -namespace api { - -""" - -cpp = file_header -cpp += """\ -#include "api_pb2.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace api { - -""" - -content += "namespace enums {\n\n" - -for enum in file.enum_type: - s, c = build_enum_type(enum) - content += s - cpp += c - -content += "\n} // namespace enums\n\n" - -mt = file.message_type - -for m in mt: - s, c = build_message_type(m) - content += s - cpp += c - -content += """\ - -} // namespace api -} // namespace esphome -""" -cpp += """\ - -} // namespace api -} // namespace esphome -""" - -with open(root / "api_pb2.h", "w") as f: - f.write(content) - -with open(root / "api_pb2.cpp", "w") as f: - f.write(cpp) - SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 RECEIVE_CASES = {} -class_name = "APIServerConnectionBase" - ifdefs = {} @@ -768,7 +708,6 @@ def build_service_message_type(mt): ifdef = get_opt(mt, pb.ifdef) log = get_opt(mt, pb.log, True) - nodelay = get_opt(mt, pb.no_delay, False) hout = "" cout = "" @@ -781,14 +720,14 @@ def build_service_message_type(mt): # Generate send func = f"send_{snake}" hout += f"bool {func}(const {mt.name} &msg);\n" - cout += f"bool {class_name}::{func}(const {mt.name} &msg) {{\n" + cout += f"bool APIServerConnectionBase::{func}(const {mt.name} &msg) {{\n" if log: - cout += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + cout += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - cout += f"#endif\n" + cout += "#endif\n" # cout += f' this->set_nodelay({str(nodelay).lower()});\n' cout += f" return this->send_message_<{mt.name}>(msg, {id_});\n" - cout += f"}}\n" + cout += "}\n" if source in (SOURCE_BOTH, SOURCE_CLIENT): # Generate receive func = f"on_{snake}" @@ -797,169 +736,242 @@ def build_service_message_type(mt): if ifdef is not None: case += f"#ifdef {ifdef}\n" case += f"{mt.name} msg;\n" - case += f"msg.decode(msg_data, msg_size);\n" + case += "msg.decode(msg_data, msg_size);\n" if log: - case += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - case += f"#endif\n" + case += "#endif\n" case += f"this->{func}(msg);\n" if ifdef is not None: - case += f"#endif\n" + case += "#endif\n" case += "break;" RECEIVE_CASES[id_] = case if ifdef is not None: - hout += f"#endif\n" - cout += f"#endif\n" + hout += "#endif\n" + cout += "#endif\n" return hout, cout -hpp = file_header -hpp += """\ -#pragma once +def main(): + cwd = Path(__file__).resolve().parent + root = cwd.parent.parent / "esphome" / "components" / "api" + prot_file = root / "api.protoc" + call(["protoc", "-o", str(prot_file), "-I", str(root), "api.proto"]) + proto_content = prot_file.read_bytes() -#include "api_pb2.h" -#include "esphome/core/defines.h" + # pylint: disable-next=no-member + d = descriptor.FileDescriptorSet.FromString(proto_content) -namespace esphome { -namespace api { + file = d.file[0] + content = FILE_HEADER + content += """\ + #pragma once -""" + #include "proto.h" -cpp = file_header -cpp += """\ -#include "api_pb2_service.h" -#include "esphome/core/log.h" + namespace esphome { + namespace api { -namespace esphome { -namespace api { + """ -static const char *const TAG = "api.service"; + cpp = FILE_HEADER + cpp += """\ + #include "api_pb2.h" + #include "esphome/core/log.h" -""" + #include -hpp += f"class {class_name} : public ProtoService {{\n" -hpp += " public:\n" + namespace esphome { + namespace api { -for mt in file.message_type: - obj = build_service_message_type(mt) - if obj is None: - continue - hout, cout = obj - hpp += indent(hout) + "\n" - cpp += cout + """ -cases = list(RECEIVE_CASES.items()) -cases.sort() -hpp += " protected:\n" -hpp += f" bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" -out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" -out += f" switch (msg_type) {{\n" -for i, case in cases: - c = f"case {i}: {{\n" - c += indent(case) + "\n" - c += f"}}" - out += indent(c, " ") + "\n" -out += " default:\n" -out += " return false;\n" -out += " }\n" -out += " return true;\n" -out += "}\n" -cpp += out -hpp += "};\n" + content += "namespace enums {\n\n" -serv = file.service[0] -class_name = "APIServerConnection" -hpp += "\n" -hpp += f"class {class_name} : public {class_name}Base {{\n" -hpp += " public:\n" -hpp_protected = "" -cpp += "\n" + for enum in file.enum_type: + s, c = build_enum_type(enum) + content += s + cpp += c -m = serv.method[0] -for m in serv.method: - func = m.name - inp = m.input_type[1:] - ret = m.output_type[1:] - is_void = ret == "void" - snake = camel_to_snake(inp) - on_func = f"on_{snake}" - needs_conn = get_opt(m, pb.needs_setup_connection, True) - needs_auth = get_opt(m, pb.needs_authentication, True) + content += "\n} // namespace enums\n\n" - ifdef = ifdefs.get(inp, None) + mt = file.message_type - if ifdef is not None: - hpp += f"#ifdef {ifdef}\n" - hpp_protected += f"#ifdef {ifdef}\n" - cpp += f"#ifdef {ifdef}\n" + for m in mt: + s, c = build_message_type(m) + content += s + cpp += c - hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" - hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" - cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" - body = "" - if needs_conn: - body += "if (!this->is_connection_setup()) {\n" - body += " this->on_no_setup_connection();\n" - body += " return;\n" - body += "}\n" - if needs_auth: - body += "if (!this->is_authenticated()) {\n" - body += " this->on_unauthenticated_access();\n" - body += " return;\n" - body += "}\n" + content += """\ - if is_void: - body += f"this->{func}(msg);\n" - else: - body += f"{ret} ret = this->{func}(msg);\n" - ret_snake = camel_to_snake(ret) - body += f"if (!this->send_{ret_snake}(ret)) {{\n" - body += f" this->on_fatal_error();\n" - body += "}\n" - cpp += indent(body) + "\n" + "}\n" + } // namespace api + } // namespace esphome + """ + cpp += """\ - if ifdef is not None: - hpp += f"#endif\n" - hpp_protected += f"#endif\n" - cpp += f"#endif\n" + } // namespace api + } // namespace esphome + """ -hpp += " protected:\n" -hpp += hpp_protected -hpp += "};\n" + with open(root / "api_pb2.h", "w", encoding="utf-8") as f: + f.write(content) -hpp += """\ + with open(root / "api_pb2.cpp", "w", encoding="utf-8") as f: + f.write(cpp) -} // namespace api -} // namespace esphome -""" -cpp += """\ + hpp = FILE_HEADER + hpp += """\ + #pragma once -} // namespace api -} // namespace esphome -""" + #include "api_pb2.h" + #include "esphome/core/defines.h" -with open(root / "api_pb2_service.h", "w") as f: - f.write(hpp) + namespace esphome { + namespace api { -with open(root / "api_pb2_service.cpp", "w") as f: - f.write(cpp) + """ -prot.unlink() + cpp = FILE_HEADER + cpp += """\ + #include "api_pb2_service.h" + #include "esphome/core/log.h" -try: - import clang_format + namespace esphome { + namespace api { - def exec_clang_format(path): - clang_format_path = os.path.join( - os.path.dirname(clang_format.__file__), "data", "bin", "clang-format" - ) - call([clang_format_path, "-i", path]) + static const char *const TAG = "api.service"; - exec_clang_format(root / "api_pb2_service.h") - exec_clang_format(root / "api_pb2_service.cpp") - exec_clang_format(root / "api_pb2.h") - exec_clang_format(root / "api_pb2.cpp") -except ImportError: - pass + """ + + class_name = "APIServerConnectionBase" + + hpp += f"class {class_name} : public ProtoService {{\n" + hpp += " public:\n" + + for mt in file.message_type: + obj = build_service_message_type(mt) + if obj is None: + continue + hout, cout = obj + hpp += indent(hout) + "\n" + cpp += cout + + cases = list(RECEIVE_CASES.items()) + cases.sort() + hpp += " protected:\n" + hpp += " bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + out += " switch (msg_type) {\n" + for i, case in cases: + c = f"case {i}: {{\n" + c += indent(case) + "\n" + c += "}" + out += indent(c, " ") + "\n" + out += " default:\n" + out += " return false;\n" + out += " }\n" + out += " return true;\n" + out += "}\n" + cpp += out + hpp += "};\n" + + serv = file.service[0] + class_name = "APIServerConnection" + hpp += "\n" + hpp += f"class {class_name} : public {class_name}Base {{\n" + hpp += " public:\n" + hpp_protected = "" + cpp += "\n" + + m = serv.method[0] + for m in serv.method: + func = m.name + inp = m.input_type[1:] + ret = m.output_type[1:] + is_void = ret == "void" + snake = camel_to_snake(inp) + on_func = f"on_{snake}" + needs_conn = get_opt(m, pb.needs_setup_connection, True) + needs_auth = get_opt(m, pb.needs_authentication, True) + + ifdef = ifdefs.get(inp, None) + + if ifdef is not None: + hpp += f"#ifdef {ifdef}\n" + hpp_protected += f"#ifdef {ifdef}\n" + cpp += f"#ifdef {ifdef}\n" + + hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" + hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" + cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" + body = "" + if needs_conn: + body += "if (!this->is_connection_setup()) {\n" + body += " this->on_no_setup_connection();\n" + body += " return;\n" + body += "}\n" + if needs_auth: + body += "if (!this->is_authenticated()) {\n" + body += " this->on_unauthenticated_access();\n" + body += " return;\n" + body += "}\n" + + if is_void: + body += f"this->{func}(msg);\n" + else: + body += f"{ret} ret = this->{func}(msg);\n" + ret_snake = camel_to_snake(ret) + body += f"if (!this->send_{ret_snake}(ret)) {{\n" + body += " this->on_fatal_error();\n" + body += "}\n" + cpp += indent(body) + "\n" + "}\n" + + if ifdef is not None: + hpp += "#endif\n" + hpp_protected += "#endif\n" + cpp += "#endif\n" + + hpp += " protected:\n" + hpp += hpp_protected + hpp += "};\n" + + hpp += """\ + + } // namespace api + } // namespace esphome + """ + cpp += """\ + + } // namespace api + } // namespace esphome + """ + + with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f: + f.write(hpp) + + with open(root / "api_pb2_service.cpp", "w", encoding="utf-8") as f: + f.write(cpp) + + prot_file.unlink() + + try: + import clang_format + + def exec_clang_format(path): + clang_format_path = os.path.join( + os.path.dirname(clang_format.__file__), "data", "bin", "clang-format" + ) + call([clang_format_path, "-i", path]) + + exec_clang_format(root / "api_pb2_service.h") + exec_clang_format(root / "api_pb2_service.cpp") + exec_clang_format(root / "api_pb2.h") + exec_clang_format(root / "api_pb2.cpp") + except ImportError: + pass + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index fb2010fe3e..fc6ccadc5f 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -61,6 +61,7 @@ solve_registry = [] def get_component_names(): + # pylint: disable-next=redefined-outer-name,reimported from esphome.loader import CORE_COMPONENTS_PATH component_names = ["esphome", "sensor", "esp32", "esp8266"] @@ -82,9 +83,12 @@ def load_components(): components[domain] = get_component(domain) +# pylint: disable=wrong-import-position from esphome.const import CONF_TYPE, KEY_CORE from esphome.core import CORE +# pylint: enable=wrong-import-position + CORE.data[KEY_CORE] = {} load_components() @@ -114,7 +118,7 @@ def write_file(name, obj): def delete_extra_files(keep_names): for d in os.listdir(args.output_path): - if d.endswith(".json") and not d[:-5] in keep_names: + if d.endswith(".json") and d[:-5] not in keep_names: os.remove(os.path.join(args.output_path, d)) print(f"Deleted {d}") @@ -552,11 +556,11 @@ def shrink(): s = f"{domain}.{schema_name}" if ( not s.endswith("." + S_CONFIG_SCHEMA) - and s not in referenced_schemas.keys() + and s not in referenced_schemas and not is_platform_schema(s) ): print(f"Removing {s}") - output[domain][S_SCHEMAS].pop(schema_name) + domain_schemas[S_SCHEMAS].pop(schema_name) def build_schema(): @@ -564,7 +568,7 @@ def build_schema(): # check esphome was not loaded globally (IDE auto imports) if len(ejs.extended_schemas) == 0: - raise Exception( + raise LookupError( "no data collected. Did you globally import an ESPHome component?" ) @@ -703,7 +707,7 @@ def convert(schema, config_var, path): if schema_instance is schema: assert S_CONFIG_VARS not in config_var assert S_EXTENDS not in config_var - if not S_TYPE in config_var: + if S_TYPE not in config_var: config_var[S_TYPE] = S_SCHEMA # assert config_var[S_TYPE] == S_SCHEMA @@ -765,9 +769,9 @@ def convert(schema, config_var, path): elif schema == automation.validate_potentially_and_condition: config_var[S_TYPE] = "registry" config_var["registry"] = "condition" - elif schema == cv.int_ or schema == cv.int_range: + elif schema in (cv.int_, cv.int_range): config_var[S_TYPE] = "integer" - elif schema == cv.string or schema == cv.string_strict or schema == cv.valid_name: + elif schema in (cv.string, cv.string_strict, cv.valid_name): config_var[S_TYPE] = "string" elif isinstance(schema, vol.Schema): @@ -779,6 +783,7 @@ def convert(schema, config_var, path): config_var |= pin_validators[repr_schema] config_var[S_TYPE] = "pin" + # pylint: disable-next=too-many-nested-blocks elif repr_schema in ejs.hidden_schemas: schema_type = ejs.hidden_schemas[repr_schema] @@ -869,7 +874,7 @@ def convert(schema, config_var, path): config_var["use_id_type"] = str(data.base) config_var[S_TYPE] = "use_id" else: - raise Exception("Unknown extracted schema type") + raise TypeError("Unknown extracted schema type") elif config_var.get("key") == "GeneratedID": if path.startswith("i2c/CONFIG_SCHEMA/") and path.endswith("/id"): config_var["id_type"] = { @@ -884,7 +889,7 @@ def convert(schema, config_var, path): elif path == "pins/esp32/val 1/id": config_var["id_type"] = "pin" else: - raise Exception("Cannot determine id_type for " + path) + raise TypeError("Cannot determine id_type for " + path) elif repr_schema in ejs.registry_schemas: solve_registry.append((ejs.registry_schemas[repr_schema], config_var)) @@ -948,11 +953,7 @@ def convert_keys(converted, schema, path): result["key"] = "GeneratedID" elif isinstance(k, cv.Required): result["key"] = "Required" - elif ( - isinstance(k, cv.Optional) - or isinstance(k, cv.Inclusive) - or isinstance(k, cv.Exclusive) - ): + elif isinstance(k, (cv.Optional, cv.Inclusive, cv.Exclusive)): result["key"] = "Optional" else: converted["key"] = "String" diff --git a/script/bump-version.py b/script/bump-version.py index 3e1e473c4b..a55bb65cd6 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -2,7 +2,6 @@ import argparse import re -import subprocess from dataclasses import dataclass import sys @@ -40,12 +39,12 @@ class Version: def sub(path, pattern, repl, expected_count=1): - with open(path) as fh: + with open(path, encoding="utf-8") as fh: content = fh.read() content, count = re.subn(pattern, repl, content, flags=re.MULTILINE) if expected_count is not None: assert count == expected_count, f"Pattern {pattern} replacement failed!" - with open(path, "w") as fh: + with open(path, "w", encoding="utf-8") as fh: fh.write(content) diff --git a/script/ci-custom.py b/script/ci-custom.py index cc9bdcadbb..41ce030d48 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 -from helpers import styled, print_error_for_file, git_ls_files, filter_changed import argparse import codecs import collections -import colorama import fnmatch import functools import os.path @@ -12,6 +10,9 @@ import re import sys import time +import colorama +from helpers import filter_changed, git_ls_files, print_error_for_file, styled + sys.path.append(os.path.dirname(__file__)) @@ -30,31 +31,6 @@ def find_all(a_str, sub): column += len(sub) -colorama.init() - -parser = argparse.ArgumentParser() -parser.add_argument( - "files", nargs="*", default=[], help="files to be processed (regex on path)" -) -parser.add_argument( - "-c", "--changed", action="store_true", help="Only run on changed files" -) -parser.add_argument( - "--print-slowest", action="store_true", help="Print the slowest checks" -) -args = parser.parse_args() - -EXECUTABLE_BIT = git_ls_files() -files = list(EXECUTABLE_BIT.keys()) -# Match against re -file_name_re = re.compile("|".join(args.files)) -files = [p for p in files if file_name_re.search(p)] - -if args.changed: - files = filter_changed(files) - -files.sort() - file_types = ( ".h", ".c", @@ -86,6 +62,30 @@ ignore_types = (".ico", ".png", ".woff", ".woff2", "") LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] LINT_POST_CHECKS = [] +EXECUTABLE_BIT = {} + +errors = collections.defaultdict(list) + + +def add_errors(fname, errs): + if not isinstance(errs, list): + errs = [errs] + for err in errs: + if err is None: + continue + try: + lineno, col, msg = err + except ValueError: + lineno = 1 + col = 1 + msg = err + if not isinstance(msg, str): + raise ValueError("Error is not instance of string!") + if not isinstance(lineno, int): + raise ValueError("Line number is not an int!") + if not isinstance(col, int): + raise ValueError("Column number is not an int!") + errors[fname].append((lineno, col, msg)) def run_check(lint_obj, fname, *args): @@ -155,7 +155,7 @@ def lint_re_check(regex, **kwargs): def decorator(func): @functools.wraps(func) def new_func(fname, content): - errors = [] + errs = [] for match in prog.finditer(content): if "NOLINT" in match.group(0): continue @@ -165,8 +165,8 @@ def lint_re_check(regex, **kwargs): err = func(fname, match) if err is None: continue - errors.append((lineno, col + 1, err)) - return errors + errs.append((lineno, col + 1, err)) + return errs return decor(new_func) @@ -182,13 +182,13 @@ def lint_content_find_check(find, only_first=False, **kwargs): find_ = find if callable(find): find_ = find(fname, content) - errors = [] + errs = [] for line, col in find_all(content, find_): err = func(fname) - errors.append((line + 1, col + 1, err)) + errs.append((line + 1, col + 1, err)) if only_first: break - return errors + return errs return decor(new_func) @@ -235,8 +235,8 @@ def lint_executable_bit(fname): ex = EXECUTABLE_BIT[fname] if ex != 100644: return ( - "File has invalid executable bit {}. If running from a windows machine please " - "see disabling executable bit in git.".format(ex) + f"File has invalid executable bit {ex}. If running from a windows machine please " + "see disabling executable bit in git." ) return None @@ -285,8 +285,8 @@ def lint_no_defines(fname, match): s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};") return ( "#define macros for integer constants are not allowed, please use " - "{} style instead (replace uint8_t with the appropriate " - "datatype). See also Google style guide.".format(s) + f"{s} style instead (replace uint8_t with the appropriate " + "datatype). See also Google style guide." ) @@ -296,11 +296,11 @@ def lint_no_long_delays(fname, match): if duration_ms < 50: return None return ( - "{} - long calls to delay() are not allowed in ESPHome because everything executes " - "in one thread. Calling delay() will block the main thread and slow down ESPHome.\n" + f"{highlight(match.group(0).strip())} - long calls to delay() are not allowed " + "in ESPHome because everything executes in one thread. Calling delay() will " + "block the main thread and slow down ESPHome.\n" "If there's no way to work around the delay() and it doesn't execute often, please add " "a '// NOLINT' comment to the line." - "".format(highlight(match.group(0).strip())) ) @@ -311,28 +311,28 @@ def lint_const_ordered(fname, content): Reason: Otherwise people add it to the end, and then that results in merge conflicts. """ lines = content.splitlines() - errors = [] + errs = [] for start in ["CONF_", "ICON_", "UNIT_"]: matching = [ (i + 1, line) for i, line in enumerate(lines) if line.startswith(start) ] ordered = list(sorted(matching, key=lambda x: x[1].replace("_", " "))) ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)] - for (mi, ml), (oi, ol) in zip(matching, ordered): - if ml == ol: + for (mi, mline), (_, ol) in zip(matching, ordered): + if mline == ol: continue - target = next(i for i, l in ordered if l == ml) - target_text = next(l for i, l in matching if target == i) - errors.append( + target = next(i for i, line in ordered if line == mline) + target_text = next(line for i, line in matching if target == i) + errs.append( ( mi, 1, - f"Constant {highlight(ml)} is not ordered, please make sure all " + f"Constant {highlight(mline)} is not ordered, please make sure all " f"constants are ordered. See line {mi} (should go to line {target}, " f"{target_text})", ) ) - return errors + return errs @lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=["*.py"]) @@ -344,15 +344,14 @@ def lint_conf_matches(fname, match): if const_norm == value_norm: return None return ( - "Constant {} does not match value {}! Please make sure the constant's name matches its " - "value!" - "".format(highlight("CONF_" + const), highlight(value)) + f"Constant {highlight('CONF_' + const)} does not match value {highlight(value)}! " + "Please make sure the constant's name matches its value!" ) CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$' -with codecs.open("esphome/const.py", "r", encoding="utf-8") as f_handle: - constants_content = f_handle.read() +with codecs.open("esphome/const.py", "r", encoding="utf-8") as const_f_handle: + constants_content = const_f_handle.read() CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)] CONSTANTS_USES = collections.defaultdict(list) @@ -365,8 +364,8 @@ def lint_conf_from_const_py(fname, match): CONSTANTS_USES[name].append(fname) return None return ( - "Constant {} has already been defined in const.py - please import the constant from " - "const.py directly.".format(highlight(name)) + f"Constant {highlight(name)} has already been defined in const.py - " + "please import the constant from const.py directly." ) @@ -473,16 +472,15 @@ def lint_no_byte_datatype(fname, match): @lint_post_check def lint_constants_usage(): - errors = [] + errs = [] for constant, uses in CONSTANTS_USES.items(): if len(uses) < 4: continue - errors.append( - "Constant {} is defined in {} files. Please move all definitions of the " - "constant to const.py (Uses: {})" - "".format(highlight(constant), len(uses), ", ".join(uses)) + errs.append( + f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " + f"constant to const.py (Uses: {', '.join(uses)})" ) - return errors + return errs def relative_cpp_search_text(fname, content): @@ -553,7 +551,7 @@ def lint_namespace(fname, content): return ( "Invalid namespace found in C++ file. All integration C++ files should put all " "functions in a separate namespace that matches the integration's name. " - "Please make sure the file contains {}".format(highlight(search)) + f"Please make sure the file contains {highlight(search)}" ) @@ -639,66 +637,73 @@ def lint_log_in_header(fname): ) -errors = collections.defaultdict(list) +def main(): + colorama.init() + parser = argparse.ArgumentParser() + parser.add_argument( + "files", nargs="*", default=[], help="files to be processed (regex on path)" + ) + parser.add_argument( + "-c", "--changed", action="store_true", help="Only run on changed files" + ) + parser.add_argument( + "--print-slowest", action="store_true", help="Print the slowest checks" + ) + args = parser.parse_args() -def add_errors(fname, errs): - if not isinstance(errs, list): - errs = [errs] - for err in errs: - if err is None: + global EXECUTABLE_BIT + EXECUTABLE_BIT = git_ls_files() + files = list(EXECUTABLE_BIT.keys()) + # Match against re + file_name_re = re.compile("|".join(args.files)) + files = [p for p in files if file_name_re.search(p)] + + if args.changed: + files = filter_changed(files) + + files.sort() + + for fname in files: + _, ext = os.path.splitext(fname) + run_checks(LINT_FILE_CHECKS, fname, fname) + if ext in ignore_types: continue try: - lineno, col, msg = err - except ValueError: - lineno = 1 - col = 1 - msg = err - if not isinstance(msg, str): - raise ValueError("Error is not instance of string!") - if not isinstance(lineno, int): - raise ValueError("Line number is not an int!") - if not isinstance(col, int): - raise ValueError("Column number is not an int!") - errors[fname].append((lineno, col, msg)) + with codecs.open(fname, "r", encoding="utf-8") as f_handle: + content = f_handle.read() + except UnicodeDecodeError: + add_errors( + fname, + "File is not readable as UTF-8. Please set your editor to UTF-8 mode.", + ) + continue + run_checks(LINT_CONTENT_CHECKS, fname, fname, content) + run_checks(LINT_POST_CHECKS, "POST") -for fname in files: - _, ext = os.path.splitext(fname) - run_checks(LINT_FILE_CHECKS, fname, fname) - if ext in ignore_types: - continue - try: - with codecs.open(fname, "r", encoding="utf-8") as f_handle: - content = f_handle.read() - except UnicodeDecodeError: - add_errors( - fname, - "File is not readable as UTF-8. Please set your editor to UTF-8 mode.", + for f, errs in sorted(errors.items()): + bold = functools.partial(styled, colorama.Style.BRIGHT) + bold_red = functools.partial(styled, (colorama.Style.BRIGHT, colorama.Fore.RED)) + err_str = ( + f"{bold(f'{f}:{lineno}:{col}:')} {bold_red('lint:')} {msg}\n" + for lineno, col, msg in errs ) - continue - run_checks(LINT_CONTENT_CHECKS, fname, fname, content) + print_error_for_file(f, "\n".join(err_str)) -run_checks(LINT_POST_CHECKS, "POST") + if args.print_slowest: + lint_times = [] + for lint in LINT_FILE_CHECKS + LINT_CONTENT_CHECKS + LINT_POST_CHECKS: + durations = lint.get("durations", []) + lint_times.append((sum(durations), len(durations), lint["func"].__name__)) + lint_times.sort(key=lambda x: -x[0]) + for i in range(min(len(lint_times), 10)): + dur, invocations, name = lint_times[i] + print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)") + print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s") -for f, errs in sorted(errors.items()): - bold = functools.partial(styled, colorama.Style.BRIGHT) - bold_red = functools.partial(styled, (colorama.Style.BRIGHT, colorama.Fore.RED)) - err_str = ( - f"{bold(f'{f}:{lineno}:{col}:')} {bold_red('lint:')} {msg}\n" - for lineno, col, msg in errs - ) - print_error_for_file(f, "\n".join(err_str)) + return len(errors) -if args.print_slowest: - lint_times = [] - for lint in LINT_FILE_CHECKS + LINT_CONTENT_CHECKS + LINT_POST_CHECKS: - durations = lint.get("durations", []) - lint_times.append((sum(durations), len(durations), lint["func"].__name__)) - lint_times.sort(key=lambda x: -x[0]) - for i in range(min(len(lint_times), 10)): - dur, invocations, name = lint_times[i] - print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)") - print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s") -sys.exit(len(errors)) +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/helpers.py b/script/helpers.py index c042362aeb..b1908e9875 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,10 +1,11 @@ -import colorama +import json import os.path import re import subprocess -import json from pathlib import Path +import colorama + root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") temp_folder = os.path.join(root_path, ".temp") @@ -44,7 +45,7 @@ def build_all_include(): content = "\n".join(headers) p = Path(temp_header_file) p.parent.mkdir(exist_ok=True) - p.write_text(content) + p.write_text(content, encoding="utf-8") def walk_files(path): @@ -54,14 +55,14 @@ def walk_files(path): def get_output(*args): - proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, err = proc.communicate() + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + output, _ = proc.communicate() return output.decode("utf-8") def get_err(*args): - proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, err = proc.communicate() + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + _, err = proc.communicate() return err.decode("utf-8") @@ -78,7 +79,7 @@ def changed_files(): merge_base = splitlines_no_ends(get_output(*command))[0] break # pylint: disable=bare-except - except: + except: # noqa: E722 pass else: raise ValueError("Git not configured") @@ -103,7 +104,7 @@ def filter_changed(files): def filter_grep(files, value): matched = [] for file in files: - with open(file) as handle: + with open(file, encoding="utf-8") as handle: contents = handle.read() if value in contents: matched.append(file) @@ -114,8 +115,8 @@ def git_ls_files(patterns=None): command = ["git", "ls-files", "-s"] if patterns is not None: command.extend(patterns) - proc = subprocess.Popen(command, stdout=subprocess.PIPE) - output, err = proc.communicate() + with subprocess.Popen(command, stdout=subprocess.PIPE) as proc: + output, _ = proc.communicate() lines = [x.split() for x in output.decode("utf-8").splitlines()] return {s[3].strip(): int(s[0]) for s in lines} diff --git a/script/sync-device_class.py b/script/sync-device_class.py index ae6f4be0c8..8f91b97997 100755 --- a/script/sync-device_class.py +++ b/script/sync-device_class.py @@ -2,6 +2,7 @@ import re +# pylint: disable=import-error from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass from homeassistant.components.cover import CoverDeviceClass @@ -9,6 +10,8 @@ from homeassistant.components.number import NumberDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass +# pylint: enable=import-error + BLOCKLIST = ( # requires special support on HA side "enum", @@ -25,10 +28,10 @@ DOMAINS = { def sub(path, pattern, repl): - with open(path) as handle: + with open(path, encoding="utf-8") as handle: content = handle.read() content = re.sub(pattern, repl, content, flags=re.MULTILINE) - with open(path, "w") as handle: + with open(path, "w", encoding="utf-8") as handle: handle.write(content) From 46255ad4dfcd00fff079192d197879f91fd7dd3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Dec 2023 21:35:31 -1000 Subject: [PATCH 229/436] Fix dashboard logs when api is disabled and using MQTT (#5992) --- esphome/dashboard/web_server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index c4b84d3fe3..6a80865906 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -301,11 +301,16 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): config_file = settings.rel_path(configuration) port = json_message["port"] if ( - port == "OTA" + port == "OTA" # pylint: disable=too-many-boolean-expressions and (mdns := dashboard.mdns_status) and (entry := entries.get(config_file)) + and entry.loaded_integrations + and "api" in entry.loaded_integrations and (address := await mdns.async_resolve_host(entry.name)) ): + # Use the IP address if available but only + # if the API is loaded and the device is online + # since MQTT logging will not work otherwise port = address return [ From 46c4c61b40114f3cce38842c2900e9f1af4c8245 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 22 Dec 2023 21:10:35 +1300 Subject: [PATCH 230/436] Fix broken configs with non-existent components (#5993) --- esphome/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/config.py b/esphome/config.py index e9433d537e..4aca0d6056 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -315,7 +315,11 @@ class LoadValidationStep(ConfigValidationStep): return result.add_output_path([self.domain], self.domain) component = get_component(self.domain) - if component.multi_conf_no_default and isinstance(self.conf, core.AutoLoad): + if ( + component is not None + and component.multi_conf_no_default + and isinstance(self.conf, core.AutoLoad) + ): self.conf = [] result[self.domain] = self.conf path = [self.domain] From a97fc4f75810776227f862c78476aca565f2d0c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Dec 2023 15:43:17 -1000 Subject: [PATCH 231/436] dashboard: Only ping when polling is active (#6001) fixes https://github.com/esphome/issues/issues/5257 --- esphome/dashboard/status/ping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index d8281d9de1..989cd1570f 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -31,6 +31,7 @@ class PingStatus: while not dashboard.stop_event.is_set(): # Only ping if the dashboard is open await dashboard.ping_request.wait() + dashboard.ping_request.clear() current_entries = dashboard.entries.async_all() to_ping: list[DashboardEntry] = [ entry for entry in current_entries if entry.address is not None From 8e674990b0c829ee130927f4af06b9f02ec60a63 Mon Sep 17 00:00:00 2001 From: Attila Farago Date: Sat, 23 Dec 2023 15:17:00 +0100 Subject: [PATCH 232/436] web_server support for home assistant like styling (#5854) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/web_server/web_server.cpp | 38 ++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0d72e274cd..54e9e6ebcc 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -397,19 +397,21 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #define set_json_id(root, obj, sensor, start_config) \ (root)["id"] = sensor; \ - if (((start_config) == DETAIL_ALL)) \ - (root)["name"] = (obj)->get_name(); + if (((start_config) == DETAIL_ALL)) { \ + (root)["name"] = (obj)->get_name(); \ + (root)["icon"] = (obj)->get_icon(); \ + (root)["entity_category"] = (obj)->get_entity_category(); \ + if ((obj)->is_disabled_by_default()) \ + (root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \ + } #define set_json_value(root, obj, sensor, value, start_config) \ - set_json_id((root), (obj), sensor, start_config)(root)["value"] = value; - -#define set_json_state_value(root, obj, sensor, state, value, start_config) \ - set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; + set_json_id((root), (obj), sensor, start_config); \ + (root)["value"] = value; #define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \ - set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; \ - if (((start_config) == DETAIL_ALL)) \ - (root)["icon"] = (obj)->get_icon(); + set_json_value(root, obj, sensor, value, start_config); \ + (root)["state"] = state; #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { @@ -436,6 +438,10 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail state += " " + obj->get_unit_of_measurement(); } set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); + if (start_config == DETAIL_ALL) { + if (!obj->get_unit_of_measurement().empty()) + root["uom"] = obj->get_unit_of_measurement(); + } }); } #endif @@ -529,7 +535,8 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool s } std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { return json::build_json([obj, value, start_config](JsonObject root) { - set_json_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, + start_config); }); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -548,7 +555,8 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con void WebServer::on_fan_update(fan::Fan *obj) { this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); } std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { return json::build_json([obj, start_config](JsonObject root) { - set_json_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, start_config); + set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, + start_config); const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; @@ -773,8 +781,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { return json::build_json([obj, start_config](JsonObject root) { - set_json_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", + obj->position, start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_tilt()) @@ -824,6 +832,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["max_value"] = obj->traits.get_max_value(); root["step"] = obj->traits.get_step(); root["mode"] = (int) obj->traits.get_mode(); + if (!obj->traits.get_unit_of_measurement().empty()) + root["uom"] = obj->traits.get_unit_of_measurement(); } if (std::isnan(value)) { root["value"] = "\"NaN\""; @@ -930,7 +940,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { return json::build_json([obj, value, start_config](JsonObject root) { - set_json_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root.createNestedArray("option"); for (auto &option : obj->traits.get_options()) { From 6583026e1473f4e5246f850b813d3999ade49f95 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Sun, 24 Dec 2023 14:54:53 +0100 Subject: [PATCH 233/436] tt21100: restore init read (#6008) --- esphome/components/tt21100/touchscreen/tt21100.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index 6b5cba74cd..ba4b0ee02d 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -64,6 +64,9 @@ void TT21100Touchscreen::setup() { // Update display dimensions if they were updated during display setup this->x_raw_max_ = this->get_width_(); this->y_raw_max_ = this->get_height_(); + + // Trigger initial read to activate the interrupt + this->store_.touched = true; } void TT21100Touchscreen::update_touches() { From fe15d993f9b79a59531fb0cf46fe02be7514316e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Dec 2023 03:56:31 -1000 Subject: [PATCH 234/436] dashboard: Fix file writes on Windows (#6013) --- esphome/dashboard/util/file.py | 10 +++++++++- tests/dashboard/util/test_file.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/util/file.py b/esphome/dashboard/util/file.py index 5f3c5f5f1b..661d5f34cf 100644 --- a/esphome/dashboard/util/file.py +++ b/esphome/dashboard/util/file.py @@ -30,6 +30,7 @@ def write_file( """ tmp_filename = "" + missing_fchmod = False try: # Modern versions of Python tempfile create this file with mode 0o600 with tempfile.NamedTemporaryFile( @@ -38,8 +39,15 @@ def write_file( fdesc.write(utf8_data) tmp_filename = fdesc.name if not private: - os.fchmod(fdesc.fileno(), 0o644) + try: + os.fchmod(fdesc.fileno(), 0o644) + except AttributeError: + # os.fchmod is not available on Windows + missing_fchmod = True + os.replace(tmp_filename, filename) + if missing_fchmod: + os.chmod(filename, 0o644) finally: if os.path.exists(tmp_filename): try: diff --git a/tests/dashboard/util/test_file.py b/tests/dashboard/util/test_file.py index 89e6b97086..270ab565f1 100644 --- a/tests/dashboard/util/test_file.py +++ b/tests/dashboard/util/test_file.py @@ -13,7 +13,7 @@ def test_write_utf8_file(tmp_path: Path) -> None: assert tmp_path.joinpath("foo.txt").read_text() == "foo" with pytest.raises(OSError): - write_utf8_file(Path("/not-writable"), "bar") + write_utf8_file(Path("/dev/not-writable"), "bar") def test_write_file(tmp_path: Path) -> None: From de6fc6b1dd2ea4edf077eb5e478d69946ea513e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Dec 2023 03:57:15 -1000 Subject: [PATCH 235/436] Fix docker builds (#6012) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7c162fc316..468124e3ed 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -34,7 +34,7 @@ RUN \ python3-wheel=0.38.4-2 \ iputils-ping=3:20221126-1 \ git=1:2.39.2-1.1 \ - curl=7.88.1-10+deb12u4 \ + curl=7.88.1-10+deb12u5 \ openssh-client=1:9.2p1-2+deb12u1 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ From 46fc37b6910491cefb3bb76c438db1ee05643c76 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Sun, 24 Dec 2023 14:58:27 +0100 Subject: [PATCH 236/436] Display: fix class inherence in Python script (#6009) --- esphome/components/display/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 9f4e922a37..91f10c5458 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -18,8 +18,8 @@ from esphome.core import coroutine_with_priority IS_PLATFORM_COMPONENT = True display_ns = cg.esphome_ns.namespace("display") -Display = display_ns.class_("Display") -DisplayBuffer = display_ns.class_("DisplayBuffer") +Display = display_ns.class_("Display", cg.PollingComponent) +DisplayBuffer = display_ns.class_("DisplayBuffer", Display) DisplayPage = display_ns.class_("DisplayPage") DisplayPagePtr = DisplayPage.operator("ptr") DisplayRef = Display.operator("ref") From 93ac765425e3e57b5ee14df804c6ce24d5331f5a Mon Sep 17 00:00:00 2001 From: Fabian Date: Sun, 24 Dec 2023 20:16:53 +0100 Subject: [PATCH 237/436] [Touchscreen] Add expire of touch record. (#5986) * Add expire of touch record. * Implement suggested changes. * Alternative implementation to detect touch release. * add `cancel_timeout`. * Add touch timeout as configurable element. --------- Co-authored-by: Your Name --- .../lilygo_t5_47/touchscreen/__init__.py | 2 +- esphome/components/touchscreen/__init__.py | 42 ++++++++++++------- .../components/touchscreen/touchscreen.cpp | 8 ++++ esphome/components/touchscreen/touchscreen.h | 2 + 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/esphome/components/lilygo_t5_47/touchscreen/__init__.py b/esphome/components/lilygo_t5_47/touchscreen/__init__.py index 01b03c807f..17f7262785 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/__init__.py +++ b/esphome/components/lilygo_t5_47/touchscreen/__init__.py @@ -18,7 +18,7 @@ LilygoT547Touchscreen = lilygo_t5_47_ns.class_( CONF_LILYGO_T5_47_TOUCHSCREEN_ID = "lilygo_t5_47_touchscreen_id" -CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( +CONFIG_SCHEMA = touchscreen.touchscreen_schema("250ms").extend( cv.Schema( { cv.GenerateID(): cv.declare_id(LilygoT547Touchscreen), diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index bc09c6364d..99aee5c9fb 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -24,6 +24,7 @@ CONF_DISPLAY = "display" CONF_TOUCHSCREEN_ID = "touchscreen_id" CONF_REPORT_INTERVAL = "report_interval" # not used yet: CONF_ON_UPDATE = "on_update" +CONF_TOUCH_TIMEOUT = "touch_timeout" CONF_MIRROR_X = "mirror_x" CONF_MIRROR_Y = "mirror_y" @@ -31,21 +32,29 @@ CONF_SWAP_XY = "swap_xy" CONF_TRANSFORM = "transform" -TOUCHSCREEN_SCHEMA = cv.Schema( - { - cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, - cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, - cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, - } - ), - cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), - cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), - cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), - } -).extend(cv.polling_component_schema("50ms")) +def touchscreen_schema(default_touch_timeout): + return cv.Schema( + { + cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + cv.Optional(CONF_TOUCH_TIMEOUT, default=default_touch_timeout): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(milliseconds=65535)), + ), + cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), + } + ).extend(cv.polling_component_schema("50ms")) + + +TOUCHSCREEN_SCHEMA = touchscreen_schema(cv.UNDEFINED) async def register_touchscreen(var, config): @@ -54,6 +63,9 @@ async def register_touchscreen(var, config): disp = await cg.get_variable(config[CONF_DISPLAY]) cg.add(var.set_display(disp)) + if CONF_TOUCH_TIMEOUT in config: + cg.add(var.set_touch_timeout(config[CONF_TOUCH_TIMEOUT])) + if CONF_TRANSFORM in config: transform = config[CONF_TRANSFORM] cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index f095c2af8c..18a4230197 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -47,6 +47,11 @@ void Touchscreen::loop() { } else { this->store_.touched = false; this->defer([this]() { this->send_touches_(); }); + if (this->touch_timeout_ > 0) { + // Simulate a touch after touch_timeout_> ms. This will reset any existing timeout operation. + // This is to detect touch release. + this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; }); + } } } } @@ -90,6 +95,9 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r void Touchscreen::send_touches_() { if (!this->is_touched_) { + if (this->touch_timeout_ > 0) { + this->cancel_timeout(TAG); + } this->release_trigger_.trigger(); for (auto *listener : this->touch_listeners_) listener->release(); diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 74747c589c..06aff68f07 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -46,6 +46,7 @@ class Touchscreen : public PollingComponent { void set_display(display::Display *display) { this->display_ = display; } display::Display *get_display() const { return this->display_; } + void set_touch_timeout(uint16_t val) { this->touch_timeout_ = val; } void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; } void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; } void set_swap_xy(bool swap) { this->swap_x_y_ = swap; } @@ -100,6 +101,7 @@ class Touchscreen : public PollingComponent { display::Display *display_{nullptr}; int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0}; + uint16_t touch_timeout_{0}; bool invert_x_{false}, invert_y_{false}, swap_x_y_{false}; Trigger touch_trigger_; From 3be97868fcfa0a2be59ffb5a31dd7f4961004620 Mon Sep 17 00:00:00 2001 From: Anton Viktorov Date: Wed, 27 Dec 2023 02:01:15 +0100 Subject: [PATCH 238/436] Support for ST7567 display 128x64 (I2C, SPI) (#5952) --- CODEOWNERS | 3 + esphome/components/st7567_base/__init__.py | 55 +++++++ .../components/st7567_base/st7567_base.cpp | 152 ++++++++++++++++++ esphome/components/st7567_base/st7567_base.h | 100 ++++++++++++ esphome/components/st7567_i2c/__init__.py | 1 + esphome/components/st7567_i2c/display.py | 29 ++++ esphome/components/st7567_i2c/st7567_i2c.cpp | 60 +++++++ esphome/components/st7567_i2c/st7567_i2c.h | 23 +++ esphome/components/st7567_spi/__init__.py | 1 + esphome/components/st7567_spi/display.py | 34 ++++ esphome/components/st7567_spi/st7567_spi.cpp | 66 ++++++++ esphome/components/st7567_spi/st7567_spi.h | 29 ++++ tests/test1.yaml | 19 +++ 13 files changed, 572 insertions(+) create mode 100644 esphome/components/st7567_base/__init__.py create mode 100644 esphome/components/st7567_base/st7567_base.cpp create mode 100644 esphome/components/st7567_base/st7567_base.h create mode 100644 esphome/components/st7567_i2c/__init__.py create mode 100644 esphome/components/st7567_i2c/display.py create mode 100644 esphome/components/st7567_i2c/st7567_i2c.cpp create mode 100644 esphome/components/st7567_i2c/st7567_i2c.h create mode 100644 esphome/components/st7567_spi/__init__.py create mode 100644 esphome/components/st7567_spi/display.py create mode 100644 esphome/components/st7567_spi/st7567_spi.cpp create mode 100644 esphome/components/st7567_spi/st7567_spi.h diff --git a/CODEOWNERS b/CODEOWNERS index b8c870f20f..dff5d8beb5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -316,6 +316,9 @@ esphome/components/ssd1331_base/* @kbx81 esphome/components/ssd1331_spi/* @kbx81 esphome/components/ssd1351_base/* @kbx81 esphome/components/ssd1351_spi/* @kbx81 +esphome/components/st7567_base/* @latonita +esphome/components/st7567_i2c/* @latonita +esphome/components/st7567_spi/* @latonita esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 esphome/components/st7920/* @marsjan155 diff --git a/esphome/components/st7567_base/__init__.py b/esphome/components/st7567_base/__init__.py new file mode 100644 index 0000000000..462d7be6a1 --- /dev/null +++ b/esphome/components/st7567_base/__init__.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import ( + CONF_LAMBDA, + CONF_RESET_PIN, +) + +CODEOWNERS = ["@latonita"] + +st7567_base_ns = cg.esphome_ns.namespace("st7567_base") +ST7567 = st7567_base_ns.class_("ST7567", cg.PollingComponent, display.DisplayBuffer) +ST7567Model = st7567_base_ns.enum("ST7567Model") + +# todo in future: reuse following constants from const.py when they are released +CONF_INVERT_COLORS = "invert_colors" +CONF_TRANSFORM = "transform" +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" + + +ST7567_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + } +).extend(cv.polling_component_schema("1s")) + + +async def setup_st7567(var, config): + await display.register_display(var, config) + + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + cg.add(var.init_invert_colors(config[CONF_INVERT_COLORS])) + + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.init_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.init_mirror_y(transform[CONF_MIRROR_Y])) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/st7567_base/st7567_base.cpp b/esphome/components/st7567_base/st7567_base.cpp new file mode 100644 index 0000000000..b22a7d7fd5 --- /dev/null +++ b/esphome/components/st7567_base/st7567_base.cpp @@ -0,0 +1,152 @@ +#include "st7567_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace st7567_base { + +static const char *const TAG = "st7567"; + +void ST7567::setup() { + this->init_internal_(this->get_buffer_length_()); + this->display_init_(); +} + +void ST7567::display_init_() { + ESP_LOGD(TAG, "Initializing ST7567 display..."); + this->display_init_registers_(); + this->clear(); + this->write_display_data(); + this->command(ST7567_DISPLAY_ON); +} + +void ST7567::display_init_registers_() { + this->command(ST7567_BIAS_9); + this->command(this->mirror_x_ ? ST7567_SEG_REVERSE : ST7567_SEG_NORMAL); + this->command(this->mirror_y_ ? ST7567_COM_NORMAL : ST7567_COM_REMAP); + this->command(ST7567_POWER_CTL | 0x4); + this->command(ST7567_POWER_CTL | 0x6); + this->command(ST7567_POWER_CTL | 0x7); + + this->set_brightness(this->brightness_); + this->set_contrast(this->contrast_); + + this->command(ST7567_INVERT_OFF | this->invert_colors_); + + this->command(ST7567_BOOSTER_ON); + this->command(ST7567_REGULATOR_ON); + this->command(ST7567_POWER_ON); + + this->command(ST7567_SCAN_START_LINE); + this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_); +} + +void ST7567::display_sw_refresh_() { + ESP_LOGD(TAG, "Performing refresh sequence..."); + this->command(ST7567_SW_REFRESH); + this->display_init_registers_(); +} + +void ST7567::request_refresh() { + // as per datasheet: It is recommended to use the refresh sequence regularly in a specified interval. + this->refresh_requested_ = true; +} + +void ST7567::update() { + this->do_update_(); + if (this->refresh_requested_) { + this->refresh_requested_ = false; + this->display_sw_refresh_(); + } + this->write_display_data(); +} + +void ST7567::set_all_pixels_on(bool enable) { + this->all_pixels_on_ = enable; + this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_); +} + +void ST7567::set_invert_colors(bool invert_colors) { + this->invert_colors_ = invert_colors; + this->command(ST7567_INVERT_OFF | this->invert_colors_); +} + +void ST7567::set_contrast(uint8_t val) { + this->contrast_ = val & 0b111111; + // 0..63, 26 is normal + + // two byte command + // first byte 0x81 + // second byte 0-63 + + this->command(ST7567_SET_EV_CMD); + this->command(this->contrast_); +} + +void ST7567::set_brightness(uint8_t val) { + this->brightness_ = val & 0b111; + // 0..7, 5 normal + + //********Adjust display brightness******** + // 0x20-0x27 is the internal Rb/Ra resistance + // adjustment setting of V5 voltage RR=4.5V + + this->command(ST7567_RESISTOR_RATIO | this->brightness_); +} + +bool ST7567::is_on() { return this->is_on_; } + +void ST7567::turn_on() { + this->command(ST7567_DISPLAY_ON); + this->is_on_ = true; +} + +void ST7567::turn_off() { + this->command(ST7567_DISPLAY_OFF); + this->is_on_ = false; +} + +void ST7567::set_scroll(uint8_t line) { this->start_line_ = line % this->get_height_internal(); } + +int ST7567::get_width_internal() { return 128; } + +int ST7567::get_height_internal() { return 64; } + +// 128x64, but memory size 132x64, line starts from 0, but if mirrored then it starts from 131, not 127 +size_t ST7567::get_buffer_length_() { + return size_t(this->get_width_internal() + 4) * size_t(this->get_height_internal()) / 8u; +} + +void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + + uint16_t pos = x + (y / 8) * this->get_width_internal(); + uint8_t subpos = y & 0x07; + if (color.is_on()) { + this->buffer_[pos] |= (1 << subpos); + } else { + this->buffer_[pos] &= ~(1 << subpos); + } +} + +void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } + +void ST7567::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +const char *ST7567::model_str_() { return "ST7567 128x64"; } + +} // namespace st7567_base +} // namespace esphome diff --git a/esphome/components/st7567_base/st7567_base.h b/esphome/components/st7567_base/st7567_base.h new file mode 100644 index 0000000000..e3053673d1 --- /dev/null +++ b/esphome/components/st7567_base/st7567_base.h @@ -0,0 +1,100 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7567_base { + +static const uint8_t ST7567_BOOSTER_ON = 0x2C; // internal power supply on +static const uint8_t ST7567_REGULATOR_ON = 0x2E; // internal power supply on +static const uint8_t ST7567_POWER_ON = 0x2F; // internal power supply on + +static const uint8_t ST7567_DISPLAY_ON = 0xAF; // Display ON. Normal Display Mode. +static const uint8_t ST7567_DISPLAY_OFF = 0xAE; // Display OFF. All SEGs/COMs output with VSS +static const uint8_t ST7567_SET_START_LINE = 0x40; +static const uint8_t ST7567_POWER_CTL = 0x28; +static const uint8_t ST7567_SEG_NORMAL = 0xA0; // +static const uint8_t ST7567_SEG_REVERSE = 0xA1; // mirror X axis (horizontal) +static const uint8_t ST7567_COM_NORMAL = 0xC0; // +static const uint8_t ST7567_COM_REMAP = 0xC8; // mirror Y axis (vertical) +static const uint8_t ST7567_PIXELS_NORMAL = 0xA4; // display ram content +static const uint8_t ST7567_PIXELS_ALL_ON = 0xA5; // all pixels on +static const uint8_t ST7567_INVERT_OFF = 0xA6; // normal pixels +static const uint8_t ST7567_INVERT_ON = 0xA7; // inverted pixels +static const uint8_t ST7567_SCAN_START_LINE = 0x40; // scrolling = 0x40 + (0..63) +static const uint8_t ST7567_COL_ADDR_H = 0x10; // x pos (0..95) 4 MSB +static const uint8_t ST7567_COL_ADDR_L = 0x00; // x pos (0..95) 4 LSB +static const uint8_t ST7567_PAGE_ADDR = 0xB0; // y pos, 8.5 rows (0..8) +static const uint8_t ST7567_BIAS_9 = 0xA2; +static const uint8_t ST7567_CONTRAST = 0x80; // 0x80 + (0..31) +static const uint8_t ST7567_SET_EV_CMD = 0x81; +static const uint8_t ST7567_SET_EV_PARAM = 0x00; +static const uint8_t ST7567_RESISTOR_RATIO = 0x20; +static const uint8_t ST7567_SW_REFRESH = 0xE2; + +class ST7567 : public display::DisplayBuffer { + public: + void setup() override; + + void update() override; + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } + void init_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } + void init_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + + void set_invert_colors(bool invert_colors); // inversion of screen colors + void set_contrast(uint8_t val); // 0..63, 27-30 normal + void set_brightness(uint8_t val); // 0..7, 5 normal + void set_all_pixels_on(bool enable); // turn on all pixels, this doesn't affect RAM + void set_scroll(uint8_t line); // set display start line: for screen scrolling w/o affecting RAM + + bool is_on(); + void turn_on(); + void turn_off(); + + void request_refresh(); // from datasheet: It is recommended to use the refresh sequence regularly in a specified + // interval. + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + + void init_reset_(); + void display_init_(); + void display_init_registers_(); + void display_sw_refresh_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + + int get_offset_x_() { return mirror_x_ ? 4 : 0; }; + + const char *model_str_(); + + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + // float contrast_{1.0}; + // float brightness_{1.0}; + uint8_t contrast_{27}; + uint8_t brightness_{5}; + bool mirror_x_{true}; + bool mirror_y_{true}; + bool invert_colors_{false}; + bool all_pixels_on_{false}; + uint8_t start_line_{0}; + bool refresh_requested_{false}; +}; + +} // namespace st7567_base +} // namespace esphome diff --git a/esphome/components/st7567_i2c/__init__.py b/esphome/components/st7567_i2c/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/st7567_i2c/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/st7567_i2c/display.py b/esphome/components/st7567_i2c/display.py new file mode 100644 index 0000000000..fa92d652d7 --- /dev/null +++ b/esphome/components/st7567_i2c/display.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import st7567_base, i2c +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@latonita"] + +AUTO_LOAD = ["st7567_base"] +DEPENDENCIES = ["i2c"] + +st7567_i2c = cg.esphome_ns.namespace("st7567_i2c") +I2CST7567 = st7567_i2c.class_("I2CST7567", st7567_base.ST7567, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + st7567_base.ST7567_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2CST7567), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x3F)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await st7567_base.setup_st7567(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp new file mode 100644 index 0000000000..05173d1be5 --- /dev/null +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -0,0 +1,60 @@ +#include "st7567_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7567_i2c { + +static const char *const TAG = "st7567_i2c"; + +void I2CST7567::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2C ST7567 display..."); + this->init_reset_(); + + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + ST7567::setup(); +} + +void I2CST7567::dump_config() { + LOG_DISPLAY("", "I2CST7567", this); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_)); + ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_)); + ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_)); + LOG_UPDATE_INTERVAL(this); + + if (this->error_code_ == COMMUNICATION_FAILED) { + ESP_LOGE(TAG, "Communication with I2C ST7567 failed!"); + } +} + +void I2CST7567::command(uint8_t value) { this->write_byte(0x00, value); } + +void HOT I2CST7567::write_display_data() { + // ST7567A has built-in RAM with 132x65 bit capacity which stores the display data. + // but only first 128 pixels from each line are shown on screen + // if screen got flipped horizontally then it shows last 128 pixels, + // so we need to write x coordinate starting from column 4, not column 0 + this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_); + for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { + this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page + this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address + this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address + + static const size_t BLOCK_SIZE = 64; + for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) { + this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x], + this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x, + true); + } + } +} + +} // namespace st7567_i2c +} // namespace esphome diff --git a/esphome/components/st7567_i2c/st7567_i2c.h b/esphome/components/st7567_i2c/st7567_i2c.h new file mode 100644 index 0000000000..f760a54945 --- /dev/null +++ b/esphome/components/st7567_i2c/st7567_i2c.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/st7567_base/st7567_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace st7567_i2c { + +class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + protected: + void command(uint8_t value) override; + void write_display_data() override; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; +}; + +} // namespace st7567_i2c +} // namespace esphome diff --git a/esphome/components/st7567_spi/__init__.py b/esphome/components/st7567_spi/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/st7567_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/st7567_spi/display.py b/esphome/components/st7567_spi/display.py new file mode 100644 index 0000000000..aabe02a2d8 --- /dev/null +++ b/esphome/components/st7567_spi/display.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, st7567_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@latonita"] + +AUTO_LOAD = ["st7567_base"] +DEPENDENCIES = ["spi"] + +st7567_spi = cg.esphome_ns.namespace("st7567_spi") +SPIST7567 = st7567_spi.class_("SPIST7567", st7567_base.ST7567, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + st7567_base.ST7567_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPIST7567), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await st7567_base.setup_st7567(var, config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/st7567_spi/st7567_spi.cpp b/esphome/components/st7567_spi/st7567_spi.cpp new file mode 100644 index 0000000000..25698d0dd0 --- /dev/null +++ b/esphome/components/st7567_spi/st7567_spi.cpp @@ -0,0 +1,66 @@ +#include "st7567_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7567_spi { + +static const char *const TAG = "st7567_spi"; + +void SPIST7567::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI ST7567 display..."); + this->spi_setup(); + this->dc_pin_->setup(); + if (this->cs_) + this->cs_->setup(); + + this->init_reset_(); + ST7567::setup(); +} + +void SPIST7567::dump_config() { + LOG_DISPLAY("", "SPI ST7567", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_)); + ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_)); + ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_)); + LOG_UPDATE_INTERVAL(this); +} + +void SPIST7567::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +void HOT SPIST7567::write_display_data() { + // ST7567A has built-in RAM with 132x65 bit capacity which stores the display data. + // but only first 128 pixels from each line are shown on screen + // if screen got flipped horizontally then it shows last 128 pixels, + // so we need to write x coordinate starting from column 4, not column 0 + this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_); + for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { + this->dc_pin_->digital_write(false); + this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page + this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address + this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address + this->dc_pin_->digital_write(true); + + this->enable(); + this->write_array(&this->buffer_[y * this->get_width_internal()], this->get_width_internal()); + this->disable(); + } +} + +} // namespace st7567_spi +} // namespace esphome diff --git a/esphome/components/st7567_spi/st7567_spi.h b/esphome/components/st7567_spi/st7567_spi.h new file mode 100644 index 0000000000..e8d1ebe0cc --- /dev/null +++ b/esphome/components/st7567_spi/st7567_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/st7567_base/st7567_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace st7567_spi { + +class SPIST7567 : public st7567_base::ST7567, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace st7567_spi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 720d4e8e82..7046ac8139 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3259,6 +3259,25 @@ display: inverted: true lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7567_i2c + id: st7735_display_i2c + address: 0x3F + i2c_id: i2c_bus + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7567_spi + id: st7735_display_spi + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO16 + reset_pin: + allow_other_uses: true + number: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 id: st7735_display model: INITR_BLACKTAB From d4d49e38fc4cb34794468bd2e2514d1f55154ea5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Dec 2023 17:51:00 -1000 Subject: [PATCH 239/436] Fix device not requesting Home Assistant time at the update interval (#6022) --- esphome/components/api/api_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 0348112fcd..8df860bb09 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -319,7 +319,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED) + if (!client->remove_ && client->is_authenticated()) client->send_time_request(); } } From 21ec42f495b33c9b2957b234581f5b19148b4eb6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:00:19 +1100 Subject: [PATCH 240/436] Add constants used by multiple display drivers to global const.py (#6033) * Add constants used by multiple display drivers to global const.py * Add further constants * Refactor st7789v and st7735v --- esphome/components/ili9xxx/display.py | 16 ++++++++-------- esphome/components/st7567_base/__init__.py | 8 ++++---- esphome/components/st7735/display.py | 2 +- esphome/components/st7789v/display.py | 4 ++-- esphome/components/touchscreen/__init__.py | 14 ++++++++------ esphome/const.py | 8 ++++++++ 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index cd68f1ae27..b3fe8b2b41 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -17,6 +17,14 @@ from esphome.const import ( CONF_WIDTH, CONF_HEIGHT, CONF_ROTATION, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_SWAP_XY, + CONF_COLOR_ORDER, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_TRANSFORM, + CONF_INVERT_COLORS, ) DEPENDENCIES = ["spi"] @@ -70,14 +78,6 @@ COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" CONF_INVERT_DISPLAY = "invert_display" -CONF_INVERT_COLORS = "invert_colors" -CONF_MIRROR_X = "mirror_x" -CONF_MIRROR_Y = "mirror_y" -CONF_SWAP_XY = "swap_xy" -CONF_COLOR_ORDER = "color_order" -CONF_OFFSET_HEIGHT = "offset_height" -CONF_OFFSET_WIDTH = "offset_width" -CONF_TRANSFORM = "transform" def _validate(config): diff --git a/esphome/components/st7567_base/__init__.py b/esphome/components/st7567_base/__init__.py index 462d7be6a1..7ce50fd99f 100644 --- a/esphome/components/st7567_base/__init__.py +++ b/esphome/components/st7567_base/__init__.py @@ -5,6 +5,10 @@ from esphome.components import display from esphome.const import ( CONF_LAMBDA, CONF_RESET_PIN, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_TRANSFORM, + CONF_INVERT_COLORS, ) CODEOWNERS = ["@latonita"] @@ -14,10 +18,6 @@ ST7567 = st7567_base_ns.class_("ST7567", cg.PollingComponent, display.DisplayBuf ST7567Model = st7567_base_ns.enum("ST7567Model") # todo in future: reuse following constants from const.py when they are released -CONF_INVERT_COLORS = "invert_colors" -CONF_TRANSFORM = "transform" -CONF_MIRROR_X = "mirror_x" -CONF_MIRROR_Y = "mirror_y" ST7567_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index 4ff5cafaf8..d5bb2fa3d6 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_MODEL, CONF_RESET_PIN, CONF_PAGES, + CONF_INVERT_COLORS, ) from . import st7735_ns @@ -23,7 +24,6 @@ CONF_ROW_START = "row_start" CONF_COL_START = "col_start" CONF_EIGHT_BIT_COLOR = "eight_bit_color" CONF_USE_BGR = "use_bgr" -CONF_INVERT_COLORS = "invert_colors" SPIST7735 = st7735_ns.class_( "ST7735", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 5b2b5a126c..04dce2cf6c 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -14,12 +14,12 @@ from esphome.const import ( CONF_POWER_SUPPLY, CONF_ROTATION, CONF_CS_PIN, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, ) from . import st7789v_ns CONF_EIGHTBITCOLOR = "eightbitcolor" -CONF_OFFSET_HEIGHT = "offset_height" -CONF_OFFSET_WIDTH = "offset_width" CODEOWNERS = ["@kbx81"] diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 99aee5c9fb..c4945617f9 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -3,7 +3,14 @@ import esphome.codegen as cg from esphome.components import display from esphome import automation -from esphome.const import CONF_ON_TOUCH, CONF_ON_RELEASE +from esphome.const import ( + CONF_ON_TOUCH, + CONF_ON_RELEASE, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_SWAP_XY, + CONF_TRANSFORM, +) from esphome.core import coroutine_with_priority CODEOWNERS = ["@jesserockz", "@nielsnl68"] @@ -26,11 +33,6 @@ CONF_REPORT_INTERVAL = "report_interval" # not used yet: CONF_ON_UPDATE = "on_update" CONF_TOUCH_TIMEOUT = "touch_timeout" -CONF_MIRROR_X = "mirror_x" -CONF_MIRROR_Y = "mirror_y" -CONF_SWAP_XY = "swap_xy" -CONF_TRANSFORM = "transform" - def touchscreen_schema(default_touch_timeout): return cv.Schema( diff --git a/esphome/const.py b/esphome/const.py index 4a4a59f659..7e27254d76 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -127,6 +127,7 @@ CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_INTERLOCK = "color_interlock" CONF_COLOR_MODE = "color_mode" +CONF_COLOR_ORDER = "color_order" CONF_COLOR_PALETTE = "color_palette" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" @@ -370,6 +371,7 @@ CONF_INTERRUPT_PIN = "interrupt_pin" CONF_INTERVAL = "interval" CONF_INVALID_COOLDOWN = "invalid_cooldown" CONF_INVERT = "invert" +CONF_INVERT_COLORS = "invert_colors" CONF_INVERTED = "inverted" CONF_IP_ADDRESS = "ip_address" CONF_IRQ_PIN = "irq_pin" @@ -454,6 +456,8 @@ CONF_MIN_VALUE = "min_value" CONF_MIN_VERSION = "min_version" CONF_MINUTE = "minute" CONF_MINUTES = "minutes" +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" CONF_MISO_PIN = "miso_pin" CONF_MODE = "mode" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" @@ -485,6 +489,8 @@ CONF_NUMBER_DATAPOINT = "number_datapoint" CONF_OFF_MODE = "off_mode" CONF_OFF_SPEED_CYCLE = "off_speed_cycle" CONF_OFFSET = "offset" +CONF_OFFSET_HEIGHT = "offset_height" +CONF_OFFSET_WIDTH = "offset_width" CONF_ON = "on" CONF_ON_BLE_ADVERTISE = "on_ble_advertise" CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise" @@ -749,6 +755,7 @@ CONF_SUPPORTED_PRESETS = "supported_presets" CONF_SUPPORTED_SWING_MODES = "supported_swing_modes" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" +CONF_SWAP_XY = "swap_xy" CONF_SWING_BOTH_ACTION = "swing_both_action" CONF_SWING_HORIZONTAL_ACTION = "swing_horizontal_action" CONF_SWING_MODE = "swing_mode" @@ -799,6 +806,7 @@ CONF_TOPIC_PREFIX = "topic_prefix" CONF_TOTAL = "total" CONF_TOTAL_POWER = "total_power" CONF_TRACES = "traces" +CONF_TRANSFORM = "transform" CONF_TRANSITION_LENGTH = "transition_length" CONF_TRIGGER_ID = "trigger_id" CONF_TRIGGER_PIN = "trigger_pin" From d3567f9ac6ad079fb59b69b2ef03b7d0eb1660f9 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 29 Dec 2023 06:15:06 +0100 Subject: [PATCH 241/436] Nextion queue size (#6029) * Nextion `queue_size` function Returns the size of Nextion queue. For troubleshooting only. * Move `queue_size` to `nextion.h` This is where the queue is * Inline doc * clang-format --- esphome/components/nextion/nextion.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index acbf394fc6..2f52a032c4 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -960,6 +960,21 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe this->exit_reparse_on_start_ = exit_reparse_on_start; } + /** + * @brief Retrieves the number of commands pending in the Nextion command queue. + * + * This function returns the current count of commands that have been queued but not yet processed + * for the Nextion display. The Nextion command queue is used to store commands that are sent to + * the Nextion display for various operations like updating the display, changing interface elements, + * or other interactive features. A larger queue size might indicate a higher processing time or potential + * delays in command execution. This function is useful for monitoring the command flow and managing + * the execution efficiency of the Nextion display interface. + * + * @return size_t The number of commands currently in the Nextion queue. This count includes all commands + * that have been added to the queue and are awaiting processing. + */ + size_t queue_size() { return this->nextion_queue_.size(); } + protected: std::deque nextion_queue_; std::deque waveform_queue_; From 5ebb68f4ffde847b3ac313d941f784ec4b0a4d44 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Dec 2023 18:35:44 +1100 Subject: [PATCH 242/436] Ble client additions and fixes (#5277) * Add config to disable auto-connect of BLE client. Correct initialise MAC address of BLE client. * Checkpont * Fixes for automation progress. * Fixes for automation progress. * Checkpoint; fix notify for ble_client * Fix BLE client binary_output * Various fixes * Consider notifications on when receiving REG_FOR event. * Add testing branch to workflow * Add workflow * CI changes * CI changes * CI clang * CI changes * CI changes * Add comment about logging macros * Add test, sanitise comment * Revert testing change to ci config * Update codeowners * Revert ci config change * Fix some state changes * Add default case. * Minor fixes * Add auto-connect to logconfig --- CODEOWNERS | 2 +- esphome/components/ble_client/__init__.py | 34 ++- esphome/components/ble_client/automation.cpp | 68 +---- esphome/components/ble_client/automation.h | 248 ++++++++++++++---- esphome/components/ble_client/ble_client.cpp | 22 +- .../ble_client/output/ble_binary_output.cpp | 62 ++--- .../ble_client/output/ble_binary_output.h | 4 +- .../components/ble_client/sensor/automation.h | 16 +- .../ble_client/sensor/ble_rssi_sensor.cpp | 13 +- .../ble_client/sensor/ble_sensor.cpp | 20 +- .../ble_client/switch/ble_switch.cpp | 7 +- .../text_sensor/ble_text_sensor.cpp | 18 +- .../esp32_ble_client/ble_client_base.cpp | 134 +++++++--- .../esp32_ble_client/ble_client_base.h | 18 +- tests/test1.yaml | 12 + 15 files changed, 443 insertions(+), 235 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index dff5d8beb5..c655f94a1b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,7 +52,7 @@ esphome/components/bk72xx/* @kuba2k2 esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- esphome/components/bl0942/* @dbuezas -esphome/components/ble_client/* @buxtronix +esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme680_bsec/* @trvrnrth esphome/components/bmi160/* @flaviut diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 8f70ad3417..34b9868edc 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.automation import maybe_simple_id from esphome.components import esp32_ble_tracker, esp32_ble_client from esphome.const import ( CONF_CHARACTERISTIC_UUID, @@ -15,7 +16,7 @@ from esphome.const import ( from esphome import automation AUTO_LOAD = ["esp32_ble_client"] -CODEOWNERS = ["@buxtronix"] +CODEOWNERS = ["@buxtronix", "@clydebarrow"] DEPENDENCIES = ["esp32_ble_tracker"] ble_client_ns = cg.esphome_ns.namespace("ble_client") @@ -43,6 +44,10 @@ BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_( # Actions BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) +BLEConnectAction = ble_client_ns.class_("BLEClientConnectAction", automation.Action) +BLEDisconnectAction = ble_client_ns.class_( + "BLEClientDisconnectAction", automation.Action +) BLEPasskeyReplyAction = ble_client_ns.class_( "BLEClientPasskeyReplyAction", automation.Action ) @@ -58,6 +63,7 @@ CONF_ACCEPT = "accept" CONF_ON_PASSKEY_REQUEST = "on_passkey_request" CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification" CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request" +CONF_AUTO_CONNECT = "auto_connect" # Espressif platformio framework is built with MAX_BLE_CONN to 3, so # enforce this in yaml checks. @@ -69,6 +75,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(BLEClient), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_NAME): cv.string, + cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean, cv.Optional(CONF_ON_CONNECT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -135,6 +142,12 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema( } ) +BLE_CONNECT_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(CONF_ID): cv.use_id(BLEClient), + } +) + BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.use_id(BLEClient), @@ -157,6 +170,24 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( ) +@automation.register_action( + "ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA +) +async def ble_disconnect_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + return var + + +@automation.register_action( + "ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA +) +async def ble_connect_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + return var + + @automation.register_action( "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA ) @@ -261,6 +292,7 @@ async def to_code(config): await cg.register_component(var, config) await esp32_ble_tracker.register_client(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_auto_connect(config[CONF_AUTO_CONNECT])) for conf in config.get(CONF_ON_CONNECT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 429f906a5f..9a0233eb70 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -2,76 +2,10 @@ #include "automation.h" -#include -#include -#include - -#include "esphome/core/log.h" - namespace esphome { namespace ble_client { -static const char *const TAG = "ble_client.automation"; -void BLEWriterClientNode::write(const std::vector &value) { - if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected"); - return; - } else if (this->ble_char_handle_ == 0) { - ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found"); - return; - } - esp_gatt_write_type_t write_type; - if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { - write_type = ESP_GATT_WRITE_TYPE_RSP; - ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); - } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { - write_type = ESP_GATT_WRITE_TYPE_NO_RSP; - ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); - } else { - ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str()); - return; - } - ESP_LOGVV(TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); - esp_err_t err = - esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->ble_char_handle_, - value.size(), const_cast(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); - } -} - -void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - switch (event) { - case ESP_GATTC_REG_EVT: - break; - case ESP_GATTC_OPEN_EVT: - this->node_state = espbt::ClientState::ESTABLISHED; - ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str()); - break; - case ESP_GATTC_SEARCH_CMPL_EVT: { - auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); - if (chr == nullptr) { - ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s", - this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str()); - break; - } - this->ble_char_handle_ = chr->handle; - this->char_props_ = chr->properties; - this->node_state = espbt::ClientState::ESTABLISHED; - ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - ble_client_->address_str().c_str()); - break; - } - case ESP_GATTC_DISCONNECT_EVT: - this->node_state = espbt::ClientState::IDLE; - this->ble_char_handle_ = 0; - ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str()); - break; - default: - break; - } -} +const char *const Automation::TAG = "ble_client.automation"; } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 423f74b85a..a5c661e2f5 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -7,9 +7,19 @@ #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" +#include "esphome/core/log.h" namespace esphome { namespace ble_client { + +// placeholder class for static TAG . +class Automation { + public: + // could be made inline with C++17 + static const char *const TAG; +}; + +// implement on_connect automation. class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { public: explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } @@ -23,17 +33,28 @@ class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { } }; +// on_disconnect automation class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { public: explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { - if (event == ESP_GATTC_DISCONNECT_EVT && - memcmp(param->disconnect.remote_bda, this->parent_->get_remote_bda(), 6) == 0) - this->trigger(); - if (event == ESP_GATTC_SEARCH_CMPL_EVT) - this->node_state = espbt::ClientState::ESTABLISHED; + // test for CLOSE and not DISCONNECT - DISCONNECT can occur even if no virtual connection (OPEN event) occurred. + // So this will not trigger unless a complete open has previously succeeded. + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_CLOSE_EVT: { + this->trigger(); + break; + } + default: { + break; + } + } } }; @@ -42,10 +63,8 @@ class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode { explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { - if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && - memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) this->trigger(); - } } }; @@ -54,10 +73,8 @@ class BLEClientPasskeyNotificationTrigger : public Trigger, public BLE explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { - if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && - memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { - uint32_t passkey = param->ble_security.key_notif.passkey; - this->trigger(passkey); + if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) { + this->trigger(param->ble_security.key_notif.passkey); } } }; @@ -67,24 +84,20 @@ class BLEClientNumericComparisonRequestTrigger : public Trigger, publi explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { - if (event == ESP_GAP_BLE_NC_REQ_EVT && - memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { - uint32_t passkey = param->ble_security.key_notif.passkey; - this->trigger(passkey); + if (event == ESP_GAP_BLE_NC_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) { + this->trigger(param->ble_security.key_notif.passkey); } } }; -class BLEWriterClientNode : public BLEClientNode { +// implement the ble_client.ble_write action. +template class BLEClientWriteAction : public Action, public BLEClientNode { public: - BLEWriterClientNode(BLEClient *ble_client) { + BLEClientWriteAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; } - // Attempts to write the contents of value to char_uuid_. - void write(const std::vector &value); - void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } @@ -93,29 +106,6 @@ class BLEWriterClientNode : public BLEClientNode { void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - - private: - BLEClient *ble_client_; - int ble_char_handle_ = 0; - esp_gatt_char_prop_t char_props_; - espbt::ESPBTUUID service_uuid_; - espbt::ESPBTUUID char_uuid_; -}; - -template class BLEClientWriteAction : public Action, public BLEWriterClientNode { - public: - BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {} - - void play(Ts... x) override { - if (has_simple_value_) { - return write(this->value_simple_); - } else { - return write(this->value_template_(x...)); - } - } - void set_value_template(std::function(Ts...)> func) { this->value_template_ = std::move(func); has_simple_value_ = false; @@ -126,10 +116,94 @@ template class BLEClientWriteAction : public Action, publ has_simple_value_ = true; } + void play(Ts... x) override {} + + void play_complex(Ts... x) override { + this->num_running_++; + this->var_ = std::make_tuple(x...); + auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); + // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. + if (!write(value)) + this->play_next_(x...); + } + + /** + * Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG + * macros in header files (Can't even write it in a comment!) + * Not sure why, because they seem to work just fine. + * The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than + * 17, so the methods have to be here. The esph_log_X macros are equivalent in function, but don't trigger the CI + * errors. + */ + // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. + bool write(const std::vector &value) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); + return false; + } + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); + esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), + this->char_handle_, value.size(), const_cast(value.data()), + this->write_type_, ESP_GATT_AUTH_REQ_NONE); + if (err != ESP_OK) { + esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); + return false; + } + return true; + } + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + switch (event) { + case ESP_GATTC_WRITE_CHAR_EVT: + // upstream code checked the MAC address, verify the characteristic. + if (param->write.handle == this->char_handle_) + this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); + break; + case ESP_GATTC_DISCONNECT_EVT: + if (this->num_running_ != 0) + this->stop_complex(); + break; + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + esph_log_w("ble_write_action", "Characteristic %s was not found in service %s", + this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str()); + break; + } + this->char_handle_ = chr->handle; + this->char_props_ = chr->properties; + if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { + this->write_type_ = ESP_GATT_WRITE_TYPE_RSP; + esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); + } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { + this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP; + esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); + } else { + esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str()); + break; + } + this->node_state = espbt::ClientState::ESTABLISHED; + esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), + ble_client_->address_str().c_str()); + break; + } + default: + break; + } + } + private: + BLEClient *ble_client_; bool has_simple_value_ = true; std::vector value_simple_; std::function(Ts...)> value_template_{}; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + std::tuple var_{}; + uint16_t char_handle_{}; + esp_gatt_char_prop_t char_props_{}; + esp_gatt_write_type_t write_type_{}; }; template class BLEClientPasskeyReplyAction : public Action { @@ -212,6 +286,92 @@ template class BLEClientRemoveBondAction : public Action BLEClient *parent_{nullptr}; }; +template class BLEClientConnectAction : public Action, public BLEClientNode { + public: + BLEClientConnectAction(BLEClient *ble_client) { + ble_client->register_ble_node(this); + ble_client_ = ble_client; + } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + if (this->num_running_ == 0) + return; + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); + break; + // if the connection is closed, terminate the automation chain. + case ESP_GATTC_DISCONNECT_EVT: + this->stop_complex(); + break; + default: + break; + } + } + + // not used since we override play_complex_ + void play(Ts... x) override {} + + void play_complex(Ts... x) override { + // it makes no sense to have multiple instances of this running at the same time. + // this would occur only if the same automation was re-triggered while still + // running. So just cancel the second chain if this is detected. + if (this->num_running_ != 0) { + this->stop_complex(); + return; + } + this->num_running_++; + if (this->node_state == espbt::ClientState::ESTABLISHED) { + this->play_next_(x...); + } else { + this->var_ = std::make_tuple(x...); + this->ble_client_->connect(); + } + } + + private: + BLEClient *ble_client_; + std::tuple var_{}; +}; + +template class BLEClientDisconnectAction : public Action, public BLEClientNode { + public: + BLEClientDisconnectAction(BLEClient *ble_client) { + ble_client->register_ble_node(this); + ble_client_ = ble_client; + } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + if (this->num_running_ == 0) + return; + switch (event) { + case ESP_GATTC_CLOSE_EVT: + case ESP_GATTC_DISCONNECT_EVT: + this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); + break; + default: + break; + } + } + + // not used since we override play_complex_ + void play(Ts... x) override {} + + void play_complex(Ts... x) override { + this->num_running_++; + if (this->node_state == espbt::ClientState::IDLE) { + this->play_next_(x...); + } else { + this->var_ = std::make_tuple(x...); + this->ble_client_->disconnect(); + } + } + + private: + BLEClient *ble_client_; + std::tuple var_{}; +}; } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index f3a9f01a1a..19cf2bc1f3 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -26,6 +26,7 @@ void BLEClient::loop() { void BLEClient::dump_config() { ESP_LOGCONFIG(TAG, "BLE Client:"); ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_)); } bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { @@ -37,31 +38,24 @@ bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { void BLEClient::set_enabled(bool enabled) { if (enabled == this->enabled) return; - if (!enabled && this->state() != espbt::ClientState::IDLE) { - ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); - auto ret = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); - if (ret) { - ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); - } - } this->enabled = enabled; + if (!enabled) { + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + this->disconnect(); + } } bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, esp_ble_gattc_cb_param_t *param) { - bool all_established = this->all_nodes_established_(); - if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param)) return false; for (auto *node : this->nodes_) node->gattc_event_handler(event, esp_gattc_if, param); - // Delete characteristics after clients have used them to save RAM. - if (!all_established && this->all_nodes_established_()) { - for (auto &svc : this->services_) - delete svc; // NOLINT(cppcoreguidelines-owning-memory) - this->services_.clear(); + if (!this->services_.empty() && this->all_nodes_established_()) { + this->release_services(); + ESP_LOGD(TAG, "All clients established, services released"); } return true; } diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index 6709803936..3f05bc4b84 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -19,26 +19,36 @@ void BLEBinaryOutput::dump_config() { void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { - case ESP_GATTC_OPEN_EVT: - this->client_state_ = espbt::ClientState::ESTABLISHED; - ESP_LOGW(TAG, "[%s] Connected successfully!", this->char_uuid_.to_string().c_str()); - break; - case ESP_GATTC_DISCONNECT_EVT: - ESP_LOGW(TAG, "[%s] Disconnected", this->char_uuid_.to_string().c_str()); - this->client_state_ = espbt::ClientState::IDLE; - break; - case ESP_GATTC_WRITE_CHAR_EVT: { - if (param->write.status == 0) { - break; - } - + case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] Characteristic not found.", this->char_uuid_.to_string().c_str()); + ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_string().c_str(), + this->service_uuid_.to_string().c_str()); break; } - if (param->write.handle == chr->handle) { - ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status); + this->char_handle_ = chr->handle; + this->char_props_ = chr->properties; + if (this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { + this->write_type_ = ESP_GATT_WRITE_TYPE_RSP; + ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); + } else if (!this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { + this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP; + ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); + } else { + ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_string().c_str(), + this->require_response_ ? "" : "out"); + break; + } + this->node_state = espbt::ClientState::ESTABLISHED; + ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), + this->parent()->address_str().c_str()); + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_WRITE_CHAR_EVT: { + if (param->write.handle == this->char_handle_) { + if (param->write.status != 0) + ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status); } break; } @@ -48,26 +58,18 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i } void BLEBinaryOutput::write_state(bool state) { - if (this->client_state_ != espbt::ClientState::ESTABLISHED) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", this->char_uuid_.to_string().c_str()); return; } - - auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); - if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] Characteristic not found. State update can not be written.", - this->char_uuid_.to_string().c_str()); - return; - } - uint8_t state_as_uint = (uint8_t) state; ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint); - if (this->require_response_) { - chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_RSP); - } else { - chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_NO_RSP); - } + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, + sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE); + if (err != ESP_GATT_OK) + ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err); } } // namespace ble_client diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 83eabcf5f2..0a1e186b26 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -32,7 +32,9 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi bool require_response_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; - espbt::ClientState client_state_; + uint16_t char_handle_{}; + esp_gatt_char_prop_t char_props_{}; + esp_gatt_write_type_t write_type_{}; }; } // namespace ble_client diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index d830165d30..56ab7ba4c9 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -14,15 +14,17 @@ class BLESensorNotifyTrigger : public Trigger, public BLESensor { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { switch (event) { - case ESP_GATTC_SEARCH_CMPL_EVT: { - this->sensor_->node_state = espbt::ClientState::ESTABLISHED; + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle == this->sensor_->handle) + this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); break; } - case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || - param->notify.handle != this->sensor_->handle) - break; - this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + // confirms notifications are being listened for. While enabling of notifications may still be in + // progress by the parent, we assume it will happen. + if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->sensor_->handle) + this->node_state = espbt::ClientState::ESTABLISHED; + break; } default: break; diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index a36e191e32..81d244ce6d 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -22,26 +22,19 @@ void BLEClientRSSISensor::dump_config() { void BLEClientRSSISensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { - case ESP_GATTC_OPEN_EVT: { - if (param->open.status == ESP_GATT_OK) { - ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); - break; - } - break; - } - case ESP_GATTC_DISCONNECT_EVT: { - ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + case ESP_GATTC_CLOSE_EVT: { this->status_set_warning(); this->publish_state(NAN); break; } - case ESP_GATTC_SEARCH_CMPL_EVT: + case ESP_GATTC_SEARCH_CMPL_EVT: { this->node_state = espbt::ClientState::ESTABLISHED; if (this->should_update_) { this->should_update_ = false; this->get_rssi_(); } break; + } default: break; } diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index a05efad60b..43f61f5304 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -33,7 +33,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } break; } - case ESP_GATTC_DISCONNECT_EVT: { + case ESP_GATTC_CLOSE_EVT: { ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); this->status_set_warning(); this->publish_state(NAN); @@ -74,8 +74,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->get_conn_id()) - break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); break; @@ -87,15 +85,23 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) - break; - ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); + if (param->notify.handle != this->handle) + break; this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len)); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->node_state = espbt::ClientState::ESTABLISHED; + if (param->reg_for_notify.handle == this->handle) { + if (param->reg_for_notify.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error registering for notifications at handle %d, status=%d", param->reg_for_notify.handle, + param->reg_for_notify.status); + break; + } + this->node_state = espbt::ClientState::ESTABLISHED; + ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_string().c_str()); + } break; } default: diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp index 6de5252404..9d92b1b2b5 100644 --- a/esphome/components/ble_client/switch/ble_switch.cpp +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -17,14 +17,11 @@ void BLEClientSwitch::write_state(bool state) { void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { - case ESP_GATTC_REG_EVT: + case ESP_GATTC_CLOSE_EVT: this->publish_state(this->parent_->enabled); break; - case ESP_GATTC_OPEN_EVT: + case ESP_GATTC_SEARCH_CMPL_EVT: this->node_state = espbt::ClientState::ESTABLISHED; - break; - case ESP_GATTC_DISCONNECT_EVT: - this->node_state = espbt::ClientState::IDLE; this->publish_state(this->parent_->enabled); break; default: diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index 1a304593c7..33938ee7b7 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -36,8 +36,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } break; } - case ESP_GATTC_DISCONNECT_EVT: { - ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + case ESP_GATTC_CLOSE_EVT: { this->status_set_warning(); this->publish_state(EMPTY); break; @@ -77,20 +76,18 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->get_conn_id()) - break; - if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); - break; - } if (param->read.handle == this->handle) { + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } this->status_clear_warning(); this->publish_state(this->parse_data(param->read.value, param->read.value_len)); } break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) + if (param->notify.handle != this->handle) break; ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); @@ -98,7 +95,8 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->node_state = espbt::ClientState::ESTABLISHED; + if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->handle) + this->node_state = espbt::ClientState::ESTABLISHED; break; } default: diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index cc6d3d7d4d..ae83715aea 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -45,21 +45,19 @@ void BLEClientBase::loop() { float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { + if (!this->auto_connect_) + return false; if (this->address_ == 0 || device.address_uint64() != this->address_) return false; if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) return false; - ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str()); - this->set_state(espbt::ClientState::DISCOVERED); + this->log_event_("Found device"); + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) + esp32_ble_tracker::global_esp32_ble_tracker->print_bt_device_info(device); - auto addr = device.address_uint64(); - this->remote_bda_[0] = (addr >> 40) & 0xFF; - this->remote_bda_[1] = (addr >> 32) & 0xFF; - this->remote_bda_[2] = (addr >> 24) & 0xFF; - this->remote_bda_[3] = (addr >> 16) & 0xFF; - this->remote_bda_[4] = (addr >> 8) & 0xFF; - this->remote_bda_[5] = (addr >> 0) & 0xFF; + this->set_state(espbt::ClientState::DISCOVERED); + this->set_address(device.address_uint64()); this->remote_addr_type_ = device.get_address_type(); return true; } @@ -108,6 +106,10 @@ void BLEClientBase::release_services() { #endif } +void BLEClientBase::log_event_(const char *name) { + ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); +} + bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, esp_ble_gattc_cb_param_t *param) { if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) @@ -131,51 +133,73 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_OPEN_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str()); + if (!this->check_addr(param->open.remote_bda)) + return false; + this->log_event_("ESP_GATTC_OPEN_EVT"); this->conn_id_ = param->open.conn_id; this->service_count_ = 0; if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), param->open.status); this->set_state(espbt::ClientState::IDLE); - break; + return false; } auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); if (ret) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, this->address_str_.c_str(), ret); } + this->set_state(espbt::ClientState::CONNECTED); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); - this->set_state(espbt::ClientState::CONNECTED); + // only set our state, subclients might have more stuff to do yet. this->state_ = espbt::ClientState::ESTABLISHED; break; } esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } - case ESP_GATTC_CFG_MTU_EVT: { - if (param->cfg_mtu.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, - this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); - this->set_state(espbt::ClientState::IDLE); - break; - } - ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), - param->cfg_mtu.status, param->cfg_mtu.mtu); - this->mtu_ = param->cfg_mtu.mtu; + case ESP_GATTC_CONNECT_EVT: { + if (!this->check_addr(param->connect.remote_bda)) + return false; + this->log_event_("ESP_GATTC_CONNECT_EVT"); break; } case ESP_GATTC_DISCONNECT_EVT: { - if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0) + if (!this->check_addr(param->disconnect.remote_bda)) return false; - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, this->address_str_.c_str(), param->disconnect.reason); this->release_services(); this->set_state(espbt::ClientState::IDLE); break; } + + case ESP_GATTC_CFG_MTU_EVT: { + if (this->conn_id_ != param->cfg_mtu.conn_id) + return false; + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, + this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); + // No state change required here - disconnect event will follow if needed. + break; + } + ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), + param->cfg_mtu.status, param->cfg_mtu.mtu); + this->mtu_ = param->cfg_mtu.mtu; + break; + } + case ESP_GATTC_CLOSE_EVT: { + if (this->conn_id_ != param->close.conn_id) + return false; + this->log_event_("ESP_GATTC_CLOSE_EVT"); + this->release_services(); + this->set_state(espbt::ClientState::IDLE); + break; + } case ESP_GATTC_SEARCH_RES_EVT: { + if (this->conn_id_ != param->search_res.conn_id) + return false; this->service_count_++; if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // V3 clients don't need services initialized since @@ -191,7 +215,9 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_SEARCH_CMPL_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str()); + if (this->conn_id_ != param->search_cmpl.conn_id) + return false; + this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT"); for (auto &svc : this->services_) { ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), svc->uuid.to_string().c_str()); @@ -199,11 +225,41 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->address_str_.c_str(), svc->start_handle, svc->end_handle); } ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); - this->set_state(espbt::ClientState::CONNECTED); this->state_ = espbt::ClientState::ESTABLISHED; break; } + case ESP_GATTC_READ_DESCR_EVT: { + if (this->conn_id_ != param->write.conn_id) + return false; + this->log_event_("ESP_GATTC_READ_DESCR_EVT"); + break; + } + case ESP_GATTC_WRITE_DESCR_EVT: { + if (this->conn_id_ != param->write.conn_id) + return false; + this->log_event_("ESP_GATTC_WRITE_DESCR_EVT"); + break; + } + case ESP_GATTC_WRITE_CHAR_EVT: { + if (this->conn_id_ != param->write.conn_id) + return false; + this->log_event_("ESP_GATTC_WRITE_CHAR_EVT"); + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (this->conn_id_ != param->read.conn_id) + return false; + this->log_event_("ESP_GATTC_READ_CHAR_EVT"); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (this->conn_id_ != param->notify.conn_id) + return false; + this->log_event_("ESP_GATTC_NOTIFY_EVT"); + break; + } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT"); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // Client is responsible for flipping the descriptor value @@ -212,9 +268,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } esp_gattc_descr_elem_t desc_result; uint16_t count = 1; - esp_gatt_status_t descr_status = - esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, - NOTIFY_DESC_UUID, &desc_result, &count); + esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle( + this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count); if (descr_status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, this->address_str_.c_str(), descr_status); @@ -222,7 +277,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } esp_gattc_char_elem_t char_result; esp_gatt_status_t char_status = - esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, + esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, param->reg_for_notify.handle, &char_result, &count, 0); if (char_status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, @@ -238,6 +293,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_err_t status = esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); if (status) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, this->address_str_.c_str(), status); @@ -246,24 +302,31 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } default: + // ideally would check all other events for matching conn_id + ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event); break; } return true; } +// clients can't call defer() directly since it's protected. +void BLEClientBase::run_later(std::function &&f) { // NOLINT + this->defer(std::move(f)); +} + void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { // This event is sent by the server when it requests security case ESP_GAP_BLE_SEC_REQ_EVT: - if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) - break; + if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr)) + return; ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); break; // This event is sent once authentication has completed case ESP_GAP_BLE_AUTH_CMPL_EVT: - if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) - break; + if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr)) + return; esp_bd_addr_t bd_addr; memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), @@ -273,11 +336,12 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ param->ble_security.auth_cmpl.fail_reason); } else { this->paired_ = true; - ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, + ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); } break; + // There are other events we'll want to implement at some point to support things like pass key // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md default: diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 97886d0b19..fd586e59d6 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -27,6 +27,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void loop() override; float get_setup_priority() const override; + void run_later(std::function &&f); // NOLINT bool parse_device(const espbt::ESPBTDevice &device) override; void on_scan_end() override {} bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -39,10 +40,17 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; } + void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } + void set_address(uint64_t address) { this->address_ = address; + this->remote_bda_[0] = (address >> 40) & 0xFF; + this->remote_bda_[1] = (address >> 32) & 0xFF; + this->remote_bda_[2] = (address >> 24) & 0xFF; + this->remote_bda_[3] = (address >> 16) & 0xFF; + this->remote_bda_[4] = (address >> 8) & 0xFF; + this->remote_bda_[5] = (address >> 0) & 0xFF; if (address == 0) { - memset(this->remote_bda_, 0, sizeof(this->remote_bda_)); this->address_str_ = ""; } else { this->address_str_ = @@ -79,20 +87,24 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; } + bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; } + protected: int gattc_if_; esp_bd_addr_t remote_bda_; - esp_ble_addr_type_t remote_addr_type_; + esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; uint16_t conn_id_{0xFFFF}; uint64_t address_{0}; + bool auto_connect_{false}; std::string address_str_{}; uint8_t connection_index_; int16_t service_count_{0}; uint16_t mtu_{23}; bool paired_{false}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; - std::vector services_; + + void log_event_(const char *name); }; } // namespace esp32_ble_client diff --git a/tests/test1.yaml b/tests/test1.yaml index 7046ac8139..bc7a94bc5a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -358,8 +358,10 @@ esp32_ble_tracker: ble_client: - mac_address: AA:BB:CC:DD:EE:FF id: ble_foo + auto_connect: true - mac_address: 11:22:33:44:55:66 id: ble_blah + auto_connect: false on_connect: then: - switch.turn_on: ble1_status @@ -3026,6 +3028,16 @@ interval: page_id: page1 then: - logger.log: Seeing page 1 + - interval: 60min + then: + - ble_client.connect: ble_blah + - ble_client.ble_write: + id: ble_blah + service_uuid: EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6 + characteristic_uuid: EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6 + value: !lambda |- + return {1, 0}; + - ble_client.disconnect: ble_blah color: - id: kbx_red From 2a43e55452b49f766d1cbbb00a7ff650a2aba1de Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Fri, 29 Dec 2023 22:08:26 +0100 Subject: [PATCH 243/436] HaierProtocol library updated to 0.9.25 to fix the answer_timeout bug (#6015) --- esphome/components/haier/climate.py | 2 +- platformio.ini | 2 +- tests/test3.yaml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index c6998ce0c5..1cb8773495 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -495,4 +495,4 @@ async def to_code(config): trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf ) # https://github.com/paveldn/HaierProtocol - cg.add_library("pavlodn/HaierProtocol", "0.9.24") + cg.add_library("pavlodn/HaierProtocol", "0.9.25") diff --git a/platformio.ini b/platformio.ini index 68c4220aab..2dfaa79a52 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,7 +39,7 @@ lib_deps = bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 - pavlodn/HaierProtocol@0.9.24 ; haier + pavlodn/HaierProtocol@0.9.25 ; haier ; 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 build_flags = diff --git a/tests/test3.yaml b/tests/test3.yaml index e39e711ab3..c31eb45fbd 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1024,6 +1024,7 @@ climate: name: Haier AC uart_id: uart_12 wifi_signal: true + answer_timeout: 200ms beeper: true outdoor_temperature: name: Haier AC outdoor temperature From 773cd0f414bf917d150606b2b7b0166d60d98563 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 31 Dec 2023 21:01:16 +1100 Subject: [PATCH 244/436] GT911 touchscreen: Fix bug causing touch button release to fail (#6042) * Fix bug causing gt911 touch button release to fail * Cache button state and report changes only --- .../gt911/touchscreen/gt911_touchscreen.cpp | 15 ++++++++------- .../gt911/touchscreen/gt911_touchscreen.h | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 84854d5b0d..68ed66a89f 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -14,6 +14,7 @@ static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48}; static const size_t MAX_TOUCHES = 5; // max number of possible touches reported +static const size_t MAX_BUTTONS = 4; // max number of buttons scanned #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ @@ -79,9 +80,6 @@ void GT911Touchscreen::update_touches() { return; } - if (num_of_touches == 0) - return; - err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); ERROR_CHECK(err); // num_of_touches is guaranteed to be 0..5. Also read the key data @@ -94,10 +92,13 @@ void GT911Touchscreen::update_touches() { uint16_t y = encode_uint16(data[i][4], data[i][3]); this->add_raw_touch_position_(id, x, y); } - auto keys = data[num_of_touches][0]; - for (size_t i = 0; i != 4; i++) { - for (auto *listener : this->button_listeners_) - listener->update_button(i, (keys & (1 << i)) != 0); + auto keys = data[num_of_touches][0] & ((1 << MAX_BUTTONS) - 1); + if (keys != this->button_state_) { + this->button_state_ = keys; + for (size_t i = 0; i != MAX_BUTTONS; i++) { + for (auto *listener : this->button_listeners_) + listener->update_button(i, (keys & (1 << i)) != 0); + } } } diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index 44875de5f1..a9e1279ed3 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -26,6 +26,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice InternalGPIOPin *interrupt_pin_{}; std::vector button_listeners_; + uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. }; } // namespace gt911 From ae52164d9c9c15b8b15c4b6c2127509ed5b092d3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 2 Jan 2024 10:34:40 +1100 Subject: [PATCH 245/436] Display: Introduce `draw_pixels_at()` method for fast block display rendering (#6034) * Introduce `draw_pixels_at()` method for fast block display rendering * Add check for 18 vs 16 bit display. --- esphome/components/display/display.cpp | 35 +++++++++++++++++++ esphome/components/display/display.h | 29 +++++++++++++++ .../components/ili9xxx/ili9xxx_display.cpp | 29 +++++++++++++++ esphome/components/ili9xxx/ili9xxx_display.h | 3 ++ 4 files changed, 96 insertions(+) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 88ee64ea55..f32fda4794 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -35,6 +35,41 @@ void HOT Display::line(int x1, int y1, int x2, int y2, Color color) { } } } + +void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, + ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels + uint32_t color_value; + for (int y = 0; y != h; y++) { + size_t source_idx = (y_offset + y) * line_stride + x_offset; + size_t source_idx_mod; + for (int x = 0; x != w; x++, source_idx++) { + switch (bitness) { + default: + color_value = ptr[source_idx]; + break; + case COLOR_BITNESS_565: + source_idx_mod = source_idx * 2; + if (big_endian) { + color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1]; + } else { + color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8); + } + break; + case COLOR_BITNESS_888: + source_idx_mod = source_idx * 3; + if (big_endian) { + color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2]; + } else { + color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16); + } + break; + } + this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness)); + } + } +} + void HOT Display::horizontal_line(int x, int y, int width, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = x; i < x + width; i++) diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 3afcfb9528..2a2a9b80c8 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 "display_color_utils.h" #ifdef USE_GRAPH #include "esphome/components/graph/graph.h" @@ -185,6 +186,34 @@ class Display : public PollingComponent { /// Set a single pixel at the specified coordinates to the given color. virtual void draw_pixel_at(int x, int y, Color color) = 0; + /** Given an array of pixels encoded in the nominated format, draw these into the display's buffer. + * The naive implementation here will work in all cases, but can be overridden by sub-classes + * in order to optimise the procedure. + * The parameters describe a rectangular block of pixels, potentially within a larger buffer. + * + * \param x_start The starting destination x position + * \param y_start The starting destination y position + * \param w the width of the pixel block + * \param h the height of the pixel block + * \param ptr A pointer to the start of the data to be copied + * \param order The ordering of the colors + * \param bitness Defines the number of bits and their format for each pixel + * \param big_endian True if 16 bit values are stored big-endian + * \param x_offset The initial x-offset into the source buffer. + * \param y_offset The initial y-offset into the source buffer. + * \param x_pad How many pixels are in each line after the end of the pixels to be copied. + * + * The length of each source buffer line (stride) will be x_offset + w + x_pad. + */ + virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, + ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad); + + /// Convenience overload for base case where the pixels are packed into the buffer with no gaps (e.g. suits LVGL.) + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, + ColorBitness bitness, bool big_endian) { + this->draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, 0, 0, 0); + } + /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index b315c8be87..ab577b3875 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -276,6 +276,35 @@ void ILI9XXXDisplay::display_() { this->y_high_ = 0; } +// note that this bypasses the buffer and writes directly to the display. +void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, + display::ColorOrder order, display::ColorBitness bitness, bool big_endian, + int x_offset, int y_offset, int x_pad) { + if (w <= 0 || h <= 0) + return; + // if color mapping or software rotation is required, hand this off to the parent implementation. This will + // do color conversion pixel-by-pixel into the buffer and draw it later. If this is happening the user has not + // configured the renderer well. + if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || !big_endian || + this->is_18bitdisplay_) { + return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, + x_pad); + } + this->enable(); + this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother + this->write_array(ptr, w * h * 2); + } else { + auto stride = x_offset + w + x_pad; + for (size_t y = 0; y != h; y++) { + this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); + } + } + this->disable(); +} + // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color // values per bit is huge uint32_t ILI9XXXDisplay::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index bf4889afe1..590be3e364 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display_color_utils.h" #include "ili9xxx_defines.h" #include "ili9xxx_init.h" @@ -84,6 +85,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer, void setup() override; display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; protected: void draw_absolute_pixel_internal(int x, int y, Color color) override; From a2e152ad1252444a9d12e3a129270f62076d4c12 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 3 Jan 2024 16:00:52 +1100 Subject: [PATCH 246/436] clang-format and clang-tidy scripts: More robust algorithm to find correct executable (#6041) * More robust algorithm to find correct executable * Revise message wording * Add clang-tidy and clang-format to requirements.txt. Add to message explaining install process. * Extracted get_binary to helpers.py. Use execptions for clean exit. * Add parameter types * clang-{tidy,format} in requirements_test.txt clean up script exit * Kill processes on ^C * Move clang-tidy and clang-format into requirements_dev.txt --- requirements_dev.txt | 3 +++ requirements_test.txt | 2 -- script/clang-format | 41 +++++++++++++++++++---------------------- script/clang-tidy | 36 +++++++++++++++--------------------- script/helpers.py | 36 ++++++++++++++++++++++++++++++++++++ script/setup | 6 +++++- 6 files changed, 78 insertions(+), 46 deletions(-) create mode 100644 requirements_dev.txt diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000000..6b6319d0a0 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,3 @@ +# Useful stuff when working in a development environment +clang-format==13.0.1 +clang-tidy==14.0.6 diff --git a/requirements_test.txt b/requirements_test.txt index 18c6dedf3e..401f9cb30f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,5 +11,3 @@ pytest-mock==3.12.0 pytest-asyncio==0.23.2 asyncmock==0.4.2 hypothesis==5.49.0 - -clang-format==13.0.1 ; platform_machine != 'armv7l' diff --git a/script/clang-format b/script/clang-format index 165fbd269f..b065d80795 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,6 +1,12 @@ #!/usr/bin/env python3 -from helpers import print_error_for_file, get_output, git_ls_files, filter_changed +from helpers import ( + print_error_for_file, + get_output, + git_ls_files, + filter_changed, + get_binary, +) import argparse import click import colorama @@ -13,11 +19,12 @@ import sys import threading -def run_format(args, queue, lock, failed_files): + +def run_format(executable, args, queue, lock, failed_files): """Takes filenames out of queue and runs clang-format on them.""" while True: path = queue.get() - invocation = ["clang-format-13"] + invocation = [executable] if args.inplace: invocation.append("-i") else: @@ -58,22 +65,6 @@ def main(): ) args = parser.parse_args() - try: - get_output("clang-format-13", "-version") - except: - print( - """ - Oops. It looks like clang-format is not installed. - - Please check you can run "clang-format-13 -version" in your terminal and install - clang-format (v13) if necessary. - - Note you can also upload your code as a pull request on GitHub and see the CI check - output to apply clang-format. - """ - ) - return 1 - files = [] for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]): files.append(os.path.relpath(path, os.getcwd())) @@ -90,11 +81,12 @@ def main(): failed_files = [] try: + executable = get_binary("clang-format", 13) task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread( - target=run_format, args=(args, task_queue, lock, failed_files) + target=run_format, args=(executable, args, task_queue, lock, failed_files) ) t.daemon = True t.start() @@ -109,13 +101,18 @@ def main(): # Wait for all threads to be done. task_queue.join() + except FileNotFoundError as ex: + return 1 except KeyboardInterrupt: print() print("Ctrl-C detected, goodbye.") + # Kill subprocesses (and ourselves!) + # No simple, clean alternative appears to be available. os.kill(0, 9) + return 2 # Will not execute. - sys.exit(len(failed_files)) + return len(failed_files) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/script/clang-tidy b/script/clang-tidy index b025221fa8..97e4ba0d48 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -11,6 +11,7 @@ from helpers import ( load_idedata, root_path, basepath, + get_binary, ) import argparse import click @@ -26,6 +27,7 @@ import tempfile import threading + def clang_options(idedata): cmd = [] @@ -110,10 +112,12 @@ def clang_options(idedata): return cmd -def run_tidy(args, options, tmpdir, queue, lock, failed_files): +pids = set() + +def run_tidy(executable, args, options, tmpdir, queue, lock, failed_files): while True: path = queue.get() - invocation = ["clang-tidy-14"] + invocation = [executable] if tmpdir is not None: invocation.append("--export-fixes") @@ -193,22 +197,6 @@ def main(): ) args = parser.parse_args() - try: - get_output("clang-tidy-14", "-version") - except: - print( - """ - Oops. It looks like clang-tidy-14 is not installed. - - Please check you can run "clang-tidy-14 -version" in your terminal and install - clang-tidy (v14) if necessary. - - Note you can also upload your code as a pull request on GitHub and see the CI check - output to apply clang-tidy. - """ - ) - return 1 - idedata = load_idedata(args.environment) options = clang_options(idedata) @@ -242,12 +230,13 @@ def main(): failed_files = [] try: + executable = get_binary("clang-tidy", 14) task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread( target=run_tidy, - args=(args, options, tmpdir, task_queue, lock, failed_files), + args=(executable, args, options, tmpdir, task_queue, lock, failed_files), ) t.daemon = True t.start() @@ -262,12 +251,17 @@ def main(): # Wait for all threads to be done. task_queue.join() + except FileNotFoundError as ex: + return 1 except KeyboardInterrupt: print() print("Ctrl-C detected, goodbye.") if tmpdir: shutil.rmtree(tmpdir) + # Kill subprocesses (and ourselves!) + # No simple, clean alternative appears to be available. os.kill(0, 9) + return 2 # Will not execute. if args.fix and failed_files: print("Applying fixes ...") @@ -277,8 +271,8 @@ def main(): print("Error applying fixes.\n", file=sys.stderr) raise - sys.exit(len(failed_files)) + return len(failed_files) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/script/helpers.py b/script/helpers.py index b1908e9875..a971fdf475 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -153,3 +153,39 @@ def load_idedata(environment): temp_idedata.write_text(json.dumps(data, indent=2) + "\n") return data + + +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: + pass + binary_file = name + try: + result = subprocess.run( + [binary_file, "-version"], text=True, capture_output=True + ) + if result.returncode == 0 and (f"version {version}") in result.stdout: + return binary_file + raise FileNotFoundError(f"{name} not found") + + except FileNotFoundError as ex: + print( + f""" + Oops. It looks like {name} is not installed. It should be available under venv/bin + and in PATH after running in turn: + script/setup + source venv/bin/activate. + + Please confirm you can run "{name} -version" or "{name}-{version} -version" + in your terminal and install + {name} (v{version}) if necessary. + + Note you can also upload your code as a pull request on GitHub and see the CI check + output to apply {name} + """ + ) + raise diff --git a/script/setup b/script/setup index ba3b544352..9f448cf5c4 100755 --- a/script/setup +++ b/script/setup @@ -15,10 +15,14 @@ if [ -n "$DEVCONTAINER" ];then git config --global --add safe.directory "$PWD" fi -pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt +pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -r requirements_dev.txt pip3 install setuptools wheel pip3 install --no-use-pep517 -e . pre-commit install script/platformio_install_deps.py platformio.ini --libraries --tools --platforms + +echo +echo +echo "Virtual environment created; source venv/bin/activate to use it" From fdd54d74a3ac8893fec8719760698dd3554cb16f Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 7 Jan 2024 19:39:53 -0800 Subject: [PATCH 247/436] Don't crash with invalid adc pin (#6059) * Don't crash with invalid adc pin * lint --- esphome/components/adc/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 952fbdd9b9..87d769fec2 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -139,6 +139,9 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { VARIANT_ESP32C3: { 5: adc2_channel_t.ADC2_CHANNEL_0, }, + VARIANT_ESP32C2: {}, + VARIANT_ESP32C6: {}, + VARIANT_ESP32H2: {}, } From 4202fe65b522f03c8bddbad301381d4685f015fa Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 9 Jan 2024 00:05:52 +0100 Subject: [PATCH 248/436] fix compilation error for libretiny (#6064) --- esphome/components/libretiny/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index e36c08d522..7dca370eff 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -309,7 +309,7 @@ async def component_to_code(config): lt_options["LT_UART_SILENT_ENABLED"] = 0 lt_options["LT_UART_SILENT_ALL"] = 0 # set default UART port - if uart_port := framework.get(CONF_UART_PORT, None) is not None: + if (uart_port := framework.get(CONF_UART_PORT, None)) is not None: lt_options["LT_UART_DEFAULT_PORT"] = uart_port # add custom options lt_options.update(framework[CONF_OPTIONS]) From 14bffaf8a7726ceaa6611fbd101c6dc4549b3b84 Mon Sep 17 00:00:00 2001 From: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:12:28 +0100 Subject: [PATCH 249/436] Add questionmark to default glyphs. (#6053) --- esphome/components/font/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 22a5f6b2c5..a803c7567b 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema) DEFAULT_GLYPHS = ( - ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' + ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) CONF_RAW_GLYPH_ID = "raw_glyph_id" From 696bfe6a87124f7cbde34180b05dc3e60def252f Mon Sep 17 00:00:00 2001 From: functionpointer Date: Tue, 9 Jan 2024 00:26:13 +0100 Subject: [PATCH 250/436] pylontech: Fix parsing error with US2000 (#6061) --- esphome/components/pylontech/__init__.py | 2 +- esphome/components/pylontech/pylontech.cpp | 57 +++++++++++++--------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py index 56fac92e89..197f7e7904 100644 --- a/esphome/components/pylontech/__init__.py +++ b/esphome/components/pylontech/__init__.py @@ -19,7 +19,7 @@ PylontechComponent = pylontech_ns.class_( ) PylontechBattery = pylontech_ns.class_("PylontechBattery") -CV_NUM_BATTERIES = cv.int_range(1, 6) +CV_NUM_BATTERIES = cv.int_range(1, 16) PYLONTECH_COMPONENT_SCHEMA = cv.Schema( { diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 4bfa876110..b33f4d4874 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -1,5 +1,6 @@ #include "pylontech.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace pylontech { @@ -34,26 +35,30 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - uint8_t data; - - // pylontech sends a lot of data very suddenly - // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow - while (this->available() > 0) { - if (this->read_byte(&data)) { - buffer_[buffer_index_write_] += (char) data; - if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || - buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { - // complete line received - buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; + if (this->available() > 0) { + // pylontech sends a lot of data very suddenly + // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow + uint8_t data; + int recv = 0; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_[buffer_index_write_] += (char) data; + recv++; + if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || + buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { + // complete line received + buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; + } } } - } - - // only process one line per call of loop() to not block esphome for too long - if (buffer_index_read_ != buffer_index_write_) { - this->process_line_(buffer_[buffer_index_read_]); - buffer_[buffer_index_read_].clear(); - buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS; + ESP_LOGV(TAG, "received %d bytes", recv); + } else { + // only process one line per call of loop() to not block esphome for too long + if (buffer_index_read_ != buffer_index_write_) { + this->process_line_(buffer_[buffer_index_read_]); + buffer_[buffer_index_read_].clear(); + buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS; + } } } @@ -66,10 +71,11 @@ void PylontechComponent::process_line_(std::string &buffer) { // clang-format on PylontechListener::LineContents l{}; - const int parsed = sscanf( // NOLINT - buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %d %*s", // NOLINT - &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT - l.curr_st, l.temp_st, &l.coulomb, &l.mostempr); // NOLINT + char mostempr_s[6]; + const int parsed = sscanf( // NOLINT + buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT + &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT + l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT if (l.bat_num <= 0) { ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); @@ -79,6 +85,13 @@ void PylontechComponent::process_line_(std::string &buffer) { ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); return; } + auto mostempr_parsed = parse_number(mostempr_s); + if (mostempr_parsed.has_value()) { + l.mostempr = mostempr_parsed.value(); + } else { + l.mostempr = -300; + ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num); + } for (PylontechListener *listener : this->listeners_) { listener->on_line_read(&l); From 9bdb9dc1a37c0a73258c8756b264874dcc90ed75 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Tue, 9 Jan 2024 00:30:37 +0100 Subject: [PATCH 251/436] pylontech: fix voltage_low and voltage_high wrong unit (#6060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: だから <82636574+Dackara@users.noreply.github.com> --- esphome/components/pylontech/sensor/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py index 0423f3370c..a1477c627f 100644 --- a/esphome/components/pylontech/sensor/__init__.py +++ b/esphome/components/pylontech/sensor/__init__.py @@ -59,14 +59,14 @@ TYPES: dict[str, cv.Schema] = { device_class=DEVICE_CLASS_TEMPERATURE, ), CONF_VOLTAGE_LOW: sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_VOLTAGE_HIGH: sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_COULOMB: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, From 886d1a2d00d77e1a572fdeda6341593c8557c119 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:38:50 +0900 Subject: [PATCH 252/436] Bump flake8 from 6.1.0 to 7.0.0 (#6058) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 401f9cb30f..0348ef6cb2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,5 @@ pylint==3.0.3 -flake8==6.1.0 # also change in .pre-commit-config.yaml when updating +flake8==7.0.0 # also change in .pre-commit-config.yaml when updating black==23.12.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit From 6061699eff0a651643e03e1130b6b6bd45d2a4e1 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:41:34 +0100 Subject: [PATCH 253/436] Nextion enable upload from https when using esp-idf (#6051) --- esphome/components/nextion/display.py | 6 ++++++ .../components/nextion/nextion_upload_idf.cpp | 20 +++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index fd61dfa2be..27f2030f0d 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import display, uart +from esphome.components import esp32 from esphome.const import ( CONF_ID, CONF_LAMBDA, @@ -96,6 +97,11 @@ async def to_code(config): if CORE.is_esp32 and CORE.using_arduino: cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) + elif CORE.is_esp32 and CORE.using_esp_idf: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True + ) elif CORE.is_esp8266 and CORE.using_arduino: cg.add_library("ESP8266HTTPClient", None) diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 57bb9c45e8..709ff65b12 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -24,7 +24,7 @@ int Nextion::upload_range(const std::string &url, int range_start) { ESP_LOGVV(TAG, "url: %s", url.c_str()); uint range_size = this->tft_size_ - range_start; ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_; if (range_size <= 0 or range_end <= range_start) { ESP_LOGE(TAG, "Invalid range"); @@ -37,6 +37,8 @@ int Nextion::upload_range(const std::string &url, int range_start) { esp_http_client_config_t config = { .url = url.c_str(), .cert_pem = nullptr, + .disable_auto_redirect = false, + .max_redirection_count = 10, }; esp_http_client_handle_t client = esp_http_client_init(&config); @@ -44,7 +46,7 @@ int Nextion::upload_range(const std::string &url, int range_start) { sprintf(range_header, "bytes=%d-%d", range_start, range_end); ESP_LOGV(TAG, "Requesting range: %s", range_header); esp_http_client_set_header(client, "Range", range_header); - ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); ESP_LOGV(TAG, "Opening http connetion"); esp_err_t err; @@ -70,13 +72,13 @@ int Nextion::upload_range(const std::string &url, int range_start) { std::string recv_string; if (buffer == nullptr) { ESP_LOGE(TAG, "Failed to allocate memory for buffer"); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); } else { ESP_LOGV(TAG, "Memory for buffer allocated successfully"); while (true) { App.feed_wdt(); - ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); int read_len = esp_http_client_read(client, reinterpret_cast(buffer), 4096); ESP_LOGVV(TAG, "Read %d bytes from HTTP client, writing to UART", read_len); if (read_len > 0) { @@ -145,17 +147,19 @@ bool Nextion::upload_tft() { // Define the configuration for the HTTP client ESP_LOGV(TAG, "Establishing connection to HTTP server"); - ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, .method = HTTP_METHOD_HEAD, .timeout_ms = 15000, + .disable_auto_redirect = false, + .max_redirection_count = 10, }; // Initialize the HTTP client with the configuration ESP_LOGV(TAG, "Initializing HTTP client"); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_handle_t http = esp_http_client_init(&config); if (!http) { ESP_LOGE(TAG, "Failed to initialize HTTP client."); @@ -164,7 +168,7 @@ bool Nextion::upload_tft() { // Perform the HTTP request ESP_LOGV(TAG, "Check if the client could connect"); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_err_t err = esp_http_client_perform(http); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); @@ -256,7 +260,7 @@ bool Nextion::upload_end(bool successful) { this->soft_reset(); vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT if (successful) { - ESP_LOGD(TAG, "Restarting esphome"); + ESP_LOGD(TAG, "Restarting ESPHome"); esp_restart(); // NOLINT(readability-static-accessed-through-instance) } return successful; From e3d146ee44e133d14cf797ab3bea42b7ff72d8c0 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Mon, 8 Jan 2024 16:44:08 -0800 Subject: [PATCH 254/436] Support full (>460 char) dumps of Pronto IR commands (#6040) Co-authored-by: Rob Paskowitz --- .../remote_base/pronto_protocol.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 4b6977e1a2..ccae64449a 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -227,16 +227,17 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { } void ProntoProtocol::dump(const ProntoData &data) { - std::string first, rest; - if (data.data.size() < 230) { - first = data.data; - } else { - first = data.data.substr(0, 229); - rest = data.data.substr(230); - } - ESP_LOGI(TAG, "Received Pronto: data=%s", first.c_str()); - if (!rest.empty()) { - ESP_LOGI(TAG, "%s", rest.c_str()); + std::string rest; + + rest = data.data; + ESP_LOGI(TAG, "Received Pronto: data="); + while (true) { + ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str()); + if (rest.size() > 230) { + rest = rest.substr(230); + } else { + break; + } } } From 2bb5343d2787f2d1d120aa81b808944247aa2672 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:45:46 +0100 Subject: [PATCH 255/436] Extends UART change at runtime to ESP8266 (#6019) --- esphome/components/uart/uart_component.h | 4 ++-- .../uart/uart_component_esp8266.cpp | 20 +++++++++++++++++-- .../components/uart/uart_component_esp8266.h | 15 ++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 6f27f36bcb..a57910c1a1 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -122,7 +122,7 @@ class UARTComponent { // @return Baud rate in bits per second. uint32_t get_baud_rate() const { return baud_rate_; } -#ifdef USE_ESP32 +#if defined(USE_ESP8266) || defined(USE_ESP32) /** * Load the UART settings. * @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly. @@ -147,7 +147,7 @@ class UARTComponent { * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ virtual void load_settings(){}; -#endif // USE_ESP32 +#endif // USE_ESP8266 || USE_ESP32 #ifdef USE_UART_DEBUGGER void add_debug_callback(std::function &&callback) { diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 529108f439..fa8dc3fb17 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -98,10 +98,26 @@ void ESP8266UartComponent::setup() { } } +void ESP8266UartComponent::load_settings(bool dump_config) { + ESP_LOGCONFIG(TAG, "Loading UART bus settings..."); + if (this->hw_serial_ != nullptr) { + SerialConfig config = static_cast(get_config()); + this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); + } else { + this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, + this->parity_, this->rx_buffer_size_); + } + if (dump_config) { + ESP_LOGCONFIG(TAG, "UART bus was reloaded."); + this->dump_config(); + } +} + void ESP8266UartComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus:"); - LOG_PIN(" TX Pin: ", tx_pin_); - LOG_PIN(" RX Pin: ", rx_pin_); + LOG_PIN(" TX Pin: ", this->tx_pin_); + LOG_PIN(" RX Pin: ", this->rx_pin_); if (this->rx_pin_ != nullptr) { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT } diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index eed14f3265..749dd4c61e 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -63,6 +63,21 @@ class ESP8266UartComponent : public UARTComponent, public Component { uint32_t get_config(); + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } + protected: void check_logger_conflict() override; From 869cdf122de7beb9a77618d3fee9e020057eadf1 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:47:48 +0100 Subject: [PATCH 256/436] Nextion draw QR code at runtime (#6027) --- esphome/components/nextion/nextion.h | 44 +++++++++++++++++++ .../components/nextion/nextion_commands.cpp | 13 ++++++ 2 files changed, 57 insertions(+) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 2f52a032c4..eef2c61638 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -750,6 +750,50 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void filled_circle(int center_x, int center_y, int radius, Color color); + /** + * Draws a QR code in the screen + * @param x1 The top left x coordinate to start the QR code. + * @param y1 The top left y coordinate to start the QR code. + * @param content The content of the QR code (as a plain text - Nextion will generate the QR code). + * @param size The size (in pixels) for the QR code. Defaults to 200px. + * @param background_color The background color to draw with (as rgb565 integer). Defaults to 65535 (white). + * @param foreground_color The foreground color to draw with (as rgb565 integer). Defaults to 0 (black). + * @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo). + * @param border_width The border width (in pixels) for the QR code. Defaults to 8px. + * + * Example: + * ```cpp + * it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;"); + * ``` + * + * Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25). + */ + void qrcode(int x1, int y1, const char *content, int size = 200, uint16_t background_color = 65535, + uint16_t foreground_color = 0, int logo_pic = -1, uint8_t border_width = 8); + /** + * Draws a QR code in the screen + * @param x1 The top left x coordinate to start the QR code. + * @param y1 The top left y coordinate to start the QR code. + * @param content The content of the QR code (as a plain text - Nextion will generate the QR code). + * @param size The size (in pixels) for the QR code. Defaults to 200px. + * @param background_color The background color to draw with (as Color). Defaults to 65535 (white). + * @param foreground_color The foreground color to draw with (as Color). Defaults to 0 (black). + * @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo). + * @param border_width The border width (in pixels) for the QR code. Defaults to 8px. + * + * Example: + * ```cpp + * auto blue = Color(0, 0, 255); + * auto red = Color(255, 0, 0); + * it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;", 150, blue, red); + * ``` + * + * Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25) with size of 150px in + * red on a blue background. + */ + void qrcode(int x1, int y1, const char *content, int size, Color background_color = Color(255, 255, 255), + Color foreground_color = Color(0, 0, 0), int logo_pic = -1, uint8_t border_width = 8); + /** Set the brightness of the backlight. * * @param brightness The brightness percentage from 0 to 1.0. diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 8512ea5573..c4849d6050 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -294,6 +294,19 @@ void Nextion::filled_circle(int center_x, int center_y, int radius, Color color) display::ColorUtil::color_to_565(color)); } +void Nextion::qrcode(int x1, int y1, const char *content, int size, uint16_t background_color, + uint16_t foreground_color, int logo_pic, uint8_t border_width) { + this->add_no_result_to_queue_with_printf_("qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size, + background_color, foreground_color, logo_pic, border_width, content); +} + +void Nextion::qrcode(int x1, int y1, const char *content, int size, Color background_color, Color foreground_color, + int logo_pic, uint8_t border_width) { + this->add_no_result_to_queue_with_printf_( + "qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size, display::ColorUtil::color_to_565(background_color), + display::ColorUtil::color_to_565(foreground_color), logo_pic, border_width, content); +} + void Nextion::set_nextion_rtc_time(ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year); this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month); From 79d00ec9136a03dcd86b1714edbeb9cba4d6eb4a Mon Sep 17 00:00:00 2001 From: Dusan Cervenka Date: Tue, 9 Jan 2024 02:07:21 +0100 Subject: [PATCH 257/436] Extend i2s config options (#6056) --- esphome/components/i2s_audio/microphone/__init__.py | 6 ++++++ .../i2s_audio/microphone/i2s_audio_microphone.cpp | 4 ++-- .../components/i2s_audio/microphone/i2s_audio_microphone.h | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index b917da3045..5ee359dc26 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -20,7 +20,9 @@ DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" CONF_ADC_TYPE = "adc_type" CONF_PDM = "pdm" +CONF_SAMPLE_RATE = "sample_rate" CONF_BITS_PER_SAMPLE = "bits_per_sample" +CONF_USE_APLL = "use_apll" I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component @@ -62,9 +64,11 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( _validate_bits, cv.enum(BITS_PER_SAMPLE) ), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -105,6 +109,8 @@ async def to_code(config): cg.add(var.set_pdm(config[CONF_PDM])) cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_use_apll(config[CONF_USE_APLL])) await microphone.register_microphone(var, config) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index ec2fe258c9..602d537bcb 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -47,14 +47,14 @@ void I2SAudioMicrophone::start_() { } i2s_driver_config_t config = { .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), - .sample_rate = 16000, + .sample_rate = this->sample_rate_, .bits_per_sample = this->bits_per_sample_, .channel_format = this->channel_, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 4, .dma_buf_len = 256, - .use_apll = false, + .use_apll = this->use_apll_, .tx_desc_auto_clear = false, .fixed_mclk = 0, .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index dc6b70047a..68b9a94fbd 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -31,7 +31,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub #endif void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } protected: void start_(); @@ -45,7 +47,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub #endif bool pdm_{false}; i2s_channel_fmt_t channel_; + uint32_t sample_rate_; i2s_bits_per_sample_t bits_per_sample_; + bool use_apll_; HighFrequencyLoopRequester high_freq_; }; From 65e6f9cba98803fc75ee035a225213dbf47932cd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:07:45 +1100 Subject: [PATCH 258/436] Add getter for image data_start (#6036) --- esphome/components/image/image.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 4e869f5204..5f1f50a134 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -37,6 +37,7 @@ class Image : public display::BaseImage { Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; int get_width() const override; int get_height() const override; + const uint8_t *get_data_start() { return this->data_start_; } ImageType get_type() const; void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; From d9def0cb3a757750df4bf2c2a73af761bd559a51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 15:08:50 -1000 Subject: [PATCH 259/436] Bump hypothesis to 6.92.1 (#6011) --- requirements_test.txt | 2 +- tests/unit_tests/test_core.py | 2 +- tests/unit_tests/test_helpers.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0348ef6cb2..9015152794 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,4 +10,4 @@ pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-asyncio==0.23.2 asyncmock==0.4.2 -hypothesis==5.49.0 +hypothesis==6.92.1 diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index efa9ff5677..2860486efe 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip_addresses +from hypothesis.strategies import ip_addresses from strategies import mac_addr_strings from esphome import core, const diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index fc6bdbcdec..26ebdcf6af 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip_addresses +from hypothesis.strategies import ip_addresses from esphome import helpers From 2be19c4e458c6e86f9357b241792ba2fd0889d2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 15:13:18 -1000 Subject: [PATCH 260/436] Bump recommended ESP32 IDF to 4.4.6 (#6048) --- esphome/components/esp32/__init__.py | 6 +++--- .../components/esp32_ble_tracker/__init__.py | 20 ++++++++++++------- platformio.ini | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 5d17633975..50d6d229f9 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -226,7 +226,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 5) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 6) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 @@ -271,8 +271,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 1, 0), None), + "dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 1, 2), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2ead59c025..1edeaadbfd 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,23 +1,26 @@ import re + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation +from esphome.components import esp32_ble +from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.const import ( CONF_ACTIVE, + CONF_DURATION, CONF_ID, CONF_INTERVAL, - CONF_DURATION, - CONF_TRIGGER_ID, CONF_MAC_ADDRESS, - CONF_SERVICE_UUID, CONF_MANUFACTURER_ID, CONF_ON_BLE_ADVERTISE, - CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, + CONF_ON_BLE_SERVICE_DATA_ADVERTISE, + CONF_SERVICE_UUID, + CONF_TRIGGER_ID, + KEY_CORE, + KEY_FRAMEWORK_VERSION, ) -from esphome.components import esp32_ble from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] @@ -263,7 +266,10 @@ async def to_code(config): # https://github.com/espressif/esp-idf/issues/2503 # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 - add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6): + add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) + else: + add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts diff --git a/platformio.ini b/platformio.ini index 2dfaa79a52..f5f510244c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = platformio/espressif32@5.4.0 platform_packages = - platformio/framework-espidf@~3.40405.0 + platformio/framework-espidf@~3.40406.0 framework = espidf lib_deps = From 87301a2e766435f97ece93deb200bc3bd69ed3e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:33:50 -1000 Subject: [PATCH 261/436] Bump pytest from 7.4.3 to 7.4.4 (#6046) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9015152794..bf7103f025 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.4.3 +pytest==7.4.4 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-asyncio==0.23.2 From 6dfdcff66caf3f62de6442f7ecb2f194d1232c11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 15:35:43 -1000 Subject: [PATCH 262/436] dashboard: refactor ping implementation to be more efficient (#6002) --- esphome/dashboard/const.py | 2 + esphome/dashboard/core.py | 5 +- esphome/dashboard/dashboard.py | 99 ++++++++++++++++++++++++++++++++ esphome/dashboard/dns.py | 43 ++++++++++++++ esphome/dashboard/settings.py | 15 +++++ esphome/dashboard/status/ping.py | 85 ++++++++++++++++++++++----- esphome/dashboard/web_server.py | 25 ++++++-- requirements.txt | 2 + 8 files changed, 255 insertions(+), 21 deletions(-) create mode 100644 esphome/dashboard/dns.py diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py index ed2b81d3e8..190d6c4a9a 100644 --- a/esphome/dashboard/const.py +++ b/esphome/dashboard/const.py @@ -4,5 +4,7 @@ EVENT_ENTRY_ADDED = "entry_added" EVENT_ENTRY_REMOVED = "entry_removed" EVENT_ENTRY_UPDATED = "entry_updated" EVENT_ENTRY_STATE_CHANGED = "entry_state_changed" +MAX_EXECUTOR_WORKERS = 48 + SENTINEL = object() diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index ffec9784e8..e22d95fba9 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -8,6 +8,7 @@ from functools import partial from typing import TYPE_CHECKING, Any, Callable from ..zeroconf import DiscoveredImport +from .dns import DNSCache from .entries import DashboardEntries from .settings import DashboardSettings @@ -69,6 +70,7 @@ class ESPHomeDashboard: "mqtt_ping_request", "mdns_status", "settings", + "dns_cache", ) def __init__(self) -> None: @@ -81,7 +83,8 @@ class ESPHomeDashboard: self.ping_request: asyncio.Event | None = None self.mqtt_ping_request = threading.Event() self.mdns_status: MDNSStatus | None = None - self.settings: DashboardSettings = DashboardSettings() + self.settings = DashboardSettings() + self.dns_cache = DNSCache() async def async_setup(self) -> None: """Setup the dashboard.""" diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 789b14653c..2be98ab3e4 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,11 +1,19 @@ from __future__ import annotations import asyncio +import logging import os import socket +import threading +import traceback +from asyncio import events +from concurrent.futures import ThreadPoolExecutor +from time import monotonic +from typing import Any from esphome.storage_json import EsphomeStorageJSON, esphome_storage_path +from .const import MAX_EXECUTOR_WORKERS from .core import DASHBOARD from .web_server import make_app, start_web_server @@ -14,6 +22,95 @@ ENV_DEV = "ESPHOME_DASHBOARD_DEV" settings = DASHBOARD.settings +def can_use_pidfd() -> bool: + """Check if pidfd_open is available. + + Back ported from cpython 3.12 + """ + if not hasattr(os, "pidfd_open"): + return False + try: + pid = os.getpid() + os.close(os.pidfd_open(pid, 0)) + except OSError: + # blocked by security policy like SECCOMP + return False + return True + + +class DashboardEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + """Event loop policy for Home Assistant.""" + + def __init__(self, debug: bool) -> None: + """Init the event loop policy.""" + super().__init__() + self.debug = debug + self._watcher: asyncio.AbstractChildWatcher | None = None + + def _init_watcher(self) -> None: + """Initialize the watcher for child processes. + + Back ported from cpython 3.12 + """ + with events._lock: # type: ignore[attr-defined] # pylint: disable=protected-access + if self._watcher is None: # pragma: no branch + if can_use_pidfd(): + self._watcher = asyncio.PidfdChildWatcher() + else: + self._watcher = asyncio.ThreadedChildWatcher() + if threading.current_thread() is threading.main_thread(): + self._watcher.attach_loop( + self._local._loop # type: ignore[attr-defined] # pylint: disable=protected-access + ) + + @property + def loop_name(self) -> str: + """Return name of the loop.""" + return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined] + + def new_event_loop(self) -> asyncio.AbstractEventLoop: + """Get the event loop.""" + loop: asyncio.AbstractEventLoop = super().new_event_loop() + loop.set_exception_handler(_async_loop_exception_handler) + + if self.debug: + loop.set_debug(True) + + executor = ThreadPoolExecutor( + thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS + ) + loop.set_default_executor(executor) + # bind the built-in time.monotonic directly as loop.time to avoid the + # overhead of the additional method call since its the most called loop + # method and its roughly 10%+ of all the call time in base_events.py + loop.time = monotonic # type: ignore[method-assign] + return loop + + +def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: + """Handle all exception inside the core loop.""" + kwargs = {} + if exception := context.get("exception"): + kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) + + logger = logging.getLogger(__package__) + if source_traceback := context.get("source_traceback"): + stack_summary = "".join(traceback.format_list(source_traceback)) + logger.error( + "Error doing job: %s: %s", + context["message"], + stack_summary, + **kwargs, # type: ignore[arg-type] + ) + return + + logger.error( + "Error doing job: %s", + context["message"], + **kwargs, # type: ignore[arg-type] + ) + + def start_dashboard(args) -> None: """Start the dashboard.""" settings.parse_args(args) @@ -26,6 +123,8 @@ def start_dashboard(args) -> None: storage.save(path) settings.cookie_secret = storage.cookie_secret + asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose)) + try: asyncio.run(async_start(args)) except KeyboardInterrupt: diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py new file mode 100644 index 0000000000..b78a909220 --- /dev/null +++ b/esphome/dashboard/dns.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import asyncio +import sys + +from icmplib import NameLookupError, async_resolve + +if sys.version_info >= (3, 11): + from asyncio import timeout as async_timeout +else: + from async_timeout import timeout as async_timeout + + +async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: + """Wrap the icmplib async_resolve function.""" + try: + async with async_timeout(2): + return await async_resolve(hostname) + except (asyncio.TimeoutError, NameLookupError, UnicodeError) as ex: + return ex + + +class DNSCache: + """DNS cache for the dashboard.""" + + def __init__(self, ttl: int | None = 120) -> None: + """Initialize the DNSCache.""" + self._cache: dict[str, tuple[float, list[str] | Exception]] = {} + self._ttl = ttl + + async def async_resolve( + self, hostname: str, now_monotonic: float + ) -> list[str] | Exception: + """Resolve a hostname to a list of IP address.""" + if expire_time_addresses := self._cache.get(hostname): + expire_time, addresses = expire_time_addresses + if expire_time > now_monotonic: + return addresses + + expires = now_monotonic + self._ttl + addresses = await _async_resolve_wrapper(hostname) + self._cache[hostname] = (expires, addresses) + return addresses diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 1a5b1620e8..1f05abab4c 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -14,7 +14,19 @@ from .util.password import password_hash class DashboardSettings: """Settings for the dashboard.""" + __slots__ = ( + "config_dir", + "password_hash", + "username", + "using_password", + "on_ha_addon", + "cookie_secret", + "absolute_config_dir", + "verbose", + ) + def __init__(self) -> None: + """Initialize the dashboard settings.""" self.config_dir: str = "" self.password_hash: str = "" self.username: str = "" @@ -22,8 +34,10 @@ class DashboardSettings: self.on_ha_addon: bool = False self.cookie_secret: str | None = None self.absolute_config_dir: Path | None = None + self.verbose: bool = False def parse_args(self, args: Any) -> None: + """Parse the arguments.""" self.on_ha_addon: bool = args.ha_addon password = args.password or os.getenv("PASSWORD") or "" if not self.on_ha_addon: @@ -33,6 +47,7 @@ class DashboardSettings: self.password_hash = password_hash(password) self.config_dir = args.configuration self.absolute_config_dir = Path(self.config_dir).resolve() + self.verbose = args.verbose CORE.config_path = os.path.join(self.config_dir, ".") @property diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 989cd1570f..6630f03c9d 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -1,20 +1,20 @@ from __future__ import annotations import asyncio -import os +import logging +import time from typing import cast +from icmplib import Host, SocketPermissionError, async_ping + +from ..const import MAX_EXECUTOR_WORKERS from ..core import DASHBOARD -from ..entries import DashboardEntry, bool_to_entry_state +from ..entries import DashboardEntry, EntryState, bool_to_entry_state from ..util.itertools import chunked -from ..util.subprocess import async_system_command_status +_LOGGER = logging.getLogger(__name__) -async def _async_ping_host(host: str) -> bool: - """Ping a host.""" - return await async_system_command_status( - ["ping", "-n" if os.name == "nt" else "-c", "1", host] - ) +GROUP_SIZE = int(MAX_EXECUTOR_WORKERS / 2) class PingStatus: @@ -27,6 +27,10 @@ class PingStatus: """Run the ping status.""" dashboard = DASHBOARD entries = dashboard.entries + privileged = await _can_use_icmp_lib_with_privilege() + if privileged is None: + _LOGGER.warning("Cannot use icmplib because privileges are insufficient") + return while not dashboard.stop_event.is_set(): # Only ping if the dashboard is open @@ -36,15 +40,68 @@ class PingStatus: to_ping: list[DashboardEntry] = [ entry for entry in current_entries if entry.address is not None ] - for ping_group in chunked(to_ping, 16): + + # Resolve DNS for all entries + entries_with_addresses: dict[DashboardEntry, list[str]] = {} + for ping_group in chunked(to_ping, GROUP_SIZE): ping_group = cast(list[DashboardEntry], ping_group) - results = await asyncio.gather( - *(_async_ping_host(entry.address) for entry in ping_group), + now_monotonic = time.monotonic() + dns_results = await asyncio.gather( + *( + dashboard.dns_cache.async_resolve(entry.address, now_monotonic) + for entry in ping_group + ), return_exceptions=True, ) - for entry, result in zip(ping_group, results): + + for entry, result in zip(ping_group, dns_results): if isinstance(result, Exception): - result = False + entries.async_set_state(entry, EntryState.UNKNOWN) + continue + if isinstance(result, BaseException): + raise result + entries_with_addresses[entry] = result + + # Ping all entries with valid addresses + for ping_group in chunked(entries_with_addresses.items(), GROUP_SIZE): + entry_addresses = cast(tuple[DashboardEntry, list[str]], ping_group) + + results = await asyncio.gather( + *( + async_ping(addresses[0], privileged=privileged) + for _, addresses in entry_addresses + ), + return_exceptions=True, + ) + + for entry_addresses, result in zip(entry_addresses, results): + if isinstance(result, Exception): + ping_result = False elif isinstance(result, BaseException): raise result - entries.async_set_state(entry, bool_to_entry_state(result)) + else: + host: Host = result + ping_result = host.is_alive + entry, _ = entry_addresses + entries.async_set_state(entry, bool_to_entry_state(ping_result)) + + +async def _can_use_icmp_lib_with_privilege() -> None | bool: + """Verify we can create a raw socket.""" + try: + await async_ping("127.0.0.1", count=0, timeout=0, privileged=True) + except SocketPermissionError: + try: + await async_ping("127.0.0.1", count=0, timeout=0, privileged=False) + except SocketPermissionError: + _LOGGER.debug( + "Cannot use icmplib because privileges are insufficient to create the" + " socket" + ) + return None + + _LOGGER.debug("Using icmplib in privileged=False mode") + return False + + _LOGGER.debug("Using icmplib in privileged=True mode") + return True diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 6a80865906..c16461d174 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -9,6 +9,7 @@ import hashlib import json import logging import os +import time import secrets import shutil import subprocess @@ -302,16 +303,28 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): port = json_message["port"] if ( port == "OTA" # pylint: disable=too-many-boolean-expressions - and (mdns := dashboard.mdns_status) and (entry := entries.get(config_file)) and entry.loaded_integrations and "api" in entry.loaded_integrations - and (address := await mdns.async_resolve_host(entry.name)) ): - # Use the IP address if available but only - # if the API is loaded and the device is online - # since MQTT logging will not work otherwise - port = address + if (mdns := dashboard.mdns_status) and ( + address := await mdns.async_resolve_host(entry.name) + ): + # Use the IP address if available but only + # if the API is loaded and the device is online + # since MQTT logging will not work otherwise + port = address + elif ( + entry.address + and ( + address_list := await dashboard.dns_cache.async_resolve( + entry.address, time.monotonic() + ) + ) + and not isinstance(address_list, Exception) + ): + # If mdns is not available, try to use the DNS cache + port = address_list[0] return [ *DASHBOARD_COMMAND, diff --git a/requirements.txt b/requirements.txt index 115f85de3e..5281b64e66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ +async_timeout==4.0.3; python_version <= "3.10" voluptuous==0.14.1 PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 +icmplib==3.0.4 tornado==6.4 tzlocal==5.2 # from time tzdata>=2021.1 # from time From 97be209aec830789184df9e2174bee5427a24afd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:11:48 -1000 Subject: [PATCH 263/436] Bump pytest-asyncio from 0.23.2 to 0.23.3 (#6047) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index bf7103f025..35f48e767f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==7.4.4 pytest-cov==4.1.0 pytest-mock==3.12.0 -pytest-asyncio==0.23.2 +pytest-asyncio==0.23.3 asyncmock==0.4.2 hypothesis==6.92.1 From aa8a533da6eb35f0ab6ffb64423a6bffe99c2f03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:12:08 -1000 Subject: [PATCH 264/436] Bump black from 23.12.0 to 23.12.1 (#6018) --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36ec1894d8..b2f44d088f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black args: diff --git a/requirements_test.txt b/requirements_test.txt index 35f48e767f..74d66f5b25 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.0.3 flake8==7.0.0 # also change in .pre-commit-config.yaml when updating -black==23.12.0 # also change in .pre-commit-config.yaml when updating +black==23.12.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit From fcd549e5b6d5a776551acde2e38c59d04535b2b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 17:18:13 -1000 Subject: [PATCH 265/436] Run python tests on windows and macos (#6010) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 13 +++++-- .github/workflows/ci.yml | 44 +++++++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 18a2485dbb..3c1a5e2b04 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -28,11 +28,20 @@ runs: # yamllint disable-line rule:line-length key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }} - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' + if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows' shell: bash run: | python -m venv venv - . venv/bin/activate + source venv/bin/activate + python --version + pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip install -e . + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows' + shell: bash + run: | + python -m venv venv + ./venv/Scripts/activate python --version pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -e . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8182f92f94..1ddc49b504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,35 @@ jobs: pytest: name: Run pytest - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + os: + - ubuntu-latest + - macOS-latest + - windows-latest + exclude: + # Minimize CI resource usage + # by only running the Python version + # version used for docker images on Windows and macOS + - python-version: "3.12" + os: windows-latest + - python-version: "3.10" + os: windows-latest + - python-version: "3.9" + os: windows-latest + - python-version: "3.12" + os: macOS-latest + - python-version: "3.10" + os: macOS-latest + - python-version: "3.9" + os: macOS-latest + runs-on: ${{ matrix.os }} needs: - common steps: @@ -175,14 +203,24 @@ jobs: - name: Restore Python uses: ./.github/actions/restore-python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ matrix.python-version }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run pytest + if: matrix.os == 'windows-latest' + run: | + ./venv/Scripts/activate + pytest -vv --cov-report=xml --tb=native tests + - name: Run pytest + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' run: | . venv/bin/activate - pytest -vv --tb=native tests + pytest -vv --cov-report=xml --tb=native tests + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} clang-format: name: Check clang-format From 4b783c03723f50df6969e78ef26729b4b05fa70b Mon Sep 17 00:00:00 2001 From: Andrey Bodrov Date: Wed, 10 Jan 2024 07:31:38 +0300 Subject: [PATCH 266/436] BME280 SPI (#5538) * bme spi finally * linter * CO * tidy * lint * tidy [2] * tidy[-1] * final solution * Update test1.yaml remove failed test * Update test1.1.yaml add test to another file with free GPIO5 pin * fix spi read bytes * fix tests * rename bme280 to bme280_i2c --- CODEOWNERS | 2 + esphome/components/bme280/sensor.py | 116 ------------------ esphome/components/bme280_base/__init__.py | 1 + .../bme280_base.cpp} | 68 +++++----- .../bme280.h => bme280_base/bme280_base.h} | 14 ++- esphome/components/bme280_base/sensor.py | 106 ++++++++++++++++ .../{bme280 => bme280_i2c}/__init__.py | 0 esphome/components/bme280_i2c/bme280_i2c.cpp | 30 +++++ esphome/components/bme280_i2c/bme280_i2c.h | 20 +++ esphome/components/bme280_i2c/sensor.py | 19 +++ esphome/components/bme280_spi/__init__.py | 1 + esphome/components/bme280_spi/bme280_spi.cpp | 66 ++++++++++ esphome/components/bme280_spi/bme280_spi.h | 20 +++ esphome/components/bme280_spi/sensor.py | 24 ++++ tests/test1.yaml | 17 ++- 15 files changed, 350 insertions(+), 154 deletions(-) delete mode 100644 esphome/components/bme280/sensor.py create mode 100644 esphome/components/bme280_base/__init__.py rename esphome/components/{bme280/bme280.cpp => bme280_base/bme280_base.cpp} (93%) rename esphome/components/{bme280/bme280.h => bme280_base/bme280_base.h} (90%) create mode 100644 esphome/components/bme280_base/sensor.py rename esphome/components/{bme280 => bme280_i2c}/__init__.py (100%) create mode 100644 esphome/components/bme280_i2c/bme280_i2c.cpp create mode 100644 esphome/components/bme280_i2c/bme280_i2c.h create mode 100644 esphome/components/bme280_i2c/sensor.py create mode 100644 esphome/components/bme280_spi/__init__.py create mode 100644 esphome/components/bme280_spi/bme280_spi.cpp create mode 100644 esphome/components/bme280_spi/bme280_spi.h create mode 100644 esphome/components/bme280_spi/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c655f94a1b..0ff5ce4508 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,8 @@ esphome/components/bl0940/* @tobias- esphome/components/bl0942/* @dbuezas esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/bluetooth_proxy/* @jesserockz +esphome/components/bme280_base/* @esphome/core +esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth esphome/components/bmi160/* @flaviut esphome/components/bmp3xx/* @martgras diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py deleted file mode 100644 index 35744a436d..0000000000 --- a/esphome/components/bme280/sensor.py +++ /dev/null @@ -1,116 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import i2c, sensor -from esphome.const import ( - CONF_HUMIDITY, - CONF_ID, - CONF_IIR_FILTER, - CONF_OVERSAMPLING, - CONF_PRESSURE, - CONF_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, - UNIT_PERCENT, -) - -DEPENDENCIES = ["i2c"] - -bme280_ns = cg.esphome_ns.namespace("bme280") -BME280Oversampling = bme280_ns.enum("BME280Oversampling") -OVERSAMPLING_OPTIONS = { - "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE, - "1X": BME280Oversampling.BME280_OVERSAMPLING_1X, - "2X": BME280Oversampling.BME280_OVERSAMPLING_2X, - "4X": BME280Oversampling.BME280_OVERSAMPLING_4X, - "8X": BME280Oversampling.BME280_OVERSAMPLING_8X, - "16X": BME280Oversampling.BME280_OVERSAMPLING_16X, -} - -BME280IIRFilter = bme280_ns.enum("BME280IIRFilter") -IIR_FILTER_OPTIONS = { - "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF, - "2X": BME280IIRFilter.BME280_IIR_FILTER_2X, - "4X": BME280IIRFilter.BME280_IIR_FILTER_4X, - "8X": BME280IIRFilter.BME280_IIR_FILTER_8X, - "16X": BME280IIRFilter.BME280_IIR_FILTER_16X, -} - -BME280Component = bme280_ns.class_( - "BME280Component", cg.PollingComponent, i2c.I2CDevice -) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(BME280Component), - 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, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - unit_of_measurement=UNIT_HECTOPASCAL, - accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( - IIR_FILTER_OPTIONS, upper=True - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x77)) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) - - if temperature_config := config.get(CONF_TEMPERATURE): - sens = await sensor.new_sensor(temperature_config) - cg.add(var.set_temperature_sensor(sens)) - cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING])) - - if pressure_config := config.get(CONF_PRESSURE): - sens = await sensor.new_sensor(pressure_config) - cg.add(var.set_pressure_sensor(sens)) - cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) - - if humidity_config := config.get(CONF_HUMIDITY): - sens = await sensor.new_sensor(humidity_config) - cg.add(var.set_humidity_sensor(sens)) - cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING])) - - cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) diff --git a/esphome/components/bme280_base/__init__.py b/esphome/components/bme280_base/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/bme280_base/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280_base/bme280_base.cpp similarity index 93% rename from esphome/components/bme280/bme280.cpp rename to esphome/components/bme280_base/bme280_base.cpp index 786fc01d28..3c6e15cbca 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -1,9 +1,14 @@ -#include "bme280.h" +#include +#include + +#include "bme280_base.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include +#include namespace esphome { -namespace bme280 { +namespace bme280_base { static const char *const TAG = "bme280.sensor"; @@ -46,7 +51,24 @@ static const uint8_t BME280_STATUS_IM_UPDATE = 0b01; inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } -static const char *oversampling_to_str(BME280Oversampling oversampling) { +const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT + switch (filter) { + case BME280_IIR_FILTER_OFF: + return "OFF"; + case BME280_IIR_FILTER_2X: + return "2x"; + case BME280_IIR_FILTER_4X: + return "4x"; + case BME280_IIR_FILTER_8X: + return "8x"; + case BME280_IIR_FILTER_16X: + return "16x"; + default: + return "UNKNOWN"; + } +} + +const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT switch (oversampling) { case BME280_OVERSAMPLING_NONE: return "None"; @@ -65,23 +87,6 @@ static const char *oversampling_to_str(BME280Oversampling oversampling) { } } -static const char *iir_filter_to_str(BME280IIRFilter filter) { - switch (filter) { - case BME280_IIR_FILTER_OFF: - return "OFF"; - case BME280_IIR_FILTER_2X: - return "2x"; - case BME280_IIR_FILTER_4X: - return "4x"; - case BME280_IIR_FILTER_8X: - return "8x"; - case BME280_IIR_FILTER_16X: - return "16x"; - default: - return "UNKNOWN"; - } -} - void BME280Component::setup() { ESP_LOGCONFIG(TAG, "Setting up BME280..."); uint8_t chip_id = 0; @@ -112,7 +117,7 @@ void BME280Component::setup() { // Wait until the NVM data has finished loading. uint8_t status; uint8_t retry = 5; - do { + do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { ESP_LOGW(TAG, "Error reading status register."); @@ -175,7 +180,6 @@ void BME280Component::setup() { } void BME280Component::dump_config() { ESP_LOGCONFIG(TAG, "BME280:"); - LOG_I2C_DEVICE(this); switch (this->error_code_) { case COMMUNICATION_FAILED: ESP_LOGE(TAG, "Communication with BME280 failed!"); @@ -226,14 +230,14 @@ void BME280Component::update() { return; } int32_t t_fine = 0; - float temperature = this->read_temperature_(data, &t_fine); + float const temperature = this->read_temperature_(data, &t_fine); if (std::isnan(temperature)) { ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); this->status_set_warning(); return; } - float pressure = this->read_pressure_(data, t_fine); - float humidity = this->read_humidity_(data, t_fine); + float const pressure = this->read_pressure_(data, t_fine); + float const humidity = this->read_humidity_(data, t_fine); ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); if (this->temperature_sensor_ != nullptr) @@ -257,11 +261,11 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) { const int32_t t2 = this->calibration_.t2; const int32_t t3 = this->calibration_.t3; - int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; - int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; + int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; + int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; *t_fine = var1 + var2; - float temperature = (*t_fine * 5 + 128) >> 8; + float const temperature = (*t_fine * 5 + 128) >> 8; return temperature / 100.0f; } @@ -303,11 +307,11 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) { } float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { - uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); + uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); if (raw_adc == 0x8000) return NAN; - int32_t adc = raw_adc; + int32_t const adc = raw_adc; const int32_t h1 = this->calibration_.h1; const int32_t h2 = this->calibration_.h2; @@ -325,7 +329,7 @@ float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r; v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r; - float h = v_x1_u32r >> 12; + float const h = v_x1_u32r >> 12; return h / 1024.0f; } @@ -351,5 +355,5 @@ uint16_t BME280Component::read_u16_le_(uint8_t a_register) { } int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } -} // namespace bme280 +} // namespace bme280_base } // namespace esphome diff --git a/esphome/components/bme280/bme280.h b/esphome/components/bme280_base/bme280_base.h similarity index 90% rename from esphome/components/bme280/bme280.h rename to esphome/components/bme280_base/bme280_base.h index 50d398c40f..0f55ad0101 100644 --- a/esphome/components/bme280/bme280.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -2,10 +2,9 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" namespace esphome { -namespace bme280 { +namespace bme280_base { /// Internal struct storing the calibration values of an BME280. struct BME280CalibrationData { @@ -57,8 +56,8 @@ enum BME280IIRFilter { BME280_IIR_FILTER_16X = 0b100, }; -/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor. -class BME280Component : public PollingComponent, public i2c::I2CDevice { +/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor. +class BME280Component : public PollingComponent { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } @@ -91,6 +90,11 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice { uint16_t read_u16_le_(uint8_t a_register); int16_t read_s16_le_(uint8_t a_register); + virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0; + BME280CalibrationData calibration_; BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X}; BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X}; @@ -106,5 +110,5 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace bme280 +} // namespace bme280_base } // namespace esphome diff --git a/esphome/components/bme280_base/sensor.py b/esphome/components/bme280_base/sensor.py new file mode 100644 index 0000000000..3a745ed348 --- /dev/null +++ b/esphome/components/bme280_base/sensor.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PERCENT, +) + +bme280_ns = cg.esphome_ns.namespace("bme280_base") +BME280Oversampling = bme280_ns.enum("BME280Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE, + "1X": BME280Oversampling.BME280_OVERSAMPLING_1X, + "2X": BME280Oversampling.BME280_OVERSAMPLING_2X, + "4X": BME280Oversampling.BME280_OVERSAMPLING_4X, + "8X": BME280Oversampling.BME280_OVERSAMPLING_8X, + "16X": BME280Oversampling.BME280_OVERSAMPLING_16X, +} + +BME280IIRFilter = bme280_ns.enum("BME280IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF, + "2X": BME280IIRFilter.BME280_IIR_FILTER_2X, + "4X": BME280IIRFilter.BME280_IIR_FILTER_4X, + "8X": BME280IIRFilter.BME280_IIR_FILTER_8X, + "16X": BME280IIRFilter.BME280_IIR_FILTER_16X, +} + +CONFIG_SCHEMA_BASE = cv.Schema( + { + 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, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config, func=None): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if func is not None: + await func(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING])) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING])) + + cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) diff --git a/esphome/components/bme280/__init__.py b/esphome/components/bme280_i2c/__init__.py similarity index 100% rename from esphome/components/bme280/__init__.py rename to esphome/components/bme280_i2c/__init__.py diff --git a/esphome/components/bme280_i2c/bme280_i2c.cpp b/esphome/components/bme280_i2c/bme280_i2c.cpp new file mode 100644 index 0000000000..e29675b5b7 --- /dev/null +++ b/esphome/components/bme280_i2c/bme280_i2c.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "bme280_i2c.h" +#include "esphome/components/i2c/i2c.h" +#include "../bme280_base/bme280_base.h" + +namespace esphome { +namespace bme280_i2c { + +bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { + return I2CDevice::read_byte(a_register, data); +}; +bool BME280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { + return I2CDevice::write_byte(a_register, data); +}; +bool BME280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::read_bytes(a_register, data, len); +}; +bool BME280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + return I2CDevice::read_byte_16(a_register, data); +}; + +void BME280I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BME280Component::dump_config(); +} + +} // namespace bme280_i2c +} // namespace esphome diff --git a/esphome/components/bme280_i2c/bme280_i2c.h b/esphome/components/bme280_i2c/bme280_i2c.h new file mode 100644 index 0000000000..c5e2f7e342 --- /dev/null +++ b/esphome/components/bme280_i2c/bme280_i2c.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bme280_base/bme280_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bme280_i2c { + +static const char *const TAG = "bme280_i2c.sensor"; + +class BME280I2CComponent : public esphome::bme280_base::BME280Component, public i2c::I2CDevice { + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; + void dump_config() override; +}; + +} // namespace bme280_i2c +} // namespace esphome diff --git a/esphome/components/bme280_i2c/sensor.py b/esphome/components/bme280_i2c/sensor.py new file mode 100644 index 0000000000..489c52969d --- /dev/null +++ b/esphome/components/bme280_i2c/sensor.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +from esphome.components import i2c +from ..bme280_base.sensor import to_code as to_code_base, cv, CONFIG_SCHEMA_BASE + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["bme280_base"] + +bme280_ns = cg.esphome_ns.namespace("bme280_i2c") +BME280I2CComponent = bme280_ns.class_( + "BME280I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)}) + + +async def to_code(config): + await to_code_base(config, func=i2c.register_i2c_device) diff --git a/esphome/components/bme280_spi/__init__.py b/esphome/components/bme280_spi/__init__.py new file mode 100644 index 0000000000..a1d33e4d7a --- /dev/null +++ b/esphome/components/bme280_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@apbodrov"] diff --git a/esphome/components/bme280_spi/bme280_spi.cpp b/esphome/components/bme280_spi/bme280_spi.cpp new file mode 100644 index 0000000000..921128c8f5 --- /dev/null +++ b/esphome/components/bme280_spi/bme280_spi.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include "bme280_spi.h" +#include + +int set_bit(uint8_t num, int position) { + int mask = 1 << position; + return num | mask; +} + +int clear_bit(uint8_t num, int position) { + int mask = 1 << position; + return num & ~mask; +} + +namespace esphome { +namespace bme280_spi { + +void BME280SPIComponent::setup() { + this->spi_setup(); + BME280Component::setup(); +}; + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used +// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte +// 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf + +bool BME280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + // cause: *data = this->delegate_->transfer(tmp) doesnt work + this->delegate_->transfer(set_bit(a_register, 7)); + *data = this->delegate_->transfer(0); + this->disable(); + return true; +} + +bool BME280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->delegate_->transfer(clear_bit(a_register, 7)); + this->delegate_->transfer(data); + this->disable(); + return true; +} + +bool BME280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->delegate_->transfer(set_bit(a_register, 7)); + this->delegate_->read_array(data, len); + this->disable(); + return true; +} + +bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + this->enable(); + this->delegate_->transfer(set_bit(a_register, 7)); + ((uint8_t *) data)[1] = this->delegate_->transfer(0); + ((uint8_t *) data)[0] = this->delegate_->transfer(0); + this->disable(); + return true; +} + +} // namespace bme280_spi +} // namespace esphome diff --git a/esphome/components/bme280_spi/bme280_spi.h b/esphome/components/bme280_spi/bme280_spi.h new file mode 100644 index 0000000000..b6b8997fa7 --- /dev/null +++ b/esphome/components/bme280_spi/bme280_spi.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bme280_base/bme280_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace bme280_spi { + +class BME280SPIComponent : public esphome::bme280_base::BME280Component, + public spi::SPIDevice { + void setup() override; + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; +}; + +} // namespace bme280_spi +} // namespace esphome diff --git a/esphome/components/bme280_spi/sensor.py b/esphome/components/bme280_spi/sensor.py new file mode 100644 index 0000000000..3cfe1b3cdd --- /dev/null +++ b/esphome/components/bme280_spi/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.bme280_base.sensor import ( + to_code as to_code_base, + cv, + CONFIG_SCHEMA_BASE, +) + +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["bme280_base"] + + +bme280_spi_ns = cg.esphome_ns.namespace("bme280_spi") +BME280SPIComponent = bme280_spi_ns.class_( + "BME280SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend( + {cv.GenerateID(): cv.declare_id(BME280SPIComponent)} +) + + +async def to_code(config): + await to_code_base(config, func=spi.register_spi_device) diff --git a/tests/test1.yaml b/tests/test1.yaml index bc7a94bc5a..3ca6faca8a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -690,7 +690,7 @@ sensor: update_interval: 30s mode: low_power i2c_id: i2c_bus - - platform: bme280 + - platform: bme280_i2c temperature: name: Outside Temperature oversampling: 16x @@ -704,6 +704,21 @@ sensor: iir_filter: 16x update_interval: 15s i2c_id: i2c_bus + - platform: bme280_spi + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + oversampling: none + humidity: + name: Outside Humidity + oversampling: 8x + cs_pin: + allow_other_uses: true + number: GPIO23 + iir_filter: 16x + update_interval: 15s - platform: bme680 temperature: name: Outside Temperature From 082d9fcf0e43de4311a2929d9f09b3aa7d92e9e2 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 10 Jan 2024 17:05:34 -0600 Subject: [PATCH 267/436] ESP32-C3 USB_CDC fixes (#6069) --- esphome/components/logger/__init__.py | 9 ++++++--- esphome/components/logger/logger.cpp | 12 ++++-------- esphome/components/logger/logger.h | 5 +++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 6cad783db9..fd64c65c77 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -84,7 +84,7 @@ UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], VARIANT_ESP32S2: [UART0, UART1, USB_CDC], VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32C3: [UART0, UART1, USB_SERIAL_JTAG], + VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32C2: [UART0, UART1], VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], @@ -172,9 +172,10 @@ CONFIG_SCHEMA = cv.All( esp8266=UART0, esp32=UART0, esp32_s2=USB_CDC, - esp32_s3_idf=USB_SERIAL_JTAG, - esp32_c3_idf=USB_SERIAL_JTAG, esp32_s3_arduino=USB_CDC, + esp32_s3_idf=USB_SERIAL_JTAG, + esp32_c3_arduino=USB_CDC, + esp32_c3_idf=USB_SERIAL_JTAG, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, @@ -265,6 +266,8 @@ async def to_code(config): if CORE.using_arduino: if config[CONF_HARDWARE_UART] == USB_CDC: cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32C3: + cg.add_build_flag("-DARDUINO_USB_MODE=1") if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index e0f7e77d2c..d5f5c275eb 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -272,17 +272,13 @@ void Logger::pre_setup() { #endif #if defined(USE_ESP32) && \ (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3)) -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) case UART_SELECTION_USB_CDC: -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) case UART_SELECTION_USB_SERIAL_JTAG: #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 -#ifdef USE_ESP32_VARIANT_ESP32C3 - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); -#endif // USE_ESP32_VARIANT_ESP32C3 -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) #if ARDUINO_USB_CDC_ON_BOOT this->hw_serial_ = &Serial; Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection @@ -291,7 +287,7 @@ void Logger::pre_setup() { this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); #endif // ARDUINO_USB_CDC_ON_BOOT -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3 break; #endif // USE_ESP32 && (USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3) #ifdef USE_RP2040 diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 68efc056df..c7f0fe4139 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -45,9 +45,10 @@ enum UARTSelection { UART_SELECTION_UART2, #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + (defined(USE_ESP32_VARIANT_ESP32C3) && defined(USE_ARDUINO)) UART_SELECTION_USB_CDC, -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32H2) UART_SELECTION_USB_SERIAL_JTAG, From d616025fede0b8f2ac7e1ba8010d3467d65019c9 Mon Sep 17 00:00:00 2001 From: Simone Rossetto Date: Thu, 11 Jan 2024 06:09:42 +0100 Subject: [PATCH 268/436] Actions to enable and disable WireGuard connection (#5690) --- esphome/components/wireguard/__init__.py | 55 +++++++++++++++++++ esphome/components/wireguard/binary_sensor.py | 9 +++ esphome/components/wireguard/wireguard.cpp | 46 ++++++++++++++-- esphome/components/wireguard/wireguard.h | 42 ++++++++++++++ tests/test10.yaml | 25 +++++++++ 5 files changed, 172 insertions(+), 5 deletions(-) diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index acb5f690ec..b59a6011cd 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ) from esphome.components import time from esphome.core import TimePeriod +from esphome import automation CONF_NETMASK = "netmask" CONF_PRIVATE_KEY = "private_key" @@ -30,6 +31,16 @@ _WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$") wireguard_ns = cg.esphome_ns.namespace("wireguard") Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent) +WireguardPeerOnlineCondition = wireguard_ns.class_( + "WireguardPeerOnlineCondition", automation.Condition +) +WireguardEnabledCondition = wireguard_ns.class_( + "WireguardEnabledCondition", automation.Condition +) +WireguardEnableAction = wireguard_ns.class_("WireguardEnableAction", automation.Action) +WireguardDisableAction = wireguard_ns.class_( + "WireguardDisableAction", automation.Action +) def _wireguard_key(value): @@ -112,3 +123,47 @@ async def to_code(config): cg.add_library("droscy/esp_wireguard", "0.3.2") await cg.register_component(var, config) + + +@automation.register_condition( + "wireguard.peer_online", + WireguardPeerOnlineCondition, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_peer_up_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_condition( + "wireguard.enabled", + WireguardEnabledCondition, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_enabled_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "wireguard.enable", + WireguardEnableAction, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_enable_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "wireguard.disable", + WireguardDisableAction, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_disable_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/wireguard/binary_sensor.py b/esphome/components/wireguard/binary_sensor.py index 14ff2b0159..bf60aaa1d6 100644 --- a/esphome/components/wireguard/binary_sensor.py +++ b/esphome/components/wireguard/binary_sensor.py @@ -4,11 +4,13 @@ from esphome.components import binary_sensor from esphome.const import ( CONF_STATUS, DEVICE_CLASS_CONNECTIVITY, + ENTITY_CATEGORY_DIAGNOSTIC, ) from . import Wireguard CONF_WIREGUARD_ID = "wireguard_id" +CONF_ENABLED = "enabled" DEPENDENCIES = ["wireguard"] @@ -17,6 +19,9 @@ CONFIG_SCHEMA = { cv.Optional(CONF_STATUS): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_CONNECTIVITY, ), + cv.Optional(CONF_ENABLED): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), } @@ -26,3 +31,7 @@ async def to_code(config): if status_config := config.get(CONF_STATUS): sens = await binary_sensor.new_binary_sensor(status_config) cg.add(parent.set_status_sensor(sens)) + + if enabled_config := config.get(CONF_ENABLED): + sens = await binary_sensor.new_binary_sensor(enabled_config) + cg.add(parent.set_enabled_sensor(sens)) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index f89a5ebbad..cca30d4310 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -48,6 +48,8 @@ void Wireguard::setup() { if (this->preshared_key_.length() > 0) this->wg_config_.preshared_key = this->preshared_key_.c_str(); + this->publish_enabled_state(); + this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); if (this->wg_initialized_ == ESP_OK) { @@ -68,6 +70,10 @@ void Wireguard::setup() { } void Wireguard::loop() { + if (!this->enabled_) { + return; + } + if ((this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) && (!network::is_connected())) { ESP_LOGV(TAG, "local network connection has been lost, stopping WireGuard..."); this->stop_connection_(); @@ -79,8 +85,9 @@ void Wireguard::update() { time_t lhs = this->get_latest_handshake(); bool lhs_updated = (lhs > this->latest_saved_handshake_); - ESP_LOGV(TAG, "handshake: latest=%.0f, saved=%.0f, updated=%d", (double) lhs, (double) this->latest_saved_handshake_, - (int) lhs_updated); + ESP_LOGV(TAG, "enabled=%d, connected=%d, peer_up=%d, handshake: current=%.0f latest=%.0f updated=%d", + (int) this->enabled_, (int) (this->wg_connected_ == ESP_OK), (int) peer_up, (double) lhs, + (double) this->latest_saved_handshake_, (int) lhs_updated); if (lhs_updated) { this->latest_saved_handshake_ = lhs; @@ -102,13 +109,13 @@ void Wireguard::update() { if (this->wg_peer_offline_time_ == 0) { ESP_LOGW(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str()); this->wg_peer_offline_time_ = millis(); - } else { + } else if (this->enabled_) { ESP_LOGD(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str()); this->start_connection_(); } // check reboot timeout every time the peer is down - if (this->reboot_timeout_ > 0) { + if (this->enabled_ && this->reboot_timeout_ > 0) { if (millis() - this->wg_peer_offline_time_ > this->reboot_timeout_) { ESP_LOGE(TAG, "WireGuard remote peer is unreachable, rebooting..."); App.reboot(); @@ -154,7 +161,7 @@ void Wireguard::dump_config() { void Wireguard::on_shutdown() { this->stop_connection_(); } -bool Wireguard::can_proceed() { return (this->proceed_allowed_ || this->is_peer_up()); } +bool Wireguard::can_proceed() { return (this->proceed_allowed_ || this->is_peer_up() || !this->enabled_); } bool Wireguard::is_peer_up() const { return (this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) && @@ -187,6 +194,7 @@ void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = src #ifdef USE_BINARY_SENSOR void Wireguard::set_status_sensor(binary_sensor::BinarySensor *sensor) { this->status_sensor_ = sensor; } +void Wireguard::set_enabled_sensor(binary_sensor::BinarySensor *sensor) { this->enabled_sensor_ = sensor; } #endif #ifdef USE_SENSOR @@ -199,7 +207,35 @@ void Wireguard::set_address_sensor(text_sensor::TextSensor *sensor) { this->addr void Wireguard::disable_auto_proceed() { this->proceed_allowed_ = false; } +void Wireguard::enable() { + this->enabled_ = true; + ESP_LOGI(TAG, "WireGuard enabled"); + this->publish_enabled_state(); +} + +void Wireguard::disable() { + this->enabled_ = false; + this->defer(std::bind(&Wireguard::stop_connection_, this)); // defer to avoid blocking running loop + ESP_LOGI(TAG, "WireGuard disabled"); + this->publish_enabled_state(); +} + +void Wireguard::publish_enabled_state() { +#ifdef USE_BINARY_SENSOR + if (this->enabled_sensor_ != nullptr) { + this->enabled_sensor_->publish_state(this->enabled_); + } +#endif +} + +bool Wireguard::is_enabled() { return this->enabled_; } + void Wireguard::start_connection_() { + if (!this->enabled_) { + ESP_LOGV(TAG, "WireGuard is disabled, cannot start connection"); + return; + } + if (this->wg_initialized_ != ESP_OK) { ESP_LOGE(TAG, "cannot start WireGuard, initialization in error with code %d", this->wg_initialized_); return; diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index c47d9e6603..7753a8dfc2 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -26,6 +26,7 @@ namespace esphome { namespace wireguard { +/// Main Wireguard component class. class Wireguard : public PollingComponent { public: void setup() override; @@ -53,6 +54,7 @@ class Wireguard : public PollingComponent { #ifdef USE_BINARY_SENSOR void set_status_sensor(binary_sensor::BinarySensor *sensor); + void set_enabled_sensor(binary_sensor::BinarySensor *sensor); #endif #ifdef USE_SENSOR @@ -66,6 +68,18 @@ class Wireguard : public PollingComponent { /// Block the setup step until peer is connected. void disable_auto_proceed(); + /// Enable the WireGuard component. + void enable(); + + /// Stop any running connection and disable the WireGuard component. + void disable(); + + /// Publish the enabled state if the enabled binary sensor is configured. + void publish_enabled_state(); + + /// Return if the WireGuard component is or is not enabled. + bool is_enabled(); + bool is_peer_up() const; time_t get_latest_handshake() const; @@ -87,6 +101,7 @@ class Wireguard : public PollingComponent { #ifdef USE_BINARY_SENSOR binary_sensor::BinarySensor *status_sensor_ = nullptr; + binary_sensor::BinarySensor *enabled_sensor_ = nullptr; #endif #ifdef USE_SENSOR @@ -100,6 +115,9 @@ class Wireguard : public PollingComponent { /// Set to false to block the setup step until peer is connected. bool proceed_allowed_ = true; + /// When false the wireguard link will not be established + bool enabled_ = true; + wireguard_config_t wg_config_ = ESP_WIREGUARD_CONFIG_DEFAULT(); wireguard_ctx_t wg_ctx_ = ESP_WIREGUARD_CONTEXT_DEFAULT(); @@ -128,6 +146,30 @@ void resume_wdt(); /// Strip most part of the key only for secure printing std::string mask_key(const std::string &key); +/// Condition to check if remote peer is online. +template class WireguardPeerOnlineCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_peer_up(); } +}; + +/// Condition to check if Wireguard component is enabled. +template class WireguardEnabledCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_enabled(); } +}; + +/// Action to enable Wireguard component. +template class WireguardEnableAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->enable(); } +}; + +/// Action to disable Wireguard component. +template class WireguardDisableAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->disable(); } +}; + } // namespace wireguard } // namespace esphome diff --git a/tests/test10.yaml b/tests/test10.yaml index dda7601048..7e3a685b36 100644 --- a/tests/test10.yaml +++ b/tests/test10.yaml @@ -44,6 +44,8 @@ binary_sensor: - platform: wireguard status: name: 'WireGuard Status' + enabled: + name: 'WireGuard Enabled' sensor: - platform: wireguard @@ -54,3 +56,26 @@ text_sensor: - platform: wireguard address: name: 'WireGuard Address' + +button: + - platform: template + name: 'Toggle WireGuard' + entity_category: config + on_press: + - if: + condition: wireguard.enabled + then: + - wireguard.disable: + else: + - wireguard.enable: + + - platform: template + name: 'Log WireGuard status' + entity_category: config + on_press: + - if: + condition: wireguard.peer_online + then: + - logger.log: 'wireguard remote peer is online' + else: + - logger.log: 'wireguard remote peer is offline' From 4cc17dac0dff22ef476aa7b98abf17b675749379 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Thu, 11 Jan 2024 16:18:22 +1000 Subject: [PATCH 269/436] hydreon_rgxx - fix missing cg.add(var.set_model(...)) (#6065) --- esphome/components/hydreon_rgxx/hydreon_rgxx.cpp | 10 ++++++---- esphome/components/hydreon_rgxx/sensor.py | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 58e00ba7a5..c026d7cce6 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -17,6 +17,12 @@ void HydreonRGxxComponent::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!"); } + if (model_ == RG9) { + ESP_LOGCONFIG(TAG, " Model: RG9"); + ESP_LOGCONFIG(TAG, " Disable Led: %s", TRUEFALSE(this->disable_led_)); + } else { + ESP_LOGCONFIG(TAG, " Model: RG15"); + } LOG_UPDATE_INTERVAL(this); int i = 0; @@ -25,10 +31,6 @@ void HydreonRGxxComponent::dump_config() { LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \ } HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, ); - - if (this->model_ == RG9) { - ESP_LOGCONFIG(TAG, "disable_led: %s", TRUEFALSE(this->disable_led_)); - } } void HydreonRGxxComponent::setup() { diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 0fc380f959..f9cb316c24 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -138,6 +138,7 @@ async def to_code(config): sens = await sensor.new_sensor(config[conf]) cg.add(var.set_sensor(sens, i)) + cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_request_temperature(CONF_TEMPERATURE in config)) if CONF_DISABLE_LED in config: From 343a8c063e145ecf0d8e32478581a57ff421d50b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 12 Jan 2024 01:11:42 -0800 Subject: [PATCH 270/436] fix sen5x negative temperature (#6082) --- esphome/components/sen5x/sen5x.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index c90880bc9f..0efc961943 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -352,7 +352,7 @@ void SEN5XComponent::update() { float humidity = measurements[4] / 100.0; if (measurements[4] == 0xFFFF) humidity = NAN; - float temperature = measurements[5] / 200.0; + float temperature = (int16_t) measurements[5] / 200.0; if (measurements[5] == 0xFFFF) temperature = NAN; float voc = measurements[6] / 10.0; From aa04a3caaf83ae71e4edac681bc9a534ada265be Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 12 Jan 2024 01:20:08 -0800 Subject: [PATCH 271/436] negative values for all DHT22 variants (#6074) Co-authored-by: Samuel Sieb --- esphome/components/dht/dht.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index c70b227330..07634cafdf 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -217,8 +217,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF); - if (this->model_ != DHT_MODEL_DHT22_TYPE2 && (raw_temperature & 0x8000) != 0) - raw_temperature = ~(raw_temperature & 0x7FFF); + if (raw_temperature & 0x8000) { + if (!(raw_temperature & 0x4000)) + raw_temperature = ~(raw_temperature & 0x7FFF); + } else if (raw_temperature & 0x800) { + raw_temperature |= 0xf000; + } if (raw_temperature == 1 && raw_humidity == 10) { if (report_errors) { From ed2ab9e96287cd4ff6a96b0086cdf0ef9d845446 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 12 Jan 2024 01:47:50 -0800 Subject: [PATCH 272/436] fix negative temperature for pmsx003 (#6083) * fix negative temperature for pmsx003 * Update esphome/components/pmsx003/pmsx003.cpp --- esphome/components/pmsx003/pmsx003.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 04aba4382b..62488b765c 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -195,7 +195,7 @@ void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) { void PMSX003Component::parse_data_() { switch (this->type_) { case PMSX003_TYPE_5003ST: { - float temperature = this->get_16_bit_uint_(30) / 10.0f; + float temperature = (int16_t) this->get_16_bit_uint_(30) / 10.0f; float humidity = this->get_16_bit_uint_(32) / 10.0f; ESP_LOGD(TAG, "Got Temperature: %.1f°C, Humidity: %.1f%%", temperature, humidity); From d551a2eba2c006758eb97cd3cf4c7110d1c15f50 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 13 Jan 2024 02:21:28 -0600 Subject: [PATCH 273/436] Improv Serial -- don't wait for incoming bytes (#6089) --- esphome/components/improv_serial/improv_serial_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 600069b781..2318fd43cb 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -52,7 +52,7 @@ optional ImprovSerialComponent::read_byte_() { size_t available; uart_get_buffered_data_len(this->uart_num_, &available); if (available) { - uart_read_bytes(this->uart_num_, &data, 1, 20 / portTICK_PERIOD_MS); + uart_read_bytes(this->uart_num_, &data, 1, 0); byte = data; } } @@ -71,7 +71,7 @@ optional ImprovSerialComponent::read_byte_() { #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) case logger::UART_SELECTION_USB_SERIAL_JTAG: { - if (usb_serial_jtag_read_bytes((char *) &data, 1, 20 / portTICK_PERIOD_MS)) { + if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) { byte = data; } break; From 8e83c7dd193083ace6d0faabf81dd60993fe8d5d Mon Sep 17 00:00:00 2001 From: guillempages Date: Sat, 13 Jan 2024 22:01:32 +0100 Subject: [PATCH 274/436] Let show_*_page actions depend on "Display" (#6092) Instead of forcing a DisplayBuffer, let the display page actions use Displays without buffer. --- esphome/components/display/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 91f10c5458..992799008a 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -145,7 +145,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): DisplayPageShowNextAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), + cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -159,7 +159,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): DisplayPageShowPrevAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), + cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -173,7 +173,7 @@ async def display_page_show_previous_to_code(config, action_id, template_arg, ar DisplayIsDisplayingPageCondition, cv.maybe_simple_value( { - cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer), + cv.GenerateID(CONF_ID): cv.use_id(Display), cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage), }, key=CONF_PAGE_ID, From f567b5d28b8c75340cb64a9237219c09ee9d6c50 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 15 Jan 2024 00:01:20 +0100 Subject: [PATCH 275/436] add STATE_CLASS_TOTAL_INCREASING to bl0940 and bl0942 (#6090) --- esphome/components/bl0940/sensor.py | 2 ++ esphome/components/bl0942/sensor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index 702230e020..a197becef8 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -18,6 +18,7 @@ from esphome.const import ( UNIT_KILOWATT_HOURS, UNIT_VOLT, UNIT_WATT, + STATE_CLASS_TOTAL_INCREASING, ) DEPENDENCIES = ["uart"] @@ -54,6 +55,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/bl0942/sensor.py b/esphome/components/bl0942/sensor.py index 663eea0c4d..9612df6d4c 100644 --- a/esphome/components/bl0942/sensor.py +++ b/esphome/components/bl0942/sensor.py @@ -19,6 +19,7 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, UNIT_HERTZ, + STATE_CLASS_TOTAL_INCREASING, ) DEPENDENCIES = ["uart"] @@ -52,6 +53,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, From 5220c9edf82aad55e1df811260675486ac338fb1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Jan 2024 13:06:13 -1000 Subject: [PATCH 276/436] Fallback to pure-python loader for better error when YAML loading fails (#6081) --- esphome/yaml_util.py | 113 ++++++++++-------- .../yaml_util/broken_includetest.yaml | 18 +++ .../includes/broken_included.yaml.txt | 5 + tests/unit_tests/test_yaml_util.py | 11 ++ 4 files changed, 98 insertions(+), 49 deletions(-) create mode 100644 tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/includes/broken_included.yaml.txt diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index aa9fe45ebb..f5e36b79e7 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,36 +1,37 @@ +from __future__ import annotations + import fnmatch import functools import inspect import logging import math import os - import uuid +from typing import Any + import yaml import yaml.constructor +from yaml import SafeLoader as PurePythonLoader + +try: + from yaml import CSafeLoader as FastestAvailableSafeLoader +except ImportError: + FastestAvailableSafeLoader = PurePythonLoader from esphome import core -from esphome.config_helpers import read_config_file, Extend, Remove +from esphome.config_helpers import Extend, Remove, read_config_file from esphome.core import ( + CORE, + DocumentRange, EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, - DocumentRange, - CORE, ) from esphome.helpers import add_class_to_obj from esphome.util import OrderedDict, filter_yaml_files -try: - from yaml import CSafeLoader as FastestAvailableSafeLoader -except ImportError: - from yaml import ( # type: ignore[assignment] - SafeLoader as FastestAvailableSafeLoader, - ) - - _LOGGER = logging.getLogger(__name__) # Mostly copied from Home Assistant because that code works fine and @@ -97,7 +98,7 @@ def _add_data_ref(fn): return wrapped -class ESPHomeLoader(FastestAvailableSafeLoader): +class ESPHomeLoaderMixin: """Loader class that keeps track of line numbers.""" @_add_data_ref @@ -282,8 +283,8 @@ class ESPHomeLoader(FastestAvailableSafeLoader): return file, vars def substitute_vars(config, vars): - from esphome.const import CONF_SUBSTITUTIONS, CONF_DEFAULTS from esphome.components import substitutions + from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS org_subs = None result = config @@ -375,50 +376,64 @@ class ESPHomeLoader(FastestAvailableSafeLoader): return Remove(str(node.value)) -ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int) -ESPHomeLoader.add_constructor( - "tag:yaml.org,2002:float", ESPHomeLoader.construct_yaml_float -) -ESPHomeLoader.add_constructor( - "tag:yaml.org,2002:binary", ESPHomeLoader.construct_yaml_binary -) -ESPHomeLoader.add_constructor( - "tag:yaml.org,2002:omap", ESPHomeLoader.construct_yaml_omap -) -ESPHomeLoader.add_constructor("tag:yaml.org,2002:str", ESPHomeLoader.construct_yaml_str) -ESPHomeLoader.add_constructor("tag:yaml.org,2002:seq", ESPHomeLoader.construct_yaml_seq) -ESPHomeLoader.add_constructor("tag:yaml.org,2002:map", ESPHomeLoader.construct_yaml_map) -ESPHomeLoader.add_constructor("!env_var", ESPHomeLoader.construct_env_var) -ESPHomeLoader.add_constructor("!secret", ESPHomeLoader.construct_secret) -ESPHomeLoader.add_constructor("!include", ESPHomeLoader.construct_include) -ESPHomeLoader.add_constructor( - "!include_dir_list", ESPHomeLoader.construct_include_dir_list -) -ESPHomeLoader.add_constructor( - "!include_dir_merge_list", ESPHomeLoader.construct_include_dir_merge_list -) -ESPHomeLoader.add_constructor( - "!include_dir_named", ESPHomeLoader.construct_include_dir_named -) -ESPHomeLoader.add_constructor( - "!include_dir_merge_named", ESPHomeLoader.construct_include_dir_merge_named -) -ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) -ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) -ESPHomeLoader.add_constructor("!extend", ESPHomeLoader.construct_extend) -ESPHomeLoader.add_constructor("!remove", ESPHomeLoader.construct_remove) +class ESPHomeLoader(ESPHomeLoaderMixin, FastestAvailableSafeLoader): + """Loader class that keeps track of line numbers.""" -def load_yaml(fname, clear_secrets=True): +class ESPHomePurePythonLoader(ESPHomeLoaderMixin, PurePythonLoader): + """Loader class that keeps track of line numbers.""" + + +for _loader in (ESPHomeLoader, ESPHomePurePythonLoader): + _loader.add_constructor("tag:yaml.org,2002:int", _loader.construct_yaml_int) + _loader.add_constructor("tag:yaml.org,2002:float", _loader.construct_yaml_float) + _loader.add_constructor("tag:yaml.org,2002:binary", _loader.construct_yaml_binary) + _loader.add_constructor("tag:yaml.org,2002:omap", _loader.construct_yaml_omap) + _loader.add_constructor("tag:yaml.org,2002:str", _loader.construct_yaml_str) + _loader.add_constructor("tag:yaml.org,2002:seq", _loader.construct_yaml_seq) + _loader.add_constructor("tag:yaml.org,2002:map", _loader.construct_yaml_map) + _loader.add_constructor("!env_var", _loader.construct_env_var) + _loader.add_constructor("!secret", _loader.construct_secret) + _loader.add_constructor("!include", _loader.construct_include) + _loader.add_constructor("!include_dir_list", _loader.construct_include_dir_list) + _loader.add_constructor( + "!include_dir_merge_list", _loader.construct_include_dir_merge_list + ) + _loader.add_constructor("!include_dir_named", _loader.construct_include_dir_named) + _loader.add_constructor( + "!include_dir_merge_named", _loader.construct_include_dir_merge_named + ) + _loader.add_constructor("!lambda", _loader.construct_lambda) + _loader.add_constructor("!force", _loader.construct_force) + _loader.add_constructor("!extend", _loader.construct_extend) + _loader.add_constructor("!remove", _loader.construct_remove) + + +def load_yaml(fname: str, clear_secrets: bool = True) -> Any: if clear_secrets: _SECRET_VALUES.clear() _SECRET_CACHE.clear() return _load_yaml_internal(fname) -def _load_yaml_internal(fname): +def _load_yaml_internal(fname: str) -> Any: + """Load a YAML file.""" content = read_config_file(fname) - loader = ESPHomeLoader(content) + try: + return _load_yaml_internal_with_type(ESPHomeLoader, fname, content) + except EsphomeError: + # Loading failed, so we now load with the Python loader which has more + # readable exceptions + return _load_yaml_internal_with_type(ESPHomePurePythonLoader, fname, content) + + +def _load_yaml_internal_with_type( + loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], + fname: str, + content: str, +) -> Any: + """Load a YAML file.""" + loader = loader_type(content) loader.name = fname try: return loader.get_single_data() or OrderedDict() diff --git a/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml b/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml new file mode 100644 index 0000000000..aaca55b807 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml @@ -0,0 +1,18 @@ +--- +substitutions: + name: original + +wifi: !include + file: includes/broken_included.yaml.txt + vars: + name: my_custom_ssid + +esphome: + # should be substituted as 'original', + # not overwritten by vars in the !include above + name: ${name} + name_add_mac_suffix: true + platform: esp8266 + board: !include {file: includes/scalar.yaml, vars: {var1: nodemcu}} + + libraries: !include {file: includes/list.yaml, vars: {var1: Wire}} diff --git a/tests/unit_tests/fixtures/yaml_util/includes/broken_included.yaml.txt b/tests/unit_tests/fixtures/yaml_util/includes/broken_included.yaml.txt new file mode 100644 index 0000000000..6e53395c86 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/broken_included.yaml.txt @@ -0,0 +1,5 @@ +--- +# yamllint disable-line + ssid: ${name} +# yamllint disable-line + fdf: error diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 8ee991f5b3..78b6a2ad84 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1,5 +1,6 @@ from esphome import yaml_util from esphome.components import substitutions +from esphome.core import EsphomeError def test_include_with_vars(fixture_path): @@ -11,3 +12,13 @@ def test_include_with_vars(fixture_path): assert actual["esphome"]["libraries"][0] == "Wire" assert actual["esphome"]["board"] == "nodemcu" assert actual["wifi"]["ssid"] == "my_custom_ssid" + + +def test_loading_a_broken_yaml_file(fixture_path): + """Ensure we fallback to pure python to give good errors.""" + yaml_file = fixture_path / "yaml_util" / "broken_includetest.yaml" + + try: + yaml_util.load_yaml(yaml_file) + except EsphomeError as err: + assert "broken_included.yaml" in str(err) From dd2dca4d08593dfddbac7145188a759acbba6423 Mon Sep 17 00:00:00 2001 From: Pieter Frenssen Date: Mon, 15 Jan 2024 01:48:02 +0200 Subject: [PATCH 277/436] Bump pillow to 10.2.0. (#6091) --- esphome/components/font/__init__.py | 8 ++++---- requirements_optional.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index a803c7567b..5b4682a808 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -67,13 +67,13 @@ def validate_pillow_installed(value): except ImportError as err: raise cv.Invalid( "Please install the pillow python package to use this feature. " - '(pip install "pillow==10.1.0")' + '(pip install "pillow==10.2.0")' ) from err - if version.parse(PIL.__version__) != version.parse("10.1.0"): + if version.parse(PIL.__version__) != version.parse("10.2.0"): raise cv.Invalid( - "Please update your pillow installation to 10.1.0. " - '(pip install "pillow==10.1.0")' + "Please update your pillow installation to 10.2.0. " + '(pip install "pillow==10.2.0")' ) return value diff --git a/requirements_optional.txt b/requirements_optional.txt index bc4ea08c92..54494b4585 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,3 +1,3 @@ -pillow==10.1.0 +pillow==10.2.0 cairosvg==2.7.1 cryptography==41.0.4 From 8b2d76e8cebca0055b08319076f5cc8d94f25383 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 14 Jan 2024 16:05:47 -0800 Subject: [PATCH 278/436] convert cse7766 to non-polling (#6095) Co-authored-by: Samuel Sieb --- esphome/components/cse7766/cse7766.cpp | 52 +++++--------------- esphome/components/cse7766/cse7766.h | 11 +---- esphome/components/cse7766/sensor.py | 66 ++++++++++++-------------- 3 files changed, 44 insertions(+), 85 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 60132fd98f..9c5016c503 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -113,8 +113,9 @@ void CSE7766Component::parse_data_() { bool have_voltage = adj & 0x40; if (have_voltage) { // voltage cycle of serial port outputted is a complete cycle; - this->voltage_acc_ += voltage_calib / float(voltage_cycle); - this->voltage_counts_ += 1; + float voltage = voltage_calib / float(voltage_cycle); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); } bool have_power = adj & 0x10; @@ -126,8 +127,8 @@ void CSE7766Component::parse_data_() { if (!power_cycle_exceeds_range) { power = power_calib / float(power_cycle); } - this->power_acc_ += power; - this->power_counts_ += 1; + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); uint32_t difference; if (this->cf_pulses_last_ == 0) { @@ -141,7 +142,10 @@ void CSE7766Component::parse_data_() { } this->cf_pulses_last_ = cf_pulses; this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f; - this->energy_total_counts_ += 1; + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(this->energy_total_); + } else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) { + this->energy_sensor_->publish_state(0); } if (adj & 0x20) { @@ -150,42 +154,13 @@ void CSE7766Component::parse_data_() { if (have_voltage && !have_power) { // Testing has shown that when we have voltage and current but not power, that means the power is 0. // We report a power of 0, which in turn means we should report a current of 0. - this->power_counts_ += 1; + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(0); } else if (power != 0.0f) { current = current_calib / float(current_cycle); } - this->current_acc_ += current; - this->current_counts_ += 1; - } -} -void CSE7766Component::update() { - const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) { - if (counts != 0) { - const auto avg = acc / counts; - - ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%" PRIu32 " %s=%.1f", name, acc, name, counts, name, avg); - - if (sensor != nullptr) { - sensor->publish_state(avg); - } - - acc = 0.0f; - counts = 0; - } - }; - - publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_); - publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_); - publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_); - - if (this->energy_total_counts_ != 0) { - ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%" PRIu32, this->energy_total_, - this->energy_total_counts_); - - if (this->energy_sensor_ != nullptr) { - this->energy_sensor_->publish_state(this->energy_total_); - } - this->energy_total_counts_ = 0; + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); } } @@ -196,7 +171,6 @@ uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) { void CSE7766Component::dump_config() { ESP_LOGCONFIG(TAG, "CSE7766:"); - LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 2f30eec09f..3ab8d609bd 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -7,7 +7,7 @@ namespace esphome { namespace cse7766 { -class CSE7766Component : public PollingComponent, public uart::UARTDevice { +class CSE7766Component : public Component, public uart::UARTDevice { public: void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } @@ -16,7 +16,6 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice { void loop() override; float get_setup_priority() const override; - void update() override; void dump_config() override; protected: @@ -31,16 +30,8 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice { sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; - float voltage_acc_{0.0f}; - float current_acc_{0.0f}; - float power_acc_{0.0f}; float energy_total_{0.0f}; uint32_t cf_pulses_last_{0}; - uint32_t voltage_counts_{0}; - uint32_t current_counts_{0}; - uint32_t power_counts_{0}; - // Setting this to 1 means it will always publish 0 once at startup - uint32_t energy_total_counts_{1}; }; } // namespace cse7766 diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index d98b351287..f2750bb4f2 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -22,43 +22,37 @@ from esphome.const import ( DEPENDENCIES = ["uart"] cse7766_ns = cg.esphome_ns.namespace("cse7766") -CSE7766Component = cse7766_ns.class_( - "CSE7766Component", cg.PollingComponent, uart.UARTDevice -) +CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(CSE7766Component), - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=3, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(uart.UART_DEVICE_SCHEMA) -) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CSE7766Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + } +).extend(uart.UART_DEVICE_SCHEMA) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "cse7766", baud_rate=4800, require_rx=True ) From 83baa24022ef56100dfb6ae2ce02d2bdc0fe0ebe Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 15 Jan 2024 03:07:06 +0100 Subject: [PATCH 279/436] Use touch state from ft63x6 driver. (#6055) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ft63x6/ft63x6.cpp | 65 +++++++++++++--------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp index b674ded22c..f796f0242a 100644 --- a/esphome/components/ft63x6/ft63x6.cpp +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -13,14 +13,14 @@ namespace esphome { namespace ft63x6 { -static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02; - -static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; +static const uint8_t FT63X6_ADDR_TOUCH1_STATE = 0x03; static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; +static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; -static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; +static const uint8_t FT63X6_ADDR_TOUCH2_STATE = 0x09; static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09; +static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B; static const char *const TAG = "FT63X6Touchscreen"; @@ -40,26 +40,11 @@ void FT63X6Touchscreen::setup() { this->hard_reset_(); // Get touch resolution - this->x_raw_max_ = 320; - this->y_raw_max_ = 480; -} - -void FT63X6Touchscreen::update_touches() { - int touch_count = this->read_touch_count_(); - if (touch_count == 0) { - return; + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = 320; } - - uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 - int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); - int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); - this->add_raw_touch_position_(touch_id, x, y); - - if (touch_count >= 2) { - touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01) - x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); - y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); - this->add_raw_touch_position_(touch_id, x, y); + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = 480; } } @@ -76,23 +61,31 @@ void FT63X6Touchscreen::dump_config() { LOG_I2C_DEVICE(this); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_UPDATE_INTERVAL(this); } -uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); } +void FT63X6Touchscreen::update_touches() { + uint8_t data[15]; + uint16_t touch_id, x, y; -// Touch functions -uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) { - uint8_t read_buf[2]; - read_buf[0] = this->read_byte_(coordinate); - read_buf[1] = this->read_byte_(coordinate + 1); - return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; -} -uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; } + if (!this->read_bytes(0x00, (uint8_t *) data, 15)) { + ESP_LOGE(TAG, "Failed to read touch data"); + this->skip_update_ = true; + return; + } -uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { - uint8_t byte = 0; - this->read_byte(addr, &byte); - return byte; + if (((data[FT63X6_ADDR_TOUCH1_STATE] >> 6) & 0x01) == 0) { + touch_id = data[FT63X6_ADDR_TOUCH1_ID] >> 4; // id1 = 0 or 1 + x = encode_uint16(data[FT63X6_ADDR_TOUCH1_X] & 0x0F, data[FT63X6_ADDR_TOUCH1_X + 1]); + y = encode_uint16(data[FT63X6_ADDR_TOUCH1_Y] & 0x0F, data[FT63X6_ADDR_TOUCH1_Y + 1]); + this->add_raw_touch_position_(touch_id, x, y); + } + if (((data[FT63X6_ADDR_TOUCH2_STATE] >> 6) & 0x01) == 0) { + touch_id = data[FT63X6_ADDR_TOUCH2_ID] >> 4; // id1 = 0 or 1 + x = encode_uint16(data[FT63X6_ADDR_TOUCH2_X] & 0x0F, data[FT63X6_ADDR_TOUCH2_X + 1]); + y = encode_uint16(data[FT63X6_ADDR_TOUCH2_Y] & 0x0F, data[FT63X6_ADDR_TOUCH2_Y + 1]); + this->add_raw_touch_position_(touch_id, x, y); + } } } // namespace ft63x6 From e39099137d04c1a7d2214cd33614b6b2449e8c79 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 15 Jan 2024 03:08:10 +0100 Subject: [PATCH 280/436] update script/setup so it works fine on windows (#6087) --- script/setup | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/script/setup b/script/setup index 9f448cf5c4..f286b4672a 100755 --- a/script/setup +++ b/script/setup @@ -4,10 +4,13 @@ set -e cd "$(dirname "$0")/.." - +location="venv/bin/activate" if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ] && [ ! "$ESPHOME_NO_VENV" ]; then python3 -m venv venv - source venv/bin/activate + if [ -f venv/Scripts/activate ]; then + location="venv/Scripts/activate" + fi + source $location; fi # Avoid unsafe git error when running inside devcontainer @@ -25,4 +28,4 @@ script/platformio_install_deps.py platformio.ini --libraries --tools --platforms echo echo -echo "Virtual environment created; source venv/bin/activate to use it" +echo "Virtual environment created. Run 'source $location' to use it." From 8cd17986740e6433e6b557965dfb1b7db79cad0c Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 15 Jan 2024 03:09:35 +0100 Subject: [PATCH 281/436] add Pico-ResTouch-LCD-3.5 (#6078) --- esphome/components/ili9xxx/display.py | 6 +++++ .../components/ili9xxx/ili9xxx_display.cpp | 3 +++ esphome/components/ili9xxx/ili9xxx_display.h | 10 ++++++++- esphome/components/ili9xxx/ili9xxx_init.h | 22 +++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index b3fe8b2b41..af8141f9fc 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -66,6 +66,7 @@ MODELS = { "ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay), "S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay), "S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay), + "WSPICOLCD": ili9xxx_ns.class_("ILI9XXXWSPICOLCD", ILI9XXXDisplay), } COLOR_ORDERS = { @@ -78,6 +79,7 @@ COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" CONF_INVERT_DISPLAY = "invert_display" +CONF_18BIT_MODE = "18bit_mode" def _validate(config): @@ -139,6 +141,7 @@ CONFIG_SCHEMA = cv.All( "'invert_display' has been replaced by 'invert_colors'" ), cv.Optional(CONF_INVERT_COLORS): cv.boolean, + cv.Optional(CONF_18BIT_MODE): cv.boolean, cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True), cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation, cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema( @@ -241,3 +244,6 @@ async def to_code(config): if CONF_INVERT_COLORS in config: cg.add(var.invert_colors(config[CONF_INVERT_COLORS])) + + if CONF_18BIT_MODE in config: + cg.add(var.set_18bit_mode(config[CONF_18BIT_MODE])) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index ab577b3875..2844856246 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -34,6 +34,9 @@ void ILI9XXXDisplay::setup() { mad |= MADCTL_MY; this->send_command(ILI9XXX_MADCTL, &mad, 1); + mad = this->is_18bitdisplay_ ? 0x66 : 0x55; + this->send_command(ILI9XXX_PIXFMT, &mad, 1); + this->x_low_ = this->width_; this->y_low_ = this->height_; this->x_high_ = 0; diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 590be3e364..c470f1e75d 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -53,9 +53,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer, addr += num_args; } } + float get_setup_priority() const override; void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } - float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_palette(const uint8_t *palette) { this->palette_ = palette; } void set_buffer_color_mode(ILI9XXXColorMode color_mode) { this->buffer_color_mode_ = color_mode; } @@ -67,7 +67,10 @@ class ILI9XXXDisplay : public display::DisplayBuffer, this->offset_x_ = offset_x; this->offset_y_ = offset_y; } + void set_18bit_mode(bool mode) { this->is_18bitdisplay_ = mode; }; + void invert_colors(bool invert); + void command(uint8_t value); void data(uint8_t value); void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); @@ -209,5 +212,10 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay { ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {} }; +class ILI9XXXWSPICOLCD : public ILI9XXXDisplay { + public: + ILI9XXXWSPICOLCD() : ILI9XXXDisplay(INITCMD_WSPICOLCD, 320, 480, true) {} +}; + } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index a74824052f..0bf1d5761d 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -316,6 +316,28 @@ static const uint8_t PROGMEM INITCMD_ST7789V[] = { 0x00 // End of list }; +static const uint8_t PROGMEM INITCMD_WSPICOLCD[] = { +ILI9XXX_SLPOUT, 0x80, +ILI9XXX_PIXFMT, 1, 0x66, +ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2 -ok +ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL - ok +ILI9XXX_PWCTR3, 1, 0x44, +//ILI9XXX_VMCTR1, 4, 0x00, 0x00, 0x00, 0x00, +ILI9XXX_VMCTR1, 3, 0x00, 0x12, 0x80, // nVM VCM_REG VCM_REG_EN - not ok? +ILI9XXX_IFMODE, 1, 0x00, // -ok +ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz -- seems to help the background! -ok +ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot - ok +ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan -ok +ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3 +ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, +ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, + +ILI9XXX_INVON, 0x80, +ILI9XXX_MADCTL, 1, 0x48, +ILI9XXX_DISPON, 0x80, +0x00 // End of list +}; + // clang-format on } // namespace ili9xxx } // namespace esphome From 412c999f1497b2935fedaf27d62d1fd3259c54e7 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 15 Jan 2024 07:41:01 +0100 Subject: [PATCH 282/436] Revert "add Pico-ResTouch-LCD-3.5" (#6098) --- esphome/components/ili9xxx/display.py | 6 ----- .../components/ili9xxx/ili9xxx_display.cpp | 3 --- esphome/components/ili9xxx/ili9xxx_display.h | 10 +-------- esphome/components/ili9xxx/ili9xxx_init.h | 22 ------------------- 4 files changed, 1 insertion(+), 40 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index af8141f9fc..b3fe8b2b41 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -66,7 +66,6 @@ MODELS = { "ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay), "S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay), "S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay), - "WSPICOLCD": ili9xxx_ns.class_("ILI9XXXWSPICOLCD", ILI9XXXDisplay), } COLOR_ORDERS = { @@ -79,7 +78,6 @@ COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" CONF_INVERT_DISPLAY = "invert_display" -CONF_18BIT_MODE = "18bit_mode" def _validate(config): @@ -141,7 +139,6 @@ CONFIG_SCHEMA = cv.All( "'invert_display' has been replaced by 'invert_colors'" ), cv.Optional(CONF_INVERT_COLORS): cv.boolean, - cv.Optional(CONF_18BIT_MODE): cv.boolean, cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True), cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation, cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema( @@ -244,6 +241,3 @@ async def to_code(config): if CONF_INVERT_COLORS in config: cg.add(var.invert_colors(config[CONF_INVERT_COLORS])) - - if CONF_18BIT_MODE in config: - cg.add(var.set_18bit_mode(config[CONF_18BIT_MODE])) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 2844856246..ab577b3875 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -34,9 +34,6 @@ void ILI9XXXDisplay::setup() { mad |= MADCTL_MY; this->send_command(ILI9XXX_MADCTL, &mad, 1); - mad = this->is_18bitdisplay_ ? 0x66 : 0x55; - this->send_command(ILI9XXX_PIXFMT, &mad, 1); - this->x_low_ = this->width_; this->y_low_ = this->height_; this->x_high_ = 0; diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index c470f1e75d..590be3e364 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -53,9 +53,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer, addr += num_args; } } - float get_setup_priority() const override; void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_palette(const uint8_t *palette) { this->palette_ = palette; } void set_buffer_color_mode(ILI9XXXColorMode color_mode) { this->buffer_color_mode_ = color_mode; } @@ -67,10 +67,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer, this->offset_x_ = offset_x; this->offset_y_ = offset_y; } - void set_18bit_mode(bool mode) { this->is_18bitdisplay_ = mode; }; - void invert_colors(bool invert); - void command(uint8_t value); void data(uint8_t value); void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); @@ -212,10 +209,5 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay { ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {} }; -class ILI9XXXWSPICOLCD : public ILI9XXXDisplay { - public: - ILI9XXXWSPICOLCD() : ILI9XXXDisplay(INITCMD_WSPICOLCD, 320, 480, true) {} -}; - } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 0bf1d5761d..a74824052f 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -316,28 +316,6 @@ static const uint8_t PROGMEM INITCMD_ST7789V[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_WSPICOLCD[] = { -ILI9XXX_SLPOUT, 0x80, -ILI9XXX_PIXFMT, 1, 0x66, -ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2 -ok -ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL - ok -ILI9XXX_PWCTR3, 1, 0x44, -//ILI9XXX_VMCTR1, 4, 0x00, 0x00, 0x00, 0x00, -ILI9XXX_VMCTR1, 3, 0x00, 0x12, 0x80, // nVM VCM_REG VCM_REG_EN - not ok? -ILI9XXX_IFMODE, 1, 0x00, // -ok -ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz -- seems to help the background! -ok -ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot - ok -ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan -ok -ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3 -ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, -ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, - -ILI9XXX_INVON, 0x80, -ILI9XXX_MADCTL, 1, 0x48, -ILI9XXX_DISPON, 0x80, -0x00 // End of list -}; - // clang-format on } // namespace ili9xxx } // namespace esphome From 87cab92af66ee4340f0cd578f4162eb64533ff27 Mon Sep 17 00:00:00 2001 From: aschmitz <29508+aschmitz@users.noreply.github.com> Date: Mon, 15 Jan 2024 01:08:19 -0600 Subject: [PATCH 283/436] fix: negative temperatures on PMS5003T sensors (#6100) --- esphome/components/pmsx003/pmsx003.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 62488b765c..de2b23b8eb 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -279,7 +279,7 @@ void PMSX003Component::parse_data_() { // Note the pm particles 50um & 100um are not returned, // as PMS5003T uses those data values for temperature and humidity. - float temperature = this->get_16_bit_uint_(24) / 10.0f; + float temperature = (int16_t) this->get_16_bit_uint_(24) / 10.0f; float humidity = this->get_16_bit_uint_(26) / 10.0f; ESP_LOGD(TAG, From 72ab1700e71305038d8f8c71f3adb53bed64c526 Mon Sep 17 00:00:00 2001 From: mathieu-mp Date: Tue, 16 Jan 2024 07:38:09 +0100 Subject: [PATCH 284/436] Add triangle shapes to display component (#6096) --- esphome/components/display/display.cpp | 116 +++++++++++++++++++++++++ esphome/components/display/display.h | 15 ++++ 2 files changed, 131 insertions(+) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index f32fda4794..0c3631342e 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -141,6 +141,122 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color) } } while (dx <= 0); } +void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { + this->line(x1, y1, x2, y2); + this->line(x1, y1, x3, y3); + this->line(x2, y2, x3, y3); +} +void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) { + if (*y1 > *y2) { + int x_temp = *x1, y_temp = *y1; + *x1 = *x2, *y1 = *y2; + *x2 = x_temp, *y2 = y_temp; + } + if (*y1 > *y3) { + int x_temp = *x1, y_temp = *y1; + *x1 = *x3, *y1 = *y3; + *x3 = x_temp, *y3 = y_temp; + } + if (*y2 > *y3) { + int x_temp = *x2, y_temp = *y2; + *x2 = *x3, *y2 = *y3; + *x3 = x_temp, *y3 = y_temp; + } +} +void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { + // y2 must be equal to y3 (same horizontal line) + + // Initialize Bresenham's algorithm for side 1 + int s1_current_x = x1; + int s1_current_y = y1; + bool s1_axis_swap = false; + int s1_dx = abs(x2 - x1); + int s1_dy = abs(y2 - y1); + int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1; + int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1; + if (s1_dy > s1_dx) { // swap values + int tmp = s1_dx; + s1_dx = s1_dy; + s1_dy = tmp; + s1_axis_swap = true; + } + int s1_error = 2 * s1_dy - s1_dx; + + // Initialize Bresenham's algorithm for side 2 + int s2_current_x = x1; + int s2_current_y = y1; + bool s2_axis_swap = false; + int s2_dx = abs(x3 - x1); + int s2_dy = abs(y3 - y1); + int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1; + int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1; + if (s2_dy > s2_dx) { // swap values + int tmp = s2_dx; + s2_dx = s2_dy; + s2_dy = tmp; + s2_axis_swap = true; + } + int s2_error = 2 * s2_dy - s2_dx; + + // Iterate on side 1 and allow side 2 to be processed to match the advance of the y-axis. + for (int i = 0; i <= s1_dx; i++) { + if (s1_current_x <= s2_current_x) { + this->horizontal_line(s1_current_x, s1_current_y, s2_current_x - s1_current_x + 1, color); + } else { + this->horizontal_line(s2_current_x, s2_current_y, s1_current_x - s2_current_x + 1, color); + } + + // Bresenham's #1 + // Side 1 s1_current_x and s1_current_y calculation + while (s1_error >= 0) { + if (s1_axis_swap) { + s1_current_x += s1_sign_x; + } else { + s1_current_y += s1_sign_y; + } + s1_error = s1_error - 2 * s1_dx; + } + if (s1_axis_swap) { + s1_current_y += s1_sign_y; + } else { + s1_current_x += s1_sign_x; + } + s1_error = s1_error + 2 * s1_dy; + + // Bresenham's #2 + // Side 2 s2_current_x and s2_current_y calculation + while (s2_current_y != s1_current_y) { + while (s2_error >= 0) { + if (s2_axis_swap) { + s2_current_x += s2_sign_x; + } else { + s2_current_y += s2_sign_y; + } + s2_error = s2_error - 2 * s2_dx; + } + if (s2_axis_swap) { + s2_current_y += s2_sign_y; + } else { + s2_current_x += s2_sign_x; + } + s2_error = s2_error + 2 * s2_dy; + } + } +} +void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { + // Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point + this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3); + + if (y2 == y3) { // Check for special case of a bottom-flat triangle + this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color); + } else if (y1 == y2) { // Check for special case of a top-flat triangle + this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color); + } else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle + int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2; + this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color); + this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color); + } +} void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) { int x_start, y_start; diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 2a2a9b80c8..daa5028d6b 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -236,6 +236,12 @@ class Display : public PollingComponent { /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); + /// Draw the outline of a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color. + void triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON); + + /// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color. + void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON); + /** Print `text` with the anchor point at [x,y] with `font`. * * @param x The x coordinate of the text alignment anchor point. @@ -532,6 +538,15 @@ class Display : public PollingComponent { void do_update_(); void clear_clipping_(); + /** + * This method fills a triangle using only integer variables by using a + * modified bresenham algorithm. + * It is mandatory that [x2,y2] and [x3,y3] lie on the same horizontal line, + * so y2 must be equal to y3. + */ + void filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color); + void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); + DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; optional writer_{}; DisplayPage *page_{nullptr}; From 249cd6758879c56b72cc1b165a02cd8df1e551f0 Mon Sep 17 00:00:00 2001 From: alexborro Date: Tue, 16 Jan 2024 08:38:19 +0100 Subject: [PATCH 285/436] Fingerprint_grow: Trigger on finger scan start and on finger scan misplaced (#6003) --- .../components/fingerprint_grow/__init__.py | 32 +++++++++++++ .../fingerprint_grow/fingerprint_grow.cpp | 46 +++++++++++++------ .../fingerprint_grow/fingerprint_grow.h | 24 ++++++++++ esphome/const.py | 2 + tests/test3.yaml | 6 +++ 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 5249107f17..26a01fc1d2 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -13,8 +13,10 @@ from esphome.const import ( CONF_ON_ENROLLMENT_DONE, CONF_ON_ENROLLMENT_FAILED, CONF_ON_ENROLLMENT_SCAN, + CONF_ON_FINGER_SCAN_START, CONF_ON_FINGER_SCAN_MATCHED, CONF_ON_FINGER_SCAN_UNMATCHED, + CONF_ON_FINGER_SCAN_MISPLACED, CONF_ON_FINGER_SCAN_INVALID, CONF_PASSWORD, CONF_SENSING_PIN, @@ -35,6 +37,10 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_( "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice ) +FingerScanStartTrigger = fingerprint_grow_ns.class_( + "FingerScanStartTrigger", automation.Trigger.template() +) + FingerScanMatchedTrigger = fingerprint_grow_ns.class_( "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) ) @@ -43,6 +49,10 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( "FingerScanUnmatchedTrigger", automation.Trigger.template() ) +FingerScanMisplacedTrigger = fingerprint_grow_ns.class_( + "FingerScanMisplacedTrigger", automation.Trigger.template() +) + FingerScanInvalidTrigger = fingerprint_grow_ns.class_( "FingerScanInvalidTrigger", automation.Trigger.template() ) @@ -99,6 +109,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_PASSWORD): cv.uint32_t, cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, + cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanStartTrigger + ), + } + ), cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -113,6 +130,13 @@ CONFIG_SCHEMA = ( ), } ), + cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanMisplacedTrigger + ), + } + ), cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -164,6 +188,10 @@ async def to_code(config): sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN]) cg.add(var.set_sensing_pin(sensing_pin)) + for conf in config.get(CONF_ON_FINGER_SCAN_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( @@ -174,6 +202,10 @@ async def to_code(config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 2486e02964..0a46755bd3 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -15,16 +15,18 @@ void FingerprintGrowComponent::update() { return; } - if (this->sensing_pin_ != nullptr) { + if (this->has_sensing_pin_) { if (this->sensing_pin_->digital_read()) { ESP_LOGV(TAG, "No touch sensing"); this->waiting_removal_ = false; return; + } else if (!this->waiting_removal_) { + this->finger_scan_start_callback_.call(); } } if (this->waiting_removal_) { - if (this->scan_image_(1) == NO_FINGER) { + if ((!this->has_sensing_pin_) && (this->scan_image_(1) == NO_FINGER)) { ESP_LOGD(TAG, "Finger removed"); this->waiting_removal_ = false; } @@ -51,6 +53,7 @@ void FingerprintGrowComponent::update() { void FingerprintGrowComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); + this->has_sensing_pin_ = (this->sensing_pin_ != nullptr); if (this->check_password_()) { if (this->new_password_ != -1) { if (this->set_password_()) @@ -91,7 +94,7 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) { } void FingerprintGrowComponent::scan_and_match_() { - if (this->sensing_pin_ != nullptr) { + if (this->has_sensing_pin_) { ESP_LOGD(TAG, "Scan and match"); } else { ESP_LOGV(TAG, "Scan and match"); @@ -122,33 +125,38 @@ void FingerprintGrowComponent::scan_and_match_() { } uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { - if (this->sensing_pin_ != nullptr) { + if (this->has_sensing_pin_) { ESP_LOGD(TAG, "Getting image %d", buffer); } else { ESP_LOGV(TAG, "Getting image %d", buffer); } this->data_ = {GET_IMAGE}; - switch (this->send_command_()) { + uint8_t send_result = this->send_command_(); + switch (send_result) { case OK: break; case NO_FINGER: - if (this->sensing_pin_ != nullptr) { - ESP_LOGD(TAG, "No finger"); - this->finger_scan_invalid_callback_.call(); + if (this->has_sensing_pin_) { + this->waiting_removal_ = true; + ESP_LOGD(TAG, "Finger Misplaced"); + this->finger_scan_misplaced_callback_.call(); } else { ESP_LOGV(TAG, "No finger"); } - return this->data_[0]; + return send_result; case IMAGE_FAIL: ESP_LOGE(TAG, "Imaging error"); this->finger_scan_invalid_callback_.call(); + return send_result; default: - return this->data_[0]; + ESP_LOGD(TAG, "Unknown Scan Error: %d", send_result); + return send_result; } ESP_LOGD(TAG, "Processing image %d", buffer); this->data_ = {IMAGE_2_TZ, buffer}; - switch (this->send_command_()) { + send_result = this->send_command_(); + switch (send_result) { case OK: ESP_LOGI(TAG, "Processed image %d", buffer); break; @@ -162,7 +170,7 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { this->finger_scan_invalid_callback_.call(); break; } - return this->data_[0]; + return send_result; } uint8_t FingerprintGrowComponent::save_fingerprint_() { @@ -225,10 +233,11 @@ bool FingerprintGrowComponent::get_parameters_() { ESP_LOGD(TAG, "Getting parameters"); this->data_ = {READ_SYS_PARAM}; if (this->send_command_() == OK) { - ESP_LOGD(TAG, "Got parameters"); - if (this->status_sensor_ != nullptr) { + ESP_LOGD(TAG, "Got parameters"); // Bear in mind data_[0] is the transfer status, + if (this->status_sensor_ != nullptr) { // the parameters table start at data_[1] this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); } + this->system_identifier_code_ = ((uint16_t) this->data_[3] << 8) | this->data_[4]; this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6]; if (this->capacity_sensor_ != nullptr) { this->capacity_sensor_->publish_state(this->capacity_); @@ -430,13 +439,22 @@ uint8_t FingerprintGrowComponent::send_command_() { void FingerprintGrowComponent::dump_config() { ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:"); + ESP_LOGCONFIG(TAG, " System Identifier Code: 0x%.4X", this->system_identifier_code_); + ESP_LOGCONFIG(TAG, " Touch Sensing Pin: %s", + this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None"); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->fingerprint_count_sensor_->get_state()); LOG_SENSOR(" ", "Status", this->status_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->status_sensor_->get_state()); LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->capacity_sensor_->get_state()); LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->security_level_sensor_->get_state()); LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_finger_id_sensor_->get_state()); LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_confidence_sensor_->get_state()); } } // namespace fingerprint_grow diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 9aad94fc2a..1ab38d9fb5 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -118,12 +118,18 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { this->enrolling_binary_sensor_ = enrolling_binary_sensor; } + void add_on_finger_scan_start_callback(std::function callback) { + this->finger_scan_start_callback_.add(std::move(callback)); + } void add_on_finger_scan_matched_callback(std::function callback) { this->finger_scan_matched_callback_.add(std::move(callback)); } void add_on_finger_scan_unmatched_callback(std::function callback) { this->finger_scan_unmatched_callback_.add(std::move(callback)); } + void add_on_finger_scan_misplaced_callback(std::function callback) { + this->finger_scan_misplaced_callback_.add(std::move(callback)); + } void add_on_finger_scan_invalid_callback(std::function callback) { this->finger_scan_invalid_callback_.add(std::move(callback)); } @@ -166,8 +172,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; uint8_t enrollment_buffers_ = 5; bool waiting_removal_ = false; + bool has_sensing_pin_ = false; uint32_t last_aura_led_control_ = 0; uint16_t last_aura_led_duration_ = 0; + uint16_t system_identifier_code_ = 0; sensor::Sensor *fingerprint_count_sensor_{nullptr}; sensor::Sensor *status_sensor_{nullptr}; sensor::Sensor *capacity_sensor_{nullptr}; @@ -176,13 +184,22 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic sensor::Sensor *last_confidence_sensor_{nullptr}; binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; CallbackManager finger_scan_invalid_callback_; + CallbackManager finger_scan_start_callback_; CallbackManager finger_scan_matched_callback_; CallbackManager finger_scan_unmatched_callback_; + CallbackManager finger_scan_misplaced_callback_; CallbackManager enrollment_scan_callback_; CallbackManager enrollment_done_callback_; CallbackManager enrollment_failed_callback_; }; +class FingerScanStartTrigger : public Trigger<> { + public: + explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_start_callback([this]() { this->trigger(); }); + } +}; + class FingerScanMatchedTrigger : public Trigger { public: explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { @@ -198,6 +215,13 @@ class FingerScanUnmatchedTrigger : public Trigger<> { } }; +class FingerScanMisplacedTrigger : public Trigger<> { + public: + explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); }); + } +}; + class FingerScanInvalidTrigger : public Trigger<> { public: explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) { diff --git a/esphome/const.py b/esphome/const.py index 7e27254d76..c35adc74ee 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -508,6 +508,8 @@ CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" CONF_ON_FINGER_SCAN_INVALID = "on_finger_scan_invalid" CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" +CONF_ON_FINGER_SCAN_MISPLACED = "on_finger_scan_misplaced" +CONF_ON_FINGER_SCAN_START = "on_finger_scan_start" CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched" CONF_ON_JSON_MESSAGE = "on_json_message" CONF_ON_LOCK = "on_lock" diff --git a/tests/test3.yaml b/tests/test3.yaml index c31eb45fbd..cbd3d15b8a 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1258,6 +1258,9 @@ fingerprint_grow: number: 4 password: 0x12FE37DC new_password: 0xA65B9840 + on_finger_scan_start: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_finger_scan_start on_finger_scan_invalid: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_finger_scan_invalid @@ -1270,6 +1273,9 @@ fingerprint_grow: on_finger_scan_unmatched: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_finger_scan_unmatched + on_finger_scan_misplaced: + - homeassistant.event: + event: esphome.${device_name}_fingerprint_grow_finger_scan_misplaced on_enrollment_scan: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_enrollment_scan From 26acbbedbf2626eec1232343901a01348369a8ce Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 15 Jan 2024 23:44:12 -0800 Subject: [PATCH 286/436] Add continuous option to the graph (#6093) Co-authored-by: Samuel Sieb --- esphome/components/graph/__init__.py | 4 +++ esphome/components/graph/graph.cpp | 37 +++++++++++++++++++++++----- esphome/components/graph/graph.h | 3 +++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 046f59ca1a..0b83b71fe4 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -61,6 +61,7 @@ VALUE_POSITION_TYPE = { "BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW, } +CONF_CONTINUOUS = "continuous" GRAPH_TRACE_SCHEMA = cv.Schema( { @@ -70,6 +71,7 @@ GRAPH_TRACE_SCHEMA = cv.Schema( cv.Optional(CONF_LINE_THICKNESS): cv.positive_int, cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True), cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_CONTINUOUS): cv.boolean, } ) @@ -186,6 +188,8 @@ async def to_code(config): if CONF_COLOR in trace: c = await cg.get_variable(trace[CONF_COLOR]) cg.add(tr.set_line_color(c)) + if CONF_CONTINUOUS in trace: + cg.add(tr.set_continuous(trace[CONF_CONTINUOUS])) cg.add(var.add_trace(tr)) # Add legend if CONF_LEGEND in config: diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 294e16dbb1..0e437a3425 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -165,17 +165,42 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo for (auto *trace : traces_) { Color c = trace->get_line_color(); uint16_t thick = trace->get_line_thickness(); + bool continuous = trace->get_continuous(); + bool has_prev = false; + bool prev_b = false; + int16_t prev_y = 0; for (uint32_t i = 0; i < this->width_; i++) { float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; if (!std::isnan(v) && (thick > 0)) { - int16_t x = this->width_ - 1 - i; - uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick; - if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) { - int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2; - for (uint16_t t = 0; t < thick; t++) { - buff->draw_pixel_at(x_offset + x, y_offset + y + t, c); + int16_t x = this->width_ - 1 - i + x_offset; + uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick); + bool b = (trace->get_line_type() & bit) == bit; + if (b) { + int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; + if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { + for (uint16_t t = 0; t < thick; t++) { + buff->draw_pixel_at(x, y + t, c); + } + } else { + int16_t mid_y = (y + prev_y + thick) / 2; + if (y > prev_y) { + for (uint16_t t = prev_y + thick; t <= mid_y; t++) + buff->draw_pixel_at(x + 1, t, c); + for (uint16_t t = mid_y + 1; t < y + thick; t++) + buff->draw_pixel_at(x, t, c); + } else { + for (uint16_t t = prev_y - 1; t >= mid_y; t--) + buff->draw_pixel_at(x + 1, t, c); + for (uint16_t t = mid_y - 1; t >= y; t--) + buff->draw_pixel_at(x, t, c); + } } + prev_y = y; } + prev_b = b; + has_prev = true; + } else { + has_prev = false; } } } diff --git a/esphome/components/graph/graph.h b/esphome/components/graph/graph.h index 339a6f6d94..34accb7d3a 100644 --- a/esphome/components/graph/graph.h +++ b/esphome/components/graph/graph.h @@ -116,6 +116,8 @@ class GraphTrace { void set_line_type(enum LineType val) { this->line_type_ = val; } Color get_line_color() { return this->line_color_; } void set_line_color(Color val) { this->line_color_ = val; } + bool get_continuous() { return this->continuous_; } + void set_continuous(bool continuous) { this->continuous_ = continuous; } std::string get_name() { return name_; } const HistoryData *get_tracedata() { return &data_; } @@ -125,6 +127,7 @@ class GraphTrace { uint8_t line_thickness_{3}; enum LineType line_type_ { LINE_TYPE_SOLID }; Color line_color_{COLOR_ON}; + bool continuous_{false}; HistoryData data_; friend Graph; From e35cab018a464c0d39fe7916a2b5de9d7c9487e7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 16 Jan 2024 02:05:13 -0600 Subject: [PATCH 287/436] Add NFC binary sensor platform (#6068) --- CODEOWNERS | 2 +- esphome/components/nfc/__init__.py | 5 +- .../components/nfc/binary_sensor/__init__.py | 72 +++++++++++ .../nfc/binary_sensor/binary_sensor.cpp | 114 ++++++++++++++++++ .../nfc/binary_sensor/binary_sensor.h | 38 ++++++ esphome/components/nfc/nfc.h | 14 +++ esphome/components/pn7150/__init__.py | 2 +- esphome/components/pn7150/pn7150.cpp | 6 + esphome/components/pn7150/pn7150.h | 2 +- esphome/components/pn7160/__init__.py | 2 +- esphome/components/pn7160/pn7160.cpp | 6 + esphome/components/pn7160/pn7160.h | 2 +- tests/test1.yaml | 13 ++ 13 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 esphome/components/nfc/binary_sensor/__init__.py create mode 100644 esphome/components/nfc/binary_sensor/binary_sensor.cpp create mode 100644 esphome/components/nfc/binary_sensor/binary_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 0ff5ce4508..c497a82eab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -227,7 +227,7 @@ esphome/components/nextion/binary_sensor/* @senexcrenshaw esphome/components/nextion/sensor/* @senexcrenshaw esphome/components/nextion/switch/* @senexcrenshaw esphome/components/nextion/text_sensor/* @senexcrenshaw -esphome/components/nfc/* @jesserockz +esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core diff --git a/esphome/components/nfc/__init__.py b/esphome/components/nfc/__init__.py index c3bbc50bf9..eea1a47b24 100644 --- a/esphome/components/nfc/__init__.py +++ b/esphome/components/nfc/__init__.py @@ -1,12 +1,13 @@ from esphome import automation import esphome.codegen as cg -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kbx81"] nfc_ns = cg.esphome_ns.namespace("nfc") +Nfcc = nfc_ns.class_("Nfcc") NfcTag = nfc_ns.class_("NfcTag") - +NfcTagListener = nfc_ns.class_("NfcTagListener") NfcOnTagTrigger = nfc_ns.class_( "NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag) ) diff --git a/esphome/components/nfc/binary_sensor/__init__.py b/esphome/components/nfc/binary_sensor/__init__.py new file mode 100644 index 0000000000..21c8298ea8 --- /dev/null +++ b/esphome/components/nfc/binary_sensor/__init__.py @@ -0,0 +1,72 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_UID +from esphome.core import HexInt +from .. import nfc_ns, Nfcc, NfcTagListener + +DEPENDENCIES = ["nfc"] + +CONF_NDEF_CONTAINS = "ndef_contains" +CONF_NFCC_ID = "nfcc_id" +CONF_TAG_ID = "tag_id" + +NfcTagBinarySensor = nfc_ns.class_( + "NfcTagBinarySensor", + binary_sensor.BinarySensor, + cg.Component, + NfcTagListener, + cg.Parented.template(Nfcc), +) + + +def validate_uid(value): + value = cv.string_strict(value) + for x in value.split("-"): + if len(x) != 2: + raise cv.Invalid( + "Each part (separated by '-') of the UID must be two characters " + "long." + ) + try: + x = int(x, 16) + except ValueError as err: + raise cv.Invalid( + "Valid characters for parts of a UID are 0123456789ABCDEF." + ) from err + if x < 0 or x > 255: + raise cv.Invalid( + "Valid values for UID parts (separated by '-') are 00 to FF" + ) + return value + + +CONFIG_SCHEMA = cv.All( + binary_sensor.binary_sensor_schema(NfcTagBinarySensor) + .extend( + { + cv.GenerateID(CONF_NFCC_ID): cv.use_id(Nfcc), + cv.Optional(CONF_NDEF_CONTAINS): cv.string, + cv.Optional(CONF_TAG_ID): cv.string, + cv.Optional(CONF_UID): validate_uid, + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_NDEF_CONTAINS, CONF_TAG_ID, CONF_UID), +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_NFCC_ID]) + + hub = await cg.get_variable(config[CONF_NFCC_ID]) + cg.add(hub.register_listener(var)) + if CONF_NDEF_CONTAINS in config: + cg.add(var.set_ndef_match_string(config[CONF_NDEF_CONTAINS])) + if CONF_TAG_ID in config: + cg.add(var.set_tag_name(config[CONF_TAG_ID])) + elif CONF_UID in config: + addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")] + cg.add(var.set_uid(addr)) diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.cpp b/esphome/components/nfc/binary_sensor/binary_sensor.cpp new file mode 100644 index 0000000000..8f1f6acd51 --- /dev/null +++ b/esphome/components/nfc/binary_sensor/binary_sensor.cpp @@ -0,0 +1,114 @@ +#include "binary_sensor.h" +#include "../nfc_helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.binary_sensor"; + +void NfcTagBinarySensor::setup() { + this->parent_->register_listener(this); + this->publish_initial_state(false); +} + +void NfcTagBinarySensor::dump_config() { + std::string match_str = "name"; + + LOG_BINARY_SENSOR("", "NFC Tag Binary Sensor", this); + if (!this->match_string_.empty()) { + if (!this->match_tag_name_) { + match_str = "contains"; + } + ESP_LOGCONFIG(TAG, " Tag %s: %s", match_str.c_str(), this->match_string_.c_str()); + return; + } + if (!this->uid_.empty()) { + ESP_LOGCONFIG(TAG, " Tag UID: %s", format_bytes(this->uid_).c_str()); + } +} + +void NfcTagBinarySensor::set_ndef_match_string(const std::string &str) { + this->match_string_ = str; + this->match_tag_name_ = false; +} + +void NfcTagBinarySensor::set_tag_name(const std::string &str) { + this->match_string_ = str; + this->match_tag_name_ = true; +} + +void NfcTagBinarySensor::set_uid(const std::vector &uid) { this->uid_ = uid; } + +bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr &msg) { + for (const auto &record : msg->get_records()) { + if (record->get_payload().find(this->match_string_) != std::string::npos) { + return true; + } + } + return false; +} + +bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr &msg) { + for (const auto &record : msg->get_records()) { + if (record->get_payload().find(HA_TAG_ID_PREFIX) != std::string::npos) { + auto rec_substr = record->get_payload().substr(sizeof(HA_TAG_ID_PREFIX) - 1); + if (rec_substr.find(this->match_string_) != std::string::npos) { + return true; + } + } + } + return false; +} + +bool NfcTagBinarySensor::tag_match_uid(const std::vector &data) { + if (data.size() != this->uid_.size()) { + return false; + } + + for (size_t i = 0; i < data.size(); i++) { + if (data[i] != this->uid_[i]) { + return false; + } + } + return true; +} + +void NfcTagBinarySensor::tag_off(NfcTag &tag) { + if (!this->match_string_.empty() && tag.has_ndef_message()) { + if (this->match_tag_name_) { + if (this->tag_match_tag_name(tag.get_ndef_message())) { + this->publish_state(false); + } + } else { + if (this->tag_match_ndef_string(tag.get_ndef_message())) { + this->publish_state(false); + } + } + return; + } + if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) { + this->publish_state(false); + } +} + +void NfcTagBinarySensor::tag_on(NfcTag &tag) { + if (!this->match_string_.empty() && tag.has_ndef_message()) { + if (this->match_tag_name_) { + if (this->tag_match_tag_name(tag.get_ndef_message())) { + this->publish_state(true); + } + } else { + if (this->tag_match_ndef_string(tag.get_ndef_message())) { + this->publish_state(true); + } + } + return; + } + if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) { + this->publish_state(true); + } +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.h b/esphome/components/nfc/binary_sensor/binary_sensor.h new file mode 100644 index 0000000000..cc313c2f2b --- /dev/null +++ b/esphome/components/nfc/binary_sensor/binary_sensor.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_tag.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace nfc { + +class NfcTagBinarySensor : public binary_sensor::BinarySensor, + public Component, + public NfcTagListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_ndef_match_string(const std::string &str); + void set_tag_name(const std::string &str); + void set_uid(const std::vector &uid); + + bool tag_match_ndef_string(const std::shared_ptr &msg); + bool tag_match_tag_name(const std::shared_ptr &msg); + bool tag_match_uid(const std::vector &data); + + void tag_off(NfcTag &tag) override; + void tag_on(NfcTag &tag) override; + + protected: + bool match_tag_name_{false}; + std::string match_string_; + std::vector uid_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index d4d66f970f..23bfdd8ef0 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -66,5 +66,19 @@ bool mifare_classic_is_trailer_block(uint8_t block_num); uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length); +class NfcTagListener { + public: + virtual void tag_off(NfcTag &tag) {} + virtual void tag_on(NfcTag &tag) {} +}; + +class Nfcc { + public: + void register_listener(NfcTagListener *listener) { this->tag_listeners_.push_back(listener); } + + protected: + std::vector tag_listeners_; +}; + } // namespace nfc } // namespace esphome diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 3b80b574e9..a136028011 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -34,7 +34,7 @@ CONF_TAG_TTL = "tag_ttl" CONF_VEN_PIN = "ven_pin" pn7150_ns = cg.esphome_ns.namespace("pn7150") -PN7150 = pn7150_ns.class_("PN7150", cg.Component) +PN7150 = pn7150_ns.class_("PN7150", nfc.Nfcc, cg.Component) EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action) EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action) diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 6703ab6a12..be4d6c1bb7 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -566,6 +566,9 @@ void PN7150::erase_tag_(const uint8_t tag_index) { for (auto *trigger : this->triggers_ontagremoved_) { trigger->process(this->discovered_endpoint_[tag_index].tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_off(*this->discovered_endpoint_[tag_index].tag); + } ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); } @@ -881,6 +884,9 @@ void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi for (auto *trigger : this->triggers_ontag_) { trigger->process(working_endpoint.tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_on(*working_endpoint.tag); + } working_endpoint.trig_called = true; break; } diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 4aad4e1720..54038f5085 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -142,7 +142,7 @@ struct DiscoveredEndpoint { bool trig_called; }; -class PN7150 : public Component { +class PN7150 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index c91ca78b03..1639041b9e 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -36,7 +36,7 @@ CONF_VEN_PIN = "ven_pin" CONF_WKUP_REQ_PIN = "wkup_req_pin" pn7160_ns = cg.esphome_ns.namespace("pn7160") -PN7160 = pn7160_ns.class_("PN7160", cg.Component) +PN7160 = pn7160_ns.class_("PN7160", nfc.Nfcc, cg.Component) EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action) EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action) diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index ce5374d1d1..a7d3b38fb7 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -591,6 +591,9 @@ void PN7160::erase_tag_(const uint8_t tag_index) { for (auto *trigger : this->triggers_ontagremoved_) { trigger->process(this->discovered_endpoint_[tag_index].tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_off(*this->discovered_endpoint_[tag_index].tag); + } ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); } @@ -905,6 +908,9 @@ void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi for (auto *trigger : this->triggers_ontag_) { trigger->process(working_endpoint.tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_on(*working_endpoint.tag); + } working_endpoint.trig_called = true; break; } diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 2b3cb99453..f2e05ea1d0 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -157,7 +157,7 @@ struct DiscoveredEndpoint { bool trig_called; }; -class PN7160 : public Component { +class PN7160 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; diff --git a/tests/test1.yaml b/tests/test1.yaml index 3ca6faca8a..471e2a71a5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2028,6 +2028,18 @@ binary_sensor: - platform: dfrobot_sen0395 id: mmwave_detected_uart dfrobot_sen0395_id: mmwave + - platform: nfc + nfcc_id: nfcc_pn7160_i2c + ndef_contains: pulse + name: MFC Tag 1 + - platform: nfc + nfcc_id: nfcc_pn7160_i2c + tag_id: pulse + name: MFC Tag 2 + - platform: nfc + nfcc_id: nfcc_pn7160_i2c + uid: 59-FC-AB-15 + name: MFC Tag 3 pca9685: frequency: 500 @@ -3453,6 +3465,7 @@ pn532_i2c: i2c_id: i2c_bus pn7150_i2c: + id: nfcc_pn7150_i2c i2c_id: i2c_bus irq_pin: allow_other_uses: true From ea03058ace0d46de52c020b10daa39524bb5670c Mon Sep 17 00:00:00 2001 From: Piotr Majkrzak Date: Tue, 16 Jan 2024 09:10:44 +0100 Subject: [PATCH 288/436] Fix RMT timing clock base (#6101) --- esphome/components/esp32_rmt_led_strip/led_strip.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index df6ee2ce2f..3df4077c96 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -13,6 +13,8 @@ namespace esp32_rmt_led_strip { static const char *const TAG = "esp32_rmt_led_strip"; +static const uint32_t RMT_CLK_FREQ = 80000000; + static const uint8_t RMT_CLK_DIV = 2; void ESP32RMTLEDStripLightOutput::setup() { @@ -65,7 +67,7 @@ void ESP32RMTLEDStripLightOutput::setup() { void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low) { - float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f; + float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; // 0-bit this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); From 21337ffc67096d9256b3dd34428953b47c8283bf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 16 Jan 2024 21:37:57 +1300 Subject: [PATCH 289/436] Create RingBuffer for VoiceAssistant (#6102) --- .../voice_assistant/voice_assistant.cpp | 32 ++++-------- .../voice_assistant/voice_assistant.h | 4 +- esphome/core/ring_buffer.cpp | 49 +++++++++++++++++++ esphome/core/ring_buffer.h | 34 +++++++++++++ 4 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 esphome/core/ring_buffer.cpp create mode 100644 esphome/core/ring_buffer.h diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 29fc664342..299e624f5f 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -86,14 +86,14 @@ void VoiceAssistant::setup() { #ifdef USE_ESP_ADF this->vad_instance_ = vad_create(VAD_MODE_4); +#endif - this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); + this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t)); if (this->ring_buffer_ == nullptr) { ESP_LOGW(TAG, "Could not allocate ring buffer"); this->mark_failed(); return; } -#endif ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); @@ -112,14 +112,8 @@ int VoiceAssistant::read_microphone_() { memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t)); return 0; } -#ifdef USE_ESP_ADF // Write audio into ring buffer - int available = rb_bytes_available(this->ring_buffer_); - if (available < bytes_read) { - rb_read(this->ring_buffer_, nullptr, bytes_read - available, 0); - } - rb_write(this->ring_buffer_, (char *) this->input_buffer_, bytes_read, 0); -#endif + this->ring_buffer_->write((void *) this->input_buffer_, bytes_read); } else { ESP_LOGD(TAG, "microphone not running"); } @@ -141,9 +135,9 @@ void VoiceAssistant::loop() { switch (this->state_) { case State::IDLE: { if (this->continuous_ && this->desired_state_ == State::IDLE) { + this->ring_buffer_->reset(); #ifdef USE_ESP_ADF if (this->use_wake_word_) { - rb_reset(this->ring_buffer_); this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD); } else #endif @@ -236,19 +230,13 @@ void VoiceAssistant::loop() { break; // State changed when udp server port received } case State::STREAMING_MICROPHONE: { - size_t bytes_read = this->read_microphone_(); -#ifdef USE_ESP_ADF - if (rb_bytes_filled(this->ring_buffer_) >= SEND_BUFFER_SIZE) { - rb_read(this->ring_buffer_, (char *) this->send_buffer_, SEND_BUFFER_SIZE, 0); + this->read_microphone_(); + if (this->ring_buffer_->available() >= SEND_BUFFER_SIZE) { + this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); this->socket_->sendto(this->send_buffer_, SEND_BUFFER_SIZE, 0, (struct sockaddr *) &this->dest_addr_, sizeof(this->dest_addr_)); } -#else - if (bytes_read > 0) { - this->socket_->sendto(this->input_buffer_, bytes_read, 0, (struct sockaddr *) &this->dest_addr_, - sizeof(this->dest_addr_)); - } -#endif + break; } case State::STOP_MICROPHONE: { @@ -473,9 +461,9 @@ void VoiceAssistant::request_start(bool continuous, bool silence_detection) { if (this->state_ == State::IDLE) { this->continuous_ = continuous; this->silence_detection_ = silence_detection; + this->ring_buffer_->reset(); #ifdef USE_ESP_ADF if (this->use_wake_word_) { - rb_reset(this->ring_buffer_); this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD); } else #endif @@ -618,9 +606,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { case api::enums::VOICE_ASSISTANT_RUN_END: { ESP_LOGD(TAG, "Assist Pipeline ended"); if (this->state_ == State::STREAMING_MICROPHONE) { + this->ring_buffer_->reset(); #ifdef USE_ESP_ADF if (this->use_wake_word_) { - rb_reset(this->ring_buffer_); // No need to stop the microphone since we didn't use the speaker this->set_state_(State::WAIT_FOR_VAD, State::WAITING_FOR_VAD); } else diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index f9325dff54..d996efe08e 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -7,6 +7,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/ring_buffer.h" #include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" @@ -21,7 +22,6 @@ #ifdef USE_ESP_ADF #include -#include #endif namespace esphome { @@ -177,10 +177,10 @@ class VoiceAssistant : public Component { #ifdef USE_ESP_ADF vad_handle_t vad_instance_; - ringbuf_handle_t ring_buffer_; uint8_t vad_threshold_{5}; uint8_t vad_counter_{0}; #endif + std::unique_ptr ring_buffer_; bool use_wake_word_; uint8_t noise_suppression_level_; diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp new file mode 100644 index 0000000000..d9c56d84c5 --- /dev/null +++ b/esphome/core/ring_buffer.cpp @@ -0,0 +1,49 @@ +#include "ring_buffer.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +#include "helpers.h" + +namespace esphome { + +static const char *const TAG = "ring_buffer"; + +std::unique_ptr RingBuffer::create(size_t len) { + std::unique_ptr rb = make_unique(); + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + rb->storage_ = allocator.allocate(len); + if (rb->storage_ == nullptr) { + return nullptr; + } + + rb->handle_ = xStreamBufferCreateStatic(len, 0, rb->storage_, &rb->structure_); + return rb; +} + +size_t RingBuffer::read(void *data, size_t size, TickType_t ticks_to_wait) { + return xStreamBufferReceive(this->handle_, data, size, ticks_to_wait); +} + +size_t RingBuffer::write(void *data, size_t len) { + size_t free = this->free(); + if (free < len) { + size_t needed = len - free; + uint8_t discard[needed]; + xStreamBufferReceive(this->handle_, discard, needed, 0); + } + return xStreamBufferSend(this->handle_, data, len, 0); +} + +size_t RingBuffer::available() const { return xStreamBufferBytesAvailable(this->handle_); } + +size_t RingBuffer::free() const { return xStreamBufferSpacesAvailable(this->handle_); } + +BaseType_t RingBuffer::reset() { return xStreamBufferReset(this->handle_); } + +} // namespace esphome + +#endif diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h new file mode 100644 index 0000000000..6c6d04117a --- /dev/null +++ b/esphome/core/ring_buffer.h @@ -0,0 +1,34 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +#include +#include + +namespace esphome { + +class RingBuffer { + public: + size_t read(void *data, size_t size, TickType_t ticks_to_wait = 0); + + size_t write(void *data, size_t len); + + size_t available() const; + size_t free() const; + + BaseType_t reset(); + + static std::unique_ptr create(size_t len); + + protected: + StreamBufferHandle_t handle_; + StaticStreamBuffer_t structure_; + uint8_t *storage_; +}; + +} // namespace esphome + +#endif From 385420303758e02a2acc9dd883c1561a51eaaf91 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:12:31 +1100 Subject: [PATCH 290/436] Socket: Add recvfrom method to receive UDP with source address. (#6103) --- esphome/components/socket/bsd_sockets_impl.cpp | 7 +++++++ esphome/components/socket/socket.h | 3 +++ 2 files changed, 10 insertions(+) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 5d44cd7689..6c356106f3 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -86,6 +86,13 @@ class BSDSocketImpl : public Socket { } int listen(int backlog) override { return ::listen(fd_, backlog); } ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); } + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { +#if defined(USE_ESP32) + return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); +#else + return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); +#endif + } ssize_t readv(const struct iovec *iov, int iovcnt) override { #if defined(USE_ESP32) return ::lwip_readv(fd_, iov, iovcnt); diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index c9b8be88a0..5c12210d15 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -31,6 +31,9 @@ class Socket { virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; virtual int listen(int backlog) = 0; virtual ssize_t read(void *buf, size_t len) = 0; +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + virtual ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) = 0; +#endif virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0; virtual ssize_t write(const void *buf, size_t len) = 0; virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0; From 596943b683d45bf99b474e92f6dbc97e65a3ae3b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:23:36 +1100 Subject: [PATCH 291/436] Inkplate6: Fix crash with initial set of greyscale (#6038) --- esphome/components/inkplate6/inkplate.cpp | 3 +++ esphome/components/inkplate6/inkplate.h | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index 92a226de87..f4d0fedf83 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -55,6 +55,9 @@ void Inkplate6::setup() { this->wakeup_pin_->digital_write(false); } +/** + * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. + */ void Inkplate6::initialize_() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); ExternalRAMAllocator allocator32(ExternalRAMAllocator::ALLOW_FAILURE); diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 307d9671e6..2946c89e1c 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -68,8 +68,9 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { void set_greyscale(bool greyscale) { this->greyscale_ = greyscale; - this->initialize_(); this->block_partial_ = true; + if (this->is_ready()) + this->initialize_(); } void set_partial_updating(bool partial_updating) { this->partial_updating_ = partial_updating; } void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } From 0cd232cdf516a04574787843b6af20012f23350a Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 17 Jan 2024 00:50:53 -0600 Subject: [PATCH 292/436] Add support for VEML3235 lux sensor (#5959) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/veml3235/__init__.py | 0 esphome/components/veml3235/sensor.py | 84 +++++++++ esphome/components/veml3235/veml3235.cpp | 230 +++++++++++++++++++++++ esphome/components/veml3235/veml3235.h | 109 +++++++++++ tests/test1.yaml | 10 + 6 files changed, 434 insertions(+) create mode 100644 esphome/components/veml3235/__init__.py create mode 100644 esphome/components/veml3235/sensor.py create mode 100644 esphome/components/veml3235/veml3235.cpp create mode 100644 esphome/components/veml3235/veml3235.h diff --git a/CODEOWNERS b/CODEOWNERS index c497a82eab..fea1e6215c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -362,6 +362,7 @@ esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter esphome/components/vbus/* @ssieb +esphome/components/veml3235/* @kbx81 esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz esphome/components/wake_on_lan/* @willwill2will54 diff --git a/esphome/components/veml3235/__init__.py b/esphome/components/veml3235/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/veml3235/sensor.py b/esphome/components/veml3235/sensor.py new file mode 100644 index 0000000000..79ba510e41 --- /dev/null +++ b/esphome/components/veml3235/sensor.py @@ -0,0 +1,84 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_GAIN, + CONF_INTEGRATION_TIME, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["i2c"] + +CONF_AUTO_GAIN = "auto_gain" +CONF_AUTO_GAIN_THRESHOLD_HIGH = "auto_gain_threshold_high" +CONF_AUTO_GAIN_THRESHOLD_LOW = "auto_gain_threshold_low" +CONF_DIGITAL_GAIN = "digital_gain" + +veml3235_ns = cg.esphome_ns.namespace("veml3235") + +VEML3235Sensor = veml3235_ns.class_( + "VEML3235Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) +VEML3235IntegrationTime = veml3235_ns.enum("VEML3235IntegrationTime") +VEML3235_INTEGRATION_TIMES = { + "50ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_50MS, + "100ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_100MS, + "200ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_200MS, + "400ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_400MS, + "800ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_800MS, +} +VEML3235ComponentDigitalGain = veml3235_ns.enum("VEML3235ComponentDigitalGain") +DIGITAL_GAINS = { + "1X": VEML3235ComponentDigitalGain.VEML3235_DIGITAL_GAIN_1X, + "2X": VEML3235ComponentDigitalGain.VEML3235_DIGITAL_GAIN_2X, +} +VEML3235ComponentGain = veml3235_ns.enum("VEML3235ComponentGain") +GAINS = { + "1X": VEML3235ComponentGain.VEML3235_GAIN_1X, + "2X": VEML3235ComponentGain.VEML3235_GAIN_2X, + "4X": VEML3235ComponentGain.VEML3235_GAIN_4X, + "AUTO": VEML3235ComponentGain.VEML3235_GAIN_AUTO, +} + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + VEML3235Sensor, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Optional(CONF_DIGITAL_GAIN, default="1X"): cv.enum( + DIGITAL_GAINS, upper=True + ), + cv.Optional(CONF_AUTO_GAIN, default=True): cv.boolean, + cv.Optional(CONF_AUTO_GAIN_THRESHOLD_HIGH, default="90%"): cv.percentage, + cv.Optional(CONF_AUTO_GAIN_THRESHOLD_LOW, default="20%"): cv.percentage, + cv.Optional(CONF_GAIN, default="1X"): cv.enum(GAINS, upper=True), + cv.Optional(CONF_INTEGRATION_TIME, default="50ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.enum(VEML3235_INTEGRATION_TIMES, lower=True), + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x10)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_auto_gain(config[CONF_AUTO_GAIN])) + cg.add(var.set_auto_gain_threshold_high(config[CONF_AUTO_GAIN_THRESHOLD_HIGH])) + cg.add(var.set_auto_gain_threshold_low(config[CONF_AUTO_GAIN_THRESHOLD_LOW])) + cg.add(var.set_digital_gain(DIGITAL_GAINS[config[CONF_DIGITAL_GAIN]])) + cg.add(var.set_gain(GAINS[config[CONF_GAIN]])) + cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp new file mode 100644 index 0000000000..2410bfdda2 --- /dev/null +++ b/esphome/components/veml3235/veml3235.cpp @@ -0,0 +1,230 @@ +#include "veml3235.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace veml3235 { + +static const char *const TAG = "veml3235.sensor"; + +void VEML3235Sensor::setup() { + uint8_t device_id[] = {0, 0}; + + ESP_LOGCONFIG(TAG, "Setting up VEML3235 '%s'...", this->name_.c_str()); + + if (!this->refresh_config_reg()) { + ESP_LOGE(TAG, "Unable to write configuration"); + this->mark_failed(); + return; + } + if ((this->write(&ID_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(device_id, 2)) { + ESP_LOGE(TAG, "Unable to read ID"); + this->mark_failed(); + return; + } else if (device_id[0] != DEVICE_ID) { + ESP_LOGE(TAG, "Incorrect device ID - expected 0x%.2x, read 0x%.2x", DEVICE_ID, device_id[0]); + this->mark_failed(); + return; + } +} + +bool VEML3235Sensor::refresh_config_reg(bool force_on) { + uint16_t data = this->power_on_ || force_on ? 0 : SHUTDOWN_BITS; + + data |= (uint16_t(this->integration_time_ << CONFIG_REG_IT_BIT)); + data |= (uint16_t(this->digital_gain_ << CONFIG_REG_DG_BIT)); + data |= (uint16_t(this->gain_ << CONFIG_REG_G_BIT)); + data |= 0x1; // mandatory 1 here per RM + + ESP_LOGVV(TAG, "Writing 0x%.4x to register 0x%.2x", data, CONFIG_REG); + return this->write_byte_16(CONFIG_REG, data); +} + +float VEML3235Sensor::read_lx_() { + if (!this->power_on_) { // if off, turn on + if (!this->refresh_config_reg(true)) { + ESP_LOGW(TAG, "Turning on failed"); + this->status_set_warning(); + return NAN; + } + delay(4); // from RM: a wait time of 4 ms should be observed before the first measurement is picked up, to allow + // for a correct start of the signal processor and oscillator + } + + uint8_t als_regs[] = {0, 0}; + if ((this->write(&ALS_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(als_regs, 2)) { + this->status_set_warning(); + return NAN; + } + + this->status_clear_warning(); + + float als_raw_value_multiplier = LUX_MULTIPLIER_BASE; + uint16_t als_raw_value = encode_uint16(als_regs[1], als_regs[0]); + // determine multiplier value based on gains and integration time + if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_1X) { + als_raw_value_multiplier *= 2; + } + switch (this->gain_) { + case VEML3235_GAIN_1X: + als_raw_value_multiplier *= 4; + break; + case VEML3235_GAIN_2X: + als_raw_value_multiplier *= 2; + break; + default: + break; + } + switch (this->integration_time_) { + case VEML3235_INTEGRATION_TIME_50MS: + als_raw_value_multiplier *= 16; + break; + case VEML3235_INTEGRATION_TIME_100MS: + als_raw_value_multiplier *= 8; + break; + case VEML3235_INTEGRATION_TIME_200MS: + als_raw_value_multiplier *= 4; + break; + case VEML3235_INTEGRATION_TIME_400MS: + als_raw_value_multiplier *= 2; + break; + default: + break; + } + // finally, determine and return the actual lux value + float lx = float(als_raw_value) * als_raw_value_multiplier; + ESP_LOGVV(TAG, "'%s': ALS raw = %u, multiplier = %.5f", this->get_name().c_str(), als_raw_value, + als_raw_value_multiplier); + ESP_LOGD(TAG, "'%s': Illuminance = %.4flx", this->get_name().c_str(), lx); + + if (!this->power_on_) { // turn off if required + if (!this->refresh_config_reg()) { + ESP_LOGW(TAG, "Turning off failed"); + this->status_set_warning(); + } + } + + if (this->auto_gain_) { + this->adjust_gain_(als_raw_value); + } + + return lx; +} + +void VEML3235Sensor::adjust_gain_(const uint16_t als_raw_value) { + if ((als_raw_value > UINT16_MAX * this->auto_gain_threshold_low_) && + (als_raw_value < UINT16_MAX * this->auto_gain_threshold_high_)) { + return; + } + + if (als_raw_value >= UINT16_MAX * 0.9) { // over-saturated, reset all gains and start over + this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X; + this->gain_ = VEML3235_GAIN_1X; + this->integration_time_ = VEML3235_INTEGRATION_TIME_50MS; + this->refresh_config_reg(); + return; + } + + if (this->gain_ != VEML3235_GAIN_4X) { // increase gain if possible + switch (this->gain_) { + case VEML3235_GAIN_1X: + this->gain_ = VEML3235_GAIN_2X; + break; + case VEML3235_GAIN_2X: + this->gain_ = VEML3235_GAIN_4X; + break; + default: + break; + } + this->refresh_config_reg(); + return; + } + // gain is maxed out; reset it and try to increase digital gain + if (this->digital_gain_ != VEML3235_DIGITAL_GAIN_2X) { // increase digital gain if possible + this->digital_gain_ = VEML3235_DIGITAL_GAIN_2X; + this->gain_ = VEML3235_GAIN_1X; + this->refresh_config_reg(); + return; + } + // digital gain is maxed out; reset it and try to increase integration time + if (this->integration_time_ != VEML3235_INTEGRATION_TIME_800MS) { // increase integration time if possible + switch (this->integration_time_) { + case VEML3235_INTEGRATION_TIME_50MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_100MS; + break; + case VEML3235_INTEGRATION_TIME_100MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_200MS; + break; + case VEML3235_INTEGRATION_TIME_200MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_400MS; + break; + case VEML3235_INTEGRATION_TIME_400MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_800MS; + break; + default: + break; + } + this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X; + this->gain_ = VEML3235_GAIN_1X; + this->refresh_config_reg(); + return; + } +} + +void VEML3235Sensor::dump_config() { + uint8_t digital_gain = 1; + uint8_t gain = 1; + uint16_t integration_time = 0; + + if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_2X) { + digital_gain = 2; + } + switch (this->gain_) { + case VEML3235_GAIN_2X: + gain = 2; + break; + case VEML3235_GAIN_4X: + gain = 4; + break; + default: + break; + } + switch (this->integration_time_) { + case VEML3235_INTEGRATION_TIME_50MS: + integration_time = 50; + break; + case VEML3235_INTEGRATION_TIME_100MS: + integration_time = 100; + break; + case VEML3235_INTEGRATION_TIME_200MS: + integration_time = 200; + break; + case VEML3235_INTEGRATION_TIME_400MS: + integration_time = 400; + break; + case VEML3235_INTEGRATION_TIME_800MS: + integration_time = 800; + break; + default: + break; + } + + LOG_SENSOR("", "VEML3235", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed"); + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Auto-gain enabled: %s", YESNO(this->auto_gain_)); + if (this->auto_gain_) { + ESP_LOGCONFIG(TAG, " Auto-gain upper threshold: %f%%", this->auto_gain_threshold_high_ * 100.0); + ESP_LOGCONFIG(TAG, " Auto-gain lower threshold: %f%%", this->auto_gain_threshold_low_ * 100.0); + ESP_LOGCONFIG(TAG, " Values below will be used as initial values only"); + } + ESP_LOGCONFIG(TAG, " Digital gain: %uX", digital_gain); + ESP_LOGCONFIG(TAG, " Gain: %uX", gain); + ESP_LOGCONFIG(TAG, " Integration time: %ums", integration_time); +} + +} // namespace veml3235 +} // namespace esphome diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h new file mode 100644 index 0000000000..2b0d6b23ea --- /dev/null +++ b/esphome/components/veml3235/veml3235.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace veml3235 { + +// Register IDs/locations +// +static const uint8_t CONFIG_REG = 0x00; +static const uint8_t W_REG = 0x04; +static const uint8_t ALS_REG = 0x05; +static const uint8_t ID_REG = 0x09; + +// Bit offsets within CONFIG_REG +// +static const uint8_t CONFIG_REG_IT_BIT = 12; +static const uint8_t CONFIG_REG_DG_BIT = 5; +static const uint8_t CONFIG_REG_G_BIT = 3; + +// Other important constants +// +static const uint8_t DEVICE_ID = 0x35; +static const uint16_t SHUTDOWN_BITS = 0x0018; + +// Base multiplier value for lux computation +// +static const float LUX_MULTIPLIER_BASE = 0.00213; + +// Enum for conversion/integration time settings for the VEML3235. +// +// Specific values of the enum constants are register values taken from the VEML3235 datasheet. +// Longer times mean more accurate results, but will take more energy/more time. +// +enum VEML3235ComponentIntegrationTime { + VEML3235_INTEGRATION_TIME_50MS = 0b000, + VEML3235_INTEGRATION_TIME_100MS = 0b001, + VEML3235_INTEGRATION_TIME_200MS = 0b010, + VEML3235_INTEGRATION_TIME_400MS = 0b011, + VEML3235_INTEGRATION_TIME_800MS = 0b100, +}; + +// Enum for digital gain settings for the VEML3235. +// Higher values are better for low light situations, but can increase noise. +// +enum VEML3235ComponentDigitalGain { + VEML3235_DIGITAL_GAIN_1X = 0b0, + VEML3235_DIGITAL_GAIN_2X = 0b1, +}; + +// Enum for gain settings for the VEML3235. +// Higher values are better for low light situations, but can increase noise. +// +enum VEML3235ComponentGain { + VEML3235_GAIN_1X = 0b00, + VEML3235_GAIN_2X = 0b01, + VEML3235_GAIN_4X = 0b11, +}; + +class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override { this->publish_state(this->read_lx_()); } + float get_setup_priority() const override { return setup_priority::DATA; } + + // Used by ESPHome framework. Does NOT actually set the value on the device. + void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; } + void set_auto_gain_threshold_high(float auto_gain_threshold_high) { + this->auto_gain_threshold_high_ = auto_gain_threshold_high; + } + void set_auto_gain_threshold_low(float auto_gain_threshold_low) { + this->auto_gain_threshold_low_ = auto_gain_threshold_low; + } + void set_power_on(bool power_on) { this->power_on_ = power_on; } + void set_digital_gain(VEML3235ComponentDigitalGain digital_gain) { this->digital_gain_ = digital_gain; } + void set_gain(VEML3235ComponentGain gain) { this->gain_ = gain; } + void set_integration_time(VEML3235ComponentIntegrationTime integration_time) { + this->integration_time_ = integration_time; + } + + bool auto_gain() { return this->auto_gain_; } + float auto_gain_threshold_high() { return this->auto_gain_threshold_high_; } + float auto_gain_threshold_low() { return this->auto_gain_threshold_low_; } + VEML3235ComponentDigitalGain digital_gain() { return this->digital_gain_; } + VEML3235ComponentGain gain() { return this->gain_; } + VEML3235ComponentIntegrationTime integration_time() { return this->integration_time_; } + + // Updates the configuration register on the device + bool refresh_config_reg(bool force_on = false); + + protected: + float read_lx_(); + void adjust_gain_(uint16_t als_raw_value); + + bool auto_gain_{true}; + bool power_on_{true}; + float auto_gain_threshold_high_{0.9}; + float auto_gain_threshold_low_{0.2}; + VEML3235ComponentDigitalGain digital_gain_{VEML3235_DIGITAL_GAIN_1X}; + VEML3235ComponentGain gain_{VEML3235_GAIN_1X}; + VEML3235ComponentIntegrationTime integration_time_{VEML3235_INTEGRATION_TIME_50MS}; +}; + +} // namespace veml3235 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 471e2a71a5..afd199d264 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1370,6 +1370,16 @@ sensor: name: tsl2591 calculated_lux id: tsl2591_cl i2c_id: i2c_bus + - platform: veml3235 + id: veml3235_sensor + name: VEML3235 Light Sensor + i2c_id: i2c_bus + auto_gain: true + auto_gain_threshold_high: 90% + auto_gain_threshold_low: 15% + digital_gain: 1X + gain: 1X + integration_time: 50ms - platform: tee501 name: Office Temperature 3 address: 0x48 From b606e976e1e90eaaa2637f3eb95592a3a888c04a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 17 Jan 2024 20:28:46 +1300 Subject: [PATCH 293/436] CV: tidy up Schema wrapper (#6105) --- esphome/config_validation.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 8f2e080b46..fa1170fb93 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2004,15 +2004,19 @@ def suppress_invalid(): pass -GIT_SCHEMA = { - Required(CONF_URL): url, - Optional(CONF_REF): git_ref, - Optional(CONF_USERNAME): string, - Optional(CONF_PASSWORD): string, -} -LOCAL_SCHEMA = { - Required(CONF_PATH): directory, -} +GIT_SCHEMA = Schema( + { + Required(CONF_URL): url, + Optional(CONF_REF): git_ref, + Optional(CONF_USERNAME): string, + Optional(CONF_PASSWORD): string, + } +) +LOCAL_SCHEMA = Schema( + { + Required(CONF_PATH): directory, + } +) def validate_source_shorthand(value): @@ -2053,8 +2057,8 @@ SOURCE_SCHEMA = Any( validate_source_shorthand, typed_schema( { - TYPE_GIT: Schema(GIT_SCHEMA), - TYPE_LOCAL: Schema(LOCAL_SCHEMA), + TYPE_GIT: GIT_SCHEMA, + TYPE_LOCAL: LOCAL_SCHEMA, } ), ) From e731a2ffaa62848e43c4723680225392f5217e66 Mon Sep 17 00:00:00 2001 From: h2zero <32826625+h2zero@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:26:56 -0700 Subject: [PATCH 294/436] Add support X.509 client certificates for MQTT. (#5778) Co-authored-by: h2zero Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mqtt/__init__.py | 11 +++++++++++ esphome/components/mqtt/mqtt_backend_esp32.cpp | 10 ++++++++++ esphome/components/mqtt/mqtt_backend_esp32.h | 4 ++++ esphome/components/mqtt/mqtt_client.h | 2 ++ esphome/const.py | 2 ++ 5 files changed, 29 insertions(+) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index baf32097fa..02184c8a39 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CERTIFICATE_AUTHORITY, + CONF_CLIENT_CERTIFICATE, + CONF_CLIENT_CERTIFICATE_KEY, CONF_CLIENT_ID, CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, @@ -199,6 +201,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All( cv.string, cv.only_with_esp_idf ), + cv.Inclusive(CONF_CLIENT_CERTIFICATE, "cert-key-pair"): cv.All( + cv.string, cv.only_on_esp32 + ), + cv.Inclusive(CONF_CLIENT_CERTIFICATE_KEY, "cert-key-pair"): cv.All( + cv.string, cv.only_on_esp32 + ), cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All( cv.boolean, cv.only_with_esp_idf ), @@ -378,6 +386,9 @@ async def to_code(config): if CONF_CERTIFICATE_AUTHORITY in config: cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY])) cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK])) + if CONF_CLIENT_CERTIFICATE in config: + cg.add(var.set_cl_certificate(config[CONF_CLIENT_CERTIFICATE])) + cg.add(var.set_cl_key(config[CONF_CLIENT_CERTIFICATE_KEY])) # prevent error -0x428e # See https://github.com/espressif/esp-idf/issues/139 diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 2d4e6802f2..9c2e487ae7 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -45,6 +45,11 @@ bool MQTTBackendESP32::initialize_() { mqtt_cfg_.cert_pem = ca_certificate_.value().c_str(); mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_; mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL; + + if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { + mqtt_cfg_.client_cert_pem = this->cl_certificate_.value().c_str(); + mqtt_cfg_.client_key_pem = this->cl_key_.value().c_str(); + } } else { mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; } @@ -79,6 +84,11 @@ bool MQTTBackendESP32::initialize_() { mqtt_cfg_.broker.verification.certificate = ca_certificate_.value().c_str(); mqtt_cfg_.broker.verification.skip_cert_common_name_check = skip_cert_cn_check_; mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_SSL; + + if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { + mqtt_cfg_.credentials.authentication.certificate = this->cl_certificate_.value().c_str(); + mqtt_cfg_.credentials.authentication.key = this->cl_key_.value().c_str(); + } } else { mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; } diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index a4ee96ca59..b1f672da10 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -124,6 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend { void loop() final; void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; } + void set_cl_certificate(const std::string &cert) { cl_certificate_ = cert; } + void set_cl_key(const std::string &key) { cl_key_ = key; } void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } protected: @@ -154,6 +156,8 @@ class MQTTBackendESP32 final : public MQTTBackend { uint16_t keep_alive_; bool clean_session_; optional ca_certificate_; + optional cl_certificate_; + optional cl_key_; bool skip_cert_cn_check_{false}; // callbacks diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index bcb44ab4c2..454316aa87 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -146,6 +146,8 @@ class MQTTClientComponent : public Component { #endif #ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } + void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } + void set_cl_key(const char *key) { this->mqtt_backend_.set_cl_key(key); } void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); } #endif const Availability &get_availability(); diff --git a/esphome/const.py b/esphome/const.py index c35adc74ee..a95d1d5ac3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -112,6 +112,8 @@ CONF_CHANNELS = "channels" CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_CHIPSET = "chipset" CONF_CLEAR_IMPEDANCE = "clear_impedance" +CONF_CLIENT_CERTIFICATE = "client_certificate" +CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key" CONF_CLIENT_ID = "client_id" CONF_CLK_PIN = "clk_pin" CONF_CLOCK_PIN = "clock_pin" From 39bd829236a5dca57f3fc21caf8847b4af89a9c8 Mon Sep 17 00:00:00 2001 From: mathieu-mp Date: Thu, 18 Jan 2024 00:40:30 +0100 Subject: [PATCH 295/436] Fix color observation for triangle outline in display component (#6107) --- esphome/components/display/display.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 0c3631342e..e531c5cf5c 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -142,9 +142,9 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color) } while (dx <= 0); } void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { - this->line(x1, y1, x2, y2); - this->line(x1, y1, x3, y3); - this->line(x2, y2, x3, y3); + this->line(x1, y1, x2, y2, color); + this->line(x1, y1, x3, y3, color); + this->line(x2, y2, x3, y3, color); } void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) { if (*y1 > *y2) { From c9c8d397784ac9f47d64cb15ed46ff9d82ad21f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Leforestier?= Date: Thu, 18 Jan 2024 01:56:56 +0100 Subject: [PATCH 296/436] Add support of Honeywell HumidIcon (I2C HIH series) Temperature & Humidity sensor (#5730) --- CODEOWNERS | 1 + .../components/honeywell_hih_i2c/__init__.py | 2 + .../honeywell_hih_i2c/honeywell_hih.cpp | 97 +++++++++++++++++++ .../honeywell_hih_i2c/honeywell_hih.h | 34 +++++++ .../components/honeywell_hih_i2c/sensor.py | 56 +++++++++++ tests/test1.yaml | 7 ++ 6 files changed, 197 insertions(+) create mode 100644 esphome/components/honeywell_hih_i2c/__init__.py create mode 100644 esphome/components/honeywell_hih_i2c/honeywell_hih.cpp create mode 100644 esphome/components/honeywell_hih_i2c/honeywell_hih.h create mode 100644 esphome/components/honeywell_hih_i2c/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index fea1e6215c..7e87679ad8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,6 +137,7 @@ esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode esphome/components/homeassistant/* @OttoWinter +esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp2_i2c/* @jpfaff esphome/components/host/* @esphome/core diff --git a/esphome/components/honeywell_hih_i2c/__init__.py b/esphome/components/honeywell_hih_i2c/__init__.py new file mode 100644 index 0000000000..fbf67230f7 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/__init__.py @@ -0,0 +1,2 @@ +"""Support for Honeywell HumidIcon HIH""" +CODEOWNERS = ["@Benichou34"] diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp new file mode 100644 index 0000000000..64d5ddb541 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -0,0 +1,97 @@ +// Honeywell HumidIcon I2C Sensors +// https://prod-edam.honeywell.com/content/dam/honeywell-edam/sps/siot/en-us/products/sensors/humidity-with-temperature-sensors/common/documents/sps-siot-i2c-comms-humidicon-tn-009061-2-en-ciid-142171.pdf +// + +#include "honeywell_hih.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace honeywell_hih_i2c { + +static const char *const TAG = "honeywell_hih.i2c"; + +static const uint8_t REQUEST_CMD[1] = {0x00}; // Measurement Request Format +static const uint16_t MAX_COUNT = 0x3FFE; // 2^14 - 2 + +void HoneywellHIComponent::read_sensor_data_() { + uint8_t data[4]; + + if (this->read(data, sizeof(data)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return; + } + + const uint16_t raw_humidity = (static_cast(data[0] & 0x3F) << 8) | data[1]; + float humidity = (static_cast(raw_humidity) / MAX_COUNT) * 100; + + const uint16_t raw_temperature = (static_cast(data[2]) << 6) | (data[3] >> 2); + float temperature = (static_cast(raw_temperature) / MAX_COUNT) * 165 - 40; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); +} + +void HoneywellHIComponent::start_measurement_() { + if (this->write(REQUEST_CMD, sizeof(REQUEST_CMD)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return; + } + + this->measurement_running_ = true; +} + +bool HoneywellHIComponent::is_measurement_ready_() { + uint8_t data[1]; + + if (this->read(data, sizeof(data)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return false; + } + + // Check status bits + return ((data[0] & 0xC0) == 0x00); +} + +void HoneywellHIComponent::measurement_timeout_() { + ESP_LOGE(TAG, "Honeywell HIH Timeout!"); + this->measurement_running_ = false; + this->mark_failed(); +} + +void HoneywellHIComponent::update() { + ESP_LOGV(TAG, "Update Honeywell HIH Sensor"); + + this->start_measurement_(); + // The measurement cycle duration is typically 36.65 ms for temperature and humidity readings. + this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout_(); }); +} + +void HoneywellHIComponent::loop() { + if (this->measurement_running_ && this->is_measurement_ready_()) { + this->measurement_running_ = false; + this->cancel_timeout("meas_timeout"); + this->read_sensor_data_(); + } +} + +void HoneywellHIComponent::dump_config() { + ESP_LOGD(TAG, "Honeywell HIH:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + } + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_UPDATE_INTERVAL(this); +} + +float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace honeywell_hih_i2c +} // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h new file mode 100644 index 0000000000..4457eab1da --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -0,0 +1,34 @@ +// Honeywell HumidIcon I2C Sensors +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace honeywell_hih_i2c { + +class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + bool measurement_running_{false}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + private: + void read_sensor_data_(); + void start_measurement_(); + bool is_measurement_ready_(); + void measurement_timeout_(); +}; + +} // namespace honeywell_hih_i2c +} // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/sensor.py b/esphome/components/honeywell_hih_i2c/sensor.py new file mode 100644 index 0000000000..f5a6ad2398 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) + +DEPENDENCIES = ["i2c"] + +honeywell_hih_ns = cg.esphome_ns.namespace("honeywell_hih_i2c") +HONEYWELLHIComponent = honeywell_hih_ns.class_( + "HoneywellHIComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HONEYWELLHIComponent), + 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=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x27)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index afd199d264..038ac9c738 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -863,6 +863,13 @@ sensor: oversampling: 8x update_interval: 15s i2c_id: i2c_bus + - platform: honeywell_hih_i2c + temperature: + name: Living Room Temperature 7 + humidity: + name: Living Room Humidity 7 + update_interval: 15s + i2c_id: i2c_bus - platform: honeywellabp pressure: name: Honeywell pressure From c6f528583b2b527a4b9a91533d7aacf838c0a984 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 18 Jan 2024 08:13:40 +0100 Subject: [PATCH 297/436] Proposal: Test yaml for each component (#5398) * Test for each component. * When possible use commandline substitution. * Add wildcard support. * end file with new line. * Move component tests into subfolder. * Add component test to pipeline. * Remove trailing whitespace. * add restore python step. * Add `. venv/bin/activate` to pipeline. * step `changed-components` needs `common` step. * start `list-components-changed.py` different. * iterate on pipeline stage `list-components`. * Update `checkout` action. * Rename test folder from `tests` to `_test`. * validate file exists. * Move component test folder. * extend list-components to include child components. * File does not end with a newline * Handle empty list-components matrix. * list-components also check for changes in tests folder. * Improve `list-components.py`. * `*` is a forbidden character for filenames on windows. --------- Co-authored-by: Your Name Co-authored-by: Keith Burzinski --- .github/workflows/ci.yml | 57 +++++++ script/fulltest | 1 + script/list-components.py | 153 ++++++++++++++++++ script/test_build_components | 85 ++++++++++ tests/components/adc/test.esp32-c3.yaml | 5 + tests/components/adc/test.esp32-idf.yaml | 11 ++ tests/components/adc/test.esp32-s2.yaml | 5 + tests/components/adc/test.esp32-s3.yaml | 5 + tests/components/adc/test.esp32.yaml | 11 ++ tests/components/adc/test.esp8266.yaml | 4 + tests/components/adc/test.rp2040.yaml | 4 + .../mopeka_std_check/test.esp32.yaml | 16 ++ tests/components/template/test.all.yaml | 127 +++++++++++++++ .../build_components_base.esp32-ard.yaml | 20 +++ .../build_components_base.esp32-c3-ard.yaml | 20 +++ .../build_components_base.esp32-c3-idf.yaml | 20 +++ .../build_components_base.esp32-idf.yaml | 20 +++ .../build_components_base.esp32-s2-ard.yaml | 21 +++ .../build_components_base.esp32-s2-idf.yaml | 21 +++ .../build_components_base.esp32-s3-ard.yaml | 21 +++ .../build_components_base.esp32-s3-idf.yaml | 21 +++ .../build_components_base.esp8266.yaml | 18 +++ .../build_components_base.rp2040.yaml | 21 +++ 23 files changed, 687 insertions(+) create mode 100755 script/list-components.py create mode 100755 script/test_build_components create mode 100644 tests/components/adc/test.esp32-c3.yaml create mode 100644 tests/components/adc/test.esp32-idf.yaml create mode 100644 tests/components/adc/test.esp32-s2.yaml create mode 100644 tests/components/adc/test.esp32-s3.yaml create mode 100644 tests/components/adc/test.esp32.yaml create mode 100644 tests/components/adc/test.esp8266.yaml create mode 100644 tests/components/adc/test.rp2040.yaml create mode 100644 tests/components/mopeka_std_check/test.esp32.yaml create mode 100644 tests/components/template/test.all.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-c3-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-c3-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s2-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s2-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s3-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s3-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp8266.yaml create mode 100644 tests/test_build_components/build_components_base.rp2040.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ddc49b504..2187573709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -392,6 +392,62 @@ jobs: # yamllint disable-line rule:line-length if: always() + list-components: + runs-on: ubuntu-latest + needs: + - common + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.1 + with: + # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works. + fetch-depth: 500 + - name: Fetch dev branch + run: | + git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/dev*:refs/remotes/origin/dev* +refs/tags/dev*:refs/tags/dev* + git merge-base refs/remotes/origin/dev HEAD + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Find changed components + id: set-matrix + run: | + . venv/bin/activate + echo "matrix=$(script/list-components.py --changed | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT + + test-build-components: + name: Component test ${{ matrix.file }} + runs-on: ubuntu-latest + needs: + - common + - list-components + if: ${{ needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }} + strategy: + fail-fast: false + max-parallel: 2 + matrix: + file: ${{ fromJson(needs.list-components.outputs.matrix) }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: test_build_components -e config -c ${{ matrix.file }} + run: | + . venv/bin/activate + ./script/test_build_components -e config -c ${{ matrix.file }} + - name: test_build_components -e compile -c ${{ matrix.file }} + run: | + . venv/bin/activate + ./script/test_build_components -e compile -c ${{ matrix.file }} + ci-status: name: CI Status runs-on: ubuntu-latest @@ -406,6 +462,7 @@ jobs: - pyupgrade - compile-tests - clang-tidy + - test-build-components if: always() steps: - name: Success diff --git a/script/fulltest b/script/fulltest index a605beebfe..6440401e97 100755 --- a/script/fulltest +++ b/script/fulltest @@ -12,3 +12,4 @@ script/lint-cpp script/unit_test script/component_test script/test +script/test_build_components diff --git a/script/list-components.py b/script/list-components.py new file mode 100755 index 0000000000..3e55c0e5f7 --- /dev/null +++ b/script/list-components.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +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 esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_ESP32, + PLATFORM_ESP8266, +) + + +def filter_component_files(str): + return str.startswith("esphome/components/") | str.startswith("tests/components/") + + +def extract_component_names_array_from_files_array(files): + components = [] + for file in files: + file_parts = file.split("/") + if len(file_parts) >= 4: + component_name = file_parts[2] + if component_name not in components: + components.append(component_name) + return components + + +def add_item_to_components_graph(components_graph, parent, child): + if not parent.startswith("__") and parent != child: + if parent not in components_graph: + components_graph[parent] = [] + if child not in components_graph[parent]: + components_graph[parent].append(child) + + +def create_components_graph(): + # The root directory of the repo + root = Path(__file__).parent.parent + components_dir = root / "esphome" / "components" + # Fake some directory so that get_component works + CORE.config_path = str(root) + # Various configuration to capture different outcomes used by `AUTO_LOAD` function. + TARGET_CONFIGURATIONS = [ + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "arduino", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "esp-idf", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP32}, + ] + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + components_graph = {} + + for path in components_dir.iterdir(): + if not path.is_dir(): + continue + if not (path / "__init__.py").is_file(): + continue + name = path.name + comp = get_component(name) + if comp is None: + print( + f"Cannot find component {name}. Make sure current path is pip installed ESPHome" + ) + sys.exit(1) + + for dependency in comp.dependencies: + add_item_to_components_graph(components_graph, dependency, name) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for auto_load in comp.auto_load: + add_item_to_components_graph(components_graph, auto_load, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + for platform_path in path.iterdir(): + platform_name = platform_path.stem + platform = get_platform(platform_name, name) + if platform is None: + continue + + add_item_to_components_graph(components_graph, platform_name, name) + + for dependency in platform.dependencies: + add_item_to_components_graph(components_graph, dependency, name) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for auto_load in platform.auto_load: + add_item_to_components_graph(components_graph, auto_load, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + return components_graph + + +def find_children_of_component(components_graph, component_name, depth=0): + if component_name not in components_graph: + return [] + + children = [] + + for child in components_graph[component_name]: + children.append(child) + if depth < 10: + children.extend( + find_children_of_component(components_graph, child, depth + 1) + ) + # Remove duplicate values + return list(set(children)) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-c", "--changed", action="store_true", help="Only run on changed files" + ) + args = parser.parse_args() + + files = git_ls_files() + files = filter(filter_component_files, files) + + if args.changed: + changed = changed_files() + files = [f for f in files if f in changed] + + components = extract_component_names_array_from_files_array(files) + + if args.changed: + components_graph = create_components_graph() + + all_changed_components = components.copy() + for c in components: + all_changed_components.extend( + find_children_of_component(components_graph, c) + ) + # Remove duplicate values + all_changed_components = list(set(all_changed_components)) + + for c in sorted(all_changed_components): + print(c) + else: + for c in sorted(components): + print(c) + + +if __name__ == "__main__": + main() diff --git a/script/test_build_components b/script/test_build_components new file mode 100755 index 0000000000..f951ba7545 --- /dev/null +++ b/script/test_build_components @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -e + +# Parse parameter: +# - `e` - Parameter for `esphome` command. Default `compile`. Common alternative is `config`. +# - `c` - Component folder name to test. Default `*`. +esphome_command="compile" +target_component="*" +while getopts e:c: flag +do + case $flag in + e) esphome_command=${OPTARG};; + c) target_component=${OPTARG};; + \?) echo "Usage: $0 [-e ] [-c ]" 1>&2; exit 1;; + esac +done + +cd "$(dirname "$0")/.." + +if ! [ -d "./tests/test_build_components/build" ]; then + mkdir ./tests/test_build_components/build +fi + +start_esphome() { + # create dynamic yaml file in `build` folder. + # `./tests/test_build_components/build/[target_component].[test_name].[target_platform].yaml` + component_test_file="./tests/test_build_components/build/$target_component.$test_name.$target_platform.yaml" + + cp $target_platform_file $component_test_file + sed -i "s!\$component_test_file!../../.$f!g" $component_test_file + + # Start esphome process + echo "> [$target_component] [$test_name] [$target_platform]" + echo "esphome -s component_name $target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file" + # TODO: Validate escape of Command line substitution value + esphome -s component_name $target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file +} + +# Find all test yaml files. +# - `./tests/components/[target_component]/[test_name].[target_platform].yaml` +# - `./tests/components/[target_component]/[test_name].all.yaml` +for f in ./tests/components/$target_component/*.*.yaml; do + [ -f "$f" ] || continue + IFS='/' read -r -a folder_name <<< "$f" + target_component="${folder_name[3]}" + + IFS='.' read -r -a file_name <<< "${folder_name[4]}" + test_name="${file_name[0]}" + target_platform="${file_name[1]}" + file_name_parts=${#file_name[@]} + + if [ "$target_platform" = "all" ] || [ $file_name_parts = 2 ]; then + # Test has *not* defined a specific target platform. Need to run tests for all possible target platforms. + + for target_platform_file in ./tests/test_build_components/build_components_base.*.yaml; do + IFS='/' read -r -a folder_name <<< "$target_platform_file" + IFS='.' read -r -a file_name <<< "${folder_name[3]}" + target_platform="${file_name[1]}" + + start_esphome + done + + else + # Test has defined a specific target platform. + + # Validate we have a base test yaml for selected platform. + # The target_platform is sourced from the following location. + # 1. `./tests/test_build_components/build_components_base.[target_platform].yaml` + # 2. `./tests/test_build_components/build_components_base.[target_platform]-ard.yaml` + target_platform_file="./tests/test_build_components/build_components_base.$target_platform.yaml" + if ! [ -f "$target_platform_file" ]; then + # Try find arduino test framework as platform. + target_platform_ard="$target_platform-ard" + target_platform_file="./tests/test_build_components/build_components_base.$target_platform_ard.yaml" + if ! [ -f "$target_platform_file" ]; then + echo "No base test file [./tests/test_build_components/build_components_base.$target_platform.yaml, ./tests/build_components_base.$target_platform_ard.yaml] for component test [$f] found." + exit 1 + fi + target_platform=$target_platform_ard + fi + + start_esphome + fi +done diff --git a/tests/components/adc/test.esp32-c3.yaml b/tests/components/adc/test.esp32-c3.yaml new file mode 100644 index 0000000000..18e5ab3561 --- /dev/null +++ b/tests/components/adc/test.esp32-c3.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db diff --git a/tests/components/adc/test.esp32-idf.yaml b/tests/components/adc/test.esp32-idf.yaml new file mode 100644 index 0000000000..923fd0d706 --- /dev/null +++ b/tests/components/adc/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: adc + pin: A0 + name: Living Room Brightness + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-s2.yaml b/tests/components/adc/test.esp32-s2.yaml new file mode 100644 index 0000000000..0119ad5e4d --- /dev/null +++ b/tests/components/adc/test.esp32-s2.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db diff --git a/tests/components/adc/test.esp32-s3.yaml b/tests/components/adc/test.esp32-s3.yaml new file mode 100644 index 0000000000..0119ad5e4d --- /dev/null +++ b/tests/components/adc/test.esp32-s3.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db diff --git a/tests/components/adc/test.esp32.yaml b/tests/components/adc/test.esp32.yaml new file mode 100644 index 0000000000..923fd0d706 --- /dev/null +++ b/tests/components/adc/test.esp32.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: adc + pin: A0 + name: Living Room Brightness + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp8266.yaml b/tests/components/adc/test.esp8266.yaml new file mode 100644 index 0000000000..1ef79c7ca1 --- /dev/null +++ b/tests/components/adc/test.esp8266.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + id: my_sensor + pin: VCC diff --git a/tests/components/adc/test.rp2040.yaml b/tests/components/adc/test.rp2040.yaml new file mode 100644 index 0000000000..200b802a4d --- /dev/null +++ b/tests/components/adc/test.rp2040.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + pin: VCC + name: VSYS diff --git a/tests/components/mopeka_std_check/test.esp32.yaml b/tests/components/mopeka_std_check/test.esp32.yaml new file mode 100644 index 0000000000..830adf952f --- /dev/null +++ b/tests/components/mopeka_std_check/test.esp32.yaml @@ -0,0 +1,16 @@ +esp32_ble_tracker: + +sensor: + # Example using 11kg 100% propane tank. + - platform: mopeka_std_check + mac_address: D3:75:F2:DC:16:91 + tank_type: Europe_11kg + temperature: + name: "Propane test temp" + level: + name: "Propane test level" + distance: + name: "Propane test distance" + battery_level: + name: "Propane test battery level" + diff --git a/tests/components/template/test.all.yaml b/tests/components/template/test.all.yaml new file mode 100644 index 0000000000..ad67b4e6ae --- /dev/null +++ b/tests/components/template/test.all.yaml @@ -0,0 +1,127 @@ +sensor: + - platform: template + name: "Template Sensor" + id: template_sens + lambda: |- + if (id(some_binary_sensor).state) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +esphome: + on_boot: + - sensor.template.publish: + id: template_sens + state: 42.0 + + # Templated + - sensor.template.publish: + id: template_sens + state: !lambda 'return 42.0;' + +binary_sensor: + - platform: template + id: some_binary_sensor + name: "Garage Door Open" + lambda: |- + if (id(template_sens).state > 30) { + // Garage Door is open. + return true; + } else { + // Garage Door is closed. + return false; + } + +output: + - platform: template + id: outputsplit + type: float + write_action: + - logger.log: "write_action" + +switch: + - platform: template + name: "Template Switch" + lambda: |- + if (id(some_binary_sensor).state) { + return true; + } else { + return false; + } + turn_on_action: + - logger.log: "turn_on_action" + turn_off_action: + - logger.log: "turn_off_action" + +button: + - platform: template + name: "Template Button" + on_press: + - logger.log: Button Pressed + +cover: + - platform: template + name: "Template Cover" + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } else { + return COVER_CLOSED; + } + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + +number: + - platform: template + name: "Template number" + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: "Template select" + optimistic: true + options: + - one + - two + - three + initial_option: two + +lock: + - platform: template + name: "Template Lock" + lambda: |- + if (id(some_binary_sensor).state) { + return LOCK_STATE_LOCKED; + } else { + return LOCK_STATE_UNLOCKED; + } + lock_action: + - logger.log: lock_action + unlock_action: + - logger.log: unlock_action + open_action: + - logger.log: open_action + +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + +alarm_control_panel: + - platform: template + name: Alarm Panel + codes: + - "1234" diff --git a/tests/test_build_components/build_components_base.esp32-ard.yaml b/tests/test_build_components/build_components_base.esp32-ard.yaml new file mode 100644 index 0000000000..f460c57298 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-ard.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32ard + friendly_name: $component_name + +esp32: + board: nodemcu-32s + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-c3-ard.yaml b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8a52e0c916 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32c3ard + friendly_name: $component_name + +esp32: + board: lolin_c3_mini + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-c3-idf.yaml b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6b4b61fe58 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32c3idf + friendly_name: $component_name + +esp32: + board: lolin_c3_mini + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-idf.yaml b/tests/test_build_components/build_components_base.esp32-idf.yaml new file mode 100644 index 0000000000..ab1bda2a19 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-idf.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32idf + friendly_name: $component_name + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s2-ard.yaml b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml new file mode 100644 index 0000000000..ffb912d3d9 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s2ard + friendly_name: $component_name + +esp32: + board: esp32-s2-saola-1 + variant: ESP32S2 + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s2-idf.yaml b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml new file mode 100644 index 0000000000..4d1378b2b2 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s2ard + friendly_name: $component_name + +esp32: + board: esp32-s2-saola-1 + variant: ESP32S2 + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s3-ard.yaml b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml new file mode 100644 index 0000000000..c850c9665f --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s3ard + friendly_name: $component_name + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml new file mode 100644 index 0000000000..a43a2a6736 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s3ard + friendly_name: $component_name + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp8266.yaml b/tests/test_build_components/build_components_base.esp8266.yaml new file mode 100644 index 0000000000..d7bdc03659 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp8266.yaml @@ -0,0 +1,18 @@ +esphome: + name: componenttestesp8266 + friendly_name: $component_name + +esp8266: + board: d1_mini + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.rp2040.yaml b/tests/test_build_components/build_components_base.rp2040.yaml new file mode 100644 index 0000000000..a02942ea35 --- /dev/null +++ b/tests/test_build_components/build_components_base.rp2040.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestrp2040 + friendly_name: $component_name + +rp2040: + board: rpipicow + framework: + # Waiting for https://github.com/platformio/platform-raspberrypi/pull/36 + platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file From e2f2feafdd912f5e249ccb1b47502e4201665cf2 Mon Sep 17 00:00:00 2001 From: Rene Guca <45061891+rguca@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:30:58 +0100 Subject: [PATCH 298/436] WiFi fast_connect: save/load BSSID and channel for faster connect from sleep (#5931) --- esphome/components/wifi/wifi_component.cpp | 38 ++++++++++++++++++++++ esphome/components/wifi/wifi_component.h | 9 +++++ 2 files changed, 47 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 519489097a..05938d87a2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -55,6 +55,9 @@ void WiFiComponent::start() { uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; this->pref_ = global_preferences->make_preference(hash, true); + if (this->fast_connect_) { + this->fast_connect_pref_ = global_preferences->make_preference(hash, false); + } SavedWifiSettings save{}; if (this->pref_.load(&save)) { @@ -78,6 +81,7 @@ void WiFiComponent::start() { if (this->fast_connect_) { this->selected_ap_ = this->sta_[0]; + this->load_fast_connect_settings_(); this->start_connecting(this->selected_ap_, false); } else { this->start_scanning(); @@ -604,6 +608,11 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; + + if (this->fast_connect_) { + this->save_fast_connect_settings_(); + } + return; } @@ -705,6 +714,35 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } +void WiFiComponent::load_fast_connect_settings_() { + SavedWifiFastConnectSettings fast_connect_save{}; + + if (this->fast_connect_pref_.load(&fast_connect_save)) { + bssid_t bssid{}; + std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + this->selected_ap_.set_bssid(bssid); + this->selected_ap_.set_channel(fast_connect_save.channel); + + ESP_LOGD(TAG, "Loaded saved fast_connect wifi settings"); + } +} + +void WiFiComponent::save_fast_connect_settings_() { + bssid_t bssid = wifi_bssid(); + uint8_t channel = wifi_channel_(); + + if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { + SavedWifiFastConnectSettings fast_connect_save{}; + + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + + this->fast_connect_pref_.save(&fast_connect_save); + + ESP_LOGD(TAG, "Saved fast_connect wifi settings"); + } +} + void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6cbdc51caf..be5095105c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -48,6 +48,11 @@ struct SavedWifiSettings { char password[65]; } PACKED; // NOLINT +struct SavedWifiFastConnectSettings { + uint8_t bssid[6]; + uint8_t channel; +} PACKED; // NOLINT + enum WiFiComponentState { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, @@ -334,6 +339,9 @@ class WiFiComponent : public Component { bool is_captive_portal_active_(); bool is_esp32_improv_active_(); + void load_fast_connect_settings_(); + void save_fast_connect_settings_(); + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -381,6 +389,7 @@ class WiFiComponent : public Component { optional output_power_; bool passive_scan_{false}; ESPPreferenceObject pref_; + ESPPreferenceObject fast_connect_pref_; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT bool btm_{false}; From 45c0d10eb091c8717af55ef4651f22316f0ec4b4 Mon Sep 17 00:00:00 2001 From: "pofilo (vmerat)" Date: Thu, 18 Jan 2024 08:35:20 +0100 Subject: [PATCH 299/436] Fixes Waveshare 7.5in B V2 and V3 (#6079) --- esphome/components/waveshare_epaper/waveshare_epaper.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 0e9b129988..244b3b1ce2 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1443,6 +1443,12 @@ void WaveshareEPaper7P5InBV2::initialize() { // COMMAND TCON SETTING this->command(0x60); this->data(0x22); + + this->command(0x82); + this->data(0x08); + this->command(0x30); + this->data(0x06); + // COMMAND RESOLUTION SETTING this->command(0x65); this->data(0x00); @@ -1472,6 +1478,7 @@ void HOT WaveshareEPaper7P5InBV2::display() { this->command(0x12); delay(100); // NOLINT this->wait_until_idle_(); + this->deep_sleep(); } int WaveshareEPaper7P5InBV2::get_width_internal() { return 800; } int WaveshareEPaper7P5InBV2::get_height_internal() { return 480; } @@ -1617,7 +1624,7 @@ void HOT WaveshareEPaper7P5InBV3::display() { this->command(0x13); // Start Transmission delay(2); for (uint32_t i = 0; i < buf_len; i++) { - this->data(this->buffer_[i]); + this->data(~this->buffer_[i]); } this->command(0x12); // Display Refresh From 045836c3fe4e088475c248256f7b2e537cbabe05 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Thu, 18 Jan 2024 04:09:49 -0500 Subject: [PATCH 300/436] Add combination sensor and remove absorbed kalman_combinator component (#5438) --- CODEOWNERS | 2 +- esphome/components/combination/__init__.py | 0 .../components/combination/combination.cpp | 262 ++++++++++++++++++ esphome/components/combination/combination.h | 141 ++++++++++ esphome/components/combination/sensor.py | 176 ++++++++++++ .../components/kalman_combinator/__init__.py | 1 - .../kalman_combinator/kalman_combinator.cpp | 82 ------ .../kalman_combinator/kalman_combinator.h | 46 --- .../components/kalman_combinator/sensor.py | 92 +----- tests/test1.yaml | 54 +++- 10 files changed, 637 insertions(+), 219 deletions(-) create mode 100644 esphome/components/combination/__init__.py create mode 100644 esphome/components/combination/combination.cpp create mode 100644 esphome/components/combination/combination.h create mode 100644 esphome/components/combination/sensor.py delete mode 100644 esphome/components/kalman_combinator/kalman_combinator.cpp delete mode 100644 esphome/components/kalman_combinator/kalman_combinator.h diff --git a/CODEOWNERS b/CODEOWNERS index 7e87679ad8..95e3b35f56 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -71,6 +71,7 @@ esphome/components/cd74hc4067/* @asoehlke esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz +esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/coolix/* @glmnet esphome/components/copy/* @OttoWinter esphome/components/cover/* @esphome/core @@ -161,7 +162,6 @@ esphome/components/integration/* @OttoWinter esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter -esphome/components/kalman_combinator/* @Cat-Ion esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb esphome/components/kuntze/* @ssieb diff --git a/esphome/components/combination/__init__.py b/esphome/components/combination/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp new file mode 100644 index 0000000000..716d270390 --- /dev/null +++ b/esphome/components/combination/combination.cpp @@ -0,0 +1,262 @@ +#include "combination.h" + +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include +#include +#include + +namespace esphome { +namespace combination { + +static const char *const TAG = "combination"; + +void CombinationComponent::log_config_(const LogString *combo_type) { + LOG_SENSOR("", "Combination Sensor:", this); + ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type)); + this->log_source_sensors(); +} + +void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } + +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &stddev) { + this->sensor_pairs_.emplace_back(sensor, stddev); +} + +void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { + this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); +} + +void CombinationNoParameterComponent::log_source_sensors() { + ESP_LOGCONFIG(TAG, " Source Sensors:"); + for (const auto &sensor : this->sensors_) { + ESP_LOGCONFIG(TAG, " - %s", sensor->get_name().c_str()); + } +} + +void CombinationOneParameterComponent::log_source_sensors() { + ESP_LOGCONFIG(TAG, " Source Sensors:"); + for (const auto &sensor : this->sensor_pairs_) { + auto &entity = *sensor.first; + ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + } +} + +void CombinationNoParameterComponent::setup() { + for (const auto &sensor : this->sensors_) { + // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result + // repeatedly in the same loop if multiple source senors update. + sensor->add_on_state_callback( + [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + } +} + +void KalmanCombinationComponent::dump_config() { + this->log_config_(LOG_STR("kalman")); + ESP_LOGCONFIG(TAG, " Update variance: %f per ms", this->update_variance_value_); + + if (this->std_dev_sensor_ != nullptr) { + LOG_SENSOR(" ", "Standard Deviation Sensor:", this->std_dev_sensor_); + } +} + +void KalmanCombinationComponent::setup() { + for (const auto &sensor : this->sensor_pairs_) { + const auto stddev = sensor.second; + sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + } +} + +void KalmanCombinationComponent::update_variance_() { + uint32_t now = millis(); + + // Variance increases by update_variance_ each millisecond + auto dt = now - this->last_update_; + auto dv = this->update_variance_value_ * dt; + this->variance_ += dv; + this->last_update_ = now; +} + +void KalmanCombinationComponent::correct_(float value, float stddev) { + if (std::isnan(value) || std::isinf(stddev)) { + return; + } + + if (std::isnan(this->state_) || std::isinf(this->variance_)) { + this->state_ = value; + this->variance_ = stddev * stddev; + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(stddev); + } + return; + } + + this->update_variance_(); + + // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu + // Use the value with the smaller variance as mu1 to prevent precision errors + const bool this_first = this->variance_ < (stddev * stddev); + const float mu1 = this_first ? this->state_ : value; + const float mu2 = this_first ? value : this->state_; + + const float var1 = this_first ? this->variance_ : stddev * stddev; + const float var2 = this_first ? stddev * stddev : this->variance_; + + const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); + const float var = var1 - (var1 * var1) / (var1 + var2); + + // Update and publish state + this->state_ = mu; + this->variance_ = var; + + this->publish_state(mu); + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(std::sqrt(var)); + } +} + +void LinearCombinationComponent::setup() { + for (const auto &sensor : this->sensor_pairs_) { + // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result + // repeatedly in the same loop if multiple source senors update. + sensor.first->add_on_state_callback( + [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + } +} + +void LinearCombinationComponent::handle_new_value(float value) { + // Multiplies each sensor state by a configured coeffecient and then sums + + if (!std::isfinite(value)) + return; + + float sum = 0.0; + + for (const auto &sensor : this->sensor_pairs_) { + const float sensor_state = sensor.first->state; + if (std::isfinite(sensor_state)) { + sum += sensor_state * sensor.second(sensor_state); + } + } + + this->publish_state(sum); +}; + +void MaximumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float max_value = (-1) * std::numeric_limits::infinity(); // note x = max(x, -infinity) + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + max_value = std::max(max_value, sensor->state); + } + } + + this->publish_state(max_value); +} + +void MeanCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float sum = 0.0; + size_t count = 0.0; + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + ++count; + sum += sensor->state; + } + } + + float mean = sum / count; + + this->publish_state(mean); +} + +void MedianCombinationComponent::handle_new_value(float value) { + // Sorts sensor states in ascending order and determines the middle value + + if (!std::isfinite(value)) + return; + + std::vector sensor_states; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sensor_states.push_back(sensor->state); + } + } + + sort(sensor_states.begin(), sensor_states.end()); + size_t sensor_states_size = sensor_states.size(); + + float median = NAN; + + if (sensor_states_size) { + if (sensor_states_size % 2) { + // Odd number of measurements, use middle measurement + median = sensor_states[sensor_states_size / 2]; + } else { + // Even number of measurements, use the average of the two middle measurements + median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0; + } + } + + this->publish_state(median); +} + +void MinimumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float min_value = std::numeric_limits::infinity(); // note x = min(x, infinity) + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + min_value = std::min(min_value, sensor->state); + } + } + + this->publish_state(min_value); +} + +void MostRecentCombinationComponent::handle_new_value(float value) { this->publish_state(value); } + +void RangeCombinationComponent::handle_new_value(float value) { + // Sorts sensor states then takes difference between largest and smallest states + + if (!std::isfinite(value)) + return; + + std::vector sensor_states; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sensor_states.push_back(sensor->state); + } + } + + sort(sensor_states.begin(), sensor_states.end()); + + float range = sensor_states.back() - sensor_states.front(); + this->publish_state(range); +} + +void SumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float sum = 0.0; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sum += sensor->state; + } + } + + this->publish_state(sum); +} + +} // namespace combination +} // namespace esphome diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h new file mode 100644 index 0000000000..901aeaf259 --- /dev/null +++ b/esphome/components/combination/combination.h @@ -0,0 +1,141 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +#include + +namespace esphome { +namespace combination { + +class CombinationComponent : public Component, public sensor::Sensor { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + /// @brief Logs all source sensor's names + virtual void log_source_sensors() = 0; + + protected: + /// @brief Logs the sensor for use in dump_config + /// @param combo_type Name of the combination operation + void log_config_(const LogString *combo_type); +}; + +/// @brief Base class for operations that do not require an extra parameter to compute the combination +class CombinationNoParameterComponent : public CombinationComponent { + public: + /// @brief Adds a callback to each source sensor + void setup() override; + + void add_source(Sensor *sensor); + + /// @brief Computes the combination + /// @param value Newest sensor measurement + virtual void handle_new_value(float value) = 0; + + /// @brief Logs all source sensor's names in sensors_ + void log_source_sensors() override; + + protected: + std::vector sensors_; +}; + +// Base class for opertions that require one parameter to compute the combination +class CombinationOneParameterComponent : public CombinationComponent { + public: + void add_source(Sensor *sensor, std::function const &stddev); + void add_source(Sensor *sensor, float stddev); + + /// @brief Logs all source sensor's names in sensor_pairs_ + void log_source_sensors() override; + + protected: + std::vector>> sensor_pairs_; +}; + +class KalmanCombinationComponent : public CombinationOneParameterComponent { + public: + void dump_config() override; + void setup() override; + + void set_process_std_dev(float process_std_dev) { + this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; + } + void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } + + protected: + void update_variance_(); + void correct_(float value, float stddev); + + // Optional sensor for publishing the current error + sensor::Sensor *std_dev_sensor_{nullptr}; + + // Tick of the last update + uint32_t last_update_{0}; + // Change of the variance, per ms + float update_variance_value_{0.f}; + + // Best guess for the state and its variance + float state_{NAN}; + float variance_{INFINITY}; +}; + +class LinearCombinationComponent : public CombinationOneParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("linear")); } + void setup() override; + + void handle_new_value(float value); +}; + +class MaximumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("max")); } + + void handle_new_value(float value) override; +}; + +class MeanCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("mean")); } + + void handle_new_value(float value) override; +}; + +class MedianCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("median")); } + + void handle_new_value(float value) override; +}; + +class MinimumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("min")); } + + void handle_new_value(float value) override; +}; + +class MostRecentCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("most_recently_updated")); } + + void handle_new_value(float value) override; +}; + +class RangeCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("range")); } + + void handle_new_value(float value) override; +}; + +class SumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("sum")); } + + void handle_new_value(float value) override; +}; + +} // namespace combination +} // namespace esphome diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py new file mode 100644 index 0000000000..fad0277061 --- /dev/null +++ b/esphome/components/combination/sensor.py @@ -0,0 +1,176 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_RANGE, + CONF_SOURCE, + CONF_SUM, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, +) +from esphome.core.entity_helpers import inherit_property_from + +CODEOWNERS = ["@Cat-Ion", "@kahrendt"] + +combination_ns = cg.esphome_ns.namespace("combination") + +KalmanCombinationComponent = combination_ns.class_( + "KalmanCombinationComponent", cg.Component, sensor.Sensor +) +LinearCombinationComponent = combination_ns.class_( + "LinearCombinationComponent", cg.Component, sensor.Sensor +) +MaximumCombinationComponent = combination_ns.class_( + "MaximumCombinationComponent", cg.Component, sensor.Sensor +) +MeanCombinationComponent = combination_ns.class_( + "MeanCombinationComponent", cg.Component, sensor.Sensor +) +MedianCombinationComponent = combination_ns.class_( + "MedianCombinationComponent", cg.Component, sensor.Sensor +) +MinimumCombinationComponent = combination_ns.class_( + "MinimumCombinationComponent", cg.Component, sensor.Sensor +) +MostRecentCombinationComponent = combination_ns.class_( + "MostRecentCombinationComponent", cg.Component, sensor.Sensor +) +RangeCombinationComponent = combination_ns.class_( + "RangeCombinationComponent", cg.Component, sensor.Sensor +) +SumCombinationComponent = combination_ns.class_( + "SumCombinationComponent", cg.Component, sensor.Sensor +) + +CONF_COEFFECIENT = "coeffecient" +CONF_ERROR = "error" +CONF_KALMAN = "kalman" +CONF_LINEAR = "linear" +CONF_MAX = "max" +CONF_MEAN = "mean" +CONF_MEDIAN = "median" +CONF_MIN = "min" +CONF_MOST_RECENTLY_UPDATED = "most_recently_updated" +CONF_PROCESS_STD_DEV = "process_std_dev" +CONF_SOURCES = "sources" +CONF_STD_DEV = "std_dev" + + +KALMAN_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), + } +) + +LINEAR_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), + } +) + +SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + } +) + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_KALMAN: sensor.sensor_schema(KalmanCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, + cv.Required(CONF_SOURCES): cv.ensure_list(KALMAN_SOURCE_SCHEMA), + cv.Optional(CONF_STD_DEV): sensor.sensor_schema(), + } + ), + CONF_LINEAR: sensor.sensor_schema(LinearCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(LINEAR_SOURCE_SCHEMA)}), + CONF_MAX: sensor.sensor_schema(MaximumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MEAN: sensor.sensor_schema(MeanCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MEDIAN: sensor.sensor_schema(MedianCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MIN: sensor.sensor_schema(MinimumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MOST_RECENTLY_UPDATED: sensor.sensor_schema(MostRecentCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_RANGE: sensor.sensor_schema(RangeCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_SUM: sensor.sensor_schema(SumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + } +) + + +# Inherit some sensor values from the first source, for both the state and the error value +# CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" +properties_to_inherit = [ + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_UNIT_OF_MEASUREMENT, +] +inherit_schema_for_state = [ + inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] +inherit_schema_for_std_dev = [ + inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] + +FINAL_VALIDATE_SCHEMA = cv.All( + *inherit_schema_for_state, + *inherit_schema_for_std_dev, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): + cg.add(var.set_process_std_dev(proces_std_dev)) + + for source_conf in config[CONF_SOURCES]: + source = await cg.get_variable(source_conf[CONF_SOURCE]) + if config[CONF_TYPE] == CONF_KALMAN: + error = await cg.templatable( + source_conf[CONF_ERROR], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, error)) + elif config[CONF_TYPE] == CONF_LINEAR: + coeffecient = await cg.templatable( + source_conf[CONF_COEFFECIENT], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, coeffecient)) + else: + cg.add(var.add_source(source)) + + if CONF_STD_DEV in config: + sens = await sensor.new_sensor(config[CONF_STD_DEV]) + cg.add(var.set_std_dev_sensor(sens)) diff --git a/esphome/components/kalman_combinator/__init__.py b/esphome/components/kalman_combinator/__init__.py index 3356e61bb2..e69de29bb2 100644 --- a/esphome/components/kalman_combinator/__init__.py +++ b/esphome/components/kalman_combinator/__init__.py @@ -1 +0,0 @@ -CODEOWNERS = ["@Cat-Ion"] diff --git a/esphome/components/kalman_combinator/kalman_combinator.cpp b/esphome/components/kalman_combinator/kalman_combinator.cpp deleted file mode 100644 index 50d8f03a93..0000000000 --- a/esphome/components/kalman_combinator/kalman_combinator.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "kalman_combinator.h" -#include "esphome/core/hal.h" -#include -#include - -namespace esphome { -namespace kalman_combinator { - -void KalmanCombinatorComponent::dump_config() { - ESP_LOGCONFIG("kalman_combinator", "Kalman Combinator:"); - ESP_LOGCONFIG("kalman_combinator", " Update variance: %f per ms", this->update_variance_value_); - ESP_LOGCONFIG("kalman_combinator", " Sensors:"); - for (const auto &sensor : this->sensors_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG("kalman_combinator", " - %s", entity.get_name().c_str()); - } -} - -void KalmanCombinatorComponent::setup() { - for (const auto &sensor : this->sensors_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); - } -} - -void KalmanCombinatorComponent::add_source(Sensor *sensor, std::function const &stddev) { - this->sensors_.emplace_back(sensor, stddev); -} - -void KalmanCombinatorComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); -} - -void KalmanCombinatorComponent::update_variance_() { - uint32_t now = millis(); - - // Variance increases by update_variance_ each millisecond - auto dt = now - this->last_update_; - auto dv = this->update_variance_value_ * dt; - this->variance_ += dv; - this->last_update_ = now; -} - -void KalmanCombinatorComponent::correct_(float value, float stddev) { - if (std::isnan(value) || std::isinf(stddev)) { - return; - } - - if (std::isnan(this->state_) || std::isinf(this->variance_)) { - this->state_ = value; - this->variance_ = stddev * stddev; - if (this->std_dev_sensor_ != nullptr) { - this->std_dev_sensor_->publish_state(stddev); - } - return; - } - - this->update_variance_(); - - // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu - // Use the value with the smaller variance as mu1 to prevent precision errors - const bool this_first = this->variance_ < (stddev * stddev); - const float mu1 = this_first ? this->state_ : value; - const float mu2 = this_first ? value : this->state_; - - const float var1 = this_first ? this->variance_ : stddev * stddev; - const float var2 = this_first ? stddev * stddev : this->variance_; - - const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); - const float var = var1 - (var1 * var1) / (var1 + var2); - - // Update and publish state - this->state_ = mu; - this->variance_ = var; - - this->publish_state(mu); - if (this->std_dev_sensor_ != nullptr) { - this->std_dev_sensor_->publish_state(std::sqrt(var)); - } -} -} // namespace kalman_combinator -} // namespace esphome diff --git a/esphome/components/kalman_combinator/kalman_combinator.h b/esphome/components/kalman_combinator/kalman_combinator.h deleted file mode 100644 index afbe3ece92..0000000000 --- a/esphome/components/kalman_combinator/kalman_combinator.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include -#include - -namespace esphome { -namespace kalman_combinator { - -class KalmanCombinatorComponent : public Component, public sensor::Sensor { - public: - KalmanCombinatorComponent() = default; - - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - - void dump_config() override; - void setup() override; - - void add_source(Sensor *sensor, std::function const &stddev); - void add_source(Sensor *sensor, float stddev); - void set_process_std_dev(float process_std_dev) { - this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; - } - void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } - - private: - void update_variance_(); - void correct_(float value, float stddev); - - // Source sensors and their error functions - std::vector>> sensors_; - - // Optional sensor for publishing the current error - sensor::Sensor *std_dev_sensor_{nullptr}; - - // Tick of the last update - uint32_t last_update_{0}; - // Change of the variance, per ms - float update_variance_value_{0.f}; - - // Best guess for the state and its variance - float state_{NAN}; - float variance_{INFINITY}; -}; -} // namespace kalman_combinator -} // namespace esphome diff --git a/esphome/components/kalman_combinator/sensor.py b/esphome/components/kalman_combinator/sensor.py index 28b96077cc..eca1ba7b85 100644 --- a/esphome/components/kalman_combinator/sensor.py +++ b/esphome/components/kalman_combinator/sensor.py @@ -1,90 +1,6 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor -from esphome.const import ( - CONF_ID, - CONF_SOURCE, - CONF_ACCURACY_DECIMALS, - CONF_DEVICE_CLASS, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_UNIT_OF_MEASUREMENT, + +CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( + "The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n" + "See https://esphome.io/components/sensor/combination.html" ) -from esphome.core.entity_helpers import inherit_property_from - -kalman_combinator_ns = cg.esphome_ns.namespace("kalman_combinator") -KalmanCombinatorComponent = kalman_combinator_ns.class_( - "KalmanCombinatorComponent", cg.Component, sensor.Sensor -) - -CONF_ERROR = "error" -CONF_SOURCES = "sources" -CONF_PROCESS_STD_DEV = "process_std_dev" -CONF_STD_DEV = "std_dev" - - -CONFIG_SCHEMA = ( - sensor.sensor_schema(KalmanCombinatorComponent) - .extend(cv.COMPONENT_SCHEMA) - .extend( - { - cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, - cv.Required(CONF_SOURCES): cv.ensure_list( - cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), - } - ), - ), - cv.Optional(CONF_STD_DEV): sensor.sensor_schema(), - } - ) -) - -# Inherit some sensor values from the first source, for both the state and the error value -properties_to_inherit = [ - CONF_ACCURACY_DECIMALS, - CONF_DEVICE_CLASS, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_UNIT_OF_MEASUREMENT, - # CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" -] -inherit_schema_for_state = [ - inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) - for property in properties_to_inherit -] -inherit_schema_for_std_dev = [ - inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) - for property in properties_to_inherit -] - -FINAL_VALIDATE_SCHEMA = cv.All( - CONFIG_SCHEMA.extend( - {cv.Required(CONF_ID): cv.use_id(KalmanCombinatorComponent)}, - extra=cv.ALLOW_EXTRA, - ), - *inherit_schema_for_state, - *inherit_schema_for_std_dev, -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await sensor.register_sensor(var, config) - - cg.add(var.set_process_std_dev(config[CONF_PROCESS_STD_DEV])) - for source_conf in config[CONF_SOURCES]: - source = await cg.get_variable(source_conf[CONF_SOURCE]) - error = await cg.templatable( - source_conf[CONF_ERROR], - [(float, "x")], - cg.float_, - ) - cg.add(var.add_source(source, error)) - - if CONF_STD_DEV in config: - sens = await sensor.new_sensor(config[CONF_STD_DEV]) - cg.add(var.set_std_dev_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 038ac9c738..3558fa328e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -971,7 +971,8 @@ sensor: name: Internal Ttemperature update_interval: 15s i2c_id: i2c_bus - - platform: kalman_combinator + - platform: combination + type: kalman name: Kalman-filtered temperature process_std_dev: 0.00139 sources: @@ -980,6 +981,57 @@ sensor: return 0.4 + std::abs(x - 25) * 0.023; - source: scd4x_temperature error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: scd30_temperature + coeffecient: !lambda |- + return 0.4 + std::abs(x - 25) * 0.023; + - source: scd4x_temperature + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature - platform: htu21d temperature: name: Living Room Temperature 6 From ea9de45d164e32227bf4ea8b61bb475fbf426518 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:07:50 +0900 Subject: [PATCH 301/436] Bump platformio from 6.1.11 to 6.1.13 (#6086) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 468124e3ed..b28ca2ba66 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -81,7 +81,7 @@ RUN \ fi; \ pip3 install \ --break-system-packages --no-cache-dir \ - platformio==6.1.11 \ + platformio==6.1.13 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_platformio_interval 1000000 \ diff --git a/requirements.txt b/requirements.txt index 5281b64e66..18e0295fb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tornado==6.4 tzlocal==5.2 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.11 # When updating platformio, also update Dockerfile +platformio==6.1.13 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 From 6a6a70f1e57055ff5de8cd065fb308927db327e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:08:29 +0900 Subject: [PATCH 302/436] Bump actions/cache from 3.3.2 to 4.0.0 (#6110) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2187573709..2a108b34dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: venv # yamllint disable-line rule:line-length @@ -365,7 +365,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: ~/.platformio # yamllint disable-line rule:line-length From 8267b3274ce2c801000d16eca843a7c89561d811 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:10:23 +1100 Subject: [PATCH 303/436] Enable networking and some other components on host platform (#6114) --- esphome/components/host/__init__.py | 6 +++++- esphome/components/network/ip_address.h | 16 ++++++++++++++++ esphome/components/socket/bsd_sockets_impl.cpp | 2 +- esphome/core/component.cpp | 2 +- esphome/helpers.py | 5 +++++ platformio.ini | 1 + 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 14d2597866..eb44bcccd6 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( PLATFORM_HOST, ) from esphome.core import CORE +from esphome.helpers import IS_MACOS import esphome.config_validation as cv import esphome.codegen as cg @@ -14,7 +15,6 @@ from .const import KEY_HOST # force import gpio to register pin schema from .gpio import host_pin_to_code # noqa - CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["network"] @@ -35,5 +35,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") + 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/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 709524c9d1..02e71790a7 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -14,6 +14,13 @@ #include #endif /* USE_ADRDUINO */ +#ifdef USE_HOST +#include +using ip_addr_t = in_addr; +using ip4_addr_t = in_addr; +#define ipaddr_aton(x, y) inet_aton((x), (y)) +#endif + #if USE_ESP32_FRAMEWORK_ARDUINO #define arduino_ns Arduino_h #elif USE_LIBRETINY @@ -32,6 +39,14 @@ namespace network { struct IPAddress { public: +#ifdef USE_HOST + IPAddress() { ip_addr_.s_addr = 0; } + IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { + this->ip_addr_.s_addr = htonl((first << 24) | (second << 16) | (third << 8) | fourth); + } + IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } + IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } +#else IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { IP_ADDR4(&ip_addr_, first, second, third, fourth); @@ -107,6 +122,7 @@ struct IPAddress { } return *this; } +#endif protected: ip_addr_t ip_addr_; diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 6c356106f3..f07f5c8f81 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -87,7 +87,7 @@ class BSDSocketImpl : public Socket { int listen(int backlog) override { return ::listen(fd_, backlog); } ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); } ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { -#if defined(USE_ESP32) +#if defined(USE_ESP32) || defined(USE_HOST) return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); #else return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e2f27f9828..b0406e6502 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -169,7 +169,7 @@ float Component::get_actual_setup_priority() const { void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } bool Component::has_overridden_loop() const { -#ifdef CLANG_TIDY +#if defined(USE_HOST) || defined(CLANG_TIDY) bool loop_overridden = true; bool call_loop_overridden = true; #else diff --git a/esphome/helpers.py b/esphome/helpers.py index 254c950b5d..4c8cb4e2cc 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -3,6 +3,7 @@ from contextlib import suppress import logging import os +import platform from pathlib import Path from typing import Union import tempfile @@ -11,6 +12,10 @@ import re _LOGGER = logging.getLogger(__name__) +IS_MACOS = platform.system() == "Darwin" +IS_WINDOWS = platform.system() == "Windows" +IS_LINUX = platform.system() == "Linux" + def ensure_unique_string(preferred_string, current_strings): test_string = preferred_string diff --git a/platformio.ini b/platformio.ini index f5f510244c..e47527fe98 100644 --- a/platformio.ini +++ b/platformio.ini @@ -388,3 +388,4 @@ lib_deps = build_flags = ${common.build_flags} -DUSE_HOST + -std=c++17 From 2283b3b443c024fa4bb5f35d1f7310e5fd1db72a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:46:55 +1100 Subject: [PATCH 304/436] Fix time component for host platform (#6118) --- esphome/components/time/real_time_clock.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 0573c7de9d..9b903d098b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -1,6 +1,10 @@ #include "real_time_clock.h" #include "esphome/core/log.h" +#ifdef USE_HOST +#include +#else #include "lwip/opt.h" +#endif #ifdef USE_ESP8266 #include "sys/time.h" #endif @@ -25,7 +29,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { .tv_sec = static_cast(epoch), .tv_usec = 0, }; ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); - timezone tz = {0, 0}; + struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); if (ret == EINVAL) { // Some ESP8266 frameworks abort when timezone parameter is not NULL From 1fef769496ed89c0062d8e70f5964b8318ba4550 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 13:42:17 +1100 Subject: [PATCH 305/436] Add quad spi features (#5925) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/spi/__init__.py | 95 +++++++++++++++++++------- esphome/components/spi/spi.cpp | 6 +- esphome/components/spi/spi.h | 51 ++++++++++++-- esphome/components/spi/spi_arduino.cpp | 3 +- esphome/components/spi/spi_esp_idf.cpp | 83 ++++++++++++++++++++-- tests/test8.1.yaml | 9 +++ 6 files changed, 207 insertions(+), 40 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index d116641373..10ea906a92 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -29,12 +29,15 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + CONF_ALLOW_OTHER_USES, + CONF_DATA_PINS, ) from esphome.core import coroutine_with_priority, CORE CODEOWNERS = ["@esphome/core", "@clydebarrow"] spi_ns = cg.esphome_ns.namespace("spi") SPIComponent = spi_ns.class_("SPIComponent", cg.Component) +QuadSPIComponent = spi_ns.class_("QuadSPIComponent", cg.Component) SPIDevice = spi_ns.class_("SPIDevice") SPIDataRate = spi_ns.enum("SPIDataRate") SPIMode = spi_ns.enum("SPIMode") @@ -190,12 +193,9 @@ def get_hw_spi(config, available): def validate_spi_config(config): available = list(range(len(get_hw_interface_list()))) for spi in config: + # map pin number to schema + spi[CONF_CLK_PIN] = pins.gpio_output_pin_schema(spi[CONF_CLK_PIN]) interface = spi[CONF_INTERFACE] - if spi[CONF_FORCE_SW]: - if interface == "any": - spi[CONF_INTERFACE] = interface = "software" - elif interface != "software": - raise cv.Invalid("force_sw is deprecated - use interface: software") if interface == "software": pass elif interface == "any": @@ -229,6 +229,8 @@ def validate_spi_config(config): spi, spi[CONF_INTERFACE_INDEX] ): raise cv.Invalid("Invalid pin selections for hardware SPI interface") + if CONF_DATA_PINS in spi and CONF_INTERFACE_INDEX not in spi: + raise cv.Invalid("Quad mode requires a hardware interface") return config @@ -249,14 +251,26 @@ def get_spi_interface(index): return "new SPIClass(HSPI)" +# Do not use a pin schema for the number, as that will trigger a pin reuse error due to duplication of the +# clock pin in the standard and quad schemas. +clk_pin_validator = cv.maybe_simple_value( + { + cv.Required(CONF_NUMBER): cv.Any(cv.int_, cv.string), + cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean, + }, + key=CONF_NUMBER, +) + SPI_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(SPIComponent), - cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLK_PIN): clk_pin_validator, cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_FORCE_SW, default=False): cv.boolean, + cv.Optional(CONF_FORCE_SW): cv.invalid( + "force_sw is deprecated - use interface: software" + ), cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( *sum(get_hw_interface_list(), ["software", "hardware", "any"]), lower=True, @@ -267,8 +281,34 @@ SPI_SCHEMA = cv.All( cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), ) +SPI_QUAD_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(QuadSPIComponent), + cv.Required(CONF_CLK_PIN): clk_pin_validator, + cv.Required(CONF_DATA_PINS): cv.All( + cv.ensure_list(pins.internal_gpio_output_pin_number), + cv.Length(min=4, max=4), + ), + cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( + *sum(get_hw_interface_list(), ["hardware"]), + lower=True, + ), + } + ), + cv.only_with_esp_idf, +) + CONFIG_SCHEMA = cv.All( - cv.ensure_list(SPI_SCHEMA), + # Order is important. SPI_SCHEMA is the default. + cv.ensure_list( + cv.Any( + SPI_SCHEMA, + SPI_QUAD_SCHEMA, + msg="Standard SPI requires mosi_pin and/or miso_pin; quad SPI requires data_pins only." + + " A clock pin is always required", + ), + ), validate_spi_config, ) @@ -277,43 +317,46 @@ CONFIG_SCHEMA = cv.All( async def to_code(configs): cg.add_define("USE_SPI") cg.add_global(spi_ns.using) + if CORE.using_arduino: + cg.add_library("SPI", None) for spi in configs: var = cg.new_Pvariable(spi[CONF_ID]) await cg.register_component(var, spi) - clk = await cg.gpio_pin_expression(spi[CONF_CLK_PIN]) cg.add(var.set_clk(clk)) - if CONF_MISO_PIN in spi: - miso = await cg.gpio_pin_expression(spi[CONF_MISO_PIN]) - cg.add(var.set_miso(miso)) - if CONF_MOSI_PIN in spi: - mosi = await cg.gpio_pin_expression(spi[CONF_MOSI_PIN]) - cg.add(var.set_mosi(mosi)) - if CONF_INTERFACE_INDEX in spi: - index = spi[CONF_INTERFACE_INDEX] - cg.add(var.set_interface(cg.RawExpression(get_spi_interface(index)))) + if miso := spi.get(CONF_MISO_PIN): + cg.add(var.set_miso(await cg.gpio_pin_expression(miso))) + if mosi := spi.get(CONF_MOSI_PIN): + cg.add(var.set_mosi(await cg.gpio_pin_expression(mosi))) + if data_pins := spi.get(CONF_DATA_PINS): + cg.add(var.set_data_pins(data_pins)) + if (index := spi.get(CONF_INTERFACE_INDEX)) is not None: + interface = get_spi_interface(index) + cg.add(var.set_interface(cg.RawExpression(interface))) cg.add( var.set_interface_name( - re.sub( - r"\W", "", get_spi_interface(index).replace("new SPIClass", "") - ) + re.sub(r"\W", "", interface.replace("new SPIClass", "")) ) ) - if CORE.using_arduino: - cg.add_library("SPI", None) - def spi_device_schema( - cs_pin_required=True, default_data_rate=cv.UNDEFINED, default_mode=cv.UNDEFINED + cs_pin_required=True, + default_data_rate=cv.UNDEFINED, + default_mode=cv.UNDEFINED, + quad=False, ): """Create a schema for an SPI device. :param cs_pin_required: If true, make the CS_PIN required in the config. :param default_data_rate: Optional data_rate to use as default + :param default_mode Optional. The default SPI mode to use. + :param quad If set, will require an SPI component configured as quad data bits. :return: The SPI device schema, `extend` this in your config schema. """ schema = { - cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + cv.GenerateID(CONF_SPI_ID): cv.use_id( + QuadSPIComponent if quad else SPIComponent + ), cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA, cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( SPI_MODE_OPTIONS, upper=True diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 9d06ac0e45..b13826c443 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -49,7 +49,8 @@ void SPIComponent::setup() { } if (this->using_hw_) { - this->spi_bus_ = SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); + this->spi_bus_ = + SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_, this->data_pins_); if (this->spi_bus_ == nullptr) { ESP_LOGE(TAG, "Unable to allocate SPI interface"); this->mark_failed(); @@ -68,6 +69,9 @@ void SPIComponent::dump_config() { LOG_PIN(" CLK Pin: ", this->clk_pin_) LOG_PIN(" SDI Pin: ", this->sdi_pin_) LOG_PIN(" SDO Pin: ", this->sdo_pin_) + for (size_t i = 0; i != this->data_pins_.size(); i++) { + ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]); + } if (this->spi_bus_->is_hw()) { ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_); } else { diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 0eb4cd7eb6..f581dc3f56 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -1,11 +1,12 @@ #pragma once +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" -#include #include +#include +#include #ifdef USE_ARDUINO @@ -208,6 +209,10 @@ class SPIDelegate { esph_log_e("spi_device", "variable length write not implemented"); } + virtual void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, + const uint8_t *data, size_t length, uint8_t bus_width) { + esph_log_e("spi_device", "write_cmd_addr_data not implemented"); + } // write 16 bits virtual void write16(uint16_t data) { if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { @@ -331,6 +336,7 @@ class SPIComponent : public Component { void set_miso(GPIOPin *sdi) { this->sdi_pin_ = sdi; } void set_mosi(GPIOPin *sdo) { this->sdo_pin_ = sdo; } + void set_data_pins(std::vector pins) { this->data_pins_ = std::move(pins); } void set_interface(SPIInterface interface) { this->interface_ = interface; @@ -348,15 +354,19 @@ class SPIComponent : public Component { GPIOPin *clk_pin_{nullptr}; GPIOPin *sdi_pin_{nullptr}; GPIOPin *sdo_pin_{nullptr}; + std::vector data_pins_{}; + SPIInterface interface_{}; bool using_hw_{false}; const char *interface_name_{nullptr}; SPIBus *spi_bus_{}; std::map devices_; - static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi); + static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins); }; +using QuadSPIComponent = SPIComponent; /** * Base class for SPIDevice, un-templated. */ @@ -422,18 +432,49 @@ class SPIDevice : public SPIClient { void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); } + /** + * Write a single data item, up to 32 bits. + * @param data The data + * @param num_bits The number of bits to write. The lower num_bits of data will be sent. + */ void write(uint16_t data, size_t num_bits) { this->delegate_->write(data, num_bits); }; + /* Write command, address and data. Command and address will be written as single-bit SPI, + * data phase can be multiple bit (currently only 1 or 4) + * @param cmd_bits Number of bits to write in the command phase + * @param cmd The command value to write + * @param addr_bits Number of bits to write in addr phase + * @param address Address data + * @param data Plain data bytes + * @param length Number of data bytes + * @param bus_width The number of data lines to use for the data phase. + */ + void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data, + size_t length, uint8_t bus_width = 1) { + this->delegate_->write_cmd_addr_data(cmd_bits, cmd, addr_bits, address, data, length, bus_width); + } + void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); } + /** + * Write the array data, replace with received data. + * @param data + * @param length + */ void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); } uint8_t transfer_byte(uint8_t data) { return this->delegate_->transfer(data); } - // the driver will byte-swap if required. + /** Write 16 bit data. The driver will byte-swap if required. + */ void write_byte16(uint16_t data) { this->delegate_->write16(data); } - // avoid use of this if possible. It's inefficient and ugly. + /** + * Write an array of data as 16 bit values, byte-swapping if required. Use of this should be avoided as + * it is horribly slow. + * @param data + * @param length + */ void write_array16(const uint16_t *data, size_t length) { this->delegate_->write_array16(data, length); } void enable() { this->delegate_->begin_transaction(); } diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 4628486550..f7fe523a33 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -85,7 +85,8 @@ class SPIBusHw : public SPIBus { bool is_hw() override { return true; } }; -SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) { +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins) { return new SPIBusHw(clk, sdo, sdi, interface); } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index 03ab298019..55680f72d3 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -104,6 +104,60 @@ class SPIDelegateHw : public SPIDelegate { } } + /** + * Write command, address and data + * @param cmd_bits Number of bits to write in the command phase + * @param cmd The command value to write + * @param addr_bits Number of bits to write in addr phase + * @param address Address data + * @param data Remaining data bytes + * @param length Number of data bytes + * @param bus_width The number of data lines to use + */ + void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data, + size_t length, uint8_t bus_width) override { + spi_transaction_ext_t desc = {}; + if (length == 0 && cmd_bits == 0 && addr_bits == 0) { + esph_log_w(TAG, "Nothing to transfer"); + return; + } + desc.base.flags = SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_DUMMY; + if (bus_width == 4) { + desc.base.flags |= SPI_TRANS_MODE_QIO; + } else if (bus_width == 8) { + desc.base.flags |= SPI_TRANS_MODE_OCT; + } + desc.command_bits = cmd_bits; + desc.address_bits = addr_bits; + desc.dummy_bits = 0; + desc.base.rxlength = 0; + desc.base.cmd = cmd; + desc.base.addr = address; + do { + size_t chunk_size = std::min(length, MAX_TRANSFER_SIZE); + if (data != nullptr && chunk_size != 0) { + desc.base.length = chunk_size * 8; + desc.base.tx_buffer = data; + length -= chunk_size; + data += chunk_size; + } else { + length = 0; + desc.base.length = 0; + } + esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + return; + } + // if more data is to be sent, skip the command and address phases. + desc.command_bits = 0; + desc.address_bits = 0; + } while (length != 0); + } + void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); } uint8_t transfer(uint8_t data) override { @@ -142,13 +196,27 @@ class SPIDelegateHw : public SPIDelegate { class SPIBusHw : public SPIBus { public: - SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) { + SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel, std::vector data_pins) + : SPIBus(clk, sdo, sdi), channel_(channel) { spi_bus_config_t buscfg = {}; - buscfg.mosi_io_num = Utility::get_pin_no(sdo); - buscfg.miso_io_num = Utility::get_pin_no(sdi); buscfg.sclk_io_num = Utility::get_pin_no(clk); - buscfg.quadwp_io_num = -1; - buscfg.quadhd_io_num = -1; + buscfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_SCLK; + if (data_pins.empty()) { + buscfg.mosi_io_num = Utility::get_pin_no(sdo); + buscfg.miso_io_num = Utility::get_pin_no(sdi); + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + } else { + buscfg.data0_io_num = data_pins[0]; + buscfg.data1_io_num = data_pins[1]; + buscfg.data2_io_num = data_pins[2]; + buscfg.data3_io_num = data_pins[3]; + buscfg.data4_io_num = -1; + buscfg.data5_io_num = -1; + buscfg.data6_io_num = -1; + buscfg.data7_io_num = -1; + buscfg.flags |= SPICOMMON_BUSFLAG_QUAD; + } buscfg.max_transfer_sz = MAX_TRANSFER_SIZE; auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO); if (err != ESP_OK) @@ -166,8 +234,9 @@ class SPIBusHw : public SPIBus { bool is_hw() override { return true; } }; -SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) { - return new SPIBusHw(clk, sdo, sdi, interface); +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins) { + return new SPIBusHw(clk, sdo, sdi, interface, data_pins); } #endif diff --git a/tests/test8.1.yaml b/tests/test8.1.yaml index bc1d2e22a4..839b1f3e6e 100644 --- a/tests/test8.1.yaml +++ b/tests/test8.1.yaml @@ -28,6 +28,15 @@ spi: allow_other_uses: false mosi_pin: GPIO6 interface: any + - id: quad_spi + clk_pin: 47 + data_pins: + - + number: 40 + allow_other_uses: false + - 41 + - 42 + - 43 spi_device: id: spidev From 6561746f97c8473c5ec4a1e65bca337a4ea736d7 Mon Sep 17 00:00:00 2001 From: alexbuit Date: Fri, 19 Jan 2024 03:50:00 +0100 Subject: [PATCH 306/436] add AM2120 device type (#6115) --- esphome/components/dht/dht.cpp | 2 +- esphome/components/dht/dht.h | 2 ++ esphome/components/dht/sensor.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 07634cafdf..5112092073 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -91,7 +91,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r delayMicroseconds(40); } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { delayMicroseconds(2000); - } else if (this->model_ == DHT_MODEL_AM2302) { + } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { delayMicroseconds(1000); } else { delayMicroseconds(800); diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index f3a29f9ce9..327e8a4f5c 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -11,6 +11,7 @@ enum DHTModel { DHT_MODEL_AUTO_DETECT = 0, DHT_MODEL_DHT11, DHT_MODEL_DHT22, + DHT_MODEL_AM2120, DHT_MODEL_AM2302, DHT_MODEL_RHT03, DHT_MODEL_SI7021, @@ -27,6 +28,7 @@ class DHT : public PollingComponent { * - DHT_MODEL_AUTO_DETECT (default) * - DHT_MODEL_DHT11 * - DHT_MODEL_DHT22 + * - DHT_MODEL_AM2120 * - DHT_MODEL_AM2302 * - DHT_MODEL_RHT03 * - DHT_MODEL_SI7021 diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index cd1886728e..da92a97e1f 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -23,6 +23,7 @@ DHT_MODELS = { "AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT, "DHT11": DHTModel.DHT_MODEL_DHT11, "DHT22": DHTModel.DHT_MODEL_DHT22, + "AM2120": DHTModel.DHT_MODEL_AM2120, "AM2302": DHTModel.DHT_MODEL_AM2302, "RHT03": DHTModel.DHT_MODEL_RHT03, "SI7021": DHTModel.DHT_MODEL_SI7021, From ed771abc8aff491cae27bade58aa847a42acdd74 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:10:53 +1100 Subject: [PATCH 307/436] Add support for Waveshare EPD 2.13" V3 (#5363) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/waveshare_epaper/__init__.py | 1 + .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_213v3.cpp | 186 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.cpp | 24 ++- .../waveshare_epaper/waveshare_epaper.h | 37 +++- tests/test4.yaml | 19 +- 7 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 esphome/components/waveshare_epaper/waveshare_213v3.cpp diff --git a/CODEOWNERS b/CODEOWNERS index 95e3b35f56..db44317776 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -367,6 +367,7 @@ esphome/components/veml3235/* @kbx81 esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz esphome/components/wake_on_lan/* @willwill2will54 +esphome/components/waveshare_epaper/* @clydebarrow esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_idf/* @dentra esphome/components/whirlpool/* @glmnet diff --git a/esphome/components/waveshare_epaper/__init__.py b/esphome/components/waveshare_epaper/__init__.py index e69de29bb2..c58ce8a01e 100644 --- a/esphome/components/waveshare_epaper/__init__.py +++ b/esphome/components/waveshare_epaper/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 1dd4b7fc54..1645ce0b1d 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -72,6 +72,9 @@ WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P13InDKE", WaveshareEPaper ) +WaveshareEPaper2P13InV3 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P13InV3", WaveshareEPaper +) GDEW0154M09 = waveshare_epaper_ns.class_("GDEW0154M09", WaveshareEPaper) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel") @@ -104,6 +107,7 @@ MODELS = { "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), + "2.13inv3": ("c", WaveshareEPaper2P13InV3), "1.54in-m5coreink-m09": ("c", GDEW0154M09), } diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp new file mode 100644 index 0000000000..196aeed3f7 --- /dev/null +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -0,0 +1,186 @@ +#include "waveshare_epaper.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace waveshare_epaper { + +static const char *const TAG = "waveshare_2.13v3"; + +static const uint8_t PARTIAL_LUT[] = { + 0x32, // cmd + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, +}; + +static const uint8_t FULL_LUT[] = { + 0x32, // CMD + 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0xF, 0x0, 0x0, 0x2, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, +}; + +static const uint8_t SW_RESET = 0x12; +static const uint8_t ACTIVATE = 0x20; +static const uint8_t WRITE_BUFFER = 0x24; +static const uint8_t WRITE_BASE = 0x26; + +static const uint8_t DRV_OUT_CTL[] = {0x01, 0x27, 0x01, 0x00}; // driver output control +static const uint8_t GATEV[] = {0x03, 0x17}; +static const uint8_t SRCV[] = {0x04, 0x41, 0x0C, 0x32}; +static const uint8_t SLEEP[] = {0x10, 0x01}; +static const uint8_t DATA_ENTRY[] = {0x11, 0x03}; // data entry mode +static const uint8_t TEMP_SENS[] = {0x18, 0x80}; // Temp sensor +static const uint8_t DISPLAY_UPDATE[] = {0x21, 0x00, 0x80}; // Display update control +static const uint8_t UPSEQ[] = {0x22, 0xC0}; +static const uint8_t ON_FULL[] = {0x22, 0xC7}; +static const uint8_t ON_PARTIAL[] = {0x22, 0x0F}; +static const uint8_t VCOM[] = {0x2C, 0x36}; +static const uint8_t CMD5[] = {0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t BORDER_PART[] = {0x3C, 0x80}; // border waveform +static const uint8_t BORDER_FULL[] = {0x3C, 0x05}; // border waveform +static const uint8_t CMD1[] = {0x3F, 0x22}; +static const uint8_t RAM_X_START[] = {0x44, 0x00, 121 / 8}; // set ram_x_address_start_end +static const uint8_t RAM_Y_START[] = {0x45, 0x00, 0x00, 250 - 1, 0}; // set ram_y_address_start_end +static const uint8_t RAM_X_POS[] = {0x4E, 0x00}; // set ram_x_address_counter +// static const uint8_t RAM_Y_POS[] = {0x4F, 0x00, 0x00}; // set ram_y_address_counter +#define SEND(x) this->cmd_data(x, sizeof(x)) + +void WaveshareEPaper2P13InV3::write_lut_(const uint8_t *lut) { + this->wait_until_idle_(); + this->cmd_data(lut, sizeof(PARTIAL_LUT)); + SEND(CMD1); + SEND(GATEV); + SEND(SRCV); + SEND(VCOM); +} + +// write the buffer starting on line top, up to line bottom. +void WaveshareEPaper2P13InV3::write_buffer_(uint8_t cmd, int top, int bottom) { + this->wait_until_idle_(); + this->set_window_(top, bottom); + this->command(cmd); + this->start_data_(); + auto width_bytes = this->get_width_internal() / 8; + this->write_array(this->buffer_ + top * width_bytes, (bottom - top) * width_bytes); + this->end_data_(); +} + +void WaveshareEPaper2P13InV3::send_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + } +} + +void WaveshareEPaper2P13InV3::setup() { + setup_pins_(); + delay(20); + this->send_reset_(); + // as a one-off delay this is not worth working around. + delay(100); // NOLINT + this->wait_until_idle_(); + this->command(SW_RESET); + this->wait_until_idle_(); + + SEND(DRV_OUT_CTL); + SEND(DATA_ENTRY); + SEND(CMD5); + this->set_window_(0, this->get_height_internal()); + SEND(BORDER_FULL); + SEND(DISPLAY_UPDATE); + SEND(TEMP_SENS); + this->wait_until_idle_(); + this->write_lut_(FULL_LUT); +} + +// t and b are y positions, i.e. line numbers. +void WaveshareEPaper2P13InV3::set_window_(int t, int b) { + uint8_t buffer[3]; + + SEND(RAM_X_START); + SEND(RAM_Y_START); + SEND(RAM_X_POS); + buffer[0] = 0x4F; + buffer[1] = (uint8_t) t; + buffer[2] = (uint8_t) (t >> 8); + SEND(buffer); +} + +// must implement, but we override setup to have more control +void WaveshareEPaper2P13InV3::initialize() {} + +void WaveshareEPaper2P13InV3::partial_update_() { + this->send_reset_(); + this->set_timeout(100, [this] { + this->write_lut_(PARTIAL_LUT); + SEND(BORDER_PART); + SEND(UPSEQ); + this->command(ACTIVATE); + this->set_timeout(100, [this] { + this->wait_until_idle_(); + this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal()); + SEND(ON_PARTIAL); + this->command(ACTIVATE); // Activate Display Update Sequence + this->is_busy_ = false; + }); + }); +} + +void WaveshareEPaper2P13InV3::full_update_() { + ESP_LOGI(TAG, "Performing full e-paper update."); + this->write_lut_(FULL_LUT); + this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal()); + this->write_buffer_(WRITE_BASE, 0, this->get_height_internal()); + SEND(ON_FULL); + this->command(ACTIVATE); // don't wait here + this->is_busy_ = false; +} + +void WaveshareEPaper2P13InV3::display() { + if (this->is_busy_ || (this->busy_pin_ != nullptr && this->busy_pin_->digital_read())) + return; + this->is_busy_ = true; + const bool partial = this->at_update_ != 0; + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; + if (partial) { + this->partial_update_(); + } else { + this->full_update_(); + } +} + +int WaveshareEPaper2P13InV3::get_width_internal() { return 128; } + +int WaveshareEPaper2P13InV3::get_height_internal() { return 250; } + +uint32_t WaveshareEPaper2P13InV3::idle_timeout_() { return 5000; } + +void WaveshareEPaper2P13InV3::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this) + ESP_LOGCONFIG(TAG, " Model: 2.13inV3"); + LOG_PIN(" CS Pin: ", this->cs_) + LOG_PIN(" Reset Pin: ", this->reset_pin_) + LOG_PIN(" DC Pin: ", this->dc_pin_) + LOG_PIN(" Busy Pin: ", this->busy_pin_) + LOG_UPDATE_INTERVAL(this) +} + +void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + +} // namespace waveshare_epaper +} // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 244b3b1ce2..b0946ad9d0 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -109,8 +109,20 @@ void WaveshareEPaper::data(uint8_t value) { this->write_byte(value); this->end_data_(); } + +// write a command followed by one or more bytes of data. +// The command is the first byte, length is the total including cmd. +void WaveshareEPaper::cmd_data(const uint8_t *c_data, size_t length) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(c_data[0]); + this->dc_pin_->digital_write(true); + this->write_array(c_data + 1, length - 1); + this->disable(); +} + bool WaveshareEPaper::wait_until_idle_() { - if (this->busy_pin_ == nullptr) { + if (this->busy_pin_ == nullptr || !this->busy_pin_->digital_read()) { return true; } @@ -120,7 +132,7 @@ bool WaveshareEPaper::wait_until_idle_() { ESP_LOGE(TAG, "Timeout while displaying image!"); return false; } - delay(10); + delay(1); } return true; } @@ -2218,8 +2230,9 @@ void HOT WaveshareEPaper2P13InDKE::display() { } else { // set up partial update this->command(0x32); - for (uint8_t v : PART_UPDATE_LUT_TTGO_DKE) - this->data(v); + this->start_data_(); + this->write_array(PART_UPDATE_LUT_TTGO_DKE, sizeof(PART_UPDATE_LUT_TTGO_DKE)); + this->end_data_(); this->command(0x3F); this->data(0x22); @@ -2264,12 +2277,10 @@ void HOT WaveshareEPaper2P13InDKE::display() { this->wait_until_idle_(); // data must be sent again on partial update - delay(300); // NOLINT this->command(0x24); this->start_data_(); this->write_array(this->buffer_, this->get_buffer_length_()); this->end_data_(); - delay(300); // NOLINT } ESP_LOGI(TAG, "Completed e-paper update."); @@ -2281,6 +2292,7 @@ uint32_t WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } void WaveshareEPaper2P13InDKE::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 2.13inDKE"); + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index ee9443e8be..0f1144ccba 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -19,6 +19,7 @@ class WaveshareEPaper : public display::DisplayBuffer, void command(uint8_t value); void data(uint8_t value); + void cmd_data(const uint8_t *data, size_t length); virtual void display() = 0; virtual void initialize() = 0; @@ -49,7 +50,7 @@ class WaveshareEPaper : public display::DisplayBuffer, this->reset_pin_->digital_write(false); delay(reset_duration_); // NOLINT this->reset_pin_->digital_write(true); - delay(200); // NOLINT + delay(20); } } @@ -614,5 +615,39 @@ class WaveshareEPaper2P13InDKE : public WaveshareEPaper { uint32_t at_update_{0}; }; +class WaveshareEPaper2P13InV3 : public WaveshareEPaper { + public: + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER DOWN + this->command(0x10); + this->data(0x01); + // cannot wait until idle here, the device no longer responds + } + + void set_full_update_every(uint32_t full_update_every); + + void setup() override; + void initialize() override; + + protected: + int get_width_internal() override; + int get_height_internal() override; + uint32_t idle_timeout_() override; + + void write_buffer_(uint8_t cmd, int top, int bottom); + void set_window_(int t, int b); + void send_reset_(); + void partial_update_(); + void full_update_(); + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + bool is_busy_{false}; + void write_lut_(const uint8_t *lut); +}; } // namespace waveshare_epaper } // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 089caf073b..65068871dd 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -693,7 +693,6 @@ display: greyscale: false partial_updating: false update_interval: 60s - display_data_1_pin: number: GPIO5 allow_other_uses: true @@ -742,6 +741,24 @@ display: vcom_pin: number: GPIO1 allow_other_uses: true + - platform: waveshare_epaper + spi_id: spi_id_1 + cs_pin: + number: GPIO23 + allow_other_uses: true + dc_pin: + number: GPIO23 + allow_other_uses: true + busy_pin: + number: GPIO23 + allow_other_uses: true + reset_pin: + number: GPIO23 + allow_other_uses: true + model: 2.13inv3 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); number: - platform: tuya From 6a8da17ea39146e5aedacdcc051fca1fd7c1cd51 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 19 Jan 2024 05:18:06 +0100 Subject: [PATCH 308/436] OTA 2 which confirm each written chunk (#6066) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ota/__init__.py | 3 ++ esphome/components/ota/ota_component.cpp | 16 ++++++-- esphome/components/ota/ota_component.h | 47 +++++++++++----------- esphome/core/defines.h | 1 + esphome/espota2.py | 51 +++++++++++++----------- tests/test3.1.yaml | 1 + 6 files changed, 70 insertions(+), 49 deletions(-) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 039596d897..3c845490dc 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_OTA, KEY_PAST_SAFE_MODE, + CONF_VERSION, ) from esphome.core import CORE, coroutine_with_priority @@ -41,6 +42,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(OTAComponent), cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, + cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True), cv.SplitDefault( CONF_PORT, esp8266=8266, @@ -93,6 +95,7 @@ async def to_code(config): if CONF_PASSWORD in config: cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_PASSWORD") + cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) await cg.register_component(var, config) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 41cf333be9..15af14ff1a 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -20,8 +20,7 @@ namespace esphome { namespace ota { static const char *const TAG = "ota"; - -static const uint8_t OTA_VERSION_1_0 = 1; +static constexpr u_int16_t OTA_BLOCK_SIZE = 8192; OTAComponent *global_ota_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -101,6 +100,7 @@ void OTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Using Password."); } #endif + ESP_LOGCONFIG(TAG, " OTA version: %d.", USE_OTA_VERSION); if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) { ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %" PRIu32 " restarts", @@ -132,6 +132,9 @@ void OTAComponent::handle_() { uint8_t ota_features; std::unique_ptr backend; (void) ota_features; +#if USE_OTA_VERSION == 2 + size_t size_acknowledged = 0; +#endif if (client_ == nullptr) { struct sockaddr_storage source_addr; @@ -168,7 +171,7 @@ void OTAComponent::handle_() { // Send OK and version - 2 bytes buf[0] = OTA_RESPONSE_OK; - buf[1] = OTA_VERSION_1_0; + buf[1] = USE_OTA_VERSION; this->writeall_(buf, 2); backend = make_ota_backend(); @@ -312,6 +315,13 @@ void OTAComponent::handle_() { goto error; // NOLINT(cppcoreguidelines-avoid-goto) } total += read; +#if USE_OTA_VERSION == 2 + while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { + buf[0] = OTA_RESPONSE_CHUNK_OK; + this->writeall_(buf, 1); + size_acknowledged += OTA_BLOCK_SIZE; + } +#endif uint32_t now = millis(); if (now - last_progress > 1000) { diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h index 50d095be6c..c20f4f0709 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -10,31 +10,32 @@ namespace esphome { namespace ota { enum OTAResponseTypes { - OTA_RESPONSE_OK = 0, - OTA_RESPONSE_REQUEST_AUTH = 1, + OTA_RESPONSE_OK = 0x00, + OTA_RESPONSE_REQUEST_AUTH = 0x01, - OTA_RESPONSE_HEADER_OK = 64, - OTA_RESPONSE_AUTH_OK = 65, - OTA_RESPONSE_UPDATE_PREPARE_OK = 66, - OTA_RESPONSE_BIN_MD5_OK = 67, - OTA_RESPONSE_RECEIVE_OK = 68, - OTA_RESPONSE_UPDATE_END_OK = 69, - OTA_RESPONSE_SUPPORTS_COMPRESSION = 70, + OTA_RESPONSE_HEADER_OK = 0x40, + OTA_RESPONSE_AUTH_OK = 0x41, + OTA_RESPONSE_UPDATE_PREPARE_OK = 0x42, + OTA_RESPONSE_BIN_MD5_OK = 0x43, + OTA_RESPONSE_RECEIVE_OK = 0x44, + OTA_RESPONSE_UPDATE_END_OK = 0x45, + OTA_RESPONSE_SUPPORTS_COMPRESSION = 0x46, + OTA_RESPONSE_CHUNK_OK = 0x47, - OTA_RESPONSE_ERROR_MAGIC = 128, - OTA_RESPONSE_ERROR_UPDATE_PREPARE = 129, - OTA_RESPONSE_ERROR_AUTH_INVALID = 130, - OTA_RESPONSE_ERROR_WRITING_FLASH = 131, - OTA_RESPONSE_ERROR_UPDATE_END = 132, - OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133, - OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134, - OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135, - OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136, - OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137, - OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 138, - OTA_RESPONSE_ERROR_MD5_MISMATCH = 139, - OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 140, - OTA_RESPONSE_ERROR_UNKNOWN = 255, + OTA_RESPONSE_ERROR_MAGIC = 0x80, + OTA_RESPONSE_ERROR_UPDATE_PREPARE = 0x81, + OTA_RESPONSE_ERROR_AUTH_INVALID = 0x82, + OTA_RESPONSE_ERROR_WRITING_FLASH = 0x83, + OTA_RESPONSE_ERROR_UPDATE_END = 0x84, + OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85, + OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86, + OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87, + OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88, + OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89, + OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A, + OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B, + OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, + OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e75abdb88f..75ed24ddfe 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -37,6 +37,7 @@ #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK +#define USE_OTA_VERSION 1 #define USE_OUTPUT #define USE_POWER_SUPPLY #define USE_QR_CODE diff --git a/esphome/espota2.py b/esphome/espota2.py index dbf48a989a..cdf6d7df32 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -12,32 +12,34 @@ import time from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address -RESPONSE_OK = 0 -RESPONSE_REQUEST_AUTH = 1 +RESPONSE_OK = 0x00 +RESPONSE_REQUEST_AUTH = 0x01 -RESPONSE_HEADER_OK = 64 -RESPONSE_AUTH_OK = 65 -RESPONSE_UPDATE_PREPARE_OK = 66 -RESPONSE_BIN_MD5_OK = 67 -RESPONSE_RECEIVE_OK = 68 -RESPONSE_UPDATE_END_OK = 69 -RESPONSE_SUPPORTS_COMPRESSION = 70 +RESPONSE_HEADER_OK = 0x40 +RESPONSE_AUTH_OK = 0x41 +RESPONSE_UPDATE_PREPARE_OK = 0x42 +RESPONSE_BIN_MD5_OK = 0x43 +RESPONSE_RECEIVE_OK = 0x44 +RESPONSE_UPDATE_END_OK = 0x45 +RESPONSE_SUPPORTS_COMPRESSION = 0x46 +RESPONSE_CHUNK_OK = 0x47 -RESPONSE_ERROR_MAGIC = 128 -RESPONSE_ERROR_UPDATE_PREPARE = 129 -RESPONSE_ERROR_AUTH_INVALID = 130 -RESPONSE_ERROR_WRITING_FLASH = 131 -RESPONSE_ERROR_UPDATE_END = 132 -RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133 -RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134 -RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135 -RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136 -RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137 -RESPONSE_ERROR_NO_UPDATE_PARTITION = 138 -RESPONSE_ERROR_MD5_MISMATCH = 139 -RESPONSE_ERROR_UNKNOWN = 255 +RESPONSE_ERROR_MAGIC = 0x80 +RESPONSE_ERROR_UPDATE_PREPARE = 0x81 +RESPONSE_ERROR_AUTH_INVALID = 0x82 +RESPONSE_ERROR_WRITING_FLASH = 0x83 +RESPONSE_ERROR_UPDATE_END = 0x84 +RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85 +RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86 +RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87 +RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88 +RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89 +RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A +RESPONSE_ERROR_MD5_MISMATCH = 0x8B +RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 +OTA_VERSION_2_0 = 2 MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] @@ -203,7 +205,8 @@ def perform_ota( send_check(sock, MAGIC_BYTES, "magic bytes") _, version = receive_exactly(sock, 2, "version", RESPONSE_OK) - if version != OTA_VERSION_1_0: + _LOGGER.debug("Device support OTA version: %s", version) + if version not in (OTA_VERSION_1_0, OTA_VERSION_2_0): raise OTAError(f"Unsupported OTA version {version}") # Features @@ -279,6 +282,8 @@ def perform_ota( try: sock.sendall(chunk) + if version >= OTA_VERSION_2_0: + receive_exactly(sock, 1, "chunk OK", RESPONSE_CHUNK_OK) except OSError as err: sys.stderr.write("\n") raise OTAError(f"Error sending data: {err}") from err diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index b5428abbfa..5cbdca91c1 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -49,6 +49,7 @@ spi: number: GPIO14 ota: + version: 2 logger: From 2f09624c07d8b24c174b58ea27230f459b65df08 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:30:57 +0100 Subject: [PATCH 309/436] Remove optional<> for pointer types (#6120) --- esphome/components/bedjet/bedjet_hub.cpp | 12 +++++------- esphome/components/bedjet/bedjet_hub.h | 2 +- esphome/components/tuya/tuya.cpp | 20 ++++++++------------ esphome/components/tuya/tuya.h | 4 ++-- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 7933a35a97..6404298697 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -242,7 +242,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga this->set_notify_(true); #ifdef USE_TIME - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time(); } #endif @@ -441,9 +441,8 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { #ifdef USE_TIME void BedJetHub::send_local_time() { - if (this->time_id_.has_value()) { - auto *time_id = *this->time_id_; - ESPTime now = time_id->now(); + if (this->time_id_ != nullptr) { + ESPTime now = this->time_id_->now(); if (now.is_valid()) { this->set_clock(now.hour, now.minute); ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); @@ -454,10 +453,9 @@ void BedJetHub::send_local_time() { } void BedJetHub::setup_time_() { - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time(); - auto *time_id = *this->time_id_; - time_id->add_on_time_sync_callback([this] { this->send_local_time(); }); + this->time_id_->add_on_time_sync_callback([this] { this->send_local_time(); }); } else { ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); } diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index bb1349b2ac..6258795b02 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -141,7 +141,7 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo #ifdef USE_TIME /** Initializes time sync callbacks to support syncing current time to the BedJet. */ void setup_time_(); - optional time_id_{}; + time::RealTimeClock *time_id_{nullptr}; #endif uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index da03e3faad..1cc9681d09 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -23,8 +23,8 @@ static const int MAX_RETRIES = 5; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); - if (this->status_pin_.has_value()) { - this->status_pin_.value()->digital_write(false); + if (this->status_pin_ != nullptr) { + this->status_pin_->digital_write(false); } } @@ -70,9 +70,7 @@ void Tuya::dump_config() { ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_, this->reset_pin_reported_); } - if (this->status_pin_.has_value()) { - LOG_PIN(" Status Pin: ", this->status_pin_.value()); - } + LOG_PIN(" Status Pin: ", this->status_pin_); ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); } @@ -194,7 +192,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); bool is_pin_equals = - this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_; + this->status_pin_ != nullptr && this->status_pin_->get_pin() == this->status_pin_reported_; // Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send if (is_pin_equals) { ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_); @@ -244,13 +242,12 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff break; case TuyaCommandType::LOCAL_TIME_QUERY: #ifdef USE_TIME - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time_(); if (!this->time_sync_callback_registered_) { // tuya mcu supports time, so we let them know when our time changed - auto *time_id = *this->time_id_; - time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); + this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); }); this->time_sync_callback_registered_ = true; } } else @@ -463,7 +460,7 @@ void Tuya::send_empty_command_(TuyaCommandType command) { void Tuya::set_status_pin_() { bool is_network_ready = network::is_connected() && remote_is_connected(); - this->status_pin_.value()->digital_write(is_network_ready); + this->status_pin_->digital_write(is_network_ready); } uint8_t Tuya::get_wifi_status_code_() { @@ -511,8 +508,7 @@ void Tuya::send_wifi_status_() { #ifdef USE_TIME void Tuya::send_local_time_() { std::vector payload; - auto *time_id = *this->time_id_; - ESPTime now = time_id->now(); + ESPTime now = this->time_id_->now(); if (now.is_valid()) { uint8_t year = now.year - 2000; uint8_t month = now.month; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 27a97c3dc9..7dc405e3dd 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -130,14 +130,14 @@ class Tuya : public Component, public uart::UARTDevice { #ifdef USE_TIME void send_local_time_(); - optional time_id_{}; + time::RealTimeClock *time_id_{nullptr}; bool time_sync_callback_registered_{false}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; bool init_failed_{false}; int init_retries_{0}; uint8_t protocol_version_ = -1; - optional status_pin_{}; + InternalGPIOPin *status_pin_{nullptr}; int status_pin_reported_ = -1; int reset_pin_reported_ = -1; uint32_t last_command_timestamp_ = 0; From 0cbc06a9b91fd16e6dd9a48e75569b409039a957 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 20 Jan 2024 03:38:37 +1300 Subject: [PATCH 310/436] Fix some Voice Assistant bugs (#6121) --- esphome/components/voice_assistant/voice_assistant.cpp | 10 ++++++---- esphome/core/ring_buffer.cpp | 9 +++++---- esphome/core/ring_buffer.h | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 299e624f5f..9094b93c02 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -17,7 +17,7 @@ static const char *const TAG = "voice_assistant"; static const size_t SAMPLE_RATE_HZ = 16000; static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms -static const size_t BUFFER_SIZE = 1000 * SAMPLE_RATE_HZ / 1000; // 1s +static const size_t BUFFER_SIZE = 1024 * SAMPLE_RATE_HZ / 1000; static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t); static const size_t RECEIVE_SIZE = 1024; static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE; @@ -231,10 +231,12 @@ void VoiceAssistant::loop() { } case State::STREAMING_MICROPHONE: { this->read_microphone_(); - if (this->ring_buffer_->available() >= SEND_BUFFER_SIZE) { - this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); - this->socket_->sendto(this->send_buffer_, SEND_BUFFER_SIZE, 0, (struct sockaddr *) &this->dest_addr_, + size_t available = this->ring_buffer_->available(); + while (available >= SEND_BUFFER_SIZE) { + size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); + this->socket_->sendto(this->send_buffer_, read_bytes, 0, (struct sockaddr *) &this->dest_addr_, sizeof(this->dest_addr_)); + available = this->ring_buffer_->available(); } break; diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index d9c56d84c5..9bd3d9d853 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -15,17 +15,18 @@ std::unique_ptr RingBuffer::create(size_t len) { std::unique_ptr rb = make_unique(); ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - rb->storage_ = allocator.allocate(len); + rb->storage_ = allocator.allocate(len + 1); if (rb->storage_ == nullptr) { return nullptr; } - rb->handle_ = xStreamBufferCreateStatic(len, 0, rb->storage_, &rb->structure_); + rb->handle_ = xStreamBufferCreateStatic(len + 1, 0, rb->storage_, &rb->structure_); + ESP_LOGD(TAG, "Created ring buffer with size %u", len); return rb; } -size_t RingBuffer::read(void *data, size_t size, TickType_t ticks_to_wait) { - return xStreamBufferReceive(this->handle_, data, size, ticks_to_wait); +size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { + return xStreamBufferReceive(this->handle_, data, len, ticks_to_wait); } size_t RingBuffer::write(void *data, size_t len) { diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index 6c6d04117a..e602068844 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -12,7 +12,7 @@ namespace esphome { class RingBuffer { public: - size_t read(void *data, size_t size, TickType_t ticks_to_wait = 0); + size_t read(void *data, size_t len, TickType_t ticks_to_wait = 0); size_t write(void *data, size_t len); From c35a21773e3158484c60879a8fdcb806b9cc1163 Mon Sep 17 00:00:00 2001 From: jxl77 Date: Sun, 21 Jan 2024 02:57:39 +0100 Subject: [PATCH 311/436] Improve temperature precision in BME280 and BMP280 (#6124) * Update bme280_base.cpp Change read_temperature to get better precision float const temperature = (*t_fine * 5 + 128); return temperature / 25600.0f; * Update bmp280.cpp increase precision in read_temperature * Update bmp280.cpp clang-format correction --- esphome/components/bme280_base/bme280_base.cpp | 4 ++-- esphome/components/bmp280/bmp280.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 3c6e15cbca..76e20836c7 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -265,8 +265,8 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) { int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; *t_fine = var1 + var2; - float const temperature = (*t_fine * 5 + 128) >> 8; - return temperature / 100.0f; + float const temperature = (*t_fine * 5 + 128); + return temperature / 25600.0f; } float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) { diff --git a/esphome/components/bmp280/bmp280.cpp b/esphome/components/bmp280/bmp280.cpp index a5b2517893..c92daa07fb 100644 --- a/esphome/components/bmp280/bmp280.cpp +++ b/esphome/components/bmp280/bmp280.cpp @@ -200,8 +200,8 @@ float BMP280Component::read_temperature_(int32_t *t_fine) { int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; *t_fine = var1 + var2; - float temperature = (*t_fine * 5 + 128) >> 8; - return temperature / 100.0f; + float temperature = (*t_fine * 5 + 128); + return temperature / 25600.0f; } float BMP280Component::read_pressure_(int32_t t_fine) { From 48129974294b26cf8528d03f971d714392c23521 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 23 Jan 2024 08:49:28 +0100 Subject: [PATCH 312/436] Nextion TFT upload IDF memory optimization (#6128) * Nextion TFT upload IDF memory optimization This optimizes the memory in use for TFT upload when using `esp-idf` framework. Basically, the engine establishes 3 connections to the the http/https server: 1. Fetch the file size (used to manage chunks and file size) 2. Transfer the 1st chunk (when it evaluates Nextion response to define either to continue from that point or to another point in the file) 3. Transfer the remaining data. Until now, connection 1 was kept open during the whole process taking aprox 40kb of heap in a esp32dev (NSPanel in my tests) and the same amount of memory was needed to the 2nd and 3rd connections (which never competes to each other). With this change, each connection is closed and released before opening the next one with a significant reduction on the required heap needed for this transfer. This can still be improved to use a persistent connection, but I will look at this in the future, so it is not part of this change. In addition to the better connection management, I've added quite a lot of log (mostly at VERBOSE level), which was used for troubleshooting here. I was unsure about removing this. As it can be useful for others, I decided to keep it, but I will be fine about removing it if this is now in line with ESPHome best practices. * clang-format * Log response length --- .../components/nextion/nextion_upload_idf.cpp | 87 ++++++++++++++----- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 709ff65b12..14b1b6cfaf 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -24,7 +24,7 @@ int Nextion::upload_range(const std::string &url, int range_start) { ESP_LOGVV(TAG, "url: %s", url.c_str()); uint range_size = this->tft_size_ - range_start; ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_); - ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_; if (range_size <= 0 or range_end <= range_start) { ESP_LOGE(TAG, "Invalid range"); @@ -67,12 +67,13 @@ int Nextion::upload_range(const std::string &url, int range_start) { int total_read_len = 0, read_len; + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); ESP_LOGV(TAG, "Allocate buffer"); uint8_t *buffer = new uint8_t[4096]; std::string recv_string; if (buffer == nullptr) { ESP_LOGE(TAG, "Failed to allocate memory for buffer"); - ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); } else { ESP_LOGV(TAG, "Memory for buffer allocated successfully"); @@ -86,15 +87,14 @@ int Nextion::upload_range(const std::string &url, int range_start) { ESP_LOGVV(TAG, "Write to UART successful"); this->recv_ret_string_(recv_string, 5000, true); this->content_length_ -= read_len; - ESP_LOGD(TAG, "Uploaded %0.2f %%, remaining %d bytes", - 100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_); - if (recv_string[0] != 0x05) { // 0x05 == "ok" + ESP_LOGD(TAG, "Uploaded %0.2f %%, remaining %d bytes, heap is %" PRIu32 " bytes", + 100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_, + esp_get_free_heap_size()); + + if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request ESP_LOGD( TAG, "recv_string [%s]", format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); - } - // handle partial upload request - if (recv_string[0] == 0x08 && recv_string.size() == 5) { uint32_t result = 0; for (int j = 0; j < 4; ++j) { result += static_cast(recv_string[j + 1]) << (8 * j); @@ -103,13 +103,37 @@ int Nextion::upload_range(const std::string &url, int range_start) { ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result); this->content_length_ = this->tft_size_ - result; // Deallocate the buffer when done + ESP_LOGV(TAG, "Deallocate buffer"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); delete[] buffer; ESP_LOGVV(TAG, "Memory for buffer deallocated"); - esp_http_client_cleanup(client); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, "Close http client"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_close(client); + esp_http_client_cleanup(client); + ESP_LOGVV(TAG, "Client closed"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); return result; } + } else if (recv_string[0] != 0x05) { // 0x05 == "ok" + ESP_LOGE( + TAG, "Invalid response from Nextion: [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + ESP_LOGV(TAG, "Deallocate buffer"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + delete[] buffer; + ESP_LOGVV(TAG, "Memory for buffer deallocated"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, "Close http client"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + esp_http_client_close(client); + esp_http_client_cleanup(client); + ESP_LOGVV(TAG, "Client closed"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + return -1; } + recv_string.clear(); } else if (read_len == 0) { ESP_LOGV(TAG, "End of HTTP response reached"); @@ -121,11 +145,18 @@ int Nextion::upload_range(const std::string &url, int range_start) { } // Deallocate the buffer when done + ESP_LOGV(TAG, "Deallocate buffer"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); delete[] buffer; ESP_LOGVV(TAG, "Memory for buffer deallocated"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); } - esp_http_client_cleanup(client); + ESP_LOGV(TAG, "Close http client"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_close(client); + esp_http_client_cleanup(client); + ESP_LOGVV(TAG, "Client closed"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); return range_end + 1; } @@ -159,7 +190,7 @@ bool Nextion::upload_tft() { // Initialize the HTTP client with the configuration ESP_LOGV(TAG, "Initializing HTTP client"); - ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_handle_t http = esp_http_client_init(&config); if (!http) { ESP_LOGE(TAG, "Failed to initialize HTTP client."); @@ -168,7 +199,7 @@ bool Nextion::upload_tft() { // Perform the HTTP request ESP_LOGV(TAG, "Check if the client could connect"); - ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_err_t err = esp_http_client_perform(http); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); @@ -177,14 +208,22 @@ bool Nextion::upload_tft() { } // Check the HTTP Status Code + ESP_LOGV(TAG, "Check the HTTP Status Code"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http); ESP_LOGV(TAG, "HTTP Status Code: %d", status_code); size_t tft_file_size = esp_http_client_get_content_length(http); ESP_LOGD(TAG, "TFT file size: %zu", tft_file_size); + ESP_LOGD(TAG, "Close HTTP connection"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + esp_http_client_close(http); + esp_http_client_cleanup(http); + ESP_LOGVV(TAG, "Connection closed"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); + if (tft_file_size < 4096) { ESP_LOGE(TAG, "File size check failed. Size: %zu", tft_file_size); - esp_http_client_cleanup(http); return this->upload_end(false); } else { ESP_LOGV(TAG, "File size check passed. Proceeding..."); @@ -193,8 +232,10 @@ bool Nextion::upload_tft() { this->tft_size_ = tft_file_size; ESP_LOGD(TAG, "Updating Nextion"); - // The Nextion will ignore the update command if it is sleeping + // The Nextion will ignore the update command if it is sleeping + ESP_LOGV(TAG, "Wake-up Nextion"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); this->send_command_("sleep=0"); this->set_backlight_brightness(1.0); vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT @@ -207,26 +248,31 @@ bool Nextion::upload_tft() { sprintf(command, "whmi-wris %d,%" PRIu32 ",1", this->content_length_, this->parent_->get_baud_rate()); // Clear serial receive buffer + ESP_LOGV(TAG, "Clear serial receive buffer"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); uint8_t d; while (this->available()) { this->read_byte(&d); }; + ESP_LOGV(TAG, "Send update instruction: %s", command); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); this->send_command_(command); std::string response; ESP_LOGV(TAG, "Waiting for upgrade response"); - this->recv_ret_string_(response, 2048, true); // This can take some time to return + this->recv_ret_string_(response, 5000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. - ESP_LOGD(TAG, "Upgrade response is [%s]", - format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str()); + ESP_LOGD(TAG, "Upgrade response is [%s] - %zu bytes", + format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + response.length()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); if (response.find(0x05) != std::string::npos) { ESP_LOGV(TAG, "Preparation for tft update done"); } else { ESP_LOGE(TAG, "Preparation for tft update failed %d \"%s\"", response[0], response.c_str()); - esp_http_client_cleanup(http); return this->upload_end(false); } @@ -234,12 +280,12 @@ bool Nextion::upload_tft() { content_length_, esp_get_free_heap_size()); ESP_LOGV(TAG, "Starting transfer by chunks loop"); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); int result = 0; while (content_length_ > 0) { result = upload_range(this->tft_url_.c_str(), result); if (result < 0) { ESP_LOGE(TAG, "Error updating Nextion!"); - esp_http_client_cleanup(http); return this->upload_end(false); } App.feed_wdt(); @@ -248,9 +294,6 @@ bool Nextion::upload_tft() { ESP_LOGD(TAG, "Successfully updated Nextion!"); - ESP_LOGD(TAG, "Close HTTP connection"); - esp_http_client_close(http); - esp_http_client_cleanup(http); return upload_end(true); } From 23071e932adce4d5f7a708b7c252e5758faaf36d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 24 Jan 2024 07:40:16 +1100 Subject: [PATCH 313/436] Add support for Pico-ResTouch-LCD-3.5 to ili9xxx driver (#6129) * Working version of Waveshare 3.5 Res Touch driver. * Default color order BGR --- esphome/components/ili9xxx/display.py | 1 + .../components/ili9xxx/ili9xxx_display.cpp | 83 ++++++++----------- esphome/components/ili9xxx/ili9xxx_display.h | 52 ++++++++++-- esphome/components/ili9xxx/ili9xxx_init.h | 28 +++---- 4 files changed, 94 insertions(+), 70 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index b3fe8b2b41..0bd810ea16 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -66,6 +66,7 @@ MODELS = { "ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay), "S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay), "S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay), + "WAVESHARE_RES_3_5": ili9xxx_ns.class_("WAVESHARERES35", ILI9XXXDisplay), } COLOR_ORDERS = { diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index ab577b3875..e3f2c94880 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -7,7 +7,6 @@ namespace esphome { namespace ili9xxx { -static const char *const TAG = "ili9xxx"; static const uint16_t SPI_SETUP_US = 100; // estimated fixed overhead in microseconds for an SPI write static const uint16_t SPI_MAX_BLOCK_SIZE = 4092; // Max size of continuous SPI transfer @@ -17,13 +16,7 @@ static inline void put16_be(uint8_t *buf, uint16_t value) { buf[1] = value; } -void ILI9XXXDisplay::setup() { - ESP_LOGD(TAG, "Setting up ILI9xxx"); - - this->setup_pins_(); - this->init_lcd_(); - - this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF); +void ILI9XXXDisplay::set_madctl() { // custom x/y transform and color order uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB; if (this->swap_xy_) @@ -32,8 +25,19 @@ void ILI9XXXDisplay::setup() { mad |= MADCTL_MX; if (this->mirror_y_) mad |= MADCTL_MY; - this->send_command(ILI9XXX_MADCTL, &mad, 1); + this->command(ILI9XXX_MADCTL); + this->data(mad); + esph_log_d(TAG, "Wrote MADCTL 0x%02X", mad); +} +void ILI9XXXDisplay::setup() { + ESP_LOGD(TAG, "Setting up ILI9xxx"); + + this->setup_pins_(); + this->init_lcd_(); + + this->set_madctl(); + this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF); this->x_low_ = this->width_; this->y_low_ = this->height_; this->x_high_ = 0; @@ -89,6 +93,7 @@ void ILI9XXXDisplay::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); + ESP_LOGCONFIG(TAG, " Color order: %s", this->color_order_ == display::COLOR_ORDER_BGR ? "BGR" : "RGB"); ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_)); ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_)); ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_)); @@ -196,7 +201,6 @@ void ILI9XXXDisplay::display_() { uint8_t transfer_buffer[ILI9XXX_TRANSFER_BUFFER_SIZE]; // check if something was displayed if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { - ESP_LOGV(TAG, "Nothing to display"); return; } @@ -211,14 +215,13 @@ void ILI9XXXDisplay::display_() { size_t mw_time = (w * h * 16) / mhz + w * h * 2 / ILI9XXX_TRANSFER_BUFFER_SIZE * SPI_SETUP_US; ESP_LOGV(TAG, "Start display(xlow:%d, ylow:%d, xhigh:%d, yhigh:%d, width:%d, " - "height:%d, mode=%d, 18bit=%d, sw_time=%dus, mw_time=%dus)", + "height:%zu, mode=%d, 18bit=%d, sw_time=%zuus, mw_time=%zuus)", this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, this->buffer_color_mode_, this->is_18bitdisplay_, sw_time, mw_time); auto now = millis(); - this->enable(); if (this->buffer_color_mode_ == BITS_16 && !this->is_18bitdisplay_ && sw_time < mw_time) { // 16 bit mode maps directly to display format - ESP_LOGV(TAG, "Doing single write of %d bytes", this->width_ * h * 2); + ESP_LOGV(TAG, "Doing single write of %zu bytes", this->width_ * h * 2); set_addr_window_(0, this->y_low_, this->width_ - 1, this->y_high_); this->write_array(this->buffer_ + this->y_low_ * this->width_ * 2, h * this->width_ * 2); } else { @@ -267,7 +270,7 @@ void ILI9XXXDisplay::display_() { this->write_array(transfer_buffer, idx); } } - this->disable(); + this->end_data_(); ESP_LOGV(TAG, "Data write took %dms", (unsigned) (millis() - now)); // invalidate watermarks this->x_low_ = this->width_; @@ -290,7 +293,6 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); } - this->enable(); this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. if (x_offset == 0 && x_pad == 0 && y_offset == 0) { @@ -302,7 +304,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); } } - this->disable(); + this->end_data_(); } // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color @@ -328,20 +330,6 @@ void ILI9XXXDisplay::send_command(uint8_t command_byte, const uint8_t *data_byte this->end_data_(); } -uint8_t ILI9XXXDisplay::read_command(uint8_t command_byte, uint8_t index) { - uint8_t data = 0x10 + index; - this->send_command(0xD9, &data, 1); // Set Index Register - uint8_t result; - this->start_command_(); - this->write_byte(command_byte); - this->start_data_(); - do { - result = this->read_byte(); - } while (index--); - this->end_data_(); - return result; -} - void ILI9XXXDisplay::start_command_() { this->dc_pin_->digital_write(false); this->enable(); @@ -357,9 +345,9 @@ void ILI9XXXDisplay::end_data_() { this->disable(); } void ILI9XXXDisplay::reset_() { if (this->reset_pin_ != nullptr) { this->reset_pin_->digital_write(false); - delay(10); + delay(20); this->reset_pin_->digital_write(true); - delay(10); + delay(20); } } @@ -369,7 +357,7 @@ void ILI9XXXDisplay::init_lcd_() { while ((cmd = *addr++) > 0) { x = *addr++; num_args = x & 0x7F; - send_command(cmd, addr, num_args); + this->send_command(cmd, addr, num_args); addr += num_args; if (x & 0x80) delay(150); // NOLINT @@ -377,24 +365,19 @@ void ILI9XXXDisplay::init_lcd_() { } // Tell the display controller where we want to draw pixels. -// when called, the SPI should have already been enabled, only the D/C pin will be toggled here. void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { - uint8_t buf[4]; - this->dc_pin_->digital_write(false); - this->write_byte(ILI9XXX_CASET); // Column address set - put16_be(buf, x1 + this->offset_x_); - put16_be(buf + 2, x2 + this->offset_x_); - this->dc_pin_->digital_write(true); - this->write_array(buf, sizeof buf); - this->dc_pin_->digital_write(false); - this->write_byte(ILI9XXX_PASET); // Row address set - put16_be(buf, y1 + this->offset_y_); - put16_be(buf + 2, y2 + this->offset_y_); - this->dc_pin_->digital_write(true); - this->write_array(buf, sizeof buf); - this->dc_pin_->digital_write(false); - this->write_byte(ILI9XXX_RAMWR); // Write to RAM - this->dc_pin_->digital_write(true); + this->command(ILI9XXX_CASET); + this->data(x1 >> 8); + this->data(x1 & 0xFF); + this->data(x2 >> 8); + this->data(x2 & 0xFF); + this->command(ILI9XXX_PASET); // Page address set + this->data(y1 >> 8); + this->data(y1 & 0xFF); + this->data(y2 >> 8); + this->data(y2 & 0xFF); + this->command(ILI9XXX_RAMWR); // Write to RAM + this->start_data_(); } void ILI9XXXDisplay::invert_colors(bool invert) { diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 590be3e364..7b92bd2336 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -8,6 +8,7 @@ namespace esphome { namespace ili9xxx { +static const char *const TAG = "ili9xxx"; const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6 enum ILI9XXXColorMode { @@ -32,6 +33,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer, while ((cmd = *addr++) != 0) { num_args = *addr++ & 0x7F; bits = *addr; + esph_log_d(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, bits); switch (cmd) { case ILI9XXX_MADCTL: { this->swap_xy_ = (bits & MADCTL_MV) != 0; @@ -68,10 +70,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer, this->offset_y_ = offset_y; } void invert_colors(bool invert); - void command(uint8_t value); - void data(uint8_t value); + virtual void command(uint8_t value); + virtual void data(uint8_t value); void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); - uint8_t read_command(uint8_t command_byte, uint8_t index); void set_color_order(display::ColorOrder color_order) { this->color_order_ = color_order; } void set_swap_xy(bool swap_xy) { this->swap_xy_ = swap_xy; } void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } @@ -92,6 +93,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer, void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); + virtual void set_madctl(); void display_(); void init_lcd_(); void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2); @@ -127,7 +129,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer, bool need_update_ = false; bool is_18bitdisplay_ = false; bool pre_invertcolors_ = false; - display::ColorOrder color_order_{}; + display::ColorOrder color_order_{display::COLOR_ORDER_BGR}; bool swap_xy_{}; bool mirror_x_{}; bool mirror_y_{}; @@ -181,10 +183,48 @@ class ILI9XXXILI9486 : public ILI9XXXDisplay { ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320, false) {} }; -//----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9488 : public ILI9XXXDisplay { public: - ILI9XXXILI9488() : ILI9XXXDisplay(INITCMD_ILI9488, 480, 320, true) {} + ILI9XXXILI9488(const uint8_t *seq = INITCMD_ILI9488) : ILI9XXXDisplay(seq, 480, 320, true) {} + + protected: + void set_madctl() override { + uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB; + uint8_t dfun = 0x22; + this->width_ = 320; + this->height_ = 480; + if (!(this->swap_xy_ || this->mirror_x_ || this->mirror_y_)) { + // no transforms + } else if (this->mirror_y_ && this->mirror_x_) { + // rotate 180 + dfun = 0x42; + } else if (this->swap_xy_) { + this->width_ = 480; + this->height_ = 320; + mad |= 0x20; + if (this->mirror_x_) { + dfun = 0x02; + } else { + dfun = 0x62; + } + } + this->command(ILI9XXX_DFUNCTR); + this->data(0); + this->data(dfun); + this->command(ILI9XXX_MADCTL); + this->data(mad); + } +}; +//----------- Waveshare 3.5 Res Touch - ILI9488 interfaced via 16 bit shift register to parallel */ +class WAVESHARERES35 : public ILI9XXXILI9488 { + public: + WAVESHARERES35() : ILI9XXXILI9488(INITCMD_WAVESHARE_RES_3_5) {} + void data(uint8_t value) override { + this->start_data_(); + this->write_byte(0); + this->write_byte(value); + this->end_data_(); + } }; //----------- ILI9XXX_35_TFT origin colors rotated display -------------- diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index a74824052f..fe3f168c32 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -141,7 +141,8 @@ static const uint8_t PROGMEM INITCMD_ILI9486[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9488[] = { + +static const uint8_t INITCMD_ILI9488[] = { ILI9XXX_GMCTRP1,15, 0x0f, 0x24, 0x1c, 0x0a, 0x0f, 0x08, 0x43, 0x88, 0x32, 0x0f, 0x10, 0x06, 0x0f, 0x07, 0x00, ILI9XXX_GMCTRN1,15, 0x0F, 0x38, 0x30, 0x09, 0x0f, 0x0f, 0x4e, 0x77, 0x3c, 0x07, 0x10, 0x05, 0x23, 0x1b, 0x00, @@ -153,28 +154,27 @@ static const uint8_t PROGMEM INITCMD_ILI9488[] = { ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot - ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan - 0xE9, 1, 0x00, // Set Image Functio. Disable 24 bit data ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3 - - ILI9XXX_MADCTL, 1, 0x28, - //ILI9XXX_PIXFMT, 1, 0x55, // Interface Pixel Format = 16bit ILI9XXX_PIXFMT, 1, 0x66, //ILI9488 only supports 18-bit pixel format in 4/3 wire SPI mode - - - - // 5 frames - //ILI9XXX_ETMOD, 1, 0xC6, // - - ILI9XXX_SLPOUT, 0x80, // Exit sleep mode - //ILI9XXX_INVON , 0, ILI9XXX_DISPON, 0x80, // Set display on 0x00 // end }; +static const uint8_t INITCMD_WAVESHARE_RES_3_5[] = { + ILI9XXX_PWCTR3, 1, 0x33, + ILI9XXX_VMCTR1, 3, 0x00, 0x1e, 0x80, + ILI9XXX_FRMCTR1, 1, 0xA0, + ILI9XXX_GMCTRP1, 15, 0x0, 0x13, 0x18, 0x04, 0x0F, 0x06, 0x3a, 0x56, 0x4d, 0x03, 0x0a, 0x06, 0x30, 0x3e, 0x0f, + ILI9XXX_GMCTRN1, 15, 0x0, 0x13, 0x18, 0x01, 0x11, 0x06, 0x38, 0x34, 0x4d, 0x06, 0x0d, 0x0b, 0x31, 0x37, 0x0f, + ILI9XXX_PIXFMT, 1, 0x55, + ILI9XXX_SLPOUT, 0x80, // slpout, delay + ILI9XXX_DISPON, 0, + 0x00 // End of list +}; + static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, From 25ab6f0297fcf3a21e07644854db5083ba5a0eed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 19:11:03 -1000 Subject: [PATCH 314/436] Ensure filename is shown when YAML raises an error (#6139) * Ensure filename is shown when YAML raises an error fixes #5423 fixes #5377 * Ensure filename is shown when YAML raises an error fixes #5423 fixes #5377 * Ensure filename is shown when YAML raises an error fixes #5423 fixes #5377 * Ensure filename is shown when YAML raises an error fixes #5423 fixes #5377 * Ensure filename is shown when YAML raises an error fixes #5423 fixes #5377 --- esphome/yaml_util.py | 24 ++++++++++++------- .../fixtures/yaml_util/missing_comp.yaml | 12 ++++++++++ tests/unit_tests/test_yaml_util.py | 20 ++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 tests/unit_tests/fixtures/yaml_util/missing_comp.yaml diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index f5e36b79e7..60705082b6 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -7,6 +7,7 @@ import logging import math import os import uuid +from io import TextIOWrapper from typing import Any import yaml @@ -19,7 +20,7 @@ except ImportError: FastestAvailableSafeLoader = PurePythonLoader from esphome import core -from esphome.config_helpers import Extend, Remove, read_config_file +from esphome.config_helpers import Extend, Remove from esphome.core import ( CORE, DocumentRange, @@ -418,19 +419,26 @@ def load_yaml(fname: str, clear_secrets: bool = True) -> Any: def _load_yaml_internal(fname: str) -> Any: """Load a YAML file.""" - content = read_config_file(fname) try: - return _load_yaml_internal_with_type(ESPHomeLoader, fname, content) - except EsphomeError: - # Loading failed, so we now load with the Python loader which has more - # readable exceptions - return _load_yaml_internal_with_type(ESPHomePurePythonLoader, fname, content) + with open(fname, encoding="utf-8") as f_handle: + try: + return _load_yaml_internal_with_type(ESPHomeLoader, fname, f_handle) + except EsphomeError: + # Loading failed, so we now load with the Python loader which has more + # readable exceptions + # Rewind the stream so we can try again + f_handle.seek(0, 0) + return _load_yaml_internal_with_type( + ESPHomePurePythonLoader, fname, f_handle + ) + except (UnicodeDecodeError, OSError) as err: + raise EsphomeError(f"Error reading file {fname}: {err}") from err def _load_yaml_internal_with_type( loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], fname: str, - content: str, + content: TextIOWrapper, ) -> Any: """Load a YAML file.""" loader = loader_type(content) diff --git a/tests/unit_tests/fixtures/yaml_util/missing_comp.yaml b/tests/unit_tests/fixtures/yaml_util/missing_comp.yaml new file mode 100644 index 0000000000..d065901ed9 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/missing_comp.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ap: ~ + +image: + - id: its_a_bug + file: "mdi:bug" diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 78b6a2ad84..9178726247 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -22,3 +22,23 @@ def test_loading_a_broken_yaml_file(fixture_path): yaml_util.load_yaml(yaml_file) except EsphomeError as err: assert "broken_included.yaml" in str(err) + + +def test_loading_a_yaml_file_with_a_missing_component(fixture_path): + """Ensure we show the filename for a yaml file with a missing component.""" + yaml_file = fixture_path / "yaml_util" / "missing_comp.yaml" + + try: + yaml_util.load_yaml(yaml_file) + except EsphomeError as err: + assert "missing_comp.yaml" in str(err) + + +def test_loading_a_missing_file(fixture_path): + """We throw EsphomeError when loading a missing file.""" + yaml_file = fixture_path / "yaml_util" / "missing.yaml" + + try: + yaml_util.load_yaml(yaml_file) + except EsphomeError as err: + assert "missing.yaml" in str(err) From f2caf13d39b02c4b0c3781ef2b3db7a51701cb45 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 26 Jan 2024 16:13:38 +1100 Subject: [PATCH 315/436] ILI9XXX: Restore offset usage in set_addr_window (#6147) --- esphome/components/ili9xxx/ili9xxx_display.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index e3f2c94880..9f06c9ce0f 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -366,6 +366,10 @@ void ILI9XXXDisplay::init_lcd_() { // Tell the display controller where we want to draw pixels. void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + x1 += this->offset_x_; + x2 += this->offset_x_; + y1 += this->offset_y_; + y2 += this->offset_y_; this->command(ILI9XXX_CASET); this->data(x1 >> 8); this->data(x1 & 0xFF); From 23a9a704f349305c1785c10269a092aedb36ff7d Mon Sep 17 00:00:00 2001 From: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com> Date: Sat, 27 Jan 2024 21:15:14 +0100 Subject: [PATCH 316/436] Minimum 1 for full_update_every to prevent IntegerDivideByZero. (#6150) --- esphome/components/waveshare_epaper/display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 1645ce0b1d..b0c663770d 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -135,7 +135,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True), cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, + cv.Optional(CONF_FULL_UPDATE_EVERY): cv.int_range(min=1, max=4294967295), cv.Optional(CONF_RESET_DURATION): cv.All( cv.positive_time_period_milliseconds, cv.Range(max=core.TimePeriod(milliseconds=500)), From 92798751c2a0860dc5f85cf9f34395372cca72d6 Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Tue, 30 Jan 2024 05:16:32 +0100 Subject: [PATCH 317/436] Support tri-color waveshare eink displays 2.7inch B and B V2 (#4238) Co-authored-by: Richard Nauber --- .../components/waveshare_epaper/display.py | 18 +- .../waveshare_epaper/waveshare_epaper.cpp | 297 +++++++++++++++++- .../waveshare_epaper/waveshare_epaper.h | 120 +++++-- tests/test4.yaml | 34 ++ 4 files changed, 423 insertions(+), 46 deletions(-) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index b0c663770d..fa7c104951 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -17,8 +17,12 @@ from esphome.const import ( DEPENDENCIES = ["spi"] waveshare_epaper_ns = cg.esphome_ns.namespace("waveshare_epaper") -WaveshareEPaper = waveshare_epaper_ns.class_( - "WaveshareEPaper", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +WaveshareEPaperBase = waveshare_epaper_ns.class_( + "WaveshareEPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) +WaveshareEPaper = waveshare_epaper_ns.class_("WaveshareEPaper", WaveshareEPaperBase) +WaveshareEPaperBWR = waveshare_epaper_ns.class_( + "WaveshareEPaperBWR", WaveshareEPaperBase ) WaveshareEPaperTypeA = waveshare_epaper_ns.class_( "WaveshareEPaperTypeA", WaveshareEPaper @@ -26,6 +30,12 @@ WaveshareEPaperTypeA = waveshare_epaper_ns.class_( WaveshareEPaper2P7In = waveshare_epaper_ns.class_( "WaveshareEPaper2P7In", WaveshareEPaper ) +WaveshareEPaper2P7InB = waveshare_epaper_ns.class_( + "WaveshareEPaper2P7InB", WaveshareEPaperBWR +) +WaveshareEPaper2P7InBV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P7InBV2", WaveshareEPaperBWR +) WaveshareEPaper2P7InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper2P7InV2", WaveshareEPaper ) @@ -92,6 +102,8 @@ MODELS = { "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), "gdey029t94": ("c", GDEY029T94), "2.70in": ("b", WaveshareEPaper2P7In), + "2.70in-b": ("b", WaveshareEPaper2P7InB), + "2.70in-bv2": ("b", WaveshareEPaper2P7InBV2), "2.70inv2": ("b", WaveshareEPaper2P7InV2), "2.90in-b": ("b", WaveshareEPaper2P9InB), "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), @@ -130,7 +142,7 @@ def validate_full_update_every_only_types_ac(value): CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(WaveshareEPaper), + cv.GenerateID(): cv.declare_id(WaveshareEPaperBase), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True), cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index b0946ad9d0..9118475c36 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -83,7 +83,7 @@ static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B1[LUT_SIZE_TTGO_B1] = { 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -void WaveshareEPaper::setup_pins_() { +void WaveshareEPaperBase::setup_pins_() { this->init_internal_(this->get_buffer_length_()); this->dc_pin_->setup(); // OUTPUT this->dc_pin_->digital_write(false); @@ -98,13 +98,13 @@ void WaveshareEPaper::setup_pins_() { this->reset_(); } -float WaveshareEPaper::get_setup_priority() const { return setup_priority::PROCESSOR; } -void WaveshareEPaper::command(uint8_t value) { +float WaveshareEPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; } +void WaveshareEPaperBase::command(uint8_t value) { this->start_command_(); this->write_byte(value); this->end_command_(); } -void WaveshareEPaper::data(uint8_t value) { +void WaveshareEPaperBase::data(uint8_t value) { this->start_data_(); this->write_byte(value); this->end_data_(); @@ -112,7 +112,7 @@ void WaveshareEPaper::data(uint8_t value) { // write a command followed by one or more bytes of data. // The command is the first byte, length is the total including cmd. -void WaveshareEPaper::cmd_data(const uint8_t *c_data, size_t length) { +void WaveshareEPaperBase::cmd_data(const uint8_t *c_data, size_t length) { this->dc_pin_->digital_write(false); this->enable(); this->write_byte(c_data[0]); @@ -121,7 +121,7 @@ void WaveshareEPaper::cmd_data(const uint8_t *c_data, size_t length) { this->disable(); } -bool WaveshareEPaper::wait_until_idle_() { +bool WaveshareEPaperBase::wait_until_idle_() { if (this->busy_pin_ == nullptr || !this->busy_pin_->digital_read()) { return true; } @@ -136,7 +136,7 @@ bool WaveshareEPaper::wait_until_idle_() { } return true; } -void WaveshareEPaper::update() { +void WaveshareEPaperBase::update() { this->do_update_(); this->display(); } @@ -159,20 +159,51 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color this->buffer_[pos] &= ~(0x80 >> subpos); } } + uint32_t WaveshareEPaper::get_buffer_length_() { return this->get_width_controller() * this->get_height_internal() / 8u; +} // just a black buffer +uint32_t WaveshareEPaperBWR::get_buffer_length_() { + return this->get_width_controller() * this->get_height_internal() / 4u; +} // black and red buffer + +void WaveshareEPaperBWR::fill(Color color) { + this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } -void WaveshareEPaper::start_command_() { +void HOT WaveshareEPaperBWR::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + return; + + const uint32_t buf_half_len = this->get_buffer_length_() / 2u; + + const uint32_t pos = (x + y * this->get_width_internal()) / 8u; + const uint8_t subpos = x & 0x07; + // flip logic + if (color.is_on()) { + this->buffer_[pos] |= 0x80 >> subpos; + } else { + this->buffer_[pos] &= ~(0x80 >> subpos); + } + + // draw red pixels only, if the color contains red only + if (((color.red > 0) && (color.green == 0) && (color.blue == 0))) { + this->buffer_[pos + buf_half_len] |= 0x80 >> subpos; + } else { + this->buffer_[pos + buf_half_len] &= ~(0x80 >> subpos); + } +} + +void WaveshareEPaperBase::start_command_() { this->dc_pin_->digital_write(false); this->enable(); } -void WaveshareEPaper::end_command_() { this->disable(); } -void WaveshareEPaper::start_data_() { +void WaveshareEPaperBase::end_command_() { this->disable(); } +void WaveshareEPaperBase::start_data_() { this->dc_pin_->digital_write(true); this->enable(); } -void WaveshareEPaper::end_data_() { this->disable(); } -void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); } +void WaveshareEPaperBase::end_data_() { this->disable(); } +void WaveshareEPaperBase::on_safe_shutdown() { this->deep_sleep(); } // ======================================================== // Type A @@ -493,7 +524,7 @@ uint32_t WaveshareEPaperTypeA::idle_timeout_() { case TTGO_EPAPER_2_13_IN_B1: return 2500; default: - return WaveshareEPaper::idle_timeout_(); + return WaveshareEPaperBase::idle_timeout_(); } } @@ -699,6 +730,246 @@ void WaveshareEPaper2P7InV2::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 2.7inch_e-paper_b +// ======================================================== +// Datasheet: +// - https://www.waveshare.com/w/upload/d/d8/2.7inch-e-paper-b-specification.pdf +// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in7b.c + +static const uint8_t LUT_VCOM_DC_2_7B[44] = {0x00, 0x00, 0x00, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x0A, + 0x00, 0x00, 0x08, 0x00, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x00, 0x0A, + 0x0A, 0x00, 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, + 0x03, 0x0E, 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_WHITE_TO_WHITE_2_7B[42] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x80, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_BLACK_TO_WHITE_2_7B[42] = {0xA0, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x90, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0xB0, 0x04, 0x10, 0x00, 0x00, 0x05, 0xB0, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0xC0, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_WHITE_TO_BLACK_2_7B[] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x20, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x10, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_BLACK_TO_BLACK_2_7B[42] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x80, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +void WaveshareEPaper2P7InB::initialize() { + this->reset_(); + + // command power on + this->command(0x04); + this->wait_until_idle_(); + delay(10); + + // Command panel setting + this->command(0x00); + this->data(0xAF); // KW-BF KWR-AF BWROTP 0f + // command pll control + this->command(0x30); + this->data(0x3A); // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ + + // command power setting + this->command(0x01); + this->data(0x03); // VDS_EN, VDG_EN + this->data(0x00); // VCOM_HV, VGHL_LV[1], VGHL_LV[0] + this->data(0x2B); // VDH + this->data(0x2B); // VDL + this->data(0x09); // VDHR + + // command booster soft start + this->command(0x06); + this->data(0x07); + this->data(0x07); + this->data(0x17); + + // Power optimization - ??? + this->command(0xF8); + this->data(0x60); + this->data(0xA5); + this->command(0xF8); + this->data(0x89); + this->data(0xA5); + this->command(0xF8); + this->data(0x90); + this->data(0x00); + this->command(0xF8); + this->data(0x93); + this->data(0x2A); + this->command(0xF8); + this->data(0x73); + this->data(0x41); + + // COMMAND VCM DC SETTING + this->command(0x82); + this->data(0x12); + + // VCOM_AND_DATA_INTERVAL_SETTING + this->command(0x50); + this->data(0x87); // define by OTP + + delay(2); + // COMMAND LUT FOR VCOM + this->command(0x20); + for (uint8_t i : LUT_VCOM_DC_2_7B) + this->data(i); + // COMMAND LUT WHITE TO WHITE + this->command(0x21); + for (uint8_t i : LUT_WHITE_TO_WHITE_2_7B) + this->data(i); + // COMMAND LUT BLACK TO WHITE + this->command(0x22); + for (uint8_t i : LUT_BLACK_TO_WHITE_2_7B) + this->data(i); + // COMMAND LUT WHITE TO BLACK + this->command(0x23); + for (uint8_t i : LUT_WHITE_TO_BLACK_2_7B) { + this->data(i); + } + // COMMAND LUT BLACK TO BLACK + this->command(0x24); + + for (uint8_t i : LUT_BLACK_TO_BLACK_2_7B) { + this->data(i); + } + + delay(2); +} + +void HOT WaveshareEPaper2P7InB::display() { + uint32_t buf_len_half = this->get_buffer_length_() >> 1; + this->initialize(); + + // TCON_RESOLUTION + this->command(0x61); + this->data(this->get_width_internal() >> 8); + this->data(this->get_width_internal() & 0xff); // 176 + this->data(this->get_height_internal() >> 8); + this->data(this->get_height_internal() & 0xff); // 264 + + // COMMAND DATA START TRANSMISSION 1 (BLACK) + this->command(0x10); + delay(2); + for (uint32_t i = 0; i < buf_len_half; i++) { + this->data(this->buffer_[i]); + } + this->command(0x11); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED) + this->command(0x13); + delay(2); + for (uint32_t i = buf_len_half; i < buf_len_half * 2u; i++) { + this->data(this->buffer_[i]); + } + this->command(0x11); + + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + this->wait_until_idle_(); + + this->deep_sleep(); +} +int WaveshareEPaper2P7InB::get_width_internal() { return 176; } +int WaveshareEPaper2P7InB::get_height_internal() { return 264; } +void WaveshareEPaper2P7InB::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.7in B"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +// ======================================================== +// 2.7inch_e-paper_b_v2 +// ======================================================== +// Datasheet: +// - https://www.waveshare.com/w/upload/7/7b/2.7inch-e-paper-b-v2-specification.pdf +// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in7b_V2.c + +void WaveshareEPaper2P7InBV2::initialize() { + this->reset_(); + + this->wait_until_idle_(); + this->command(0x12); + this->wait_until_idle_(); + + this->command(0x00); + this->data(0x27); + this->data(0x01); + this->data(0x00); + + this->command(0x11); + this->data(0x03); + + // 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 yend = this->get_height_internal() - 1; + this->command(0x44); + this->data(0x00); + this->data((xend >> 3) & 0xff); + + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data(yend & 0xff); + this->data((yend >> 8) & 0xff); + + // SetCursor(self, Xstart, Ystart): + this->command(0x4E); + this->data(0x00); + this->command(0x4F); + this->data(0x00); + this->data(0x00); +} + +void HOT WaveshareEPaper2P7InBV2::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION 1 (BLACK) + this->command(0x24); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED) + this->command(0x26); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } + + delay(2); + + this->command(0x20); + + this->wait_until_idle_(); +} +int WaveshareEPaper2P7InBV2::get_width_internal() { return 176; } +int WaveshareEPaper2P7InBV2::get_height_internal() { return 264; } +void WaveshareEPaper2P7InBV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.7in B V2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + // ======================================================== // 2.90in Type B (LUT from OTP) // Datasheet: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 0f1144ccba..47f0cb27b6 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -7,9 +7,9 @@ namespace esphome { namespace waveshare_epaper { -class WaveshareEPaper : public display::DisplayBuffer, - public spi::SPIDevice { +class WaveshareEPaperBase : public display::DisplayBuffer, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; @@ -27,8 +27,6 @@ class WaveshareEPaper : public display::DisplayBuffer, void update() override; - void fill(Color color) override; - void setup() override { this->setup_pins_(); this->initialize(); @@ -36,11 +34,7 @@ class WaveshareEPaper : public display::DisplayBuffer, void on_safe_shutdown() override; - display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } - protected: - void draw_absolute_pixel_internal(int x, int y, Color color) override; - bool wait_until_idle_(); void setup_pins_(); @@ -56,7 +50,7 @@ class WaveshareEPaper : public display::DisplayBuffer, virtual int get_width_controller() { return this->get_width_internal(); }; - uint32_t get_buffer_length_(); + virtual uint32_t get_buffer_length_() = 0; // NOLINT(readability-identifier-naming) uint32_t reset_duration_{200}; void start_command_(); @@ -70,6 +64,28 @@ class WaveshareEPaper : public display::DisplayBuffer, virtual uint32_t idle_timeout_() { return 1000u; } // NOLINT(readability-identifier-naming) }; +class WaveshareEPaper : public WaveshareEPaperBase { + public: + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + uint32_t get_buffer_length_() override; +}; + +class WaveshareEPaperBWR : public WaveshareEPaperBase { + public: + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + uint32_t get_buffer_length_() override; +}; + enum WaveshareEPaperTypeAModel { WAVESHARE_EPAPER_1_54_IN = 0, WAVESHARE_EPAPER_1_54_IN_V2, @@ -134,6 +150,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN = 0, + WAVESHARE_EPAPER_2_7_IN_B, + WAVESHARE_EPAPER_2_7_IN_B_V2, WAVESHARE_EPAPER_4_2_IN, WAVESHARE_EPAPER_4_2_IN_B_V2, WAVESHARE_EPAPER_7_5_IN, @@ -155,6 +173,68 @@ class WaveshareEPaper2P7In : public WaveshareEPaper { this->data(0xA5); // check byte } + protected: + int get_width_internal() override; + int get_height_internal() override; +}; + +class WaveshareEPaper2P7InB : public WaveshareEPaperBWR { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND VCOM_AND_DATA_INTERVAL_SETTING + this->command(0x50); + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); // deep sleep + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + int get_height_internal() override; +}; + +class WaveshareEPaper2P7InBV2 : public WaveshareEPaperBWR { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x10); + this->data(0x01); + } + + protected: + int get_width_internal() override; + int get_height_internal() override; +}; + +class GDEY029T94 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + protected: int get_width_internal() override; @@ -177,26 +257,6 @@ class WaveshareEPaper2P7InV2 : public WaveshareEPaper { int get_height_internal() override; }; -class GDEY029T94 : public WaveshareEPaper { - public: - void initialize() override; - - void display() override; - - void dump_config() override; - - void deep_sleep() override { - // COMMAND DEEP SLEEP - this->command(0x07); - this->data(0xA5); // check byte - } - - protected: - int get_width_internal() override; - - int get_height_internal() override; -}; - class GDEW0154M09 : public WaveshareEPaper { public: void initialize() override; diff --git a/tests/test4.yaml b/tests/test4.yaml index 65068871dd..f68406298e 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -671,6 +671,40 @@ display: full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 + model: 2.70in-b + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 + model: 2.70in-bv2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper spi_id: spi_id_1 cs_pin: From 0fa0904bc53aa72e2ab291ba0840e73738ebef18 Mon Sep 17 00:00:00 2001 From: esphomebot Date: Thu, 1 Feb 2024 01:25:47 +1300 Subject: [PATCH 318/436] Synchronise Device Classes from Home Assistant (#6158) --- esphome/components/number/__init__.py | 2 ++ esphome/components/sensor/__init__.py | 2 ++ esphome/const.py | 1 + 3 files changed, 5 insertions(+) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 07164be5ce..ecc2ab2ee7 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -62,6 +62,7 @@ from esphome.const import ( DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, @@ -117,6 +118,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index b3bf533695..46295fe958 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -82,6 +82,7 @@ from esphome.const import ( DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, @@ -141,6 +142,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, diff --git a/esphome/const.py b/esphome/const.py index a95d1d5ac3..8f9606c5fd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1082,6 +1082,7 @@ DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" DEVICE_CLASS_VOLTAGE = "voltage" DEVICE_CLASS_VOLUME = "volume" +DEVICE_CLASS_VOLUME_FLOW_RATE = "volume_flow_rate" DEVICE_CLASS_VOLUME_STORAGE = "volume_storage" DEVICE_CLASS_WATER = "water" DEVICE_CLASS_WEIGHT = "weight" From b28821d8463db29d2298eade023a4b0a0f7e880e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Sun, 4 Feb 2024 17:57:11 -0800 Subject: [PATCH 319/436] dfrobot_sen0395: Use setLatency instead of outputLatency (#5665) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/dfrobot_sen0395/__init__.py | 11 ++++--- .../components/dfrobot_sen0395/automation.h | 2 +- .../components/dfrobot_sen0395/commands.cpp | 30 +++++++------------ esphome/components/dfrobot_sen0395/commands.h | 4 +-- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index e772db5a15..2197ee5ef8 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome import core from esphome.automation import maybe_simple_id from esphome.const import CONF_ID from esphome.components import uart @@ -101,7 +100,7 @@ def range_segment_list(input): largest_distance = -1 for distance in input: - if isinstance(distance, core.Lambda): + if isinstance(distance, cv.Lambda): continue m = cv.distance(distance) if m > 9: @@ -128,14 +127,14 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema( cv.Optional(CONF_OUTPUT_LATENCY): { cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable( cv.All( - cv.positive_time_period, - cv.Range(max=core.TimePeriod(seconds=1638.375)), + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(seconds=1638.375)), ) ), cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable( cv.All( - cv.positive_time_period, - cv.Range(max=core.TimePeriod(seconds=1638.375)), + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(seconds=1638.375)), ) ), }, diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h index 1f942c02e4..3f69e482b7 100644 --- a/esphome/components/dfrobot_sen0395/automation.h +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -50,7 +50,7 @@ class DfrobotSen0395SettingsAction : public Action, public Parenteddelay_after_detect_.value(x...); float disappear = this->delay_after_disappear_.value(x...); if (detect >= 0 && disappear >= 0) { - this->parent_->enqueue(make_unique(detect, disappear)); + this->parent_->enqueue(make_unique(detect, disappear)); } } if (this->start_after_power_on_.has_value()) { diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 3a89b2b71e..2c60fb3449 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -1,5 +1,7 @@ #include "commands.h" +#include + #include "esphome/core/log.h" #include "dfrobot_sen0395.h" @@ -194,32 +196,22 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) { return 0; // Command not done yet. } -OutputLatencyCommand::OutputLatencyCommand(float delay_after_detection, float delay_after_disappear) { - delay_after_detection = round(delay_after_detection / 0.025) * 0.025; - delay_after_disappear = round(delay_after_disappear / 0.025) * 0.025; - if (delay_after_detection < 0) - delay_after_detection = 0; - if (delay_after_detection > 1638.375) - delay_after_detection = 1638.375; - if (delay_after_disappear < 0) - delay_after_disappear = 0; - if (delay_after_disappear > 1638.375) - delay_after_disappear = 1638.375; - - this->delay_after_detection_ = delay_after_detection; - this->delay_after_disappear_ = delay_after_disappear; - - this->cmd_ = str_sprintf("outputLatency -1 %.0f %.0f", delay_after_detection / 0.025, delay_after_disappear / 0.025); +SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) { + delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f; + delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f; + this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f); + this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f); + this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_); }; -uint8_t OutputLatencyCommand::on_message(std::string &message) { +uint8_t SetLatencyCommand::on_message(std::string &message) { if (message == "sensor is not stopped") { ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!"); return 1; // Command done } else if (message == "Done") { ESP_LOGI(TAG, "Updated output latency config:"); - ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.02fs.", this->delay_after_detection_); - ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.02fs.", this->delay_after_disappear_); + ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.03f s.", this->delay_after_detection_); + ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_disappear_); ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); return 1; // Command done } diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h index 7426d9732a..cf3ba50be0 100644 --- a/esphome/components/dfrobot_sen0395/commands.h +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -62,9 +62,9 @@ class DetRangeCfgCommand : public Command { // TODO: Set min max values in component, so they can be published as sensor. }; -class OutputLatencyCommand : public Command { +class SetLatencyCommand : public Command { public: - OutputLatencyCommand(float delay_after_detection, float delay_after_disappear); + SetLatencyCommand(float delay_after_detection, float delay_after_disappear); uint8_t on_message(std::string &message) override; protected: From 5e9741f51c92482935357de359529daafeb5efad Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 4 Feb 2024 20:29:18 -0600 Subject: [PATCH 320/436] Add some components to the new testing framework (A part 1) (#6142) --- .../components/a01nyub/test.esp32-c3-idf.yaml | 13 +++++++ tests/components/a01nyub/test.esp32-c3.yaml | 13 +++++++ tests/components/a01nyub/test.esp32-idf.yaml | 13 +++++++ tests/components/a01nyub/test.esp32.yaml | 13 +++++++ tests/components/a01nyub/test.esp8266.yaml | 13 +++++++ tests/components/a01nyub/test.rp2040.yaml | 13 +++++++ .../components/a02yyuw/test.esp32-c3-idf.yaml | 13 +++++++ tests/components/a02yyuw/test.esp32-c3.yaml | 13 +++++++ tests/components/a02yyuw/test.esp32-idf.yaml | 13 +++++++ tests/components/a02yyuw/test.esp32.yaml | 13 +++++++ tests/components/a02yyuw/test.esp8266.yaml | 13 +++++++ tests/components/a02yyuw/test.rp2040.yaml | 13 +++++++ tests/components/a4988/test.esp32-c3-idf.yaml | 12 +++++++ tests/components/a4988/test.esp32-c3.yaml | 12 +++++++ tests/components/a4988/test.esp32-idf.yaml | 12 +++++++ tests/components/a4988/test.esp32.yaml | 12 +++++++ tests/components/a4988/test.esp8266.yaml | 12 +++++++ tests/components/a4988/test.rp2040.yaml | 12 +++++++ .../absolute_humidity/test.esp32-c3-idf.yaml | 21 +++++++++++ .../absolute_humidity/test.esp32-c3.yaml | 21 +++++++++++ .../absolute_humidity/test.esp32-idf.yaml | 21 +++++++++++ .../absolute_humidity/test.esp32.yaml | 21 +++++++++++ .../absolute_humidity/test.esp8266.yaml | 21 +++++++++++ .../absolute_humidity/test.rp2040.yaml | 21 +++++++++++ tests/components/ac_dimmer/test.esp32-c3.yaml | 7 ++++ tests/components/ac_dimmer/test.esp32.yaml | 7 ++++ tests/components/ac_dimmer/test.esp8266.yaml | 7 ++++ tests/components/ac_dimmer/test.rp2040.yaml | 7 ++++ .../adc128s102/test.esp32-c3-idf.yaml | 14 ++++++++ .../components/adc128s102/test.esp32-c3.yaml | 14 ++++++++ .../components/adc128s102/test.esp32-idf.yaml | 14 ++++++++ tests/components/adc128s102/test.esp32.yaml | 14 ++++++++ tests/components/adc128s102/test.esp8266.yaml | 14 ++++++++ tests/components/adc128s102/test.rp2040.yaml | 14 ++++++++ .../addressable_light/test.esp32-c3-idf.yaml | 31 ++++++++++++++++ .../addressable_light/test.esp32-c3.yaml | 31 ++++++++++++++++ .../addressable_light/test.esp32-idf.yaml | 31 ++++++++++++++++ .../addressable_light/test.esp32.yaml | 32 +++++++++++++++++ .../ade7953_i2c/test.esp32-c3-idf.yaml | 34 ++++++++++++++++++ .../components/ade7953_i2c/test.esp32-c3.yaml | 34 ++++++++++++++++++ .../ade7953_i2c/test.esp32-idf.yaml | 34 ++++++++++++++++++ tests/components/ade7953_i2c/test.esp32.yaml | 34 ++++++++++++++++++ .../components/ade7953_i2c/test.esp8266.yaml | 34 ++++++++++++++++++ tests/components/ade7953_i2c/test.rp2040.yaml | 34 ++++++++++++++++++ .../ade7953_spi/test.esp32-c3-idf.yaml | 36 +++++++++++++++++++ .../components/ade7953_spi/test.esp32-c3.yaml | 36 +++++++++++++++++++ .../ade7953_spi/test.esp32-idf.yaml | 36 +++++++++++++++++++ tests/components/ade7953_spi/test.esp32.yaml | 36 +++++++++++++++++++ .../components/ade7953_spi/test.esp8266.yaml | 36 +++++++++++++++++++ tests/components/ade7953_spi/test.rp2040.yaml | 36 +++++++++++++++++++ .../components/ads1115/test.esp32-c3-idf.yaml | 13 +++++++ tests/components/ads1115/test.esp32-c3.yaml | 13 +++++++ tests/components/ads1115/test.esp32-idf.yaml | 13 +++++++ tests/components/ads1115/test.esp32.yaml | 13 +++++++ tests/components/ads1115/test.esp8266.yaml | 13 +++++++ tests/components/ads1115/test.rp2040.yaml | 13 +++++++ tests/components/aht10/test.esp32-c3-idf.yaml | 11 ++++++ tests/components/aht10/test.esp32-c3.yaml | 11 ++++++ tests/components/aht10/test.esp32-idf.yaml | 11 ++++++ tests/components/aht10/test.esp32.yaml | 11 ++++++ tests/components/aht10/test.esp8266.yaml | 11 ++++++ tests/components/aht10/test.rp2040.yaml | 11 ++++++ .../test.esp32-c3-idf.yaml | 22 ++++++++++++ .../airthings_wave_mini/test.esp32-c3.yaml | 22 ++++++++++++ .../airthings_wave_mini/test.esp32-idf.yaml | 22 ++++++++++++ .../airthings_wave_mini/test.esp32.yaml | 22 ++++++++++++ .../test.esp32-c3-idf.yaml | 28 +++++++++++++++ .../airthings_wave_plus/test.esp32-c3.yaml | 28 +++++++++++++++ .../airthings_wave_plus/test.esp32-idf.yaml | 28 +++++++++++++++ .../airthings_wave_plus/test.esp32.yaml | 28 +++++++++++++++ 70 files changed, 1355 insertions(+) create mode 100644 tests/components/a01nyub/test.esp32-c3-idf.yaml create mode 100644 tests/components/a01nyub/test.esp32-c3.yaml create mode 100644 tests/components/a01nyub/test.esp32-idf.yaml create mode 100644 tests/components/a01nyub/test.esp32.yaml create mode 100644 tests/components/a01nyub/test.esp8266.yaml create mode 100644 tests/components/a01nyub/test.rp2040.yaml create mode 100644 tests/components/a02yyuw/test.esp32-c3-idf.yaml create mode 100644 tests/components/a02yyuw/test.esp32-c3.yaml create mode 100644 tests/components/a02yyuw/test.esp32-idf.yaml create mode 100644 tests/components/a02yyuw/test.esp32.yaml create mode 100644 tests/components/a02yyuw/test.esp8266.yaml create mode 100644 tests/components/a02yyuw/test.rp2040.yaml create mode 100644 tests/components/a4988/test.esp32-c3-idf.yaml create mode 100644 tests/components/a4988/test.esp32-c3.yaml create mode 100644 tests/components/a4988/test.esp32-idf.yaml create mode 100644 tests/components/a4988/test.esp32.yaml create mode 100644 tests/components/a4988/test.esp8266.yaml create mode 100644 tests/components/a4988/test.rp2040.yaml create mode 100644 tests/components/absolute_humidity/test.esp32-c3-idf.yaml create mode 100644 tests/components/absolute_humidity/test.esp32-c3.yaml create mode 100644 tests/components/absolute_humidity/test.esp32-idf.yaml create mode 100644 tests/components/absolute_humidity/test.esp32.yaml create mode 100644 tests/components/absolute_humidity/test.esp8266.yaml create mode 100644 tests/components/absolute_humidity/test.rp2040.yaml create mode 100644 tests/components/ac_dimmer/test.esp32-c3.yaml create mode 100644 tests/components/ac_dimmer/test.esp32.yaml create mode 100644 tests/components/ac_dimmer/test.esp8266.yaml create mode 100644 tests/components/ac_dimmer/test.rp2040.yaml create mode 100644 tests/components/adc128s102/test.esp32-c3-idf.yaml create mode 100644 tests/components/adc128s102/test.esp32-c3.yaml create mode 100644 tests/components/adc128s102/test.esp32-idf.yaml create mode 100644 tests/components/adc128s102/test.esp32.yaml create mode 100644 tests/components/adc128s102/test.esp8266.yaml create mode 100644 tests/components/adc128s102/test.rp2040.yaml create mode 100644 tests/components/addressable_light/test.esp32-c3-idf.yaml create mode 100644 tests/components/addressable_light/test.esp32-c3.yaml create mode 100644 tests/components/addressable_light/test.esp32-idf.yaml create mode 100644 tests/components/addressable_light/test.esp32.yaml create mode 100644 tests/components/ade7953_i2c/test.esp32-c3-idf.yaml create mode 100644 tests/components/ade7953_i2c/test.esp32-c3.yaml create mode 100644 tests/components/ade7953_i2c/test.esp32-idf.yaml create mode 100644 tests/components/ade7953_i2c/test.esp32.yaml create mode 100644 tests/components/ade7953_i2c/test.esp8266.yaml create mode 100644 tests/components/ade7953_i2c/test.rp2040.yaml create mode 100644 tests/components/ade7953_spi/test.esp32-c3-idf.yaml create mode 100644 tests/components/ade7953_spi/test.esp32-c3.yaml create mode 100644 tests/components/ade7953_spi/test.esp32-idf.yaml create mode 100644 tests/components/ade7953_spi/test.esp32.yaml create mode 100644 tests/components/ade7953_spi/test.esp8266.yaml create mode 100644 tests/components/ade7953_spi/test.rp2040.yaml create mode 100644 tests/components/ads1115/test.esp32-c3-idf.yaml create mode 100644 tests/components/ads1115/test.esp32-c3.yaml create mode 100644 tests/components/ads1115/test.esp32-idf.yaml create mode 100644 tests/components/ads1115/test.esp32.yaml create mode 100644 tests/components/ads1115/test.esp8266.yaml create mode 100644 tests/components/ads1115/test.rp2040.yaml create mode 100644 tests/components/aht10/test.esp32-c3-idf.yaml create mode 100644 tests/components/aht10/test.esp32-c3.yaml create mode 100644 tests/components/aht10/test.esp32-idf.yaml create mode 100644 tests/components/aht10/test.esp32.yaml create mode 100644 tests/components/aht10/test.esp8266.yaml create mode 100644 tests/components/aht10/test.rp2040.yaml create mode 100644 tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml create mode 100644 tests/components/airthings_wave_mini/test.esp32-c3.yaml create mode 100644 tests/components/airthings_wave_mini/test.esp32-idf.yaml create mode 100644 tests/components/airthings_wave_mini/test.esp32.yaml create mode 100644 tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml create mode 100644 tests/components/airthings_wave_plus/test.esp32-c3.yaml create mode 100644 tests/components/airthings_wave_plus/test.esp32-idf.yaml create mode 100644 tests/components/airthings_wave_plus/test.esp32.yaml diff --git a/tests/components/a01nyub/test.esp32-c3-idf.yaml b/tests/components/a01nyub/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3132f77136 --- /dev/null +++ b/tests/components/a01nyub/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a01nyub + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a01nyub + id: a01nyub_sensor + name: a01nyub Distance + uart_id: uart_a01nyub diff --git a/tests/components/a01nyub/test.esp32-c3.yaml b/tests/components/a01nyub/test.esp32-c3.yaml new file mode 100644 index 0000000000..3132f77136 --- /dev/null +++ b/tests/components/a01nyub/test.esp32-c3.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a01nyub + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a01nyub + id: a01nyub_sensor + name: a01nyub Distance + uart_id: uart_a01nyub diff --git a/tests/components/a01nyub/test.esp32-idf.yaml b/tests/components/a01nyub/test.esp32-idf.yaml new file mode 100644 index 0000000000..79fc9c5fbf --- /dev/null +++ b/tests/components/a01nyub/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a01nyub + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: a01nyub + id: a01nyub_sensor + name: a01nyub Distance + uart_id: uart_a01nyub diff --git a/tests/components/a01nyub/test.esp32.yaml b/tests/components/a01nyub/test.esp32.yaml new file mode 100644 index 0000000000..79fc9c5fbf --- /dev/null +++ b/tests/components/a01nyub/test.esp32.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a01nyub + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: a01nyub + id: a01nyub_sensor + name: a01nyub Distance + uart_id: uart_a01nyub diff --git a/tests/components/a01nyub/test.esp8266.yaml b/tests/components/a01nyub/test.esp8266.yaml new file mode 100644 index 0000000000..3132f77136 --- /dev/null +++ b/tests/components/a01nyub/test.esp8266.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a01nyub + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a01nyub + id: a01nyub_sensor + name: a01nyub Distance + uart_id: uart_a01nyub diff --git a/tests/components/a01nyub/test.rp2040.yaml b/tests/components/a01nyub/test.rp2040.yaml new file mode 100644 index 0000000000..3132f77136 --- /dev/null +++ b/tests/components/a01nyub/test.rp2040.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a01nyub + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a01nyub + id: a01nyub_sensor + name: a01nyub Distance + uart_id: uart_a01nyub diff --git a/tests/components/a02yyuw/test.esp32-c3-idf.yaml b/tests/components/a02yyuw/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp32-c3.yaml b/tests/components/a02yyuw/test.esp32-c3.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32-c3.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp32-idf.yaml b/tests/components/a02yyuw/test.esp32-idf.yaml new file mode 100644 index 0000000000..98d6a266b3 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp32.yaml b/tests/components/a02yyuw/test.esp32.yaml new file mode 100644 index 0000000000..98d6a266b3 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp8266.yaml b/tests/components/a02yyuw/test.esp8266.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.esp8266.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.rp2040.yaml b/tests/components/a02yyuw/test.rp2040.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.rp2040.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a4988/test.esp32-c3-idf.yaml b/tests/components/a4988/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..af4e4fa32b --- /dev/null +++ b/tests/components/a4988/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 2 + dir_pin: + number: 3 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp32-c3.yaml b/tests/components/a4988/test.esp32-c3.yaml new file mode 100644 index 0000000000..af4e4fa32b --- /dev/null +++ b/tests/components/a4988/test.esp32-c3.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 2 + dir_pin: + number: 3 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp32-idf.yaml b/tests/components/a4988/test.esp32-idf.yaml new file mode 100644 index 0000000000..0ca5e3f504 --- /dev/null +++ b/tests/components/a4988/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 22 + dir_pin: + number: 23 + sleep_pin: + number: 25 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp32.yaml b/tests/components/a4988/test.esp32.yaml new file mode 100644 index 0000000000..0ca5e3f504 --- /dev/null +++ b/tests/components/a4988/test.esp32.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 22 + dir_pin: + number: 23 + sleep_pin: + number: 25 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp8266.yaml b/tests/components/a4988/test.esp8266.yaml new file mode 100644 index 0000000000..f4c1886fc5 --- /dev/null +++ b/tests/components/a4988/test.esp8266.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 1 + dir_pin: + number: 2 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.rp2040.yaml b/tests/components/a4988/test.rp2040.yaml new file mode 100644 index 0000000000..af4e4fa32b --- /dev/null +++ b/tests/components/a4988/test.rp2040.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 2 + dir_pin: + number: 3 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/absolute_humidity/test.esp32-c3-idf.yaml b/tests/components/absolute_humidity/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..87a99f5206 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: absolute_humidity + name: Absolute Humidity + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } diff --git a/tests/components/absolute_humidity/test.esp32-c3.yaml b/tests/components/absolute_humidity/test.esp32-c3.yaml new file mode 100644 index 0000000000..87a99f5206 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32-c3.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: absolute_humidity + name: Absolute Humidity + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } diff --git a/tests/components/absolute_humidity/test.esp32-idf.yaml b/tests/components/absolute_humidity/test.esp32-idf.yaml new file mode 100644 index 0000000000..87a99f5206 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: absolute_humidity + name: Absolute Humidity + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } diff --git a/tests/components/absolute_humidity/test.esp32.yaml b/tests/components/absolute_humidity/test.esp32.yaml new file mode 100644 index 0000000000..87a99f5206 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: absolute_humidity + name: Absolute Humidity + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } diff --git a/tests/components/absolute_humidity/test.esp8266.yaml b/tests/components/absolute_humidity/test.esp8266.yaml new file mode 100644 index 0000000000..87a99f5206 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp8266.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: absolute_humidity + name: Absolute Humidity + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } diff --git a/tests/components/absolute_humidity/test.rp2040.yaml b/tests/components/absolute_humidity/test.rp2040.yaml new file mode 100644 index 0000000000..87a99f5206 --- /dev/null +++ b/tests/components/absolute_humidity/test.rp2040.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: absolute_humidity + name: Absolute Humidity + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } diff --git a/tests/components/ac_dimmer/test.esp32-c3.yaml b/tests/components/ac_dimmer/test.esp32-c3.yaml new file mode 100644 index 0000000000..f411d376be --- /dev/null +++ b/tests/components/ac_dimmer/test.esp32-c3.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 5 + zero_cross_pin: + number: 6 diff --git a/tests/components/ac_dimmer/test.esp32.yaml b/tests/components/ac_dimmer/test.esp32.yaml new file mode 100644 index 0000000000..cc17201666 --- /dev/null +++ b/tests/components/ac_dimmer/test.esp32.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 12 + zero_cross_pin: + number: 13 diff --git a/tests/components/ac_dimmer/test.esp8266.yaml b/tests/components/ac_dimmer/test.esp8266.yaml new file mode 100644 index 0000000000..af18d11c5f --- /dev/null +++ b/tests/components/ac_dimmer/test.esp8266.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 5 + zero_cross_pin: + number: 4 diff --git a/tests/components/ac_dimmer/test.rp2040.yaml b/tests/components/ac_dimmer/test.rp2040.yaml new file mode 100644 index 0000000000..f411d376be --- /dev/null +++ b/tests/components/ac_dimmer/test.rp2040.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 5 + zero_cross_pin: + number: 6 diff --git a/tests/components/adc128s102/test.esp32-c3-idf.yaml b/tests/components/adc128s102/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8edf745e58 --- /dev/null +++ b/tests/components/adc128s102/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +adc128s102: + cs_pin: 8 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp32-c3.yaml b/tests/components/adc128s102/test.esp32-c3.yaml new file mode 100644 index 0000000000..8edf745e58 --- /dev/null +++ b/tests/components/adc128s102/test.esp32-c3.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +adc128s102: + cs_pin: 8 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp32-idf.yaml b/tests/components/adc128s102/test.esp32-idf.yaml new file mode 100644 index 0000000000..005fbccc34 --- /dev/null +++ b/tests/components/adc128s102/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +adc128s102: + cs_pin: 12 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp32.yaml b/tests/components/adc128s102/test.esp32.yaml new file mode 100644 index 0000000000..005fbccc34 --- /dev/null +++ b/tests/components/adc128s102/test.esp32.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +adc128s102: + cs_pin: 12 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp8266.yaml b/tests/components/adc128s102/test.esp8266.yaml new file mode 100644 index 0000000000..09a51caec1 --- /dev/null +++ b/tests/components/adc128s102/test.esp8266.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +adc128s102: + cs_pin: 15 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.rp2040.yaml b/tests/components/adc128s102/test.rp2040.yaml new file mode 100644 index 0000000000..a7d54cbfe6 --- /dev/null +++ b/tests/components/adc128s102/test.rp2040.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +adc128s102: + cs_pin: 5 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/addressable_light/test.esp32-c3-idf.yaml b/tests/components/addressable_light/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f587113fac --- /dev/null +++ b/tests/components/addressable_light/test.esp32-c3-idf.yaml @@ -0,0 +1,31 @@ +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/addressable_light/test.esp32-c3.yaml b/tests/components/addressable_light/test.esp32-c3.yaml new file mode 100644 index 0000000000..f587113fac --- /dev/null +++ b/tests/components/addressable_light/test.esp32-c3.yaml @@ -0,0 +1,31 @@ +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/addressable_light/test.esp32-idf.yaml b/tests/components/addressable_light/test.esp32-idf.yaml new file mode 100644 index 0000000000..f587113fac --- /dev/null +++ b/tests/components/addressable_light/test.esp32-idf.yaml @@ -0,0 +1,31 @@ +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/addressable_light/test.esp32.yaml b/tests/components/addressable_light/test.esp32.yaml new file mode 100644 index 0000000000..f7717be610 --- /dev/null +++ b/tests/components/addressable_light/test.esp32.yaml @@ -0,0 +1,32 @@ +light: + - platform: fastled_clockless + id: led_matrix_32x8 + name: led_matrix_32x8 + chipset: WS2812B + pin: 2 + num_leds: 256 + rgb_order: GRB + default_transition_length: 0s + color_correct: [50%, 50%, 50%] + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml b/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d7b365a7e1 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp32-c3.yaml b/tests/components/ade7953_i2c/test.esp32-c3.yaml new file mode 100644 index 0000000000..d7b365a7e1 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32-c3.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp32-idf.yaml b/tests/components/ade7953_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..71602f20a3 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32-idf.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp32.yaml b/tests/components/ade7953_i2c/test.esp32.yaml new file mode 100644 index 0000000000..71602f20a3 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp8266.yaml b/tests/components/ade7953_i2c/test.esp8266.yaml new file mode 100644 index 0000000000..6903cd1953 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp8266.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.rp2040.yaml b/tests/components/ade7953_i2c/test.rp2040.yaml new file mode 100644 index 0000000000..d7b365a7e1 --- /dev/null +++ b/tests/components/ade7953_i2c/test.rp2040.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml b/tests/components/ade7953_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a967f28d9c --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: ade7953_spi + cs_pin: 8 + irq_pin: 9 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32-c3.yaml b/tests/components/ade7953_spi/test.esp32-c3.yaml new file mode 100644 index 0000000000..a967f28d9c --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32-c3.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: ade7953_spi + cs_pin: 8 + irq_pin: 9 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32-idf.yaml b/tests/components/ade7953_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..e9ef7e4116 --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32-idf.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: ade7953_spi + cs_pin: 5 + irq_pin: 13 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32.yaml b/tests/components/ade7953_spi/test.esp32.yaml new file mode 100644 index 0000000000..e9ef7e4116 --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: ade7953_spi + cs_pin: 5 + irq_pin: 13 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp8266.yaml b/tests/components/ade7953_spi/test.esp8266.yaml new file mode 100644 index 0000000000..b36b4445ab --- /dev/null +++ b/tests/components/ade7953_spi/test.esp8266.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: ade7953_spi + cs_pin: 15 + irq_pin: 5 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.rp2040.yaml b/tests/components/ade7953_spi/test.rp2040.yaml new file mode 100644 index 0000000000..319abd4613 --- /dev/null +++ b/tests/components/ade7953_spi/test.rp2040.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: ade7953_spi + cs_pin: 5 + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ads1115/test.esp32-c3-idf.yaml b/tests/components/ads1115/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-c3.yaml b/tests/components/ads1115/test.esp32-c3.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.esp32-c3.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-idf.yaml b/tests/components/ads1115/test.esp32-idf.yaml new file mode 100644 index 0000000000..a869f2379b --- /dev/null +++ b/tests/components/ads1115/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 16 + sda: 17 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32.yaml b/tests/components/ads1115/test.esp32.yaml new file mode 100644 index 0000000000..a869f2379b --- /dev/null +++ b/tests/components/ads1115/test.esp32.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 16 + sda: 17 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp8266.yaml b/tests/components/ads1115/test.esp8266.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.esp8266.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.rp2040.yaml b/tests/components/ads1115/test.rp2040.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.rp2040.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/aht10/test.esp32-c3-idf.yaml b/tests/components/aht10/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp32-c3.yaml b/tests/components/aht10/test.esp32-c3.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp32-idf.yaml b/tests/components/aht10/test.esp32-idf.yaml new file mode 100644 index 0000000000..499e69e5d3 --- /dev/null +++ b/tests/components/aht10/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 16 + sda: 17 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp32.yaml b/tests/components/aht10/test.esp32.yaml new file mode 100644 index 0000000000..499e69e5d3 --- /dev/null +++ b/tests/components/aht10/test.esp32.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 16 + sda: 17 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp8266.yaml b/tests/components/aht10/test.esp8266.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.esp8266.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.rp2040.yaml b/tests/components/aht10/test.rp2040.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.rp2040.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..87902e6c66 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthingsmini01 + +sensor: + - id: airthingswm + platform: airthings_wave_mini + ble_client_id: airthingsmini01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Mini Temperature + humidity: + name: Wave Mini Humidity + pressure: + name: Wave Mini Pressure + tvoc: + name: Wave Mini VOC + battery_voltage: + name: Wave Mini Battery Voltage diff --git a/tests/components/airthings_wave_mini/test.esp32-c3.yaml b/tests/components/airthings_wave_mini/test.esp32-c3.yaml new file mode 100644 index 0000000000..87902e6c66 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32-c3.yaml @@ -0,0 +1,22 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthingsmini01 + +sensor: + - id: airthingswm + platform: airthings_wave_mini + ble_client_id: airthingsmini01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Mini Temperature + humidity: + name: Wave Mini Humidity + pressure: + name: Wave Mini Pressure + tvoc: + name: Wave Mini VOC + battery_voltage: + name: Wave Mini Battery Voltage diff --git a/tests/components/airthings_wave_mini/test.esp32-idf.yaml b/tests/components/airthings_wave_mini/test.esp32-idf.yaml new file mode 100644 index 0000000000..87902e6c66 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthingsmini01 + +sensor: + - id: airthingswm + platform: airthings_wave_mini + ble_client_id: airthingsmini01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Mini Temperature + humidity: + name: Wave Mini Humidity + pressure: + name: Wave Mini Pressure + tvoc: + name: Wave Mini VOC + battery_voltage: + name: Wave Mini Battery Voltage diff --git a/tests/components/airthings_wave_mini/test.esp32.yaml b/tests/components/airthings_wave_mini/test.esp32.yaml new file mode 100644 index 0000000000..87902e6c66 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32.yaml @@ -0,0 +1,22 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthingsmini01 + +sensor: + - id: airthingswm + platform: airthings_wave_mini + ble_client_id: airthingsmini01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Mini Temperature + humidity: + name: Wave Mini Humidity + pressure: + name: Wave Mini Pressure + tvoc: + name: Wave Mini VOC + battery_voltage: + name: Wave Mini Battery Voltage diff --git a/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2124fcdaec --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthings01 + +sensor: + - id: airthingswp + platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Plus Temperature + radon: + name: Wave Plus Radon + radon_long_term: + name: Wave Plus Radon Long Term + pressure: + name: Wave Plus Pressure + humidity: + name: Wave Plus Humidity + co2: + name: Wave Plus CO2 + tvoc: + name: Wave Plus VOC + battery_voltage: + name: Wave Plus Battery Voltage diff --git a/tests/components/airthings_wave_plus/test.esp32-c3.yaml b/tests/components/airthings_wave_plus/test.esp32-c3.yaml new file mode 100644 index 0000000000..2124fcdaec --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32-c3.yaml @@ -0,0 +1,28 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthings01 + +sensor: + - id: airthingswp + platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Plus Temperature + radon: + name: Wave Plus Radon + radon_long_term: + name: Wave Plus Radon Long Term + pressure: + name: Wave Plus Pressure + humidity: + name: Wave Plus Humidity + co2: + name: Wave Plus CO2 + tvoc: + name: Wave Plus VOC + battery_voltage: + name: Wave Plus Battery Voltage diff --git a/tests/components/airthings_wave_plus/test.esp32-idf.yaml b/tests/components/airthings_wave_plus/test.esp32-idf.yaml new file mode 100644 index 0000000000..2124fcdaec --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthings01 + +sensor: + - id: airthingswp + platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Plus Temperature + radon: + name: Wave Plus Radon + radon_long_term: + name: Wave Plus Radon Long Term + pressure: + name: Wave Plus Pressure + humidity: + name: Wave Plus Humidity + co2: + name: Wave Plus CO2 + tvoc: + name: Wave Plus VOC + battery_voltage: + name: Wave Plus Battery Voltage diff --git a/tests/components/airthings_wave_plus/test.esp32.yaml b/tests/components/airthings_wave_plus/test.esp32.yaml new file mode 100644 index 0000000000..2124fcdaec --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32.yaml @@ -0,0 +1,28 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthings01 + +sensor: + - id: airthingswp + platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Plus Temperature + radon: + name: Wave Plus Radon + radon_long_term: + name: Wave Plus Radon Long Term + pressure: + name: Wave Plus Pressure + humidity: + name: Wave Plus Humidity + co2: + name: Wave Plus CO2 + tvoc: + name: Wave Plus VOC + battery_voltage: + name: Wave Plus Battery Voltage From 164b42f5aa9847af3074ce41d06d845accf04e3a Mon Sep 17 00:00:00 2001 From: Marcel Hetzendorfer Date: Tue, 6 Feb 2024 20:03:09 +0100 Subject: [PATCH 321/436] WRGB or RGBW? WS2814 (#6164) --- esphome/components/esp32_rmt_led_strip/led_strip.cpp | 10 ++++++---- esphome/components/esp32_rmt_led_strip/led_strip.h | 4 +++- esphome/components/esp32_rmt_led_strip/light.py | 3 +++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 3df4077c96..c5a7f7918d 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -161,10 +161,12 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index break; } uint8_t multiplier = this->is_rgbw_ ? 4 : 3; - return {this->buf_ + (index * multiplier) + r, - this->buf_ + (index * multiplier) + g, - this->buf_ + (index * multiplier) + b, - this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, + uint8_t white = this->is_wrgb_ ? 0 : 3; + + return {this->buf_ + (index * multiplier) + r + this->is_wrgb_, + this->buf_ + (index * multiplier) + g + this->is_wrgb_, + this->buf_ + (index * multiplier) + b + this->is_wrgb_, + this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr, &this->effect_data_[index], &this->correction_}; } diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index 11d61b07e1..51eed8e01c 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -33,7 +33,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { int32_t size() const override { return this->num_leds_; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->is_rgbw_) { + if (this->is_rgbw_ || this->is_wrgb_) { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); } else { traits.set_supported_color_modes({light::ColorMode::RGB}); @@ -44,6 +44,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { void set_pin(uint8_t pin) { this->pin_ = pin; } void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } + void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; } /// Set a maximum refresh rate in µs as some lights do not like being updated too often. void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } @@ -72,6 +73,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint8_t pin_; uint16_t num_leds_; bool is_rgbw_; + bool is_wrgb_; rmt_item32_t bit0_, bit1_; RGBOrder rgb_order_; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 122ee132a7..d38c7abeb8 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -52,6 +52,7 @@ CHIPSETS = { CONF_IS_RGBW = "is_rgbw" +CONF_IS_WRGB = "is_wrgb" CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_LOW = "bit0_low" CONF_BIT1_HIGH = "bit1_high" @@ -90,6 +91,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, + cv.Optional(CONF_IS_WRGB, default=False): cv.boolean, cv.Inclusive( CONF_BIT0_HIGH, "custom", @@ -145,6 +147,7 @@ async def to_code(config): cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) cg.add( var.set_rmt_channel( From 9dbbc80c7409dcce581b8e9cae11edbd1a74bf78 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 6 Feb 2024 13:05:04 -0600 Subject: [PATCH 322/436] Add some components to the new testing framework (A part 2) (#6162) --- .../test.esp32-c3-idf.yaml | 64 +++++++++++++++++++ .../alarm_control_panel/test.esp32-c3.yaml | 64 +++++++++++++++++++ .../alarm_control_panel/test.esp32-idf.yaml | 64 +++++++++++++++++++ .../alarm_control_panel/test.esp32.yaml | 64 +++++++++++++++++++ .../alarm_control_panel/test.esp8266.yaml | 64 +++++++++++++++++++ .../alarm_control_panel/test.rp2040.yaml | 64 +++++++++++++++++++ .../components/alpha3/test.esp32-c3-idf.yaml | 17 +++++ tests/components/alpha3/test.esp32-c3.yaml | 17 +++++ tests/components/alpha3/test.esp32-idf.yaml | 17 +++++ tests/components/alpha3/test.esp32.yaml | 17 +++++ .../components/am2320/test.esp32-c3-idf.yaml | 11 ++++ tests/components/am2320/test.esp32-c3.yaml | 11 ++++ tests/components/am2320/test.esp32-idf.yaml | 11 ++++ tests/components/am2320/test.esp32.yaml | 11 ++++ tests/components/am2320/test.esp8266.yaml | 11 ++++ tests/components/am2320/test.rp2040.yaml | 11 ++++ tests/components/am43/test.esp32-c3-idf.yaml | 19 ++++++ tests/components/am43/test.esp32-c3.yaml | 19 ++++++ tests/components/am43/test.esp32-idf.yaml | 19 ++++++ tests/components/am43/test.esp32.yaml | 19 ++++++ .../analog_threshold/test.esp32-c3-idf.yaml | 28 ++++++++ .../analog_threshold/test.esp32-c3.yaml | 28 ++++++++ .../analog_threshold/test.esp32-idf.yaml | 28 ++++++++ .../analog_threshold/test.esp32.yaml | 28 ++++++++ .../analog_threshold/test.esp8266.yaml | 28 ++++++++ .../analog_threshold/test.rp2040.yaml | 28 ++++++++ .../animation/test.esp32-c3-idf.yaml | 23 +++++++ tests/components/animation/test.esp32-c3.yaml | 23 +++++++ .../components/animation/test.esp32-idf.yaml | 23 +++++++ tests/components/animation/test.esp32.yaml | 23 +++++++ tests/components/animation/test.esp8266.yaml | 23 +++++++ tests/components/animation/test.rp2040.yaml | 23 +++++++ tests/components/anova/test.esp32-c3-idf.yaml | 11 ++++ tests/components/anova/test.esp32-c3.yaml | 11 ++++ tests/components/anova/test.esp32-idf.yaml | 11 ++++ tests/components/anova/test.esp32.yaml | 11 ++++ .../apds9960/test.esp32-c3-idf.yaml | 48 ++++++++++++++ tests/components/apds9960/test.esp32-c3.yaml | 48 ++++++++++++++ tests/components/apds9960/test.esp32-idf.yaml | 48 ++++++++++++++ tests/components/apds9960/test.esp32.yaml | 48 ++++++++++++++ tests/components/apds9960/test.esp8266.yaml | 48 ++++++++++++++ tests/components/apds9960/test.rp2040.yaml | 48 ++++++++++++++ tests/components/api/test.esp32-c3-idf.yaml | 50 +++++++++++++++ tests/components/api/test.esp32-c3.yaml | 50 +++++++++++++++ tests/components/api/test.esp32-idf.yaml | 50 +++++++++++++++ tests/components/api/test.esp32.yaml | 50 +++++++++++++++ tests/components/api/test.esp8266.yaml | 50 +++++++++++++++ tests/components/api/test.rp2040.yaml | 50 +++++++++++++++ .../as3935_i2c/test.esp32-c3-idf.yaml | 18 ++++++ .../components/as3935_i2c/test.esp32-c3.yaml | 18 ++++++ .../components/as3935_i2c/test.esp32-idf.yaml | 18 ++++++ tests/components/as3935_i2c/test.esp32.yaml | 18 ++++++ tests/components/as3935_i2c/test.esp8266.yaml | 18 ++++++ tests/components/as3935_i2c/test.rp2040.yaml | 18 ++++++ .../as3935_spi/test.esp32-c3-idf.yaml | 20 ++++++ .../components/as3935_spi/test.esp32-c3.yaml | 20 ++++++ .../components/as3935_spi/test.esp32-idf.yaml | 20 ++++++ tests/components/as3935_spi/test.esp32.yaml | 20 ++++++ tests/components/as3935_spi/test.esp8266.yaml | 20 ++++++ tests/components/as3935_spi/test.rp2040.yaml | 20 ++++++ .../components/as5600/test.esp32-c3-idf.yaml | 27 ++++++++ tests/components/as5600/test.esp32-c3.yaml | 27 ++++++++ tests/components/as5600/test.esp32-idf.yaml | 27 ++++++++ tests/components/as5600/test.esp32.yaml | 27 ++++++++ tests/components/as5600/test.esp8266.yaml | 27 ++++++++ tests/components/as5600/test.rp2040.yaml | 27 ++++++++ .../components/as7341/test.esp32-c3-idf.yaml | 31 +++++++++ tests/components/as7341/test.esp32-c3.yaml | 31 +++++++++ tests/components/as7341/test.esp32-idf.yaml | 31 +++++++++ tests/components/as7341/test.esp32.yaml | 31 +++++++++ tests/components/as7341/test.esp8266.yaml | 31 +++++++++ tests/components/as7341/test.rp2040.yaml | 31 +++++++++ .../atc_mithermometer/test.esp32-c3-idf.yaml | 13 ++++ .../atc_mithermometer/test.esp32-c3.yaml | 13 ++++ .../atc_mithermometer/test.esp32-idf.yaml | 13 ++++ .../atc_mithermometer/test.esp32.yaml | 13 ++++ .../atm90e26/test.esp32-c3-idf.yaml | 26 ++++++++ tests/components/atm90e26/test.esp32-c3.yaml | 26 ++++++++ tests/components/atm90e26/test.esp32-idf.yaml | 26 ++++++++ tests/components/atm90e26/test.esp32.yaml | 26 ++++++++ tests/components/atm90e26/test.esp8266.yaml | 26 ++++++++ tests/components/atm90e26/test.rp2040.yaml | 26 ++++++++ .../atm90e32/test.esp32-c3-idf.yaml | 51 +++++++++++++++ tests/components/atm90e32/test.esp32-c3.yaml | 51 +++++++++++++++ tests/components/atm90e32/test.esp32-idf.yaml | 51 +++++++++++++++ tests/components/atm90e32/test.esp32.yaml | 51 +++++++++++++++ tests/components/atm90e32/test.esp8266.yaml | 51 +++++++++++++++ tests/components/atm90e32/test.rp2040.yaml | 51 +++++++++++++++ 88 files changed, 2622 insertions(+) create mode 100644 tests/components/alarm_control_panel/test.esp32-c3-idf.yaml create mode 100644 tests/components/alarm_control_panel/test.esp32-c3.yaml create mode 100644 tests/components/alarm_control_panel/test.esp32-idf.yaml create mode 100644 tests/components/alarm_control_panel/test.esp32.yaml create mode 100644 tests/components/alarm_control_panel/test.esp8266.yaml create mode 100644 tests/components/alarm_control_panel/test.rp2040.yaml create mode 100644 tests/components/alpha3/test.esp32-c3-idf.yaml create mode 100644 tests/components/alpha3/test.esp32-c3.yaml create mode 100644 tests/components/alpha3/test.esp32-idf.yaml create mode 100644 tests/components/alpha3/test.esp32.yaml create mode 100644 tests/components/am2320/test.esp32-c3-idf.yaml create mode 100644 tests/components/am2320/test.esp32-c3.yaml create mode 100644 tests/components/am2320/test.esp32-idf.yaml create mode 100644 tests/components/am2320/test.esp32.yaml create mode 100644 tests/components/am2320/test.esp8266.yaml create mode 100644 tests/components/am2320/test.rp2040.yaml create mode 100644 tests/components/am43/test.esp32-c3-idf.yaml create mode 100644 tests/components/am43/test.esp32-c3.yaml create mode 100644 tests/components/am43/test.esp32-idf.yaml create mode 100644 tests/components/am43/test.esp32.yaml create mode 100644 tests/components/analog_threshold/test.esp32-c3-idf.yaml create mode 100644 tests/components/analog_threshold/test.esp32-c3.yaml create mode 100644 tests/components/analog_threshold/test.esp32-idf.yaml create mode 100644 tests/components/analog_threshold/test.esp32.yaml create mode 100644 tests/components/analog_threshold/test.esp8266.yaml create mode 100644 tests/components/analog_threshold/test.rp2040.yaml create mode 100644 tests/components/animation/test.esp32-c3-idf.yaml create mode 100644 tests/components/animation/test.esp32-c3.yaml create mode 100644 tests/components/animation/test.esp32-idf.yaml create mode 100644 tests/components/animation/test.esp32.yaml create mode 100644 tests/components/animation/test.esp8266.yaml create mode 100644 tests/components/animation/test.rp2040.yaml create mode 100644 tests/components/anova/test.esp32-c3-idf.yaml create mode 100644 tests/components/anova/test.esp32-c3.yaml create mode 100644 tests/components/anova/test.esp32-idf.yaml create mode 100644 tests/components/anova/test.esp32.yaml create mode 100644 tests/components/apds9960/test.esp32-c3-idf.yaml create mode 100644 tests/components/apds9960/test.esp32-c3.yaml create mode 100644 tests/components/apds9960/test.esp32-idf.yaml create mode 100644 tests/components/apds9960/test.esp32.yaml create mode 100644 tests/components/apds9960/test.esp8266.yaml create mode 100644 tests/components/apds9960/test.rp2040.yaml create mode 100644 tests/components/api/test.esp32-c3-idf.yaml create mode 100644 tests/components/api/test.esp32-c3.yaml create mode 100644 tests/components/api/test.esp32-idf.yaml create mode 100644 tests/components/api/test.esp32.yaml create mode 100644 tests/components/api/test.esp8266.yaml create mode 100644 tests/components/api/test.rp2040.yaml create mode 100644 tests/components/as3935_i2c/test.esp32-c3-idf.yaml create mode 100644 tests/components/as3935_i2c/test.esp32-c3.yaml create mode 100644 tests/components/as3935_i2c/test.esp32-idf.yaml create mode 100644 tests/components/as3935_i2c/test.esp32.yaml create mode 100644 tests/components/as3935_i2c/test.esp8266.yaml create mode 100644 tests/components/as3935_i2c/test.rp2040.yaml create mode 100644 tests/components/as3935_spi/test.esp32-c3-idf.yaml create mode 100644 tests/components/as3935_spi/test.esp32-c3.yaml create mode 100644 tests/components/as3935_spi/test.esp32-idf.yaml create mode 100644 tests/components/as3935_spi/test.esp32.yaml create mode 100644 tests/components/as3935_spi/test.esp8266.yaml create mode 100644 tests/components/as3935_spi/test.rp2040.yaml create mode 100644 tests/components/as5600/test.esp32-c3-idf.yaml create mode 100644 tests/components/as5600/test.esp32-c3.yaml create mode 100644 tests/components/as5600/test.esp32-idf.yaml create mode 100644 tests/components/as5600/test.esp32.yaml create mode 100644 tests/components/as5600/test.esp8266.yaml create mode 100644 tests/components/as5600/test.rp2040.yaml create mode 100644 tests/components/as7341/test.esp32-c3-idf.yaml create mode 100644 tests/components/as7341/test.esp32-c3.yaml create mode 100644 tests/components/as7341/test.esp32-idf.yaml create mode 100644 tests/components/as7341/test.esp32.yaml create mode 100644 tests/components/as7341/test.esp8266.yaml create mode 100644 tests/components/as7341/test.rp2040.yaml create mode 100644 tests/components/atc_mithermometer/test.esp32-c3-idf.yaml create mode 100644 tests/components/atc_mithermometer/test.esp32-c3.yaml create mode 100644 tests/components/atc_mithermometer/test.esp32-idf.yaml create mode 100644 tests/components/atc_mithermometer/test.esp32.yaml create mode 100644 tests/components/atm90e26/test.esp32-c3-idf.yaml create mode 100644 tests/components/atm90e26/test.esp32-c3.yaml create mode 100644 tests/components/atm90e26/test.esp32-idf.yaml create mode 100644 tests/components/atm90e26/test.esp32.yaml create mode 100644 tests/components/atm90e26/test.esp8266.yaml create mode 100644 tests/components/atm90e26/test.rp2040.yaml create mode 100644 tests/components/atm90e32/test.esp32-c3-idf.yaml create mode 100644 tests/components/atm90e32/test.esp32-c3.yaml create mode 100644 tests/components/atm90e32/test.esp32-idf.yaml create mode 100644 tests/components/atm90e32/test.esp32.yaml create mode 100644 tests/components/atm90e32/test.esp8266.yaml create mode 100644 tests/components/atm90e32/test.rp2040.yaml diff --git a/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml b/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..218274bad4 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml @@ -0,0 +1,64 @@ +binary_sensor: + - platform: gpio + id: bin1 + pin: 1 + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); + - platform: template + id: alarmcontrolpanel2 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_disarmed: + then: + - logger.log: "### DISARMED ###" + on_pending: + then: + - logger.log: "### PENDING ###" + on_arming: + then: + - logger.log: "### ARMING ###" + on_armed_home: + then: + - logger.log: "### ARMED HOME ###" + on_armed_night: + then: + - logger.log: "### ARMED NIGHT ###" + on_armed_away: + then: + - logger.log: "### ARMED AWAY ###" + on_triggered: + then: + - logger.log: "### TRIGGERED ###" + on_cleared: + then: + - logger.log: "### CLEARED ###" diff --git a/tests/components/alarm_control_panel/test.esp32-c3.yaml b/tests/components/alarm_control_panel/test.esp32-c3.yaml new file mode 100644 index 0000000000..218274bad4 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32-c3.yaml @@ -0,0 +1,64 @@ +binary_sensor: + - platform: gpio + id: bin1 + pin: 1 + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); + - platform: template + id: alarmcontrolpanel2 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_disarmed: + then: + - logger.log: "### DISARMED ###" + on_pending: + then: + - logger.log: "### PENDING ###" + on_arming: + then: + - logger.log: "### ARMING ###" + on_armed_home: + then: + - logger.log: "### ARMED HOME ###" + on_armed_night: + then: + - logger.log: "### ARMED NIGHT ###" + on_armed_away: + then: + - logger.log: "### ARMED AWAY ###" + on_triggered: + then: + - logger.log: "### TRIGGERED ###" + on_cleared: + then: + - logger.log: "### CLEARED ###" diff --git a/tests/components/alarm_control_panel/test.esp32-idf.yaml b/tests/components/alarm_control_panel/test.esp32-idf.yaml new file mode 100644 index 0000000000..218274bad4 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32-idf.yaml @@ -0,0 +1,64 @@ +binary_sensor: + - platform: gpio + id: bin1 + pin: 1 + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); + - platform: template + id: alarmcontrolpanel2 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_disarmed: + then: + - logger.log: "### DISARMED ###" + on_pending: + then: + - logger.log: "### PENDING ###" + on_arming: + then: + - logger.log: "### ARMING ###" + on_armed_home: + then: + - logger.log: "### ARMED HOME ###" + on_armed_night: + then: + - logger.log: "### ARMED NIGHT ###" + on_armed_away: + then: + - logger.log: "### ARMED AWAY ###" + on_triggered: + then: + - logger.log: "### TRIGGERED ###" + on_cleared: + then: + - logger.log: "### CLEARED ###" diff --git a/tests/components/alarm_control_panel/test.esp32.yaml b/tests/components/alarm_control_panel/test.esp32.yaml new file mode 100644 index 0000000000..218274bad4 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32.yaml @@ -0,0 +1,64 @@ +binary_sensor: + - platform: gpio + id: bin1 + pin: 1 + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); + - platform: template + id: alarmcontrolpanel2 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_disarmed: + then: + - logger.log: "### DISARMED ###" + on_pending: + then: + - logger.log: "### PENDING ###" + on_arming: + then: + - logger.log: "### ARMING ###" + on_armed_home: + then: + - logger.log: "### ARMED HOME ###" + on_armed_night: + then: + - logger.log: "### ARMED NIGHT ###" + on_armed_away: + then: + - logger.log: "### ARMED AWAY ###" + on_triggered: + then: + - logger.log: "### TRIGGERED ###" + on_cleared: + then: + - logger.log: "### CLEARED ###" diff --git a/tests/components/alarm_control_panel/test.esp8266.yaml b/tests/components/alarm_control_panel/test.esp8266.yaml new file mode 100644 index 0000000000..218274bad4 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp8266.yaml @@ -0,0 +1,64 @@ +binary_sensor: + - platform: gpio + id: bin1 + pin: 1 + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); + - platform: template + id: alarmcontrolpanel2 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_disarmed: + then: + - logger.log: "### DISARMED ###" + on_pending: + then: + - logger.log: "### PENDING ###" + on_arming: + then: + - logger.log: "### ARMING ###" + on_armed_home: + then: + - logger.log: "### ARMED HOME ###" + on_armed_night: + then: + - logger.log: "### ARMED NIGHT ###" + on_armed_away: + then: + - logger.log: "### ARMED AWAY ###" + on_triggered: + then: + - logger.log: "### TRIGGERED ###" + on_cleared: + then: + - logger.log: "### CLEARED ###" diff --git a/tests/components/alarm_control_panel/test.rp2040.yaml b/tests/components/alarm_control_panel/test.rp2040.yaml new file mode 100644 index 0000000000..218274bad4 --- /dev/null +++ b/tests/components/alarm_control_panel/test.rp2040.yaml @@ -0,0 +1,64 @@ +binary_sensor: + - platform: gpio + id: bin1 + pin: 1 + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); + - platform: template + id: alarmcontrolpanel2 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_disarmed: + then: + - logger.log: "### DISARMED ###" + on_pending: + then: + - logger.log: "### PENDING ###" + on_arming: + then: + - logger.log: "### ARMING ###" + on_armed_home: + then: + - logger.log: "### ARMED HOME ###" + on_armed_night: + then: + - logger.log: "### ARMED NIGHT ###" + on_armed_away: + then: + - logger.log: "### ARMED AWAY ###" + on_triggered: + then: + - logger.log: "### TRIGGERED ###" + on_cleared: + then: + - logger.log: "### CLEARED ###" diff --git a/tests/components/alpha3/test.esp32-c3-idf.yaml b/tests/components/alpha3/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..913f086ac4 --- /dev/null +++ b/tests/components/alpha3/test.esp32-c3-idf.yaml @@ -0,0 +1,17 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: alpha3_blec + +sensor: + - platform: alpha3 + ble_client_id: alpha3_blec + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" diff --git a/tests/components/alpha3/test.esp32-c3.yaml b/tests/components/alpha3/test.esp32-c3.yaml new file mode 100644 index 0000000000..913f086ac4 --- /dev/null +++ b/tests/components/alpha3/test.esp32-c3.yaml @@ -0,0 +1,17 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: alpha3_blec + +sensor: + - platform: alpha3 + ble_client_id: alpha3_blec + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" diff --git a/tests/components/alpha3/test.esp32-idf.yaml b/tests/components/alpha3/test.esp32-idf.yaml new file mode 100644 index 0000000000..913f086ac4 --- /dev/null +++ b/tests/components/alpha3/test.esp32-idf.yaml @@ -0,0 +1,17 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: alpha3_blec + +sensor: + - platform: alpha3 + ble_client_id: alpha3_blec + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" diff --git a/tests/components/alpha3/test.esp32.yaml b/tests/components/alpha3/test.esp32.yaml new file mode 100644 index 0000000000..913f086ac4 --- /dev/null +++ b/tests/components/alpha3/test.esp32.yaml @@ -0,0 +1,17 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: alpha3_blec + +sensor: + - platform: alpha3 + ble_client_id: alpha3_blec + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" diff --git a/tests/components/am2320/test.esp32-c3-idf.yaml b/tests/components/am2320/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp32-c3.yaml b/tests/components/am2320/test.esp32-c3.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp32-idf.yaml b/tests/components/am2320/test.esp32-idf.yaml new file mode 100644 index 0000000000..99f4173b85 --- /dev/null +++ b/tests/components/am2320/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp32.yaml b/tests/components/am2320/test.esp32.yaml new file mode 100644 index 0000000000..99f4173b85 --- /dev/null +++ b/tests/components/am2320/test.esp32.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp8266.yaml b/tests/components/am2320/test.esp8266.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.esp8266.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.rp2040.yaml b/tests/components/am2320/test.rp2040.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.rp2040.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am43/test.esp32-c3-idf.yaml b/tests/components/am43/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..60b7d81a55 --- /dev/null +++ b/tests/components/am43/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: am43_blec + +cover: + - platform: am43 + name: Test AM43 Cover + id: am43_test + ble_client_id: am43_blec + +sensor: + - platform: am43 + ble_client_id: am43_blec + battery_level: + name: Kitchen blinds battery + illuminance: + name: Kitchen blinds light diff --git a/tests/components/am43/test.esp32-c3.yaml b/tests/components/am43/test.esp32-c3.yaml new file mode 100644 index 0000000000..60b7d81a55 --- /dev/null +++ b/tests/components/am43/test.esp32-c3.yaml @@ -0,0 +1,19 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: am43_blec + +cover: + - platform: am43 + name: Test AM43 Cover + id: am43_test + ble_client_id: am43_blec + +sensor: + - platform: am43 + ble_client_id: am43_blec + battery_level: + name: Kitchen blinds battery + illuminance: + name: Kitchen blinds light diff --git a/tests/components/am43/test.esp32-idf.yaml b/tests/components/am43/test.esp32-idf.yaml new file mode 100644 index 0000000000..60b7d81a55 --- /dev/null +++ b/tests/components/am43/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: am43_blec + +cover: + - platform: am43 + name: Test AM43 Cover + id: am43_test + ble_client_id: am43_blec + +sensor: + - platform: am43 + ble_client_id: am43_blec + battery_level: + name: Kitchen blinds battery + illuminance: + name: Kitchen blinds light diff --git a/tests/components/am43/test.esp32.yaml b/tests/components/am43/test.esp32.yaml new file mode 100644 index 0000000000..60b7d81a55 --- /dev/null +++ b/tests/components/am43/test.esp32.yaml @@ -0,0 +1,19 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: am43_blec + +cover: + - platform: am43 + name: Test AM43 Cover + id: am43_test + ble_client_id: am43_blec + +sensor: + - platform: am43 + ble_client_id: am43_blec + battery_level: + name: Kitchen blinds battery + illuminance: + name: Kitchen blinds light diff --git a/tests/components/analog_threshold/test.esp32-c3-idf.yaml b/tests/components/analog_threshold/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b5c14dfe56 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +sensor: + - platform: template + id: template_sensor + name: Template Sensor + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 15s + +binary_sensor: + - platform: analog_threshold + name: Analog Threshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Threshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: diff --git a/tests/components/analog_threshold/test.esp32-c3.yaml b/tests/components/analog_threshold/test.esp32-c3.yaml new file mode 100644 index 0000000000..b5c14dfe56 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32-c3.yaml @@ -0,0 +1,28 @@ +sensor: + - platform: template + id: template_sensor + name: Template Sensor + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 15s + +binary_sensor: + - platform: analog_threshold + name: Analog Threshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Threshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: diff --git a/tests/components/analog_threshold/test.esp32-idf.yaml b/tests/components/analog_threshold/test.esp32-idf.yaml new file mode 100644 index 0000000000..b5c14dfe56 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +sensor: + - platform: template + id: template_sensor + name: Template Sensor + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 15s + +binary_sensor: + - platform: analog_threshold + name: Analog Threshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Threshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: diff --git a/tests/components/analog_threshold/test.esp32.yaml b/tests/components/analog_threshold/test.esp32.yaml new file mode 100644 index 0000000000..b5c14dfe56 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32.yaml @@ -0,0 +1,28 @@ +sensor: + - platform: template + id: template_sensor + name: Template Sensor + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 15s + +binary_sensor: + - platform: analog_threshold + name: Analog Threshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Threshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: diff --git a/tests/components/analog_threshold/test.esp8266.yaml b/tests/components/analog_threshold/test.esp8266.yaml new file mode 100644 index 0000000000..b5c14dfe56 --- /dev/null +++ b/tests/components/analog_threshold/test.esp8266.yaml @@ -0,0 +1,28 @@ +sensor: + - platform: template + id: template_sensor + name: Template Sensor + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 15s + +binary_sensor: + - platform: analog_threshold + name: Analog Threshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Threshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: diff --git a/tests/components/analog_threshold/test.rp2040.yaml b/tests/components/analog_threshold/test.rp2040.yaml new file mode 100644 index 0000000000..b5c14dfe56 --- /dev/null +++ b/tests/components/analog_threshold/test.rp2040.yaml @@ -0,0 +1,28 @@ +sensor: + - platform: template + id: template_sensor + name: Template Sensor + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 15s + +binary_sensor: + - platform: analog_threshold + name: Analog Threshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Threshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9a415255ae --- /dev/null +++ b/tests/components/animation/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: no diff --git a/tests/components/animation/test.esp32-c3.yaml b/tests/components/animation/test.esp32-c3.yaml new file mode 100644 index 0000000000..9a415255ae --- /dev/null +++ b/tests/components/animation/test.esp32-c3.yaml @@ -0,0 +1,23 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: no diff --git a/tests/components/animation/test.esp32-idf.yaml b/tests/components/animation/test.esp32-idf.yaml new file mode 100644 index 0000000000..31b78eb980 --- /dev/null +++ b/tests/components/animation/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +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 + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: no diff --git a/tests/components/animation/test.esp32.yaml b/tests/components/animation/test.esp32.yaml new file mode 100644 index 0000000000..31b78eb980 --- /dev/null +++ b/tests/components/animation/test.esp32.yaml @@ -0,0 +1,23 @@ +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 + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: no diff --git a/tests/components/animation/test.esp8266.yaml b/tests/components/animation/test.esp8266.yaml new file mode 100644 index 0000000000..2bd441de99 --- /dev/null +++ b/tests/components/animation/test.esp8266.yaml @@ -0,0 +1,23 @@ +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: no diff --git a/tests/components/animation/test.rp2040.yaml b/tests/components/animation/test.rp2040.yaml new file mode 100644 index 0000000000..0f42c33687 --- /dev/null +++ b/tests/components/animation/test.rp2040.yaml @@ -0,0 +1,23 @@ +spi: + - id: spi_main_lcd + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 21 + reset_pin: 22 + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: no diff --git a/tests/components/anova/test.esp32-c3-idf.yaml b/tests/components/anova/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c4162fe71e --- /dev/null +++ b/tests/components/anova/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: anova_blec + +climate: + - platform: anova + name: Anova cooker + ble_client_id: anova_blec + unit_of_measurement: c diff --git a/tests/components/anova/test.esp32-c3.yaml b/tests/components/anova/test.esp32-c3.yaml new file mode 100644 index 0000000000..c4162fe71e --- /dev/null +++ b/tests/components/anova/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: anova_blec + +climate: + - platform: anova + name: Anova cooker + ble_client_id: anova_blec + unit_of_measurement: c diff --git a/tests/components/anova/test.esp32-idf.yaml b/tests/components/anova/test.esp32-idf.yaml new file mode 100644 index 0000000000..c4162fe71e --- /dev/null +++ b/tests/components/anova/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: anova_blec + +climate: + - platform: anova + name: Anova cooker + ble_client_id: anova_blec + unit_of_measurement: c diff --git a/tests/components/anova/test.esp32.yaml b/tests/components/anova/test.esp32.yaml new file mode 100644 index 0000000000..c4162fe71e --- /dev/null +++ b/tests/components/anova/test.esp32.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: anova_blec + +climate: + - platform: anova + name: Anova cooker + ble_client_id: anova_blec + unit_of_measurement: c diff --git a/tests/components/apds9960/test.esp32-c3-idf.yaml b/tests/components/apds9960/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.esp32-c3-idf.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp32-c3.yaml b/tests/components/apds9960/test.esp32-c3.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.esp32-c3.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp32-idf.yaml b/tests/components/apds9960/test.esp32-idf.yaml new file mode 100644 index 0000000000..7ff70a4d47 --- /dev/null +++ b/tests/components/apds9960/test.esp32-idf.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp32.yaml b/tests/components/apds9960/test.esp32.yaml new file mode 100644 index 0000000000..7ff70a4d47 --- /dev/null +++ b/tests/components/apds9960/test.esp32.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp8266.yaml b/tests/components/apds9960/test.esp8266.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.esp8266.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.rp2040.yaml b/tests/components/apds9960/test.rp2040.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.rp2040.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8e7ca0fb06 --- /dev/null +++ b/tests/components/api/test.esp32-c3-idf.yaml @@ -0,0 +1,50 @@ +wifi: + ssid: MySSID + password: password1 + +api: + port: 8000 + password: pwd + reboot_timeout: 0min + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= + services: + - service: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - service: empty_service + then: + - logger.log: Service Called + - service: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - service: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() diff --git a/tests/components/api/test.esp32-c3.yaml b/tests/components/api/test.esp32-c3.yaml new file mode 100644 index 0000000000..8e7ca0fb06 --- /dev/null +++ b/tests/components/api/test.esp32-c3.yaml @@ -0,0 +1,50 @@ +wifi: + ssid: MySSID + password: password1 + +api: + port: 8000 + password: pwd + reboot_timeout: 0min + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= + services: + - service: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - service: empty_service + then: + - logger.log: Service Called + - service: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - service: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() diff --git a/tests/components/api/test.esp32-idf.yaml b/tests/components/api/test.esp32-idf.yaml new file mode 100644 index 0000000000..8e7ca0fb06 --- /dev/null +++ b/tests/components/api/test.esp32-idf.yaml @@ -0,0 +1,50 @@ +wifi: + ssid: MySSID + password: password1 + +api: + port: 8000 + password: pwd + reboot_timeout: 0min + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= + services: + - service: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - service: empty_service + then: + - logger.log: Service Called + - service: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - service: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() diff --git a/tests/components/api/test.esp32.yaml b/tests/components/api/test.esp32.yaml new file mode 100644 index 0000000000..8e7ca0fb06 --- /dev/null +++ b/tests/components/api/test.esp32.yaml @@ -0,0 +1,50 @@ +wifi: + ssid: MySSID + password: password1 + +api: + port: 8000 + password: pwd + reboot_timeout: 0min + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= + services: + - service: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - service: empty_service + then: + - logger.log: Service Called + - service: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - service: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() diff --git a/tests/components/api/test.esp8266.yaml b/tests/components/api/test.esp8266.yaml new file mode 100644 index 0000000000..8e7ca0fb06 --- /dev/null +++ b/tests/components/api/test.esp8266.yaml @@ -0,0 +1,50 @@ +wifi: + ssid: MySSID + password: password1 + +api: + port: 8000 + password: pwd + reboot_timeout: 0min + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= + services: + - service: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - service: empty_service + then: + - logger.log: Service Called + - service: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - service: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() diff --git a/tests/components/api/test.rp2040.yaml b/tests/components/api/test.rp2040.yaml new file mode 100644 index 0000000000..8e7ca0fb06 --- /dev/null +++ b/tests/components/api/test.rp2040.yaml @@ -0,0 +1,50 @@ +wifi: + ssid: MySSID + password: password1 + +api: + port: 8000 + password: pwd + reboot_timeout: 0min + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= + services: + - service: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - service: empty_service + then: + - logger.log: Service Called + - service: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - service: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() diff --git a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml b/tests/components/as3935_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c72556dbac --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 6 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp32-c3.yaml b/tests/components/as3935_i2c/test.esp32-c3.yaml new file mode 100644 index 0000000000..c72556dbac --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32-c3.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 6 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp32-idf.yaml b/tests/components/as3935_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..fad703bee5 --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 16 + sda: 17 + +as3935_i2c: + irq_pin: 12 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp32.yaml b/tests/components/as3935_i2c/test.esp32.yaml new file mode 100644 index 0000000000..fad703bee5 --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 16 + sda: 17 + +as3935_i2c: + irq_pin: 12 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp8266.yaml b/tests/components/as3935_i2c/test.esp8266.yaml new file mode 100644 index 0000000000..adba9e440f --- /dev/null +++ b/tests/components/as3935_i2c/test.esp8266.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 15 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.rp2040.yaml b/tests/components/as3935_i2c/test.rp2040.yaml new file mode 100644 index 0000000000..c72556dbac --- /dev/null +++ b/tests/components/as3935_i2c/test.rp2040.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 6 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32-c3-idf.yaml b/tests/components/as3935_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7a4a01aeea --- /dev/null +++ b/tests/components/as3935_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +as3935_spi: + cs_pin: 2 + irq_pin: 3 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32-c3.yaml b/tests/components/as3935_spi/test.esp32-c3.yaml new file mode 100644 index 0000000000..7a4a01aeea --- /dev/null +++ b/tests/components/as3935_spi/test.esp32-c3.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +as3935_spi: + cs_pin: 2 + irq_pin: 3 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32-idf.yaml b/tests/components/as3935_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..813a39cb23 --- /dev/null +++ b/tests/components/as3935_spi/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +as3935_spi: + cs_pin: 12 + irq_pin: 13 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32.yaml b/tests/components/as3935_spi/test.esp32.yaml new file mode 100644 index 0000000000..813a39cb23 --- /dev/null +++ b/tests/components/as3935_spi/test.esp32.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +as3935_spi: + cs_pin: 12 + irq_pin: 13 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp8266.yaml b/tests/components/as3935_spi/test.esp8266.yaml new file mode 100644 index 0000000000..38a40b0833 --- /dev/null +++ b/tests/components/as3935_spi/test.esp8266.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +as3935_spi: + cs_pin: 15 + irq_pin: 16 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.rp2040.yaml b/tests/components/as3935_spi/test.rp2040.yaml new file mode 100644 index 0000000000..528759d97d --- /dev/null +++ b/tests/components/as3935_spi/test.rp2040.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +as3935_spi: + cs_pin: 6 + irq_pin: 7 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as5600/test.esp32-c3-idf.yaml b/tests/components/as5600/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e074fa5e0c --- /dev/null +++ b/tests/components/as5600/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 6 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp32-c3.yaml b/tests/components/as5600/test.esp32-c3.yaml new file mode 100644 index 0000000000..e074fa5e0c --- /dev/null +++ b/tests/components/as5600/test.esp32-c3.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 6 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp32-idf.yaml b/tests/components/as5600/test.esp32-idf.yaml new file mode 100644 index 0000000000..312ee9ad04 --- /dev/null +++ b/tests/components/as5600/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +as5600: + dir_pin: 12 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp32.yaml b/tests/components/as5600/test.esp32.yaml new file mode 100644 index 0000000000..312ee9ad04 --- /dev/null +++ b/tests/components/as5600/test.esp32.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +as5600: + dir_pin: 12 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp8266.yaml b/tests/components/as5600/test.esp8266.yaml new file mode 100644 index 0000000000..a232d27305 --- /dev/null +++ b/tests/components/as5600/test.esp8266.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 15 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.rp2040.yaml b/tests/components/as5600/test.rp2040.yaml new file mode 100644 index 0000000000..e074fa5e0c --- /dev/null +++ b/tests/components/as5600/test.rp2040.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 6 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as7341/test.esp32-c3-idf.yaml b/tests/components/as7341/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.esp32-c3-idf.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp32-c3.yaml b/tests/components/as7341/test.esp32-c3.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.esp32-c3.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp32-idf.yaml b/tests/components/as7341/test.esp32-idf.yaml new file mode 100644 index 0000000000..d582a367ac --- /dev/null +++ b/tests/components/as7341/test.esp32-idf.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp32.yaml b/tests/components/as7341/test.esp32.yaml new file mode 100644 index 0000000000..d582a367ac --- /dev/null +++ b/tests/components/as7341/test.esp32.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp8266.yaml b/tests/components/as7341/test.esp8266.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.esp8266.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.rp2040.yaml b/tests/components/as7341/test.rp2040.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.rp2040.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml b/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0248090c23 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: atc_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: ATC Temperature + humidity: + name: ATC Humidity + battery_level: + name: ATC Battery-Level + battery_voltage: + name: ATC Battery-Voltage diff --git a/tests/components/atc_mithermometer/test.esp32-c3.yaml b/tests/components/atc_mithermometer/test.esp32-c3.yaml new file mode 100644 index 0000000000..0248090c23 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32-c3.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: atc_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: ATC Temperature + humidity: + name: ATC Humidity + battery_level: + name: ATC Battery-Level + battery_voltage: + name: ATC Battery-Voltage diff --git a/tests/components/atc_mithermometer/test.esp32-idf.yaml b/tests/components/atc_mithermometer/test.esp32-idf.yaml new file mode 100644 index 0000000000..0248090c23 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: atc_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: ATC Temperature + humidity: + name: ATC Humidity + battery_level: + name: ATC Battery-Level + battery_voltage: + name: ATC Battery-Voltage diff --git a/tests/components/atc_mithermometer/test.esp32.yaml b/tests/components/atc_mithermometer/test.esp32.yaml new file mode 100644 index 0000000000..0248090c23 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: atc_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: ATC Temperature + humidity: + name: ATC Humidity + battery_level: + name: ATC Battery-Level + battery_voltage: + name: ATC Battery-Voltage diff --git a/tests/components/atm90e26/test.esp32-c3-idf.yaml b/tests/components/atm90e26/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ce123bcf72 --- /dev/null +++ b/tests/components/atm90e26/test.esp32-c3-idf.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e26 + cs_pin: 8 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp32-c3.yaml b/tests/components/atm90e26/test.esp32-c3.yaml new file mode 100644 index 0000000000..ce123bcf72 --- /dev/null +++ b/tests/components/atm90e26/test.esp32-c3.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e26 + cs_pin: 8 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp32-idf.yaml b/tests/components/atm90e26/test.esp32-idf.yaml new file mode 100644 index 0000000000..72fb3e5b24 --- /dev/null +++ b/tests/components/atm90e26/test.esp32-idf.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e26 + cs_pin: 13 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp32.yaml b/tests/components/atm90e26/test.esp32.yaml new file mode 100644 index 0000000000..72fb3e5b24 --- /dev/null +++ b/tests/components/atm90e26/test.esp32.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e26 + cs_pin: 13 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp8266.yaml b/tests/components/atm90e26/test.esp8266.yaml new file mode 100644 index 0000000000..68d63cc278 --- /dev/null +++ b/tests/components/atm90e26/test.esp8266.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: atm90e26 + cs_pin: 5 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.rp2040.yaml b/tests/components/atm90e26/test.rp2040.yaml new file mode 100644 index 0000000000..f43277dbb1 --- /dev/null +++ b/tests/components/atm90e26/test.rp2040.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: atm90e26 + cs_pin: 5 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e32/test.esp32-c3-idf.yaml b/tests/components/atm90e32/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..263fb6d24e --- /dev/null +++ b/tests/components/atm90e32/test.esp32-c3-idf.yaml @@ -0,0 +1,51 @@ +spi: + - id: spi_atm90e32 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e32 + cs_pin: 8 + 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_b: + 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 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X diff --git a/tests/components/atm90e32/test.esp32-c3.yaml b/tests/components/atm90e32/test.esp32-c3.yaml new file mode 100644 index 0000000000..263fb6d24e --- /dev/null +++ b/tests/components/atm90e32/test.esp32-c3.yaml @@ -0,0 +1,51 @@ +spi: + - id: spi_atm90e32 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e32 + cs_pin: 8 + 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_b: + 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 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X diff --git a/tests/components/atm90e32/test.esp32-idf.yaml b/tests/components/atm90e32/test.esp32-idf.yaml new file mode 100644 index 0000000000..131270f8ad --- /dev/null +++ b/tests/components/atm90e32/test.esp32-idf.yaml @@ -0,0 +1,51 @@ +spi: + - id: spi_atm90e32 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e32 + cs_pin: 13 + 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_b: + 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 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X diff --git a/tests/components/atm90e32/test.esp32.yaml b/tests/components/atm90e32/test.esp32.yaml new file mode 100644 index 0000000000..131270f8ad --- /dev/null +++ b/tests/components/atm90e32/test.esp32.yaml @@ -0,0 +1,51 @@ +spi: + - id: spi_atm90e32 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e32 + cs_pin: 13 + 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_b: + 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 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X diff --git a/tests/components/atm90e32/test.esp8266.yaml b/tests/components/atm90e32/test.esp8266.yaml new file mode 100644 index 0000000000..e8e2abc1a9 --- /dev/null +++ b/tests/components/atm90e32/test.esp8266.yaml @@ -0,0 +1,51 @@ +spi: + - id: spi_atm90e32 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: atm90e32 + cs_pin: 5 + 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_b: + 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 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X diff --git a/tests/components/atm90e32/test.rp2040.yaml b/tests/components/atm90e32/test.rp2040.yaml new file mode 100644 index 0000000000..525e0b801a --- /dev/null +++ b/tests/components/atm90e32/test.rp2040.yaml @@ -0,0 +1,51 @@ +spi: + - id: spi_atm90e32 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: atm90e32 + cs_pin: 5 + 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_b: + 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 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X From cfe16c92ee368f3a1180d5cb9398b8927c5ac12b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Feb 2024 13:07:37 -0600 Subject: [PATCH 323/436] Bump aioesphomeapi to 21.0.2 (#6188) --- requirements.txt | 3 ++- requirements_optional.txt | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 18e0295fb2..878d2e8a50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ async_timeout==4.0.3; python_version <= "3.10" +cryptography==42.0.2 voluptuous==0.14.1 PyYAML==6.0.1 paho-mqtt==1.6.1 @@ -12,7 +13,7 @@ platformio==6.1.13 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==21.0.1 +aioesphomeapi==21.0.2 zeroconf==0.131.0 python-magic==0.4.27 diff --git a/requirements_optional.txt b/requirements_optional.txt index 54494b4585..c984d41332 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,3 +1,2 @@ pillow==10.2.0 cairosvg==2.7.1 -cryptography==41.0.4 From 05da0fb4cf109a8a76b5221c439e998b56081925 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 6 Feb 2024 13:32:40 -0600 Subject: [PATCH 324/436] Add some components to the new testing framework (B) (#6173) --- .../b_parasite/test.esp32-c3-idf.yaml | 15 +++++ .../components/b_parasite/test.esp32-c3.yaml | 15 +++++ .../components/b_parasite/test.esp32-idf.yaml | 15 +++++ tests/components/b_parasite/test.esp32.yaml | 15 +++++ tests/components/ballu/test.esp32.yaml | 12 ++++ tests/components/ballu/test.esp8266.yaml | 12 ++++ .../bang_bang/test.esp32-c3-idf.yaml | 35 +++++++++++ tests/components/bang_bang/test.esp32-c3.yaml | 35 +++++++++++ .../components/bang_bang/test.esp32-idf.yaml | 35 +++++++++++ tests/components/bang_bang/test.esp32.yaml | 35 +++++++++++ tests/components/bang_bang/test.esp8266.yaml | 35 +++++++++++ tests/components/bang_bang/test.rp2040.yaml | 35 +++++++++++ .../components/bedjet/test.esp32-c3-idf.yaml | 33 ++++++++++ tests/components/bedjet/test.esp32-c3.yaml | 33 ++++++++++ tests/components/bedjet/test.esp32-idf.yaml | 33 ++++++++++ tests/components/bedjet/test.esp32.yaml | 33 ++++++++++ .../components/bh1750/test.esp32-c3-idf.yaml | 10 +++ tests/components/bh1750/test.esp32-c3.yaml | 10 +++ tests/components/bh1750/test.esp32-idf.yaml | 10 +++ tests/components/bh1750/test.esp32.yaml | 10 +++ tests/components/bh1750/test.esp8266.yaml | 10 +++ tests/components/bh1750/test.rp2040.yaml | 10 +++ .../binary_sensor_map/test.esp32-c3-idf.yaml | 61 +++++++++++++++++++ .../binary_sensor_map/test.esp32-c3.yaml | 61 +++++++++++++++++++ .../binary_sensor_map/test.esp32-idf.yaml | 61 +++++++++++++++++++ .../binary_sensor_map/test.esp32.yaml | 61 +++++++++++++++++++ .../binary_sensor_map/test.esp8266.yaml | 61 +++++++++++++++++++ .../binary_sensor_map/test.rp2040.yaml | 61 +++++++++++++++++++ .../components/bl0939/test.esp32-c3-idf.yaml | 26 ++++++++ tests/components/bl0939/test.esp32-c3.yaml | 26 ++++++++ tests/components/bl0939/test.esp32-idf.yaml | 26 ++++++++ tests/components/bl0939/test.esp32.yaml | 26 ++++++++ tests/components/bl0939/test.esp8266.yaml | 26 ++++++++ tests/components/bl0939/test.rp2040.yaml | 26 ++++++++ .../components/bl0940/test.esp32-c3-idf.yaml | 22 +++++++ tests/components/bl0940/test.esp32-c3.yaml | 22 +++++++ tests/components/bl0940/test.esp32-idf.yaml | 22 +++++++ tests/components/bl0940/test.esp32.yaml | 22 +++++++ tests/components/bl0940/test.esp8266.yaml | 22 +++++++ tests/components/bl0940/test.rp2040.yaml | 22 +++++++ .../components/bl0942/test.esp32-c3-idf.yaml | 20 ++++++ tests/components/bl0942/test.esp32-c3.yaml | 20 ++++++ tests/components/bl0942/test.esp32-idf.yaml | 20 ++++++ tests/components/bl0942/test.esp32.yaml | 20 ++++++ tests/components/bl0942/test.esp8266.yaml | 20 ++++++ tests/components/bl0942/test.rp2040.yaml | 20 ++++++ .../ble_client/test.esp32-c3-idf.yaml | 5 ++ .../components/ble_client/test.esp32-c3.yaml | 5 ++ .../components/ble_client/test.esp32-idf.yaml | 5 ++ tests/components/ble_client/test.esp32.yaml | 5 ++ .../ble_presence/test.esp32-c3-idf.yaml | 20 ++++++ .../ble_presence/test.esp32-c3.yaml | 20 ++++++ .../ble_presence/test.esp32-idf.yaml | 20 ++++++ tests/components/ble_presence/test.esp32.yaml | 20 ++++++ .../ble_rssi/test.esp32-c3-idf.yaml | 18 ++++++ tests/components/ble_rssi/test.esp32-c3.yaml | 18 ++++++ tests/components/ble_rssi/test.esp32-idf.yaml | 18 ++++++ tests/components/ble_rssi/test.esp32.yaml | 18 ++++++ .../ble_scanner/test.esp32-c3-idf.yaml | 5 ++ .../components/ble_scanner/test.esp32-c3.yaml | 5 ++ .../ble_scanner/test.esp32-idf.yaml | 5 ++ tests/components/ble_scanner/test.esp32.yaml | 5 ++ .../bme280_i2c/test.esp32-c3-idf.yaml | 18 ++++++ .../components/bme280_i2c/test.esp32-c3.yaml | 18 ++++++ .../components/bme280_i2c/test.esp32-idf.yaml | 18 ++++++ tests/components/bme280_i2c/test.esp32.yaml | 18 ++++++ tests/components/bme280_i2c/test.esp8266.yaml | 18 ++++++ tests/components/bme280_i2c/test.rp2040.yaml | 18 ++++++ .../bme280_spi/test.esp32-c3-idf.yaml | 19 ++++++ .../components/bme280_spi/test.esp32-c3.yaml | 19 ++++++ .../components/bme280_spi/test.esp32-idf.yaml | 19 ++++++ tests/components/bme280_spi/test.esp32.yaml | 19 ++++++ tests/components/bme280_spi/test.esp8266.yaml | 19 ++++++ tests/components/bme280_spi/test.rp2040.yaml | 19 ++++++ .../components/bme680/test.esp32-c3-idf.yaml | 21 +++++++ tests/components/bme680/test.esp32-c3.yaml | 21 +++++++ tests/components/bme680/test.esp32-idf.yaml | 21 +++++++ tests/components/bme680/test.esp32.yaml | 21 +++++++ tests/components/bme680/test.esp8266.yaml | 21 +++++++ tests/components/bme680/test.rp2040.yaml | 21 +++++++ tests/components/bme680_bsec/test.esp32.yaml | 29 +++++++++ .../components/bme680_bsec/test.esp8266.yaml | 29 +++++++++ .../components/bmi160/test.esp32-c3-idf.yaml | 22 +++++++ tests/components/bmi160/test.esp32-c3.yaml | 22 +++++++ tests/components/bmi160/test.esp32-idf.yaml | 22 +++++++ tests/components/bmi160/test.esp32.yaml | 22 +++++++ tests/components/bmi160/test.esp8266.yaml | 22 +++++++ tests/components/bmi160/test.rp2040.yaml | 22 +++++++ .../components/bmp085/test.esp32-c3-idf.yaml | 15 +++++ tests/components/bmp085/test.esp32-c3.yaml | 15 +++++ tests/components/bmp085/test.esp32-idf.yaml | 15 +++++ tests/components/bmp085/test.esp32.yaml | 15 +++++ tests/components/bmp085/test.esp8266.yaml | 15 +++++ tests/components/bmp085/test.rp2040.yaml | 15 +++++ .../components/bmp280/test.esp32-c3-idf.yaml | 15 +++++ tests/components/bmp280/test.esp32-c3.yaml | 15 +++++ tests/components/bmp280/test.esp32-idf.yaml | 15 +++++ tests/components/bmp280/test.esp32.yaml | 15 +++++ tests/components/bmp280/test.esp8266.yaml | 15 +++++ tests/components/bmp280/test.rp2040.yaml | 15 +++++ .../components/bmp3xx/test.esp32-c3-idf.yaml | 14 +++++ tests/components/bmp3xx/test.esp32-c3.yaml | 14 +++++ tests/components/bmp3xx/test.esp32-idf.yaml | 14 +++++ tests/components/bmp3xx/test.esp32.yaml | 14 +++++ tests/components/bmp3xx/test.esp8266.yaml | 14 +++++ tests/components/bmp3xx/test.rp2040.yaml | 14 +++++ .../components/bmp581/test.esp32-c3-idf.yaml | 13 ++++ tests/components/bmp581/test.esp32-c3.yaml | 13 ++++ tests/components/bmp581/test.esp32-idf.yaml | 13 ++++ tests/components/bmp581/test.esp32.yaml | 13 ++++ tests/components/bmp581/test.esp8266.yaml | 13 ++++ tests/components/bmp581/test.rp2040.yaml | 13 ++++ .../bp1658cj/test.esp32-c3-idf.yaml | 22 +++++++ tests/components/bp1658cj/test.esp32-c3.yaml | 22 +++++++ tests/components/bp1658cj/test.esp32-idf.yaml | 22 +++++++ tests/components/bp1658cj/test.esp32.yaml | 22 +++++++ tests/components/bp1658cj/test.esp8266.yaml | 22 +++++++ tests/components/bp1658cj/test.rp2040.yaml | 22 +++++++ .../components/bp5758d/test.esp32-c3-idf.yaml | 25 ++++++++ tests/components/bp5758d/test.esp32-c3.yaml | 25 ++++++++ tests/components/bp5758d/test.esp32-idf.yaml | 25 ++++++++ tests/components/bp5758d/test.esp32.yaml | 25 ++++++++ tests/components/bp5758d/test.esp8266.yaml | 25 ++++++++ tests/components/bp5758d/test.rp2040.yaml | 25 ++++++++ .../components/button/test.esp32-c3-idf.yaml | 6 ++ tests/components/button/test.esp32-c3.yaml | 6 ++ tests/components/button/test.esp32-idf.yaml | 6 ++ tests/components/button/test.esp32.yaml | 6 ++ tests/components/button/test.esp8266.yaml | 6 ++ tests/components/button/test.rp2040.yaml | 6 ++ 130 files changed, 2650 insertions(+) create mode 100644 tests/components/b_parasite/test.esp32-c3-idf.yaml create mode 100644 tests/components/b_parasite/test.esp32-c3.yaml create mode 100644 tests/components/b_parasite/test.esp32-idf.yaml create mode 100644 tests/components/b_parasite/test.esp32.yaml create mode 100644 tests/components/ballu/test.esp32.yaml create mode 100644 tests/components/ballu/test.esp8266.yaml create mode 100644 tests/components/bang_bang/test.esp32-c3-idf.yaml create mode 100644 tests/components/bang_bang/test.esp32-c3.yaml create mode 100644 tests/components/bang_bang/test.esp32-idf.yaml create mode 100644 tests/components/bang_bang/test.esp32.yaml create mode 100644 tests/components/bang_bang/test.esp8266.yaml create mode 100644 tests/components/bang_bang/test.rp2040.yaml create mode 100644 tests/components/bedjet/test.esp32-c3-idf.yaml create mode 100644 tests/components/bedjet/test.esp32-c3.yaml create mode 100644 tests/components/bedjet/test.esp32-idf.yaml create mode 100644 tests/components/bedjet/test.esp32.yaml create mode 100644 tests/components/bh1750/test.esp32-c3-idf.yaml create mode 100644 tests/components/bh1750/test.esp32-c3.yaml create mode 100644 tests/components/bh1750/test.esp32-idf.yaml create mode 100644 tests/components/bh1750/test.esp32.yaml create mode 100644 tests/components/bh1750/test.esp8266.yaml create mode 100644 tests/components/bh1750/test.rp2040.yaml create mode 100644 tests/components/binary_sensor_map/test.esp32-c3-idf.yaml create mode 100644 tests/components/binary_sensor_map/test.esp32-c3.yaml create mode 100644 tests/components/binary_sensor_map/test.esp32-idf.yaml create mode 100644 tests/components/binary_sensor_map/test.esp32.yaml create mode 100644 tests/components/binary_sensor_map/test.esp8266.yaml create mode 100644 tests/components/binary_sensor_map/test.rp2040.yaml create mode 100644 tests/components/bl0939/test.esp32-c3-idf.yaml create mode 100644 tests/components/bl0939/test.esp32-c3.yaml create mode 100644 tests/components/bl0939/test.esp32-idf.yaml create mode 100644 tests/components/bl0939/test.esp32.yaml create mode 100644 tests/components/bl0939/test.esp8266.yaml create mode 100644 tests/components/bl0939/test.rp2040.yaml create mode 100644 tests/components/bl0940/test.esp32-c3-idf.yaml create mode 100644 tests/components/bl0940/test.esp32-c3.yaml create mode 100644 tests/components/bl0940/test.esp32-idf.yaml create mode 100644 tests/components/bl0940/test.esp32.yaml create mode 100644 tests/components/bl0940/test.esp8266.yaml create mode 100644 tests/components/bl0940/test.rp2040.yaml create mode 100644 tests/components/bl0942/test.esp32-c3-idf.yaml create mode 100644 tests/components/bl0942/test.esp32-c3.yaml create mode 100644 tests/components/bl0942/test.esp32-idf.yaml create mode 100644 tests/components/bl0942/test.esp32.yaml create mode 100644 tests/components/bl0942/test.esp8266.yaml create mode 100644 tests/components/bl0942/test.rp2040.yaml create mode 100644 tests/components/ble_client/test.esp32-c3-idf.yaml create mode 100644 tests/components/ble_client/test.esp32-c3.yaml create mode 100644 tests/components/ble_client/test.esp32-idf.yaml create mode 100644 tests/components/ble_client/test.esp32.yaml create mode 100644 tests/components/ble_presence/test.esp32-c3-idf.yaml create mode 100644 tests/components/ble_presence/test.esp32-c3.yaml create mode 100644 tests/components/ble_presence/test.esp32-idf.yaml create mode 100644 tests/components/ble_presence/test.esp32.yaml create mode 100644 tests/components/ble_rssi/test.esp32-c3-idf.yaml create mode 100644 tests/components/ble_rssi/test.esp32-c3.yaml create mode 100644 tests/components/ble_rssi/test.esp32-idf.yaml create mode 100644 tests/components/ble_rssi/test.esp32.yaml create mode 100644 tests/components/ble_scanner/test.esp32-c3-idf.yaml create mode 100644 tests/components/ble_scanner/test.esp32-c3.yaml create mode 100644 tests/components/ble_scanner/test.esp32-idf.yaml create mode 100644 tests/components/ble_scanner/test.esp32.yaml create mode 100644 tests/components/bme280_i2c/test.esp32-c3-idf.yaml create mode 100644 tests/components/bme280_i2c/test.esp32-c3.yaml create mode 100644 tests/components/bme280_i2c/test.esp32-idf.yaml create mode 100644 tests/components/bme280_i2c/test.esp32.yaml create mode 100644 tests/components/bme280_i2c/test.esp8266.yaml create mode 100644 tests/components/bme280_i2c/test.rp2040.yaml create mode 100644 tests/components/bme280_spi/test.esp32-c3-idf.yaml create mode 100644 tests/components/bme280_spi/test.esp32-c3.yaml create mode 100644 tests/components/bme280_spi/test.esp32-idf.yaml create mode 100644 tests/components/bme280_spi/test.esp32.yaml create mode 100644 tests/components/bme280_spi/test.esp8266.yaml create mode 100644 tests/components/bme280_spi/test.rp2040.yaml create mode 100644 tests/components/bme680/test.esp32-c3-idf.yaml create mode 100644 tests/components/bme680/test.esp32-c3.yaml create mode 100644 tests/components/bme680/test.esp32-idf.yaml create mode 100644 tests/components/bme680/test.esp32.yaml create mode 100644 tests/components/bme680/test.esp8266.yaml create mode 100644 tests/components/bme680/test.rp2040.yaml create mode 100644 tests/components/bme680_bsec/test.esp32.yaml create mode 100644 tests/components/bme680_bsec/test.esp8266.yaml create mode 100644 tests/components/bmi160/test.esp32-c3-idf.yaml create mode 100644 tests/components/bmi160/test.esp32-c3.yaml create mode 100644 tests/components/bmi160/test.esp32-idf.yaml create mode 100644 tests/components/bmi160/test.esp32.yaml create mode 100644 tests/components/bmi160/test.esp8266.yaml create mode 100644 tests/components/bmi160/test.rp2040.yaml create mode 100644 tests/components/bmp085/test.esp32-c3-idf.yaml create mode 100644 tests/components/bmp085/test.esp32-c3.yaml create mode 100644 tests/components/bmp085/test.esp32-idf.yaml create mode 100644 tests/components/bmp085/test.esp32.yaml create mode 100644 tests/components/bmp085/test.esp8266.yaml create mode 100644 tests/components/bmp085/test.rp2040.yaml create mode 100644 tests/components/bmp280/test.esp32-c3-idf.yaml create mode 100644 tests/components/bmp280/test.esp32-c3.yaml create mode 100644 tests/components/bmp280/test.esp32-idf.yaml create mode 100644 tests/components/bmp280/test.esp32.yaml create mode 100644 tests/components/bmp280/test.esp8266.yaml create mode 100644 tests/components/bmp280/test.rp2040.yaml create mode 100644 tests/components/bmp3xx/test.esp32-c3-idf.yaml create mode 100644 tests/components/bmp3xx/test.esp32-c3.yaml create mode 100644 tests/components/bmp3xx/test.esp32-idf.yaml create mode 100644 tests/components/bmp3xx/test.esp32.yaml create mode 100644 tests/components/bmp3xx/test.esp8266.yaml create mode 100644 tests/components/bmp3xx/test.rp2040.yaml create mode 100644 tests/components/bmp581/test.esp32-c3-idf.yaml create mode 100644 tests/components/bmp581/test.esp32-c3.yaml create mode 100644 tests/components/bmp581/test.esp32-idf.yaml create mode 100644 tests/components/bmp581/test.esp32.yaml create mode 100644 tests/components/bmp581/test.esp8266.yaml create mode 100644 tests/components/bmp581/test.rp2040.yaml create mode 100644 tests/components/bp1658cj/test.esp32-c3-idf.yaml create mode 100644 tests/components/bp1658cj/test.esp32-c3.yaml create mode 100644 tests/components/bp1658cj/test.esp32-idf.yaml create mode 100644 tests/components/bp1658cj/test.esp32.yaml create mode 100644 tests/components/bp1658cj/test.esp8266.yaml create mode 100644 tests/components/bp1658cj/test.rp2040.yaml create mode 100644 tests/components/bp5758d/test.esp32-c3-idf.yaml create mode 100644 tests/components/bp5758d/test.esp32-c3.yaml create mode 100644 tests/components/bp5758d/test.esp32-idf.yaml create mode 100644 tests/components/bp5758d/test.esp32.yaml create mode 100644 tests/components/bp5758d/test.esp8266.yaml create mode 100644 tests/components/bp5758d/test.rp2040.yaml create mode 100644 tests/components/button/test.esp32-c3-idf.yaml create mode 100644 tests/components/button/test.esp32-c3.yaml create mode 100644 tests/components/button/test.esp32-idf.yaml create mode 100644 tests/components/button/test.esp32.yaml create mode 100644 tests/components/button/test.esp8266.yaml create mode 100644 tests/components/button/test.rp2040.yaml diff --git a/tests/components/b_parasite/test.esp32-c3-idf.yaml b/tests/components/b_parasite/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..262e891bb2 --- /dev/null +++ b/tests/components/b_parasite/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: b_parasite + mac_address: F0:CA:F0:CA:01:01 + humidity: + name: b-parasite Air Humidity + temperature: + name: b-parasite Air Temperature + moisture: + name: b-parasite Soil Moisture + battery_voltage: + name: b-parasite Battery Voltage + illuminance: + name: b-parasite Illuminance diff --git a/tests/components/b_parasite/test.esp32-c3.yaml b/tests/components/b_parasite/test.esp32-c3.yaml new file mode 100644 index 0000000000..262e891bb2 --- /dev/null +++ b/tests/components/b_parasite/test.esp32-c3.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: b_parasite + mac_address: F0:CA:F0:CA:01:01 + humidity: + name: b-parasite Air Humidity + temperature: + name: b-parasite Air Temperature + moisture: + name: b-parasite Soil Moisture + battery_voltage: + name: b-parasite Battery Voltage + illuminance: + name: b-parasite Illuminance diff --git a/tests/components/b_parasite/test.esp32-idf.yaml b/tests/components/b_parasite/test.esp32-idf.yaml new file mode 100644 index 0000000000..262e891bb2 --- /dev/null +++ b/tests/components/b_parasite/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: b_parasite + mac_address: F0:CA:F0:CA:01:01 + humidity: + name: b-parasite Air Humidity + temperature: + name: b-parasite Air Temperature + moisture: + name: b-parasite Soil Moisture + battery_voltage: + name: b-parasite Battery Voltage + illuminance: + name: b-parasite Illuminance diff --git a/tests/components/b_parasite/test.esp32.yaml b/tests/components/b_parasite/test.esp32.yaml new file mode 100644 index 0000000000..262e891bb2 --- /dev/null +++ b/tests/components/b_parasite/test.esp32.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: b_parasite + mac_address: F0:CA:F0:CA:01:01 + humidity: + name: b-parasite Air Humidity + temperature: + name: b-parasite Air Temperature + moisture: + name: b-parasite Soil Moisture + battery_voltage: + name: b-parasite Battery Voltage + illuminance: + name: b-parasite Illuminance diff --git a/tests/components/ballu/test.esp32.yaml b/tests/components/ballu/test.esp32.yaml new file mode 100644 index 0000000000..bb7b9b0435 --- /dev/null +++ b/tests/components/ballu/test.esp32.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: ballu + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/ballu/test.esp8266.yaml b/tests/components/ballu/test.esp8266.yaml new file mode 100644 index 0000000000..05aa446739 --- /dev/null +++ b/tests/components/ballu/test.esp8266.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: ballu + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/bang_bang/test.esp32-c3-idf.yaml b/tests/components/bang_bang/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5882025191 --- /dev/null +++ b/tests/components/bang_bang/test.esp32-c3-idf.yaml @@ -0,0 +1,35 @@ +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: bang_bang + name: Bang Bang Climate + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: template_switch1 + cool_action: + - switch.turn_on: template_switch2 + heat_action: + - switch.turn_on: template_switch1 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C diff --git a/tests/components/bang_bang/test.esp32-c3.yaml b/tests/components/bang_bang/test.esp32-c3.yaml new file mode 100644 index 0000000000..5882025191 --- /dev/null +++ b/tests/components/bang_bang/test.esp32-c3.yaml @@ -0,0 +1,35 @@ +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: bang_bang + name: Bang Bang Climate + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: template_switch1 + cool_action: + - switch.turn_on: template_switch2 + heat_action: + - switch.turn_on: template_switch1 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C diff --git a/tests/components/bang_bang/test.esp32-idf.yaml b/tests/components/bang_bang/test.esp32-idf.yaml new file mode 100644 index 0000000000..5882025191 --- /dev/null +++ b/tests/components/bang_bang/test.esp32-idf.yaml @@ -0,0 +1,35 @@ +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: bang_bang + name: Bang Bang Climate + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: template_switch1 + cool_action: + - switch.turn_on: template_switch2 + heat_action: + - switch.turn_on: template_switch1 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C diff --git a/tests/components/bang_bang/test.esp32.yaml b/tests/components/bang_bang/test.esp32.yaml new file mode 100644 index 0000000000..5882025191 --- /dev/null +++ b/tests/components/bang_bang/test.esp32.yaml @@ -0,0 +1,35 @@ +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: bang_bang + name: Bang Bang Climate + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: template_switch1 + cool_action: + - switch.turn_on: template_switch2 + heat_action: + - switch.turn_on: template_switch1 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C diff --git a/tests/components/bang_bang/test.esp8266.yaml b/tests/components/bang_bang/test.esp8266.yaml new file mode 100644 index 0000000000..5882025191 --- /dev/null +++ b/tests/components/bang_bang/test.esp8266.yaml @@ -0,0 +1,35 @@ +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: bang_bang + name: Bang Bang Climate + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: template_switch1 + cool_action: + - switch.turn_on: template_switch2 + heat_action: + - switch.turn_on: template_switch1 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C diff --git a/tests/components/bang_bang/test.rp2040.yaml b/tests/components/bang_bang/test.rp2040.yaml new file mode 100644 index 0000000000..5882025191 --- /dev/null +++ b/tests/components/bang_bang/test.rp2040.yaml @@ -0,0 +1,35 @@ +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: bang_bang + name: Bang Bang Climate + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: template_switch1 + cool_action: + - switch.turn_on: template_switch2 + heat_action: + - switch.turn_on: template_switch1 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C diff --git a/tests/components/bedjet/test.esp32-c3-idf.yaml b/tests/components/bedjet/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c2be04a49a --- /dev/null +++ b/tests/components/bedjet/test.esp32-c3-idf.yaml @@ -0,0 +1,33 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: bedjet_blec + +bedjet: + - id: bedjet_hub + ble_client_id: bedjet_blec + time_id: sntp_time + +climate: + - platform: bedjet + name: My Bedjet + bedjet_id: bedjet_hub + heat_mode: extended + +fan: + - platform: bedjet + name: My Bedjet fan + bedjet_id: bedjet_hub diff --git a/tests/components/bedjet/test.esp32-c3.yaml b/tests/components/bedjet/test.esp32-c3.yaml new file mode 100644 index 0000000000..c2be04a49a --- /dev/null +++ b/tests/components/bedjet/test.esp32-c3.yaml @@ -0,0 +1,33 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: bedjet_blec + +bedjet: + - id: bedjet_hub + ble_client_id: bedjet_blec + time_id: sntp_time + +climate: + - platform: bedjet + name: My Bedjet + bedjet_id: bedjet_hub + heat_mode: extended + +fan: + - platform: bedjet + name: My Bedjet fan + bedjet_id: bedjet_hub diff --git a/tests/components/bedjet/test.esp32-idf.yaml b/tests/components/bedjet/test.esp32-idf.yaml new file mode 100644 index 0000000000..c2be04a49a --- /dev/null +++ b/tests/components/bedjet/test.esp32-idf.yaml @@ -0,0 +1,33 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: bedjet_blec + +bedjet: + - id: bedjet_hub + ble_client_id: bedjet_blec + time_id: sntp_time + +climate: + - platform: bedjet + name: My Bedjet + bedjet_id: bedjet_hub + heat_mode: extended + +fan: + - platform: bedjet + name: My Bedjet fan + bedjet_id: bedjet_hub diff --git a/tests/components/bedjet/test.esp32.yaml b/tests/components/bedjet/test.esp32.yaml new file mode 100644 index 0000000000..c2be04a49a --- /dev/null +++ b/tests/components/bedjet/test.esp32.yaml @@ -0,0 +1,33 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: bedjet_blec + +bedjet: + - id: bedjet_hub + ble_client_id: bedjet_blec + time_id: sntp_time + +climate: + - platform: bedjet + name: My Bedjet + bedjet_id: bedjet_hub + heat_mode: extended + +fan: + - platform: bedjet + name: My Bedjet fan + bedjet_id: bedjet_hub diff --git a/tests/components/bh1750/test.esp32-c3-idf.yaml b/tests/components/bh1750/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp32-c3.yaml b/tests/components/bh1750/test.esp32-c3.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.esp32-c3.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp32-idf.yaml b/tests/components/bh1750/test.esp32-idf.yaml new file mode 100644 index 0000000000..b10ec231ae --- /dev/null +++ b/tests/components/bh1750/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 16 + sda: 17 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp32.yaml b/tests/components/bh1750/test.esp32.yaml new file mode 100644 index 0000000000..b10ec231ae --- /dev/null +++ b/tests/components/bh1750/test.esp32.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 16 + sda: 17 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp8266.yaml b/tests/components/bh1750/test.esp8266.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.esp8266.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.rp2040.yaml b/tests/components/bh1750/test.rp2040.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.rp2040.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml b/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8ffdd1f379 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml @@ -0,0 +1,61 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + - platform: template + id: bin2 + lambda: |- + if (millis() > 20000) { + return true; + } else { + return false; + } + - platform: template + id: bin3 + lambda: |- + if (millis() > 30000) { + return true; + } else { + return false; + } + +sensor: + - platform: binary_sensor_map + name: Binary Sensor Map + type: group + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 diff --git a/tests/components/binary_sensor_map/test.esp32-c3.yaml b/tests/components/binary_sensor_map/test.esp32-c3.yaml new file mode 100644 index 0000000000..8ffdd1f379 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32-c3.yaml @@ -0,0 +1,61 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + - platform: template + id: bin2 + lambda: |- + if (millis() > 20000) { + return true; + } else { + return false; + } + - platform: template + id: bin3 + lambda: |- + if (millis() > 30000) { + return true; + } else { + return false; + } + +sensor: + - platform: binary_sensor_map + name: Binary Sensor Map + type: group + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 diff --git a/tests/components/binary_sensor_map/test.esp32-idf.yaml b/tests/components/binary_sensor_map/test.esp32-idf.yaml new file mode 100644 index 0000000000..8ffdd1f379 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32-idf.yaml @@ -0,0 +1,61 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + - platform: template + id: bin2 + lambda: |- + if (millis() > 20000) { + return true; + } else { + return false; + } + - platform: template + id: bin3 + lambda: |- + if (millis() > 30000) { + return true; + } else { + return false; + } + +sensor: + - platform: binary_sensor_map + name: Binary Sensor Map + type: group + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 diff --git a/tests/components/binary_sensor_map/test.esp32.yaml b/tests/components/binary_sensor_map/test.esp32.yaml new file mode 100644 index 0000000000..8ffdd1f379 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32.yaml @@ -0,0 +1,61 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + - platform: template + id: bin2 + lambda: |- + if (millis() > 20000) { + return true; + } else { + return false; + } + - platform: template + id: bin3 + lambda: |- + if (millis() > 30000) { + return true; + } else { + return false; + } + +sensor: + - platform: binary_sensor_map + name: Binary Sensor Map + type: group + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 diff --git a/tests/components/binary_sensor_map/test.esp8266.yaml b/tests/components/binary_sensor_map/test.esp8266.yaml new file mode 100644 index 0000000000..8ffdd1f379 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp8266.yaml @@ -0,0 +1,61 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + - platform: template + id: bin2 + lambda: |- + if (millis() > 20000) { + return true; + } else { + return false; + } + - platform: template + id: bin3 + lambda: |- + if (millis() > 30000) { + return true; + } else { + return false; + } + +sensor: + - platform: binary_sensor_map + name: Binary Sensor Map + type: group + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 diff --git a/tests/components/binary_sensor_map/test.rp2040.yaml b/tests/components/binary_sensor_map/test.rp2040.yaml new file mode 100644 index 0000000000..8ffdd1f379 --- /dev/null +++ b/tests/components/binary_sensor_map/test.rp2040.yaml @@ -0,0 +1,61 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + - platform: template + id: bin2 + lambda: |- + if (millis() > 20000) { + return true; + } else { + return false; + } + - platform: template + id: bin3 + lambda: |- + if (millis() > 30000) { + return true; + } else { + return false; + } + +sensor: + - platform: binary_sensor_map + name: Binary Sensor Map + type: group + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 diff --git a/tests/components/bl0939/test.esp32-c3-idf.yaml b/tests/components/bl0939/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.esp32-c3-idf.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp32-c3.yaml b/tests/components/bl0939/test.esp32-c3.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.esp32-c3.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp32-idf.yaml b/tests/components/bl0939/test.esp32-idf.yaml new file mode 100644 index 0000000000..df0e683b2f --- /dev/null +++ b/tests/components/bl0939/test.esp32-idf.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp32.yaml b/tests/components/bl0939/test.esp32.yaml new file mode 100644 index 0000000000..df0e683b2f --- /dev/null +++ b/tests/components/bl0939/test.esp32.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp8266.yaml b/tests/components/bl0939/test.esp8266.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.esp8266.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.rp2040.yaml b/tests/components/bl0939/test.rp2040.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.rp2040.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0940/test.esp32-c3-idf.yaml b/tests/components/bl0940/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp32-c3.yaml b/tests/components/bl0940/test.esp32-c3.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.esp32-c3.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp32-idf.yaml b/tests/components/bl0940/test.esp32-idf.yaml new file mode 100644 index 0000000000..c7d97ca3b9 --- /dev/null +++ b/tests/components/bl0940/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp32.yaml b/tests/components/bl0940/test.esp32.yaml new file mode 100644 index 0000000000..c7d97ca3b9 --- /dev/null +++ b/tests/components/bl0940/test.esp32.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp8266.yaml b/tests/components/bl0940/test.esp8266.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.esp8266.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.rp2040.yaml b/tests/components/bl0940/test.rp2040.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.rp2040.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0942/test.esp32-c3-idf.yaml b/tests/components/bl0942/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8d16efed4f --- /dev/null +++ b/tests/components/bl0942/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp32-c3.yaml b/tests/components/bl0942/test.esp32-c3.yaml new file mode 100644 index 0000000000..8d16efed4f --- /dev/null +++ b/tests/components/bl0942/test.esp32-c3.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp32-idf.yaml b/tests/components/bl0942/test.esp32-idf.yaml new file mode 100644 index 0000000000..45ac85aa2a --- /dev/null +++ b/tests/components/bl0942/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp32.yaml b/tests/components/bl0942/test.esp32.yaml new file mode 100644 index 0000000000..45ac85aa2a --- /dev/null +++ b/tests/components/bl0942/test.esp32.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp8266.yaml b/tests/components/bl0942/test.esp8266.yaml new file mode 100644 index 0000000000..8d16efed4f --- /dev/null +++ b/tests/components/bl0942/test.esp8266.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.rp2040.yaml b/tests/components/bl0942/test.rp2040.yaml new file mode 100644 index 0000000000..8d16efed4f --- /dev/null +++ b/tests/components/bl0942/test.rp2040.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/ble_client/test.esp32-c3-idf.yaml b/tests/components/ble_client/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b5272d01f0 --- /dev/null +++ b/tests/components/ble_client/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: test_blec diff --git a/tests/components/ble_client/test.esp32-c3.yaml b/tests/components/ble_client/test.esp32-c3.yaml new file mode 100644 index 0000000000..b5272d01f0 --- /dev/null +++ b/tests/components/ble_client/test.esp32-c3.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: test_blec diff --git a/tests/components/ble_client/test.esp32-idf.yaml b/tests/components/ble_client/test.esp32-idf.yaml new file mode 100644 index 0000000000..b5272d01f0 --- /dev/null +++ b/tests/components/ble_client/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: test_blec diff --git a/tests/components/ble_client/test.esp32.yaml b/tests/components/ble_client/test.esp32.yaml new file mode 100644 index 0000000000..b5272d01f0 --- /dev/null +++ b/tests/components/ble_client/test.esp32.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: test_blec diff --git a/tests/components/ble_presence/test.esp32-c3-idf.yaml b/tests/components/ble_presence/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dde9215470 --- /dev/null +++ b/tests/components/ble_presence/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: ble_presence + mac_address: AC:37:43:77:5F:4C + name: ESP32 BLE Tracker Google Home Mini + - platform: ble_presence + service_uuid: 11aa + name: BLE Test Service 16 Presence + - platform: ble_presence + service_uuid: "11223344" + name: BLE Test Service 32 Presence + - platform: ble_presence + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 Presence + - platform: ble_presence + ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + ibeacon_major: 100 + ibeacon_minor: 1 + name: BLE Test iBeacon Presence diff --git a/tests/components/ble_presence/test.esp32-c3.yaml b/tests/components/ble_presence/test.esp32-c3.yaml new file mode 100644 index 0000000000..dde9215470 --- /dev/null +++ b/tests/components/ble_presence/test.esp32-c3.yaml @@ -0,0 +1,20 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: ble_presence + mac_address: AC:37:43:77:5F:4C + name: ESP32 BLE Tracker Google Home Mini + - platform: ble_presence + service_uuid: 11aa + name: BLE Test Service 16 Presence + - platform: ble_presence + service_uuid: "11223344" + name: BLE Test Service 32 Presence + - platform: ble_presence + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 Presence + - platform: ble_presence + ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + ibeacon_major: 100 + ibeacon_minor: 1 + name: BLE Test iBeacon Presence diff --git a/tests/components/ble_presence/test.esp32-idf.yaml b/tests/components/ble_presence/test.esp32-idf.yaml new file mode 100644 index 0000000000..dde9215470 --- /dev/null +++ b/tests/components/ble_presence/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: ble_presence + mac_address: AC:37:43:77:5F:4C + name: ESP32 BLE Tracker Google Home Mini + - platform: ble_presence + service_uuid: 11aa + name: BLE Test Service 16 Presence + - platform: ble_presence + service_uuid: "11223344" + name: BLE Test Service 32 Presence + - platform: ble_presence + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 Presence + - platform: ble_presence + ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + ibeacon_major: 100 + ibeacon_minor: 1 + name: BLE Test iBeacon Presence diff --git a/tests/components/ble_presence/test.esp32.yaml b/tests/components/ble_presence/test.esp32.yaml new file mode 100644 index 0000000000..dde9215470 --- /dev/null +++ b/tests/components/ble_presence/test.esp32.yaml @@ -0,0 +1,20 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: ble_presence + mac_address: AC:37:43:77:5F:4C + name: ESP32 BLE Tracker Google Home Mini + - platform: ble_presence + service_uuid: 11aa + name: BLE Test Service 16 Presence + - platform: ble_presence + service_uuid: "11223344" + name: BLE Test Service 32 Presence + - platform: ble_presence + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 Presence + - platform: ble_presence + ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + ibeacon_major: 100 + ibeacon_minor: 1 + name: BLE Test iBeacon Presence diff --git a/tests/components/ble_rssi/test.esp32-c3-idf.yaml b/tests/components/ble_rssi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..52e5b865c6 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +esp32_ble_tracker: + +sensor: + - platform: ble_rssi + mac_address: AC:37:43:77:5F:4C + name: BLE Google Home Mini RSSI value + - platform: ble_rssi + service_uuid: 11aa + name: BLE Test Service 16 + - platform: ble_rssi + service_uuid: "11223344" + name: BLE Test Service 32 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test iBeacon UUID diff --git a/tests/components/ble_rssi/test.esp32-c3.yaml b/tests/components/ble_rssi/test.esp32-c3.yaml new file mode 100644 index 0000000000..52e5b865c6 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32-c3.yaml @@ -0,0 +1,18 @@ +esp32_ble_tracker: + +sensor: + - platform: ble_rssi + mac_address: AC:37:43:77:5F:4C + name: BLE Google Home Mini RSSI value + - platform: ble_rssi + service_uuid: 11aa + name: BLE Test Service 16 + - platform: ble_rssi + service_uuid: "11223344" + name: BLE Test Service 32 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test iBeacon UUID diff --git a/tests/components/ble_rssi/test.esp32-idf.yaml b/tests/components/ble_rssi/test.esp32-idf.yaml new file mode 100644 index 0000000000..52e5b865c6 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +esp32_ble_tracker: + +sensor: + - platform: ble_rssi + mac_address: AC:37:43:77:5F:4C + name: BLE Google Home Mini RSSI value + - platform: ble_rssi + service_uuid: 11aa + name: BLE Test Service 16 + - platform: ble_rssi + service_uuid: "11223344" + name: BLE Test Service 32 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test iBeacon UUID diff --git a/tests/components/ble_rssi/test.esp32.yaml b/tests/components/ble_rssi/test.esp32.yaml new file mode 100644 index 0000000000..52e5b865c6 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32.yaml @@ -0,0 +1,18 @@ +esp32_ble_tracker: + +sensor: + - platform: ble_rssi + mac_address: AC:37:43:77:5F:4C + name: BLE Google Home Mini RSSI value + - platform: ble_rssi + service_uuid: 11aa + name: BLE Test Service 16 + - platform: ble_rssi + service_uuid: "11223344" + name: BLE Test Service 32 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test iBeacon UUID diff --git a/tests/components/ble_scanner/test.esp32-c3-idf.yaml b/tests/components/ble_scanner/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..935a5a5a19 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +text_sensor: + - platform: ble_scanner + name: Scanner diff --git a/tests/components/ble_scanner/test.esp32-c3.yaml b/tests/components/ble_scanner/test.esp32-c3.yaml new file mode 100644 index 0000000000..935a5a5a19 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32-c3.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +text_sensor: + - platform: ble_scanner + name: Scanner diff --git a/tests/components/ble_scanner/test.esp32-idf.yaml b/tests/components/ble_scanner/test.esp32-idf.yaml new file mode 100644 index 0000000000..935a5a5a19 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +text_sensor: + - platform: ble_scanner + name: Scanner diff --git a/tests/components/ble_scanner/test.esp32.yaml b/tests/components/ble_scanner/test.esp32.yaml new file mode 100644 index 0000000000..935a5a5a19 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +text_sensor: + - platform: ble_scanner + name: Scanner diff --git a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml b/tests/components/bme280_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..070c2845d9 --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: bme280_i2c + address: 0x76 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_i2c/test.esp32-c3.yaml b/tests/components/bme280_i2c/test.esp32-c3.yaml new file mode 100644 index 0000000000..070c2845d9 --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32-c3.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: bme280_i2c + address: 0x76 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_i2c/test.esp32-idf.yaml b/tests/components/bme280_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..e379b98874 --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +sensor: + - platform: bme280_i2c + address: 0x76 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_i2c/test.esp32.yaml b/tests/components/bme280_i2c/test.esp32.yaml new file mode 100644 index 0000000000..e379b98874 --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +sensor: + - platform: bme280_i2c + address: 0x76 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_i2c/test.esp8266.yaml b/tests/components/bme280_i2c/test.esp8266.yaml new file mode 100644 index 0000000000..070c2845d9 --- /dev/null +++ b/tests/components/bme280_i2c/test.esp8266.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: bme280_i2c + address: 0x76 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_i2c/test.rp2040.yaml b/tests/components/bme280_i2c/test.rp2040.yaml new file mode 100644 index 0000000000..070c2845d9 --- /dev/null +++ b/tests/components/bme280_i2c/test.rp2040.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: bme280_i2c + address: 0x76 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_spi/test.esp32-c3-idf.yaml b/tests/components/bme280_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4bc7c14e6c --- /dev/null +++ b/tests/components/bme280_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +spi: + - id: spi_bme280 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: bme280_spi + cs_pin: 8 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_spi/test.esp32-c3.yaml b/tests/components/bme280_spi/test.esp32-c3.yaml new file mode 100644 index 0000000000..4bc7c14e6c --- /dev/null +++ b/tests/components/bme280_spi/test.esp32-c3.yaml @@ -0,0 +1,19 @@ +spi: + - id: spi_bme280 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: bme280_spi + cs_pin: 8 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_spi/test.esp32-idf.yaml b/tests/components/bme280_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..ebb3d98213 --- /dev/null +++ b/tests/components/bme280_spi/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +spi: + - id: spi_bme280 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: bme280_spi + cs_pin: 12 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_spi/test.esp32.yaml b/tests/components/bme280_spi/test.esp32.yaml new file mode 100644 index 0000000000..ebb3d98213 --- /dev/null +++ b/tests/components/bme280_spi/test.esp32.yaml @@ -0,0 +1,19 @@ +spi: + - id: spi_bme280 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: bme280_spi + cs_pin: 12 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_spi/test.esp8266.yaml b/tests/components/bme280_spi/test.esp8266.yaml new file mode 100644 index 0000000000..63013abb87 --- /dev/null +++ b/tests/components/bme280_spi/test.esp8266.yaml @@ -0,0 +1,19 @@ +spi: + - id: spi_bme280 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: bme280_spi + cs_pin: 15 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_spi/test.rp2040.yaml b/tests/components/bme280_spi/test.rp2040.yaml new file mode 100644 index 0000000000..ba5cb483c6 --- /dev/null +++ b/tests/components/bme280_spi/test.rp2040.yaml @@ -0,0 +1,19 @@ +spi: + - id: spi_bme280 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: bme280_spi + cs_pin: 6 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme680/test.esp32-c3-idf.yaml b/tests/components/bme680/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp32-c3.yaml b/tests/components/bme680/test.esp32-c3.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.esp32-c3.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp32-idf.yaml b/tests/components/bme680/test.esp32-idf.yaml new file mode 100644 index 0000000000..04d0ed8fe4 --- /dev/null +++ b/tests/components/bme680/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 16 + sda: 17 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp32.yaml b/tests/components/bme680/test.esp32.yaml new file mode 100644 index 0000000000..04d0ed8fe4 --- /dev/null +++ b/tests/components/bme680/test.esp32.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 16 + sda: 17 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp8266.yaml b/tests/components/bme680/test.esp8266.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.esp8266.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.rp2040.yaml b/tests/components/bme680/test.rp2040.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.rp2040.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680_bsec/test.esp32.yaml b/tests/components/bme680_bsec/test.esp32.yaml new file mode 100644 index 0000000000..4f62f13abb --- /dev/null +++ b/tests/components/bme680_bsec/test.esp32.yaml @@ -0,0 +1,29 @@ +i2c: + - id: i2c_bme680 + scl: 16 + sda: 17 + +bme680_bsec: + address: 0x77 + +sensor: + - platform: bme680_bsec + temperature: + name: BME680 Temperature + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + iaq: + name: BME680 IAQ + co2_equivalent: + name: BME680 eCO2 + breath_voc_equivalent: + name: BME680 Breath eVOC + +text_sensor: + - platform: bme680_bsec + iaq_accuracy: + name: BME680 Accuracy diff --git a/tests/components/bme680_bsec/test.esp8266.yaml b/tests/components/bme680_bsec/test.esp8266.yaml new file mode 100644 index 0000000000..84b32d3635 --- /dev/null +++ b/tests/components/bme680_bsec/test.esp8266.yaml @@ -0,0 +1,29 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +bme680_bsec: + address: 0x77 + +sensor: + - platform: bme680_bsec + temperature: + name: BME680 Temperature + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + iaq: + name: BME680 IAQ + co2_equivalent: + name: BME680 eCO2 + breath_voc_equivalent: + name: BME680 Breath eVOC + +text_sensor: + - platform: bme680_bsec + iaq_accuracy: + name: BME680 Accuracy diff --git a/tests/components/bmi160/test.esp32-c3-idf.yaml b/tests/components/bmi160/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp32-c3.yaml b/tests/components/bmi160/test.esp32-c3.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.esp32-c3.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp32-idf.yaml b/tests/components/bmi160/test.esp32-idf.yaml new file mode 100644 index 0000000000..a8a90c8c87 --- /dev/null +++ b/tests/components/bmi160/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 16 + sda: 17 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp32.yaml b/tests/components/bmi160/test.esp32.yaml new file mode 100644 index 0000000000..a8a90c8c87 --- /dev/null +++ b/tests/components/bmi160/test.esp32.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 16 + sda: 17 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp8266.yaml b/tests/components/bmi160/test.esp8266.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.esp8266.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.rp2040.yaml b/tests/components/bmi160/test.rp2040.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.rp2040.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmp085/test.esp32-c3-idf.yaml b/tests/components/bmp085/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp32-c3.yaml b/tests/components/bmp085/test.esp32-c3.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.esp32-c3.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp32-idf.yaml b/tests/components/bmp085/test.esp32-idf.yaml new file mode 100644 index 0000000000..8a4f714ddd --- /dev/null +++ b/tests/components/bmp085/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 16 + sda: 17 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp32.yaml b/tests/components/bmp085/test.esp32.yaml new file mode 100644 index 0000000000..8a4f714ddd --- /dev/null +++ b/tests/components/bmp085/test.esp32.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 16 + sda: 17 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp8266.yaml b/tests/components/bmp085/test.esp8266.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.esp8266.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.rp2040.yaml b/tests/components/bmp085/test.rp2040.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.rp2040.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp280/test.esp32-c3-idf.yaml b/tests/components/bmp280/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5f7f85d3e2 --- /dev/null +++ b/tests/components/bmp280/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp280 + scl: 5 + sda: 4 + +sensor: + - platform: bmp280 + address: 0x77 + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280/test.esp32-c3.yaml b/tests/components/bmp280/test.esp32-c3.yaml new file mode 100644 index 0000000000..5f7f85d3e2 --- /dev/null +++ b/tests/components/bmp280/test.esp32-c3.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp280 + scl: 5 + sda: 4 + +sensor: + - platform: bmp280 + address: 0x77 + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280/test.esp32-idf.yaml b/tests/components/bmp280/test.esp32-idf.yaml new file mode 100644 index 0000000000..aeb1cb262b --- /dev/null +++ b/tests/components/bmp280/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp280 + scl: 16 + sda: 17 + +sensor: + - platform: bmp280 + address: 0x77 + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280/test.esp32.yaml b/tests/components/bmp280/test.esp32.yaml new file mode 100644 index 0000000000..aeb1cb262b --- /dev/null +++ b/tests/components/bmp280/test.esp32.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp280 + scl: 16 + sda: 17 + +sensor: + - platform: bmp280 + address: 0x77 + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280/test.esp8266.yaml b/tests/components/bmp280/test.esp8266.yaml new file mode 100644 index 0000000000..5f7f85d3e2 --- /dev/null +++ b/tests/components/bmp280/test.esp8266.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp280 + scl: 5 + sda: 4 + +sensor: + - platform: bmp280 + address: 0x77 + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280/test.rp2040.yaml b/tests/components/bmp280/test.rp2040.yaml new file mode 100644 index 0000000000..5f7f85d3e2 --- /dev/null +++ b/tests/components/bmp280/test.rp2040.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp280 + scl: 5 + sda: 4 + +sensor: + - platform: bmp280 + address: 0x77 + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp3xx/test.esp32-c3-idf.yaml b/tests/components/bmp3xx/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3b244eccf9 --- /dev/null +++ b/tests/components/bmp3xx/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_bmp3xx + scl: 5 + sda: 4 + +sensor: + - platform: bmp3xx + address: 0x77 + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp3xx/test.esp32-c3.yaml b/tests/components/bmp3xx/test.esp32-c3.yaml new file mode 100644 index 0000000000..3b244eccf9 --- /dev/null +++ b/tests/components/bmp3xx/test.esp32-c3.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_bmp3xx + scl: 5 + sda: 4 + +sensor: + - platform: bmp3xx + address: 0x77 + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp3xx/test.esp32-idf.yaml b/tests/components/bmp3xx/test.esp32-idf.yaml new file mode 100644 index 0000000000..677ed8a22d --- /dev/null +++ b/tests/components/bmp3xx/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_bmp3xx + scl: 16 + sda: 17 + +sensor: + - platform: bmp3xx + address: 0x77 + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp3xx/test.esp32.yaml b/tests/components/bmp3xx/test.esp32.yaml new file mode 100644 index 0000000000..677ed8a22d --- /dev/null +++ b/tests/components/bmp3xx/test.esp32.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_bmp3xx + scl: 16 + sda: 17 + +sensor: + - platform: bmp3xx + address: 0x77 + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp3xx/test.esp8266.yaml b/tests/components/bmp3xx/test.esp8266.yaml new file mode 100644 index 0000000000..3b244eccf9 --- /dev/null +++ b/tests/components/bmp3xx/test.esp8266.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_bmp3xx + scl: 5 + sda: 4 + +sensor: + - platform: bmp3xx + address: 0x77 + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp3xx/test.rp2040.yaml b/tests/components/bmp3xx/test.rp2040.yaml new file mode 100644 index 0000000000..3b244eccf9 --- /dev/null +++ b/tests/components/bmp3xx/test.rp2040.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_bmp3xx + scl: 5 + sda: 4 + +sensor: + - platform: bmp3xx + address: 0x77 + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp581/test.esp32-c3-idf.yaml b/tests/components/bmp581/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp32-c3.yaml b/tests/components/bmp581/test.esp32-c3.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.esp32-c3.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp32-idf.yaml b/tests/components/bmp581/test.esp32-idf.yaml new file mode 100644 index 0000000000..a464b8ce6a --- /dev/null +++ b/tests/components/bmp581/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 16 + sda: 17 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp32.yaml b/tests/components/bmp581/test.esp32.yaml new file mode 100644 index 0000000000..a464b8ce6a --- /dev/null +++ b/tests/components/bmp581/test.esp32.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 16 + sda: 17 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp8266.yaml b/tests/components/bmp581/test.esp8266.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.esp8266.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.rp2040.yaml b/tests/components/bmp581/test.rp2040.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.rp2040.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bp1658cj/test.esp32-c3-idf.yaml b/tests/components/bp1658cj/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp32-c3.yaml b/tests/components/bp1658cj/test.esp32-c3.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.esp32-c3.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp32-idf.yaml b/tests/components/bp1658cj/test.esp32-idf.yaml new file mode 100644 index 0000000000..5f9e25d3bd --- /dev/null +++ b/tests/components/bp1658cj/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 16 + data_pin: 17 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp32.yaml b/tests/components/bp1658cj/test.esp32.yaml new file mode 100644 index 0000000000..5f9e25d3bd --- /dev/null +++ b/tests/components/bp1658cj/test.esp32.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 16 + data_pin: 17 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp8266.yaml b/tests/components/bp1658cj/test.esp8266.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.esp8266.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.rp2040.yaml b/tests/components/bp1658cj/test.rp2040.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.rp2040.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp5758d/test.esp32-c3-idf.yaml b/tests/components/bp5758d/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp32-c3.yaml b/tests/components/bp5758d/test.esp32-c3.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.esp32-c3.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp32-idf.yaml b/tests/components/bp5758d/test.esp32-idf.yaml new file mode 100644 index 0000000000..b7929a0518 --- /dev/null +++ b/tests/components/bp5758d/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 16 + data_pin: 17 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp32.yaml b/tests/components/bp5758d/test.esp32.yaml new file mode 100644 index 0000000000..b7929a0518 --- /dev/null +++ b/tests/components/bp5758d/test.esp32.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 16 + data_pin: 17 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp8266.yaml b/tests/components/bp5758d/test.esp8266.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.esp8266.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.rp2040.yaml b/tests/components/bp5758d/test.rp2040.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.rp2040.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/button/test.esp32-c3-idf.yaml b/tests/components/button/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d5978601f4 --- /dev/null +++ b/tests/components/button/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +button: + - platform: template + name: Button + id: some_button + on_press: + - logger.log: Button pressed diff --git a/tests/components/button/test.esp32-c3.yaml b/tests/components/button/test.esp32-c3.yaml new file mode 100644 index 0000000000..d5978601f4 --- /dev/null +++ b/tests/components/button/test.esp32-c3.yaml @@ -0,0 +1,6 @@ +button: + - platform: template + name: Button + id: some_button + on_press: + - logger.log: Button pressed diff --git a/tests/components/button/test.esp32-idf.yaml b/tests/components/button/test.esp32-idf.yaml new file mode 100644 index 0000000000..d5978601f4 --- /dev/null +++ b/tests/components/button/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +button: + - platform: template + name: Button + id: some_button + on_press: + - logger.log: Button pressed diff --git a/tests/components/button/test.esp32.yaml b/tests/components/button/test.esp32.yaml new file mode 100644 index 0000000000..d5978601f4 --- /dev/null +++ b/tests/components/button/test.esp32.yaml @@ -0,0 +1,6 @@ +button: + - platform: template + name: Button + id: some_button + on_press: + - logger.log: Button pressed diff --git a/tests/components/button/test.esp8266.yaml b/tests/components/button/test.esp8266.yaml new file mode 100644 index 0000000000..d5978601f4 --- /dev/null +++ b/tests/components/button/test.esp8266.yaml @@ -0,0 +1,6 @@ +button: + - platform: template + name: Button + id: some_button + on_press: + - logger.log: Button pressed diff --git a/tests/components/button/test.rp2040.yaml b/tests/components/button/test.rp2040.yaml new file mode 100644 index 0000000000..d5978601f4 --- /dev/null +++ b/tests/components/button/test.rp2040.yaml @@ -0,0 +1,6 @@ +button: + - platform: template + name: Button + id: some_button + on_press: + - logger.log: Button pressed From fe789c8beb852621c5c0b451f97ac6158cbdadd4 Mon Sep 17 00:00:00 2001 From: Bill Adams Date: Tue, 6 Feb 2024 15:13:55 -0800 Subject: [PATCH 325/436] Add "transformer_active" flag for use in effects. (#6157) --- esphome/components/light/light_state.cpp | 5 +++++ esphome/components/light/light_state.h | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 50ebd8882b..fe6538e65e 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -120,6 +120,7 @@ void LightState::loop() { // Apply transformer (if any) if (this->transformer_ != nullptr) { auto values = this->transformer_->apply(); + this->is_transformer_active_ = true; if (values.has_value()) { this->current_values = *values; this->output_->update_state(this); @@ -131,6 +132,7 @@ void LightState::loop() { this->current_values = this->transformer_->get_target_values(); this->transformer_->stop(); + this->is_transformer_active_ = false; this->transformer_ = nullptr; this->target_state_reached_callback_.call(); } @@ -214,6 +216,8 @@ void LightState::current_values_as_ct(float *color_temperature, float *white_bri this->gamma_correct_); } +bool LightState::is_transformer_active() { return this->is_transformer_active_; } + void LightState::start_effect_(uint32_t effect_index) { this->stop_effect_(); if (effect_index == 0) @@ -263,6 +267,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { + this->is_transformer_active_ = false; this->transformer_ = nullptr; this->current_values = target; if (set_remote_values) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ac4718ade5..b0aaa453b5 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -144,6 +144,17 @@ class LightState : public EntityBase, public Component { void current_values_as_ct(float *color_temperature, float *white_brightness); + /** + * Indicator if a transformer (e.g. transition) is active. This is useful + * for effects e.g. at the start of the apply() method, add a check like: + * + * if (this->state_->is_transformer_active()) { + * // Something is already running. + * return; + * } + */ + bool is_transformer_active(); + protected: friend LightOutput; friend LightCall; @@ -203,6 +214,9 @@ class LightState : public EntityBase, public Component { LightRestoreMode restore_mode_; /// List of effects for this light. std::vector effects_; + + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_ = false; }; } // namespace light From 0ede4a30955d6a16da29ef9467cdca51f823e5b3 Mon Sep 17 00:00:00 2001 From: Tomek Wasilczyk Date: Tue, 6 Feb 2024 17:12:14 -0800 Subject: [PATCH 326/436] CSE7766: fix power and current measurements at low loads (#6180) --- esphome/components/cse7766/cse7766.cpp | 110 +++++++++++++++++-------- 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 9c5016c503..f482ba26c3 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,6 +1,8 @@ #include "cse7766.h" #include "esphome/core/log.h" #include +#include +#include namespace esphome { namespace cse7766 { @@ -68,20 +70,26 @@ bool CSE7766Component::check_byte_() { return true; } void CSE7766Component::parse_data_() { - ESP_LOGVV(TAG, "CSE7766 Data: "); - for (uint8_t i = 0; i < 23; i++) { - ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]), - this->raw_data_[i]); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + { + std::stringstream ss; + ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0'); + for (uint8_t i = 0; i < 23; i++) { + ss << ' ' << std::setw(2) << static_cast(this->raw_data_[i]); + } + ESP_LOGVV(TAG, "%s", ss.str().c_str()); } +#endif + // Parse header uint8_t header1 = this->raw_data_[0]; + if (header1 == 0xAA) { ESP_LOGE(TAG, "CSE7766 not calibrated!"); return; } bool power_cycle_exceeds_range = false; - if ((header1 & 0xF0) == 0xF0) { if (header1 & 0xD) { ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1); @@ -94,74 +102,106 @@ void CSE7766Component::parse_data_() { if (header1 & (1 << 0)) { ESP_LOGE(TAG, " Coefficient storage area is abnormal."); } + + // Datasheet: voltage or current cycle exceeding range means invalid values return; } power_cycle_exceeds_range = header1 & (1 << 1); } - uint32_t voltage_calib = this->get_24_bit_uint_(2); + // Parse data frame + uint32_t voltage_coeff = this->get_24_bit_uint_(2); uint32_t voltage_cycle = this->get_24_bit_uint_(5); - uint32_t current_calib = this->get_24_bit_uint_(8); + uint32_t current_coeff = this->get_24_bit_uint_(8); uint32_t current_cycle = this->get_24_bit_uint_(11); - uint32_t power_calib = this->get_24_bit_uint_(14); + uint32_t power_coeff = this->get_24_bit_uint_(14); uint32_t power_cycle = this->get_24_bit_uint_(17); - uint8_t adj = this->raw_data_[20]; uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; + bool have_power = adj & 0x10; + bool have_current = adj & 0x20; bool have_voltage = adj & 0x40; + + float voltage = 0.0f; if (have_voltage) { - // voltage cycle of serial port outputted is a complete cycle; - float voltage = voltage_calib / float(voltage_cycle); - if (this->voltage_sensor_ != nullptr) + voltage = voltage_coeff / float(voltage_cycle); + if (this->voltage_sensor_ != nullptr) { this->voltage_sensor_->publish_state(voltage); + } } - bool have_power = adj & 0x10; float power = 0.0f; - - if (have_power) { - // power cycle of serial port outputted is a complete cycle; - // According to the user manual, power cycle exceeding range means the measured power is 0 - if (!power_cycle_exceeds_range) { - power = power_calib / float(power_cycle); + float energy = 0.0f; + if (power_cycle_exceeds_range) { + // Datasheet: power cycle exceeding range means active power is 0 + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(0.0f); } - if (this->power_sensor_ != nullptr) + } else if (have_power) { + power = power_coeff / float(power_cycle); + if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); + } + + // Add CF pulses to the total energy only if we have Power coefficient to multiply by - uint32_t difference; if (this->cf_pulses_last_ == 0) { this->cf_pulses_last_ = cf_pulses; } + uint32_t cf_diff; if (cf_pulses < this->cf_pulses_last_) { - difference = cf_pulses + (0x10000 - this->cf_pulses_last_); + cf_diff = cf_pulses + (0x10000 - this->cf_pulses_last_); } else { - difference = cf_pulses - this->cf_pulses_last_; + cf_diff = cf_pulses - this->cf_pulses_last_; } this->cf_pulses_last_ = cf_pulses; - this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f; + + energy = cf_diff * float(power_coeff) / 1000000.0f / 3600.0f; + this->energy_total_ += energy; if (this->energy_sensor_ != nullptr) this->energy_sensor_->publish_state(this->energy_total_); } else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) { this->energy_sensor_->publish_state(0); } - if (adj & 0x20) { - // indicates current cycle of serial port outputted is a complete cycle; - float current = 0.0f; - if (have_voltage && !have_power) { - // Testing has shown that when we have voltage and current but not power, that means the power is 0. - // We report a power of 0, which in turn means we should report a current of 0. - if (this->power_sensor_ != nullptr) - this->power_sensor_->publish_state(0); - } else if (power != 0.0f) { - current = current_calib / float(current_cycle); + float current = 0.0f; + float calculated_current = 0.0f; + if (have_current) { + // Assumption: if we don't have power measurement, then current is likely below 50mA + if (have_power && voltage > 1.0f) { + calculated_current = power / voltage; } - if (this->current_sensor_ != nullptr) + // Datasheet: minimum measured current is 50mA + if (calculated_current > 0.05f) { + current = current_coeff / float(current_cycle); + } + if (this->current_sensor_ != nullptr) { this->current_sensor_->publish_state(current); + } } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + { + std::stringstream ss; + ss << "Parsed:"; + if (have_voltage) { + ss << " V=" << voltage << "V"; + } + if (have_current) { + ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)"; + } + if (have_power) { + ss << " P=" << power << "W"; + } + if (energy != 0.0f) { + ss << " E=" << energy << "kWh (" << cf_pulses << ")"; + } + ESP_LOGVV(TAG, "%s", ss.str().c_str()); + } +#endif } uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) { From f3ef05f5c303111dc9c1dd6d8003baf794ff4939 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 7 Feb 2024 12:24:06 +1100 Subject: [PATCH 327/436] host platform: improvements and bugfixes (#6137) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/host/__init__.py | 8 +++++++- esphome/components/logger/logger.cpp | 8 ++++++++ esphome/components/sntp/sntp_component.cpp | 4 +++- esphome/components/text_sensor/filter.h | 2 +- esphome/core/component.h | 5 +++-- esphome/core/helpers.cpp | 15 +++++++++++++-- esphome/core/time.h | 1 + 7 files changed, 36 insertions(+), 7 deletions(-) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index eb44bcccd6..3bd3b6b172 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -4,6 +4,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_HOST, + CONF_MAC_ADDRESS, ) from esphome.core import CORE from esphome.helpers import IS_MACOS @@ -28,13 +29,18 @@ def set_core_data(config): CONFIG_SCHEMA = cv.All( - cv.Schema({}), + cv.Schema( + { + cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address, + } + ), set_core_data, ) 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: diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index d5f5c275eb..c8a3ba906c 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -212,6 +212,14 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { return; #endif #ifdef USE_HOST + time_t rawtime; + struct tm *timeinfo; + char buffer[80]; + + time(&rawtime); + timeinfo = localtime(&rawtime); + strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo); + fputs(buffer, stdout); puts(msg); #endif diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 418eacd870..6a60e8d5c1 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -25,6 +25,7 @@ namespace sntp { static const char *const TAG = "sntp"; void SNTPComponent::setup() { +#ifndef USE_HOST ESP_LOGCONFIG(TAG, "Setting up SNTP..."); #if defined(USE_ESP32) || defined(USE_LIBRETINY) if (sntp_enabled()) { @@ -48,6 +49,7 @@ void SNTPComponent::setup() { #endif sntp_init(); +#endif } void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, "SNTP Time:"); @@ -57,7 +59,7 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } void SNTPComponent::update() { -#ifndef USE_ESP_IDF +#if !defined(USE_ESP_IDF) && !defined(USE_HOST) // force resync if (sntp_enabled()) { sntp_stop(); diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 4e36532945..2de9010b88 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -28,7 +28,7 @@ class Filter { * @param value The new value. * @return An optional string, the new value that should be pushed out. */ - virtual optional new_value(std::string value); + virtual optional new_value(std::string value) = 0; /// Initialize this filter, please note this can be called more than once. virtual void initialize(TextSensor *parent, Filter *next); diff --git a/esphome/core/component.h b/esphome/core/component.h index 51a6296811..594f8b65af 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -1,8 +1,9 @@ #pragma once -#include -#include #include +#include +#include +#include #include "esphome/core/optional.h" diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c95c0470de..cec8a82d04 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -11,6 +11,12 @@ #include #include +#ifdef USE_HOST +#include +#include +#include +#include +#endif #if defined(USE_ESP8266) #include #include @@ -415,7 +421,7 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { int8_t step_to_accuracy_decimals(float step) { // use printf %g to find number of digits based on temperature step char buf[32]; - sprintf(buf, "%.5g", step); + snprintf(buf, sizeof buf, "%.5g", step); std::string str{buf}; size_t dot_pos = str.find('.'); @@ -551,7 +557,10 @@ void HighFrequencyLoopRequester::stop() { bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) -#if defined(USE_ESP32) +#if defined(USE_HOST) + static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS; + memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address)); +#elif defined(USE_ESP32) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) || defined(USE_ESP32_IGNORE_EFUSE_MAC_CRC) // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // returns the 802.15.4 EUI-64 address. Read directly from eFuse instead. @@ -569,6 +578,8 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame WiFi.macAddress(mac); #elif defined(USE_LIBRETINY) WiFi.macAddress(mac); +#else +// this should be an error, but that messes with CI checks. #error No mac address method defined #endif } std::string get_mac_address() { diff --git a/esphome/core/time.h b/esphome/core/time.h index 14c36311e0..670bf0ee73 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include From 558588ee8ae3e0b8f95d9a8f53b23dd92031a047 Mon Sep 17 00:00:00 2001 From: ChuckMash <86080247+ChuckMash@users.noreply.github.com> Date: Tue, 6 Feb 2024 17:41:40 -0800 Subject: [PATCH 328/436] WLED Sync fix and BK72XX support (#6190) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/wled/__init__.py | 7 ++- esphome/components/wled/wled_light_effect.cpp | 43 +++++++++++++++++-- esphome/components/wled/wled_light_effect.h | 4 ++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index 2795529203..396d5891d8 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -8,6 +8,8 @@ wled_ns = cg.esphome_ns.namespace("wled") WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect) CONFIG_SCHEMA = cv.All(cv.Schema({}), cv.only_with_arduino) +CONF_SYNC_GROUP_MASK = "sync_group_mask" +CONF_BLANK_ON_START = "blank_on_start" @register_addressable_effect( @@ -16,10 +18,13 @@ CONFIG_SCHEMA = cv.All(cv.Schema({}), cv.only_with_arduino) "WLED", { cv.Optional(CONF_PORT, default=21324): cv.port, + cv.Optional(CONF_SYNC_GROUP_MASK, default=0): cv.int_range(min=0, max=255), + cv.Optional(CONF_BLANK_ON_START, default=True): cv.boolean, }, ) async def wled_light_effect_to_code(config, effect_id): effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(effect.set_port(config[CONF_PORT])) - + cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK])) + cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START])) return effect diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 8c68bca6e3..7a82aaeb46 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -13,6 +13,10 @@ #include #endif +#ifdef USE_BK72XX +#include +#endif + namespace esphome { namespace wled { @@ -29,7 +33,11 @@ WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffe void WLEDLightEffect::start() { AddressableLightEffect::start(); - blank_at_ = 0; + if (this->blank_on_start_) { + this->blank_at_ = 0; + } else { + this->blank_at_ = UINT32_MAX; + } } void WLEDLightEffect::stop() { @@ -101,8 +109,11 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p if (!parse_drgb_frame_(it, payload, size)) return false; } else { - if (!parse_notifier_frame_(it, payload, size)) + if (!parse_notifier_frame_(it, payload, size)) { return false; + } else { + timeout = UINT8_MAX; + } } break; @@ -143,8 +154,32 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p } bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { - // Packet needs to be empty - return size == 0; + // Receive at least RGBW and Brightness for all LEDs from WLED Sync Notification + // https://kno.wled.ge/interfaces/udp-notifier/ + // https://github.com/Aircoookie/WLED/blob/main/wled00/udp.cpp + + if (size < 34) { + return false; + } + + uint8_t payload_sync_group_mask = payload[34]; + + if ((payload_sync_group_mask & this->sync_group_mask_) != this->sync_group_mask_) { + ESP_LOGD(TAG, "sync group mask does not match"); + return false; + } + + uint8_t bri = payload[0]; + uint8_t r = esp_scale8(payload[1], bri); + uint8_t g = esp_scale8(payload[2], bri); + uint8_t b = esp_scale8(payload[3], bri); + uint8_t w = esp_scale8(payload[8], bri); + + for (auto &&led : it) { + led.set(Color(r, g, b, w)); + } + + return true; } bool WLEDLightEffect::parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index 8f239276d7..a591e1fd1a 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -21,6 +21,8 @@ class WLEDLightEffect : public light::AddressableLightEffect { void stop() override; void apply(light::AddressableLight &it, const Color ¤t_color) override; void set_port(uint16_t port) { this->port_ = port; } + void set_sync_group_mask(uint8_t mask) { this->sync_group_mask_ = mask; } + void set_blank_on_start(bool blank) { this->blank_on_start_ = blank; } protected: void blank_all_leds_(light::AddressableLight &it); @@ -35,6 +37,8 @@ class WLEDLightEffect : public light::AddressableLightEffect { std::unique_ptr udp_; uint32_t blank_at_{0}; uint32_t dropped_{0}; + uint8_t sync_group_mask_{0}; + bool blank_on_start_{true}; }; } // namespace wled From a91937dca52e0e3753ae3af0d2a1a49a6bad5875 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 7 Feb 2024 15:53:44 -0600 Subject: [PATCH 329/436] Add missing vector.h for lightwaverf (#6196) --- esphome/components/lightwaverf/LwTx.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/lightwaverf/LwTx.h b/esphome/components/lightwaverf/LwTx.h index 719826640e..fe7b942a3a 100644 --- a/esphome/components/lightwaverf/LwTx.h +++ b/esphome/components/lightwaverf/LwTx.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace lightwaverf { From 3eaf59cc5a411668ecb310cfb000c8ba85716c0b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 7 Feb 2024 15:55:20 -0600 Subject: [PATCH 330/436] Add some components to the new testing framework (C) (#6174) --- .../components/canbus/test.esp32-c3-idf.yaml | 46 +++++++++++ tests/components/canbus/test.esp32-c3.yaml | 46 +++++++++++ tests/components/canbus/test.esp32-idf.yaml | 46 +++++++++++ tests/components/canbus/test.esp32.yaml | 46 +++++++++++ .../components/cap1188/test.esp32-c3-idf.yaml | 11 +++ tests/components/cap1188/test.esp32-c3.yaml | 11 +++ tests/components/cap1188/test.esp32-idf.yaml | 11 +++ tests/components/cap1188/test.esp32.yaml | 11 +++ tests/components/cap1188/test.esp8266.yaml | 11 +++ tests/components/cap1188/test.rp2040.yaml | 11 +++ .../captive_portal/test.esp32-c3-idf.yaml | 5 ++ .../captive_portal/test.esp32-c3.yaml | 5 ++ .../captive_portal/test.esp32-idf.yaml | 5 ++ .../components/captive_portal/test.esp32.yaml | 5 ++ .../captive_portal/test.esp8266.yaml | 5 ++ .../components/ccs811/test.esp32-c3-idf.yaml | 13 ++++ tests/components/ccs811/test.esp32-c3.yaml | 13 ++++ tests/components/ccs811/test.esp32-idf.yaml | 13 ++++ tests/components/ccs811/test.esp32.yaml | 13 ++++ tests/components/ccs811/test.esp8266.yaml | 13 ++++ tests/components/ccs811/test.rp2040.yaml | 13 ++++ .../cd74hc4067/test.esp32-c3-idf.yaml | 18 +++++ .../components/cd74hc4067/test.esp32-c3.yaml | 18 +++++ .../components/cd74hc4067/test.esp32-idf.yaml | 18 +++++ tests/components/cd74hc4067/test.esp32.yaml | 18 +++++ tests/components/cd74hc4067/test.esp8266.yaml | 18 +++++ tests/components/cd74hc4067/test.rp2040.yaml | 18 +++++ .../climate_ir_lg/test.esp32-c3-idf.yaml | 7 ++ .../climate_ir_lg/test.esp32-c3.yaml | 7 ++ .../climate_ir_lg/test.esp32-idf.yaml | 7 ++ .../components/climate_ir_lg/test.esp32.yaml | 7 ++ .../climate_ir_lg/test.esp8266.yaml | 7 ++ tests/components/color/test.esp32-c3-idf.yaml | 11 +++ tests/components/color/test.esp32-c3.yaml | 11 +++ tests/components/color/test.esp32-idf.yaml | 11 +++ tests/components/color/test.esp32.yaml | 11 +++ tests/components/color/test.esp8266.yaml | 11 +++ tests/components/color/test.rp2040.yaml | 11 +++ .../color_temperature/test.esp32-c3-idf.yaml | 15 ++++ .../color_temperature/test.esp32-c3.yaml | 15 ++++ .../color_temperature/test.esp32-idf.yaml | 15 ++++ .../color_temperature/test.esp32.yaml | 15 ++++ .../color_temperature/test.esp8266.yaml | 15 ++++ .../color_temperature/test.rp2040.yaml | 15 ++++ .../combination/test.esp32-c3-idf.yaml | 76 +++++++++++++++++++ .../components/combination/test.esp32-c3.yaml | 76 +++++++++++++++++++ .../combination/test.esp32-idf.yaml | 76 +++++++++++++++++++ tests/components/combination/test.esp32.yaml | 76 +++++++++++++++++++ .../components/combination/test.esp8266.yaml | 76 +++++++++++++++++++ tests/components/combination/test.rp2040.yaml | 76 +++++++++++++++++++ .../components/coolix/test.esp32-c3-idf.yaml | 7 ++ tests/components/coolix/test.esp32-c3.yaml | 7 ++ tests/components/coolix/test.esp32-idf.yaml | 7 ++ tests/components/coolix/test.esp32.yaml | 7 ++ tests/components/coolix/test.esp8266.yaml | 7 ++ tests/components/copy/test.esp32-c3-idf.yaml | 23 ++++++ tests/components/copy/test.esp32-c3.yaml | 23 ++++++ tests/components/copy/test.esp32-idf.yaml | 23 ++++++ tests/components/copy/test.esp32.yaml | 23 ++++++ tests/components/copy/test.esp8266.yaml | 23 ++++++ tests/components/copy/test.rp2040.yaml | 23 ++++++ .../components/cs5460a/test.esp32-c3-idf.yaml | 27 +++++++ tests/components/cs5460a/test.esp32-c3.yaml | 27 +++++++ tests/components/cs5460a/test.esp32-idf.yaml | 27 +++++++ tests/components/cs5460a/test.esp32.yaml | 27 +++++++ tests/components/cs5460a/test.esp8266.yaml | 27 +++++++ tests/components/cs5460a/test.rp2040.yaml | 27 +++++++ .../components/cse7761/test.esp32-c3-idf.yaml | 20 +++++ tests/components/cse7761/test.esp32-c3.yaml | 20 +++++ tests/components/cse7761/test.esp32-idf.yaml | 20 +++++ tests/components/cse7761/test.esp32.yaml | 20 +++++ tests/components/cse7761/test.esp8266.yaml | 20 +++++ tests/components/cse7761/test.rp2040.yaml | 20 +++++ .../components/cse7766/test.esp32-c3-idf.yaml | 16 ++++ tests/components/cse7766/test.esp32-c3.yaml | 16 ++++ tests/components/cse7766/test.esp32-idf.yaml | 16 ++++ tests/components/cse7766/test.esp32.yaml | 16 ++++ tests/components/cse7766/test.esp8266.yaml | 16 ++++ tests/components/cse7766/test.rp2040.yaml | 16 ++++ .../ct_clamp/test.esp32-c3-idf.yaml | 9 +++ tests/components/ct_clamp/test.esp32-c3.yaml | 9 +++ tests/components/ct_clamp/test.esp32-idf.yaml | 9 +++ tests/components/ct_clamp/test.esp32.yaml | 9 +++ tests/components/ct_clamp/test.esp8266.yaml | 9 +++ tests/components/ct_clamp/test.rp2040.yaml | 9 +++ .../current_based/test.esp32-c3-idf.yaml | 68 +++++++++++++++++ .../current_based/test.esp32-c3.yaml | 68 +++++++++++++++++ .../current_based/test.esp32-idf.yaml | 68 +++++++++++++++++ .../components/current_based/test.esp32.yaml | 68 +++++++++++++++++ .../current_based/test.esp8266.yaml | 68 +++++++++++++++++ .../components/current_based/test.rp2040.yaml | 68 +++++++++++++++++ tests/components/cwww/test.esp32-c3-idf.yaml | 16 ++++ tests/components/cwww/test.esp32-c3.yaml | 16 ++++ tests/components/cwww/test.esp32-idf.yaml | 16 ++++ tests/components/cwww/test.esp32.yaml | 16 ++++ tests/components/cwww/test.esp8266.yaml | 16 ++++ tests/components/cwww/test.rp2040.yaml | 16 ++++ 97 files changed, 2217 insertions(+) create mode 100644 tests/components/canbus/test.esp32-c3-idf.yaml create mode 100644 tests/components/canbus/test.esp32-c3.yaml create mode 100644 tests/components/canbus/test.esp32-idf.yaml create mode 100644 tests/components/canbus/test.esp32.yaml create mode 100644 tests/components/cap1188/test.esp32-c3-idf.yaml create mode 100644 tests/components/cap1188/test.esp32-c3.yaml create mode 100644 tests/components/cap1188/test.esp32-idf.yaml create mode 100644 tests/components/cap1188/test.esp32.yaml create mode 100644 tests/components/cap1188/test.esp8266.yaml create mode 100644 tests/components/cap1188/test.rp2040.yaml create mode 100644 tests/components/captive_portal/test.esp32-c3-idf.yaml create mode 100644 tests/components/captive_portal/test.esp32-c3.yaml create mode 100644 tests/components/captive_portal/test.esp32-idf.yaml create mode 100644 tests/components/captive_portal/test.esp32.yaml create mode 100644 tests/components/captive_portal/test.esp8266.yaml create mode 100644 tests/components/ccs811/test.esp32-c3-idf.yaml create mode 100644 tests/components/ccs811/test.esp32-c3.yaml create mode 100644 tests/components/ccs811/test.esp32-idf.yaml create mode 100644 tests/components/ccs811/test.esp32.yaml create mode 100644 tests/components/ccs811/test.esp8266.yaml create mode 100644 tests/components/ccs811/test.rp2040.yaml create mode 100644 tests/components/cd74hc4067/test.esp32-c3-idf.yaml create mode 100644 tests/components/cd74hc4067/test.esp32-c3.yaml create mode 100644 tests/components/cd74hc4067/test.esp32-idf.yaml create mode 100644 tests/components/cd74hc4067/test.esp32.yaml create mode 100644 tests/components/cd74hc4067/test.esp8266.yaml create mode 100644 tests/components/cd74hc4067/test.rp2040.yaml create mode 100644 tests/components/climate_ir_lg/test.esp32-c3-idf.yaml create mode 100644 tests/components/climate_ir_lg/test.esp32-c3.yaml create mode 100644 tests/components/climate_ir_lg/test.esp32-idf.yaml create mode 100644 tests/components/climate_ir_lg/test.esp32.yaml create mode 100644 tests/components/climate_ir_lg/test.esp8266.yaml create mode 100644 tests/components/color/test.esp32-c3-idf.yaml create mode 100644 tests/components/color/test.esp32-c3.yaml create mode 100644 tests/components/color/test.esp32-idf.yaml create mode 100644 tests/components/color/test.esp32.yaml create mode 100644 tests/components/color/test.esp8266.yaml create mode 100644 tests/components/color/test.rp2040.yaml create mode 100644 tests/components/color_temperature/test.esp32-c3-idf.yaml create mode 100644 tests/components/color_temperature/test.esp32-c3.yaml create mode 100644 tests/components/color_temperature/test.esp32-idf.yaml create mode 100644 tests/components/color_temperature/test.esp32.yaml create mode 100644 tests/components/color_temperature/test.esp8266.yaml create mode 100644 tests/components/color_temperature/test.rp2040.yaml create mode 100644 tests/components/combination/test.esp32-c3-idf.yaml create mode 100644 tests/components/combination/test.esp32-c3.yaml create mode 100644 tests/components/combination/test.esp32-idf.yaml create mode 100644 tests/components/combination/test.esp32.yaml create mode 100644 tests/components/combination/test.esp8266.yaml create mode 100644 tests/components/combination/test.rp2040.yaml create mode 100644 tests/components/coolix/test.esp32-c3-idf.yaml create mode 100644 tests/components/coolix/test.esp32-c3.yaml create mode 100644 tests/components/coolix/test.esp32-idf.yaml create mode 100644 tests/components/coolix/test.esp32.yaml create mode 100644 tests/components/coolix/test.esp8266.yaml create mode 100644 tests/components/copy/test.esp32-c3-idf.yaml create mode 100644 tests/components/copy/test.esp32-c3.yaml create mode 100644 tests/components/copy/test.esp32-idf.yaml create mode 100644 tests/components/copy/test.esp32.yaml create mode 100644 tests/components/copy/test.esp8266.yaml create mode 100644 tests/components/copy/test.rp2040.yaml create mode 100644 tests/components/cs5460a/test.esp32-c3-idf.yaml create mode 100644 tests/components/cs5460a/test.esp32-c3.yaml create mode 100644 tests/components/cs5460a/test.esp32-idf.yaml create mode 100644 tests/components/cs5460a/test.esp32.yaml create mode 100644 tests/components/cs5460a/test.esp8266.yaml create mode 100644 tests/components/cs5460a/test.rp2040.yaml create mode 100644 tests/components/cse7761/test.esp32-c3-idf.yaml create mode 100644 tests/components/cse7761/test.esp32-c3.yaml create mode 100644 tests/components/cse7761/test.esp32-idf.yaml create mode 100644 tests/components/cse7761/test.esp32.yaml create mode 100644 tests/components/cse7761/test.esp8266.yaml create mode 100644 tests/components/cse7761/test.rp2040.yaml create mode 100644 tests/components/cse7766/test.esp32-c3-idf.yaml create mode 100644 tests/components/cse7766/test.esp32-c3.yaml create mode 100644 tests/components/cse7766/test.esp32-idf.yaml create mode 100644 tests/components/cse7766/test.esp32.yaml create mode 100644 tests/components/cse7766/test.esp8266.yaml create mode 100644 tests/components/cse7766/test.rp2040.yaml create mode 100644 tests/components/ct_clamp/test.esp32-c3-idf.yaml create mode 100644 tests/components/ct_clamp/test.esp32-c3.yaml create mode 100644 tests/components/ct_clamp/test.esp32-idf.yaml create mode 100644 tests/components/ct_clamp/test.esp32.yaml create mode 100644 tests/components/ct_clamp/test.esp8266.yaml create mode 100644 tests/components/ct_clamp/test.rp2040.yaml create mode 100644 tests/components/current_based/test.esp32-c3-idf.yaml create mode 100644 tests/components/current_based/test.esp32-c3.yaml create mode 100644 tests/components/current_based/test.esp32-idf.yaml create mode 100644 tests/components/current_based/test.esp32.yaml create mode 100644 tests/components/current_based/test.esp8266.yaml create mode 100644 tests/components/current_based/test.rp2040.yaml create mode 100644 tests/components/cwww/test.esp32-c3-idf.yaml create mode 100644 tests/components/cwww/test.esp32-c3.yaml create mode 100644 tests/components/cwww/test.esp32-idf.yaml create mode 100644 tests/components/cwww/test.esp32.yaml create mode 100644 tests/components/cwww/test.esp8266.yaml create mode 100644 tests/components/cwww/test.rp2040.yaml diff --git a/tests/components/canbus/test.esp32-c3-idf.yaml b/tests/components/canbus/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fd146cc3a3 --- /dev/null +++ b/tests/components/canbus/test.esp32-c3-idf.yaml @@ -0,0 +1,46 @@ +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 4 + tx_pin: 5 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: Truth + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + } + +button: + - platform: template + name: Canbus Actions + on_press: + - canbus.send: "abc" + - canbus.send: [0, 1, 2] + - canbus.send: !lambda return {0, 1, 2}; diff --git a/tests/components/canbus/test.esp32-c3.yaml b/tests/components/canbus/test.esp32-c3.yaml new file mode 100644 index 0000000000..fd146cc3a3 --- /dev/null +++ b/tests/components/canbus/test.esp32-c3.yaml @@ -0,0 +1,46 @@ +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 4 + tx_pin: 5 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: Truth + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + } + +button: + - platform: template + name: Canbus Actions + on_press: + - canbus.send: "abc" + - canbus.send: [0, 1, 2] + - canbus.send: !lambda return {0, 1, 2}; diff --git a/tests/components/canbus/test.esp32-idf.yaml b/tests/components/canbus/test.esp32-idf.yaml new file mode 100644 index 0000000000..fd146cc3a3 --- /dev/null +++ b/tests/components/canbus/test.esp32-idf.yaml @@ -0,0 +1,46 @@ +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 4 + tx_pin: 5 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: Truth + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + } + +button: + - platform: template + name: Canbus Actions + on_press: + - canbus.send: "abc" + - canbus.send: [0, 1, 2] + - canbus.send: !lambda return {0, 1, 2}; diff --git a/tests/components/canbus/test.esp32.yaml b/tests/components/canbus/test.esp32.yaml new file mode 100644 index 0000000000..fd146cc3a3 --- /dev/null +++ b/tests/components/canbus/test.esp32.yaml @@ -0,0 +1,46 @@ +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 4 + tx_pin: 5 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: Truth + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + } + +button: + - platform: template + name: Canbus Actions + on_press: + - canbus.send: "abc" + - canbus.send: [0, 1, 2] + - canbus.send: !lambda return {0, 1, 2}; diff --git a/tests/components/cap1188/test.esp32-c3-idf.yaml b/tests/components/cap1188/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c6d3c95942 --- /dev/null +++ b/tests/components/cap1188/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 6 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp32-c3.yaml b/tests/components/cap1188/test.esp32-c3.yaml new file mode 100644 index 0000000000..c6d3c95942 --- /dev/null +++ b/tests/components/cap1188/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 6 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp32-idf.yaml b/tests/components/cap1188/test.esp32-idf.yaml new file mode 100644 index 0000000000..efd1d60217 --- /dev/null +++ b/tests/components/cap1188/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 16 + sda: 17 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 15 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp32.yaml b/tests/components/cap1188/test.esp32.yaml new file mode 100644 index 0000000000..efd1d60217 --- /dev/null +++ b/tests/components/cap1188/test.esp32.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 16 + sda: 17 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 15 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp8266.yaml b/tests/components/cap1188/test.esp8266.yaml new file mode 100644 index 0000000000..7573d45140 --- /dev/null +++ b/tests/components/cap1188/test.esp8266.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 15 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.rp2040.yaml b/tests/components/cap1188/test.rp2040.yaml new file mode 100644 index 0000000000..c6d3c95942 --- /dev/null +++ b/tests/components/cap1188/test.rp2040.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 6 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/captive_portal/test.esp32-c3-idf.yaml b/tests/components/captive_portal/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25bc4a887a --- /dev/null +++ b/tests/components/captive_portal/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +wifi: + ssid: MySSID + password: password1 + +captive_portal: diff --git a/tests/components/captive_portal/test.esp32-c3.yaml b/tests/components/captive_portal/test.esp32-c3.yaml new file mode 100644 index 0000000000..25bc4a887a --- /dev/null +++ b/tests/components/captive_portal/test.esp32-c3.yaml @@ -0,0 +1,5 @@ +wifi: + ssid: MySSID + password: password1 + +captive_portal: diff --git a/tests/components/captive_portal/test.esp32-idf.yaml b/tests/components/captive_portal/test.esp32-idf.yaml new file mode 100644 index 0000000000..25bc4a887a --- /dev/null +++ b/tests/components/captive_portal/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +wifi: + ssid: MySSID + password: password1 + +captive_portal: diff --git a/tests/components/captive_portal/test.esp32.yaml b/tests/components/captive_portal/test.esp32.yaml new file mode 100644 index 0000000000..25bc4a887a --- /dev/null +++ b/tests/components/captive_portal/test.esp32.yaml @@ -0,0 +1,5 @@ +wifi: + ssid: MySSID + password: password1 + +captive_portal: diff --git a/tests/components/captive_portal/test.esp8266.yaml b/tests/components/captive_portal/test.esp8266.yaml new file mode 100644 index 0000000000..25bc4a887a --- /dev/null +++ b/tests/components/captive_portal/test.esp8266.yaml @@ -0,0 +1,5 @@ +wifi: + ssid: MySSID + password: password1 + +captive_portal: diff --git a/tests/components/ccs811/test.esp32-c3-idf.yaml b/tests/components/ccs811/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp32-c3.yaml b/tests/components/ccs811/test.esp32-c3.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.esp32-c3.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp32-idf.yaml b/tests/components/ccs811/test.esp32-idf.yaml new file mode 100644 index 0000000000..08b3a48cc7 --- /dev/null +++ b/tests/components/ccs811/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 16 + sda: 17 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp32.yaml b/tests/components/ccs811/test.esp32.yaml new file mode 100644 index 0000000000..08b3a48cc7 --- /dev/null +++ b/tests/components/ccs811/test.esp32.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 16 + sda: 17 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp8266.yaml b/tests/components/ccs811/test.esp8266.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.esp8266.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.rp2040.yaml b/tests/components/ccs811/test.rp2040.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.rp2040.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml b/tests/components/cd74hc4067/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5aa653d26c --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 2 + pin_s1: 3 + pin_s2: 4 + pin_s3: 5 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp32-c3.yaml b/tests/components/cd74hc4067/test.esp32-c3.yaml new file mode 100644 index 0000000000..5aa653d26c --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32-c3.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 2 + pin_s1: 3 + pin_s2: 4 + pin_s3: 5 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp32-idf.yaml b/tests/components/cd74hc4067/test.esp32-idf.yaml new file mode 100644 index 0000000000..71a1238ccc --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 12 + pin_s1: 13 + pin_s2: 14 + pin_s3: 15 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp32.yaml b/tests/components/cd74hc4067/test.esp32.yaml new file mode 100644 index 0000000000..71a1238ccc --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 12 + pin_s1: 13 + pin_s2: 14 + pin_s3: 15 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp8266.yaml b/tests/components/cd74hc4067/test.esp8266.yaml new file mode 100644 index 0000000000..8bcce5bc17 --- /dev/null +++ b/tests/components/cd74hc4067/test.esp8266.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 12 + pin_s1: 13 + pin_s2: 14 + pin_s3: 15 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: A0 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.rp2040.yaml b/tests/components/cd74hc4067/test.rp2040.yaml new file mode 100644 index 0000000000..75adcce796 --- /dev/null +++ b/tests/components/cd74hc4067/test.rp2040.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 2 + pin_s1: 3 + pin_s2: 4 + pin_s3: 5 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 26 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp32-c3.yaml b/tests/components/climate_ir_lg/test.esp32-c3.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32-c3.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp32-idf.yaml b/tests/components/climate_ir_lg/test.esp32-idf.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp32.yaml b/tests/components/climate_ir_lg/test.esp32.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp8266.yaml b/tests/components/climate_ir_lg/test.esp8266.yaml new file mode 100644 index 0000000000..7482bf0580 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp8266.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/color/test.esp32-c3-idf.yaml b/tests/components/color/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7aa308bb63 --- /dev/null +++ b/tests/components/color/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + - id: kbx_green + hex: "3DEC55" diff --git a/tests/components/color/test.esp32-c3.yaml b/tests/components/color/test.esp32-c3.yaml new file mode 100644 index 0000000000..7aa308bb63 --- /dev/null +++ b/tests/components/color/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + - id: kbx_green + hex: "3DEC55" diff --git a/tests/components/color/test.esp32-idf.yaml b/tests/components/color/test.esp32-idf.yaml new file mode 100644 index 0000000000..7aa308bb63 --- /dev/null +++ b/tests/components/color/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + - id: kbx_green + hex: "3DEC55" diff --git a/tests/components/color/test.esp32.yaml b/tests/components/color/test.esp32.yaml new file mode 100644 index 0000000000..7aa308bb63 --- /dev/null +++ b/tests/components/color/test.esp32.yaml @@ -0,0 +1,11 @@ +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + - id: kbx_green + hex: "3DEC55" diff --git a/tests/components/color/test.esp8266.yaml b/tests/components/color/test.esp8266.yaml new file mode 100644 index 0000000000..7aa308bb63 --- /dev/null +++ b/tests/components/color/test.esp8266.yaml @@ -0,0 +1,11 @@ +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + - id: kbx_green + hex: "3DEC55" diff --git a/tests/components/color/test.rp2040.yaml b/tests/components/color/test.rp2040.yaml new file mode 100644 index 0000000000..7aa308bb63 --- /dev/null +++ b/tests/components/color/test.rp2040.yaml @@ -0,0 +1,11 @@ +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + - id: kbx_green + hex: "3DEC55" diff --git a/tests/components/color_temperature/test.esp32-c3-idf.yaml b/tests/components/color_temperature/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8d3faa54ee --- /dev/null +++ b/tests/components/color_temperature/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp32-c3.yaml b/tests/components/color_temperature/test.esp32-c3.yaml new file mode 100644 index 0000000000..8d3faa54ee --- /dev/null +++ b/tests/components/color_temperature/test.esp32-c3.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp32-idf.yaml b/tests/components/color_temperature/test.esp32-idf.yaml new file mode 100644 index 0000000000..608907d2fc --- /dev/null +++ b/tests/components/color_temperature/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp32.yaml b/tests/components/color_temperature/test.esp32.yaml new file mode 100644 index 0000000000..608907d2fc --- /dev/null +++ b/tests/components/color_temperature/test.esp32.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp8266.yaml b/tests/components/color_temperature/test.esp8266.yaml new file mode 100644 index 0000000000..ed0bfb6aa4 --- /dev/null +++ b/tests/components/color_temperature/test.esp8266.yaml @@ -0,0 +1,15 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.rp2040.yaml b/tests/components/color_temperature/test.rp2040.yaml new file mode 100644 index 0000000000..887ad1c857 --- /dev/null +++ b/tests/components/color_temperature/test.rp2040.yaml @@ -0,0 +1,15 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/combination/test.esp32-c3-idf.yaml b/tests/components/combination/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..62246190af --- /dev/null +++ b/tests/components/combination/test.esp32-c3-idf.yaml @@ -0,0 +1,76 @@ +sensor: + - platform: template + id: template_temperature1 + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature2 + lambda: |- + if (millis() > 20000) { + return 0.8; + } else { + return 0.0; + } + - platform: combination + type: kalman + name: Kalman-filtered temperature + process_std_dev: 0.00139 + sources: + - source: template_temperature1 + error: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: template_temperature1 + coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 diff --git a/tests/components/combination/test.esp32-c3.yaml b/tests/components/combination/test.esp32-c3.yaml new file mode 100644 index 0000000000..62246190af --- /dev/null +++ b/tests/components/combination/test.esp32-c3.yaml @@ -0,0 +1,76 @@ +sensor: + - platform: template + id: template_temperature1 + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature2 + lambda: |- + if (millis() > 20000) { + return 0.8; + } else { + return 0.0; + } + - platform: combination + type: kalman + name: Kalman-filtered temperature + process_std_dev: 0.00139 + sources: + - source: template_temperature1 + error: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: template_temperature1 + coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 diff --git a/tests/components/combination/test.esp32-idf.yaml b/tests/components/combination/test.esp32-idf.yaml new file mode 100644 index 0000000000..62246190af --- /dev/null +++ b/tests/components/combination/test.esp32-idf.yaml @@ -0,0 +1,76 @@ +sensor: + - platform: template + id: template_temperature1 + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature2 + lambda: |- + if (millis() > 20000) { + return 0.8; + } else { + return 0.0; + } + - platform: combination + type: kalman + name: Kalman-filtered temperature + process_std_dev: 0.00139 + sources: + - source: template_temperature1 + error: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: template_temperature1 + coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 diff --git a/tests/components/combination/test.esp32.yaml b/tests/components/combination/test.esp32.yaml new file mode 100644 index 0000000000..62246190af --- /dev/null +++ b/tests/components/combination/test.esp32.yaml @@ -0,0 +1,76 @@ +sensor: + - platform: template + id: template_temperature1 + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature2 + lambda: |- + if (millis() > 20000) { + return 0.8; + } else { + return 0.0; + } + - platform: combination + type: kalman + name: Kalman-filtered temperature + process_std_dev: 0.00139 + sources: + - source: template_temperature1 + error: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: template_temperature1 + coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 diff --git a/tests/components/combination/test.esp8266.yaml b/tests/components/combination/test.esp8266.yaml new file mode 100644 index 0000000000..62246190af --- /dev/null +++ b/tests/components/combination/test.esp8266.yaml @@ -0,0 +1,76 @@ +sensor: + - platform: template + id: template_temperature1 + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature2 + lambda: |- + if (millis() > 20000) { + return 0.8; + } else { + return 0.0; + } + - platform: combination + type: kalman + name: Kalman-filtered temperature + process_std_dev: 0.00139 + sources: + - source: template_temperature1 + error: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: template_temperature1 + coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 diff --git a/tests/components/combination/test.rp2040.yaml b/tests/components/combination/test.rp2040.yaml new file mode 100644 index 0000000000..62246190af --- /dev/null +++ b/tests/components/combination/test.rp2040.yaml @@ -0,0 +1,76 @@ +sensor: + - platform: template + id: template_temperature1 + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature2 + lambda: |- + if (millis() > 20000) { + return 0.8; + } else { + return 0.0; + } + - platform: combination + type: kalman + name: Kalman-filtered temperature + process_std_dev: 0.00139 + sources: + - source: template_temperature1 + error: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: template_temperature1 + coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 diff --git a/tests/components/coolix/test.esp32-c3-idf.yaml b/tests/components/coolix/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp32-c3.yaml b/tests/components/coolix/test.esp32-c3.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32-c3.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp32-idf.yaml b/tests/components/coolix/test.esp32-idf.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp32.yaml b/tests/components/coolix/test.esp32.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp8266.yaml b/tests/components/coolix/test.esp8266.yaml new file mode 100644 index 0000000000..61de8c7558 --- /dev/null +++ b/tests/components/coolix/test.esp8266.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/copy/test.esp32-c3-idf.yaml b/tests/components/copy/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..554638f462 --- /dev/null +++ b/tests/components/copy/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 2 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp32-c3.yaml b/tests/components/copy/test.esp32-c3.yaml new file mode 100644 index 0000000000..554638f462 --- /dev/null +++ b/tests/components/copy/test.esp32-c3.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 2 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp32-idf.yaml b/tests/components/copy/test.esp32-idf.yaml new file mode 100644 index 0000000000..806dbfe9f3 --- /dev/null +++ b/tests/components/copy/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp32.yaml b/tests/components/copy/test.esp32.yaml new file mode 100644 index 0000000000..806dbfe9f3 --- /dev/null +++ b/tests/components/copy/test.esp32.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp8266.yaml b/tests/components/copy/test.esp8266.yaml new file mode 100644 index 0000000000..1521e5f279 --- /dev/null +++ b/tests/components/copy/test.esp8266.yaml @@ -0,0 +1,23 @@ +output: + - platform: esp8266_pwm + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.rp2040.yaml b/tests/components/copy/test.rp2040.yaml new file mode 100644 index 0000000000..42e5eb8000 --- /dev/null +++ b/tests/components/copy/test.rp2040.yaml @@ -0,0 +1,23 @@ +output: + - platform: rp2040_pwm + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/cs5460a/test.esp32-c3-idf.yaml b/tests/components/cs5460a/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4ce21783a3 --- /dev/null +++ b/tests/components/cs5460a/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 8 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp32-c3.yaml b/tests/components/cs5460a/test.esp32-c3.yaml new file mode 100644 index 0000000000..4ce21783a3 --- /dev/null +++ b/tests/components/cs5460a/test.esp32-c3.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 8 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp32-idf.yaml b/tests/components/cs5460a/test.esp32-idf.yaml new file mode 100644 index 0000000000..e7eb1cbd73 --- /dev/null +++ b/tests/components/cs5460a/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 12 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp32.yaml b/tests/components/cs5460a/test.esp32.yaml new file mode 100644 index 0000000000..e7eb1cbd73 --- /dev/null +++ b/tests/components/cs5460a/test.esp32.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 12 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp8266.yaml b/tests/components/cs5460a/test.esp8266.yaml new file mode 100644 index 0000000000..c5a458d0ec --- /dev/null +++ b/tests/components/cs5460a/test.esp8266.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 15 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.rp2040.yaml b/tests/components/cs5460a/test.rp2040.yaml new file mode 100644 index 0000000000..f3daf7d72d --- /dev/null +++ b/tests/components/cs5460a/test.rp2040.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 6 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cse7761/test.esp32-c3-idf.yaml b/tests/components/cse7761/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp32-c3.yaml b/tests/components/cse7761/test.esp32-c3.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.esp32-c3.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp32-idf.yaml b/tests/components/cse7761/test.esp32-idf.yaml new file mode 100644 index 0000000000..4174e9a92e --- /dev/null +++ b/tests/components/cse7761/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp32.yaml b/tests/components/cse7761/test.esp32.yaml new file mode 100644 index 0000000000..4174e9a92e --- /dev/null +++ b/tests/components/cse7761/test.esp32.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp8266.yaml b/tests/components/cse7761/test.esp8266.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.esp8266.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.rp2040.yaml b/tests/components/cse7761/test.rp2040.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.rp2040.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7766/test.esp32-c3-idf.yaml b/tests/components/cse7766/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..432cc0a80e --- /dev/null +++ b/tests/components/cse7766/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp32-c3.yaml b/tests/components/cse7766/test.esp32-c3.yaml new file mode 100644 index 0000000000..432cc0a80e --- /dev/null +++ b/tests/components/cse7766/test.esp32-c3.yaml @@ -0,0 +1,16 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp32-idf.yaml b/tests/components/cse7766/test.esp32-idf.yaml new file mode 100644 index 0000000000..f94cd0f7d8 --- /dev/null +++ b/tests/components/cse7766/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp32.yaml b/tests/components/cse7766/test.esp32.yaml new file mode 100644 index 0000000000..f94cd0f7d8 --- /dev/null +++ b/tests/components/cse7766/test.esp32.yaml @@ -0,0 +1,16 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp8266.yaml b/tests/components/cse7766/test.esp8266.yaml new file mode 100644 index 0000000000..432cc0a80e --- /dev/null +++ b/tests/components/cse7766/test.esp8266.yaml @@ -0,0 +1,16 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.rp2040.yaml b/tests/components/cse7766/test.rp2040.yaml new file mode 100644 index 0000000000..432cc0a80e --- /dev/null +++ b/tests/components/cse7766/test.rp2040.yaml @@ -0,0 +1,16 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/ct_clamp/test.esp32-c3-idf.yaml b/tests/components/ct_clamp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e25acc95e1 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp32-c3.yaml b/tests/components/ct_clamp/test.esp32-c3.yaml new file mode 100644 index 0000000000..e25acc95e1 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32-c3.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp32-idf.yaml b/tests/components/ct_clamp/test.esp32-idf.yaml new file mode 100644 index 0000000000..1ea964fa96 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp32.yaml b/tests/components/ct_clamp/test.esp32.yaml new file mode 100644 index 0000000000..1ea964fa96 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp8266.yaml b/tests/components/ct_clamp/test.esp8266.yaml new file mode 100644 index 0000000000..9c7126480d --- /dev/null +++ b/tests/components/ct_clamp/test.esp8266.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: A0 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.rp2040.yaml b/tests/components/ct_clamp/test.rp2040.yaml new file mode 100644 index 0000000000..47077308aa --- /dev/null +++ b/tests/components/ct_clamp/test.rp2040.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 26 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/current_based/test.esp32-c3-idf.yaml b/tests/components/current_based/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..69ab8830c3 --- /dev/null +++ b/tests/components/current_based/test.esp32-c3-idf.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp32-c3.yaml b/tests/components/current_based/test.esp32-c3.yaml new file mode 100644 index 0000000000..69ab8830c3 --- /dev/null +++ b/tests/components/current_based/test.esp32-c3.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp32-idf.yaml b/tests/components/current_based/test.esp32-idf.yaml new file mode 100644 index 0000000000..90781120bc --- /dev/null +++ b/tests/components/current_based/test.esp32-idf.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp32.yaml b/tests/components/current_based/test.esp32.yaml new file mode 100644 index 0000000000..90781120bc --- /dev/null +++ b/tests/components/current_based/test.esp32.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp8266.yaml b/tests/components/current_based/test.esp8266.yaml new file mode 100644 index 0000000000..42d6d4676b --- /dev/null +++ b/tests/components/current_based/test.esp8266.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.rp2040.yaml b/tests/components/current_based/test.rp2040.yaml new file mode 100644 index 0000000000..69ab8830c3 --- /dev/null +++ b/tests/components/current_based/test.rp2040.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/cwww/test.esp32-c3-idf.yaml b/tests/components/cwww/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c829ca2a2b --- /dev/null +++ b/tests/components/cwww/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp32-c3.yaml b/tests/components/cwww/test.esp32-c3.yaml new file mode 100644 index 0000000000..c829ca2a2b --- /dev/null +++ b/tests/components/cwww/test.esp32-c3.yaml @@ -0,0 +1,16 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp32-idf.yaml b/tests/components/cwww/test.esp32-idf.yaml new file mode 100644 index 0000000000..f108d96ad3 --- /dev/null +++ b/tests/components/cwww/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp32.yaml b/tests/components/cwww/test.esp32.yaml new file mode 100644 index 0000000000..f108d96ad3 --- /dev/null +++ b/tests/components/cwww/test.esp32.yaml @@ -0,0 +1,16 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp8266.yaml b/tests/components/cwww/test.esp8266.yaml new file mode 100644 index 0000000000..50c311f616 --- /dev/null +++ b/tests/components/cwww/test.esp8266.yaml @@ -0,0 +1,16 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.rp2040.yaml b/tests/components/cwww/test.rp2040.yaml new file mode 100644 index 0000000000..505d67f862 --- /dev/null +++ b/tests/components/cwww/test.rp2040.yaml @@ -0,0 +1,16 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true From 71b3a14a29c13d9e82ccfa61bc44c47e2636aa07 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Sun, 11 Feb 2024 22:08:32 +0100 Subject: [PATCH 331/436] update docstrings in cpp_generator.py (#6212) --- esphome/cpp_generator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 909a786917..04616d97c2 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -478,7 +478,7 @@ def variable( :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ assert isinstance(id_, ID) rhs = safe_exp(rhs) @@ -526,7 +526,7 @@ def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ assert isinstance(id_, ID) rhs = safe_exp(rhs) @@ -549,7 +549,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ rhs = safe_exp(rhs) obj = MockObj(id_, "->") @@ -570,7 +570,7 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable: :param id_: The ID used to declare the variable (also specifies the type). :param args: The values to pass to the constructor. - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ if args and isinstance(args[0], TemplateArguments): id_ = id_.copy() From 061d5b49793da2f89de94f356de2e8614b13757c Mon Sep 17 00:00:00 2001 From: ChuckMash <86080247+ChuckMash@users.noreply.github.com> Date: Sun, 11 Feb 2024 14:55:06 -0800 Subject: [PATCH 332/436] Fixed group mask logic for WLED Sync fix (#6193) --- esphome/components/wled/wled_light_effect.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 7a82aaeb46..84842dff39 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -164,7 +164,7 @@ bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const u uint8_t payload_sync_group_mask = payload[34]; - if ((payload_sync_group_mask & this->sync_group_mask_) != this->sync_group_mask_) { + if (this->sync_group_mask_ && !(payload_sync_group_mask & this->sync_group_mask_)) { ESP_LOGD(TAG, "sync group mask does not match"); return false; } From e521662342bb5cb70e67b59b08c88619ca7b5c63 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Mon, 12 Feb 2024 15:38:50 -0500 Subject: [PATCH 333/436] Add micro_wake_word component (#6136) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/micro_wake_word/__init__.py | 363 +++++++++++++ .../audio_preprocessor_int8_model_data.h | 493 +++++++++++++++++ .../micro_wake_word/micro_wake_word.cpp | 503 ++++++++++++++++++ .../micro_wake_word/micro_wake_word.h | 204 +++++++ .../micro_wake_word/test.esp32-s3-idf.yaml | 15 + 6 files changed, 1579 insertions(+) create mode 100644 esphome/components/micro_wake_word/__init__.py create mode 100644 esphome/components/micro_wake_word/audio_preprocessor_int8_model_data.h create mode 100644 esphome/components/micro_wake_word/micro_wake_word.cpp create mode 100644 esphome/components/micro_wake_word/micro_wake_word.h create mode 100644 tests/components/micro_wake_word/test.esp32-s3-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index db44317776..56ec0a93cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -199,6 +199,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz +esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micronova/* @jorre05 esphome/components/microphone/* @jesserockz esphome/components/mics_4514/* @jesserockz diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py new file mode 100644 index 0000000000..2a84b7d74b --- /dev/null +++ b/esphome/components/micro_wake_word/__init__.py @@ -0,0 +1,363 @@ +import logging + +import json +import hashlib +from urllib.parse import urljoin +from pathlib import Path +import requests + +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.core import CORE, HexInt, EsphomeError + +from esphome.components import esp32, microphone +from esphome import automation, git, external_files +from esphome.automation import register_action, register_condition + + +from esphome.const import ( + __version__, + CONF_ID, + CONF_MICROPHONE, + CONF_MODEL, + CONF_URL, + CONF_FILE, + CONF_PATH, + CONF_REF, + CONF_REFRESH, + CONF_TYPE, + CONF_USERNAME, + CONF_PASSWORD, + CONF_RAW_DATA_ID, + TYPE_GIT, + TYPE_LOCAL, +) + + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@kahrendt", "@jesserockz"] +DEPENDENCIES = ["microphone"] +DOMAIN = "micro_wake_word" + +CONF_PROBABILITY_CUTOFF = "probability_cutoff" +CONF_SLIDING_WINDOW_AVERAGE_SIZE = "sliding_window_average_size" +CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" + +TYPE_HTTP = "http" + +micro_wake_word_ns = cg.esphome_ns.namespace("micro_wake_word") + +MicroWakeWord = micro_wake_word_ns.class_("MicroWakeWord", cg.Component) + +StartAction = micro_wake_word_ns.class_("StartAction", automation.Action) +StopAction = micro_wake_word_ns.class_("StopAction", automation.Action) + +IsRunningCondition = micro_wake_word_ns.class_( + "IsRunningCondition", automation.Condition +) + + +def _validate_json_filename(value): + value = cv.string(value) + if not value.endswith(".json"): + raise cv.Invalid("Manifest filename must end with .json") + return value + + +def _process_git_source(config): + repo_dir, _ = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=config[CONF_REFRESH], + domain=DOMAIN, + username=config.get(CONF_USERNAME), + password=config.get(CONF_PASSWORD), + ) + + if not (repo_dir / config[CONF_FILE]).exists(): + raise cv.Invalid("File does not exist in repository") + + return config + + +CV_GIT_SCHEMA = cv.GIT_SCHEMA +if isinstance(CV_GIT_SCHEMA, dict): + CV_GIT_SCHEMA = cv.Schema(CV_GIT_SCHEMA) + +GIT_SCHEMA = cv.All( + CV_GIT_SCHEMA.extend( + { + cv.Required(CONF_FILE): _validate_json_filename, + cv.Optional(CONF_REFRESH, default="1d"): cv.All( + cv.string, cv.source_refresh + ), + } + ), + _process_git_source, +) + +KEY_WAKE_WORD = "wake_word" +KEY_AUTHOR = "author" +KEY_WEBSITE = "website" +KEY_VERSION = "version" +KEY_MICRO = "micro" + +MANIFEST_SCHEMA_V1 = cv.Schema( + { + cv.Required(CONF_TYPE): "micro", + cv.Required(KEY_WAKE_WORD): cv.string, + cv.Required(KEY_AUTHOR): cv.string, + cv.Required(KEY_WEBSITE): cv.url, + cv.Required(KEY_VERSION): cv.All(cv.int_, 1), + cv.Required(CONF_MODEL): cv.string, + cv.Required(KEY_MICRO): cv.Schema( + { + cv.Required(CONF_PROBABILITY_CUTOFF): cv.float_, + cv.Required(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int, + } + ), + } +) + + +def _compute_local_file_path(config: dict) -> Path: + url = config[CONF_URL] + 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 _download_file(url: str, path: Path) -> bytes: + if not external_files.has_remote_file_changed(url, path): + _LOGGER.debug("Remote file has not changed, skipping download") + return path.read_bytes() + + try: + req = requests.get( + url, + timeout=external_files.NETWORK_TIMEOUT, + headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, + ) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download file from {url}: {e}") from e + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(req.content) + return req.content + + +def _process_http_source(config): + url = config[CONF_URL] + path = _compute_local_file_path(config) + + json_path = path / "manifest.json" + + json_contents = _download_file(url, json_path) + + manifest_data = json.loads(json_contents) + if not isinstance(manifest_data, dict): + raise cv.Invalid("Manifest file must contain a JSON object") + + try: + MANIFEST_SCHEMA_V1(manifest_data) + except cv.Invalid as e: + raise cv.Invalid(f"Invalid manifest file: {e}") from e + + model = manifest_data[CONF_MODEL] + model_url = urljoin(url, model) + + model_path = path / model + + _download_file(str(model_url), model_path) + + return config + + +HTTP_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _process_http_source, +) + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.All(_validate_json_filename, cv.file_), + } +) + + +def _validate_source_model_name(value): + if not isinstance(value, str): + raise cv.Invalid("Model name must be a string") + + if value.endswith(".json"): + raise cv.Invalid("Model name must not end with .json") + + return MODEL_SOURCE_SCHEMA( + { + CONF_TYPE: TYPE_HTTP, + CONF_URL: f"https://github.com/esphome/micro-wake-word-models/raw/main/models/{value}.json", + } + ) + + +def _validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + + try: # Test for model name + return _validate_source_model_name(value) + except cv.Invalid: + pass + + try: # Test for local path + return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) + except cv.Invalid: + pass + + try: # Test for http url + return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_HTTP, CONF_URL: value}) + except cv.Invalid: + pass + + git_file = git.GitFile.from_shorthand(value) + + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: git_file.git_url, + CONF_FILE: git_file.filename, + } + if git_file.ref: + conf[CONF_REF] = git_file.ref + + try: + return MODEL_SOURCE_SCHEMA(conf) + except cv.Invalid as e: + raise cv.Invalid( + f"Could not find file '{git_file.filename}' in the repository. Please make sure it exists." + ) from e + + +MODEL_SOURCE_SCHEMA = cv.Any( + _validate_source_shorthand, + cv.typed_schema( + { + TYPE_GIT: GIT_SCHEMA, + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_HTTP: HTTP_SCHEMA, + } + ), + msg="Not a valid model name, local path, http(s) url, or github shorthand", +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MicroWakeWord), + cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), + cv.Optional(CONF_PROBABILITY_CUTOFF): cv.float_, + cv.Optional(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int, + cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation( + single=True + ), + cv.Required(CONF_MODEL): MODEL_SOURCE_SCHEMA, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_with_esp_idf, +) + + +def _load_model_data(manifest_path: Path): + with open(manifest_path, encoding="utf-8") as f: + manifest = json.load(f) + + try: + MANIFEST_SCHEMA_V1(manifest) + except cv.Invalid as e: + raise EsphomeError(f"Invalid manifest file: {e}") from e + + model_path = urljoin(str(manifest_path), manifest[CONF_MODEL]) + + with open(model_path, "rb") as f: + model = f.read() + + return manifest, model + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + mic = await cg.get_variable(config[CONF_MICROPHONE]) + cg.add(var.set_microphone(mic)) + + if on_wake_word_detection_config := config.get(CONF_ON_WAKE_WORD_DETECTED): + await automation.build_automation( + var.get_wake_word_detected_trigger(), + [(cg.std_string, "wake_word")], + on_wake_word_detection_config, + ) + + esp32.add_idf_component( + name="esp-tflite-micro", + repo="https://github.com/espressif/esp-tflite-micro", + ) + + cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") + cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") + cg.add_build_flag("-DESP_NN") + + model_config = config.get(CONF_MODEL) + data = [] + if model_config[CONF_TYPE] == TYPE_GIT: + # compute path to model file + key = f"{model_config[CONF_URL]}@{model_config.get(CONF_REF)}" + base_dir = Path(CORE.data_dir) / DOMAIN + h = hashlib.new("sha256") + h.update(key.encode()) + file: Path = base_dir / h.hexdigest()[:8] / model_config[CONF_FILE] + + elif model_config[CONF_TYPE] == TYPE_LOCAL: + file = model_config[CONF_PATH] + + elif model_config[CONF_TYPE] == TYPE_HTTP: + file = _compute_local_file_path(model_config) / "manifest.json" + + manifest, data = _load_model_data(file) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_model_start(prog_arr)) + + probability_cutoff = config.get( + CONF_PROBABILITY_CUTOFF, manifest[KEY_MICRO][CONF_PROBABILITY_CUTOFF] + ) + cg.add(var.set_probability_cutoff(probability_cutoff)) + sliding_window_average_size = config.get( + CONF_SLIDING_WINDOW_AVERAGE_SIZE, + manifest[KEY_MICRO][CONF_SLIDING_WINDOW_AVERAGE_SIZE], + ) + cg.add(var.set_sliding_window_average_size(sliding_window_average_size)) + + cg.add(var.set_wake_word(manifest[KEY_WAKE_WORD])) + + +MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)}) + + +@register_action("micro_wake_word.start", StartAction, MICRO_WAKE_WORD_ACTION_SCHEMA) +@register_action("micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA) +@register_condition( + "micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA +) +async def micro_wake_word_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/micro_wake_word/audio_preprocessor_int8_model_data.h b/esphome/components/micro_wake_word/audio_preprocessor_int8_model_data.h new file mode 100644 index 0000000000..918e76045f --- /dev/null +++ b/esphome/components/micro_wake_word/audio_preprocessor_int8_model_data.h @@ -0,0 +1,493 @@ +#pragma once + +#ifdef USE_ESP_IDF + +// Converted audio_preprocessor_int8.tflite +// From https://github.com/tensorflow/tflite-micro/tree/main/tensorflow/lite/micro/examples/micro_speech/models accessed +// January 2024 +// +// Copyright 2023 The TensorFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace esphome { +namespace micro_wake_word { + +const unsigned char G_AUDIO_PREPROCESSOR_INT8_TFLITE[] = { + 0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x14, 0x00, 0x20, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10, + 0x00, 0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x88, 0x00, + 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x80, 0x0e, 0x00, 0x00, 0x90, 0x0e, 0x00, 0x00, 0xcc, 0x1f, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xe2, 0xeb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, + 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x94, 0xff, + 0xff, 0xff, 0x2a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x5f, 0x30, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc2, 0xf5, 0xff, 0xff, + 0x04, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xdc, 0xff, 0xff, 0xff, 0x2d, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x43, 0x4f, 0x4e, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, + 0x4e, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6d, 0x69, 0x6e, + 0x5f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x2e, 0x00, + 0x00, 0x00, 0x9c, 0x0d, 0x00, 0x00, 0x94, 0x0d, 0x00, 0x00, 0xc4, 0x09, 0x00, 0x00, 0x6c, 0x09, 0x00, 0x00, 0x48, + 0x09, 0x00, 0x00, 0x34, 0x09, 0x00, 0x00, 0x20, 0x09, 0x00, 0x00, 0x0c, 0x09, 0x00, 0x00, 0xf8, 0x08, 0x00, 0x00, + 0xec, 0x07, 0x00, 0x00, 0x88, 0x07, 0x00, 0x00, 0x24, 0x07, 0x00, 0x00, 0xc0, 0x06, 0x00, 0x00, 0x38, 0x04, 0x00, + 0x00, 0xb0, 0x01, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x60, 0x01, + 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x2c, + 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, + 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, + 0x00, 0xdc, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xbc, 0x00, + 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x94, + 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf2, 0xf6, 0xff, 0xff, + 0x04, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x32, 0x2e, 0x31, 0x32, 0x2e, 0x30, 0x00, + 0x00, 0x56, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x32, 0x2e, 0x38, 0x2e, 0x30, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0xe1, 0xff, 0xff, 0xd8, 0xe1, 0xff, 0xff, 0xdc, + 0xe1, 0xff, 0xff, 0xe0, 0xe1, 0xff, 0xff, 0xe4, 0xe1, 0xff, 0xff, 0xe8, 0xe1, 0xff, 0xff, 0xec, 0xe1, 0xff, 0xff, + 0xf0, 0xe1, 0xff, 0xff, 0xf4, 0xe1, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xff, 0xfc, 0xe1, 0xff, 0xff, 0x00, 0xe2, 0xff, + 0xff, 0x04, 0xe2, 0xff, 0xff, 0x08, 0xe2, 0xff, 0xff, 0x0c, 0xe2, 0xff, 0xff, 0x10, 0xe2, 0xff, 0xff, 0x14, 0xe2, + 0xff, 0xff, 0x18, 0xe2, 0xff, 0xff, 0x1c, 0xe2, 0xff, 0xff, 0x20, 0xe2, 0xff, 0xff, 0x24, 0xe2, 0xff, 0xff, 0x28, + 0xe2, 0xff, 0xff, 0x2c, 0xe2, 0xff, 0xff, 0x30, 0xe2, 0xff, 0xff, 0xd2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0xe2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0xf2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x80, 0xff, + 0xff, 0xff, 0x02, 0xf8, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x12, + 0xf8, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x22, 0xf8, 0xff, 0xff, + 0x04, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, 0x00, 0x00, 0x23, 0x0b, 0x41, + 0x01, 0x00, 0x00, 0x00, 0x00, 0xb3, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x0e, 0x80, 0x05, + 0x00, 0x00, 0x00, 0x00, 0xd1, 0x0c, 0x63, 0x04, 0x00, 0x00, 0x00, 0x00, 0x34, 0x0c, 0x3f, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x81, 0x0c, 0xf7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x0d, 0x77, 0x06, 0x00, 0x00, 0x00, 0x00, 0x7b, 0x0f, + 0xa9, 0x08, 0x01, 0x02, 0x7f, 0x0b, 0x22, 0x05, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x0e, 0xd1, 0x08, 0xdb, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x0d, 0x4a, 0x07, 0xad, 0x01, 0x2c, 0x0c, 0xc6, 0x06, 0x79, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x45, 0x0c, 0x29, 0x07, 0x23, 0x02, 0x34, 0x0d, 0x5b, 0x08, 0x96, 0x03, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x0e, 0x48, + 0x0a, 0xbd, 0x05, 0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0x0c, 0x88, 0x08, 0x43, 0x04, + 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x0b, 0xd3, 0x07, 0xcb, 0x03, 0xd2, 0x0f, 0xe7, + 0x0b, 0x09, 0x08, 0x39, 0x04, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0x0c, 0x14, 0x09, + 0x75, 0x05, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x0e, 0xdd, 0x0a, 0x6b, 0x07, 0x03, + 0x04, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x0d, 0x09, 0x0a, 0xc9, 0x06, 0x93, 0x03, 0x65, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x0d, 0x25, 0x0a, 0x12, 0x07, 0x07, 0x04, 0x05, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x0a, 0x0e, 0x17, 0x0b, 0x2c, 0x08, 0x49, 0x05, 0x6d, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x98, 0x0f, 0xcb, 0x0c, 0x04, 0x0a, 0x44, 0x07, 0x8b, 0x04, 0xd8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x0f, 0x87, + 0x0c, 0xe7, 0x09, 0x4e, 0x07, 0xba, 0x04, 0x2d, 0x02, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x0f, 0x23, 0x0d, 0xa7, 0x0a, + 0x30, 0x08, 0xbe, 0x05, 0x52, 0x03, 0xeb, 0x00, 0x89, 0x0e, 0x2c, 0x0c, 0xd4, 0x09, 0x81, 0x07, 0x33, 0x05, 0xe9, + 0x02, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x0e, 0x29, 0x0c, 0xf1, 0x09, 0xbe, 0x07, 0x90, 0x05, 0x65, 0x03, + 0x3f, 0x01, 0x1d, 0x0f, 0xff, 0x0c, 0xe5, 0x0a, 0xcf, 0x08, 0xbc, 0x06, 0xae, 0x04, 0xa3, 0x02, 0x9c, 0x00, 0x99, + 0x0e, 0x99, 0x0c, 0x9d, 0x0a, 0xa4, 0x08, 0xaf, 0x06, 0xbd, 0x04, 0xcf, 0x02, 0xe4, 0x00, 0xfc, 0x0e, 0x17, 0x0d, + 0x36, 0x0b, 0x57, 0x09, 0x7c, 0x07, 0xa4, 0x05, 0xcf, 0x03, 0xfd, 0x01, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x62, 0x0e, 0x98, 0x0c, 0xd2, 0x0a, 0x0e, 0x09, 0x4d, 0x07, 0x8f, 0x05, 0xd4, 0x03, 0x1b, 0x02, + 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb1, 0x0e, 0x00, 0x0d, 0x52, 0x0b, 0xa6, 0x09, 0xfd, 0x07, 0x56, 0x06, 0xb1, + 0x04, 0x0f, 0x03, 0x6f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x0f, 0x37, 0x0e, 0x9e, 0x0c, + 0x08, 0x0b, 0x73, 0x09, 0xe1, 0x07, 0x52, 0x06, 0xc4, 0x04, 0x38, 0x03, 0xaf, 0x01, 0x28, 0x00, 0xa3, 0x0e, 0x1f, + 0x0d, 0x9e, 0x0b, 0x1f, 0x0a, 0xa2, 0x08, 0x27, 0x07, 0xae, 0x05, 0x37, 0x04, 0xc2, 0x02, 0x4e, 0x01, 0x00, 0x00, + 0x00, 0x00, 0xdd, 0x0f, 0x6d, 0x0e, 0xff, 0x0c, 0x93, 0x0b, 0x29, 0x0a, 0xc1, 0x08, 0x5a, 0x07, 0xf5, 0x05, 0x92, + 0x04, 0x30, 0x03, 0xd1, 0x01, 0x73, 0x00, 0x16, 0x0f, 0xbc, 0x0d, 0x62, 0x0c, 0x0b, 0x0b, 0xb5, 0x09, 0x61, 0x08, + 0x0e, 0x07, 0xbd, 0x05, 0x6d, 0x04, 0x1f, 0x03, 0xd3, 0x01, 0x88, 0x00, 0x3e, 0x0f, 0xf6, 0x0d, 0xaf, 0x0c, 0x6a, + 0x0b, 0x27, 0x0a, 0xe4, 0x08, 0xa3, 0x07, 0x64, 0x06, 0x26, 0x05, 0xe9, 0x03, 0xae, 0x02, 0x74, 0x01, 0x3b, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0f, 0xce, 0x0d, 0x99, 0x0c, 0x66, 0x0b, 0x34, 0x0a, 0x03, + 0x09, 0xd3, 0x07, 0xa5, 0x06, 0x78, 0x05, 0x4c, 0x04, 0x22, 0x03, 0xf8, 0x01, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xa9, 0x0f, 0x83, 0x0e, 0x5f, 0x0d, 0x3b, 0x0c, 0x19, 0x0b, 0xf8, 0x09, 0xd8, 0x08, 0xb9, 0x07, 0x9b, 0x06, 0x7e, + 0x05, 0x63, 0x04, 0x48, 0x03, 0x2f, 0x02, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa6, 0xfa, 0xff, 0xff, 0x04, 0x00, + 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x9e, 0x0a, 0x00, 0x00, 0x00, 0x00, 0xdc, 0x04, 0xbe, 0x0e, 0x00, + 0x00, 0x00, 0x00, 0x4c, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x01, 0x7f, 0x0a, 0x00, 0x00, + 0x00, 0x00, 0x2e, 0x03, 0x9c, 0x0b, 0x00, 0x00, 0x00, 0x00, 0xcb, 0x03, 0xc0, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x7e, + 0x03, 0x08, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x60, 0x02, 0x88, 0x09, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x56, 0x07, + 0xfe, 0x0d, 0x80, 0x04, 0xdd, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x16, 0x01, 0x2e, 0x07, 0x24, 0x0d, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x02, 0xb5, 0x08, 0x52, 0x0e, 0xd3, 0x03, 0x39, 0x09, 0x86, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xba, 0x03, + 0xd6, 0x08, 0xdc, 0x0d, 0xcb, 0x02, 0xa4, 0x07, 0x69, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x01, 0xb7, 0x05, 0x42, + 0x0a, 0xba, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x03, 0x77, 0x07, 0xbc, 0x0b, 0xf1, 0x0f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x04, 0x2c, 0x08, 0x34, 0x0c, 0x2d, 0x00, 0x18, 0x04, 0xf6, + 0x07, 0xc6, 0x0b, 0x89, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x03, 0xeb, 0x06, 0x8a, 0x0a, + 0x1d, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x22, 0x05, 0x94, 0x08, 0xfc, 0x0b, 0x59, + 0x0f, 0x00, 0x00, 0x00, 0x00, 0xac, 0x02, 0xf6, 0x05, 0x36, 0x09, 0x6c, 0x0c, 0x9a, 0x0f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xbe, 0x02, 0xda, 0x05, 0xed, 0x08, 0xf8, 0x0b, 0xfa, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xf5, + 0x01, 0xe8, 0x04, 0xd3, 0x07, 0xb6, 0x0a, 0x92, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0x00, + 0x34, 0x03, 0xfb, 0x05, 0xbb, 0x08, 0x74, 0x0b, 0x27, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x78, 0x03, 0x18, + 0x06, 0xb1, 0x08, 0x45, 0x0b, 0xd2, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x00, 0xdc, 0x02, 0x58, 0x05, 0xcf, 0x07, + 0x41, 0x0a, 0xad, 0x0c, 0x14, 0x0f, 0x76, 0x01, 0xd3, 0x03, 0x2b, 0x06, 0x7e, 0x08, 0xcc, 0x0a, 0x16, 0x0d, 0x5a, + 0x0f, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x01, 0xd6, 0x03, 0x0e, 0x06, 0x41, 0x08, 0x6f, 0x0a, 0x9a, 0x0c, 0xc0, 0x0e, + 0xe2, 0x00, 0x00, 0x03, 0x1a, 0x05, 0x30, 0x07, 0x43, 0x09, 0x51, 0x0b, 0x5c, 0x0d, 0x63, 0x0f, 0x66, 0x01, 0x66, + 0x03, 0x62, 0x05, 0x5b, 0x07, 0x50, 0x09, 0x42, 0x0b, 0x30, 0x0d, 0x1b, 0x0f, 0x03, 0x01, 0xe8, 0x02, 0xc9, 0x04, + 0xa8, 0x06, 0x83, 0x08, 0x5b, 0x0a, 0x30, 0x0c, 0x02, 0x0e, 0xd1, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x9d, 0x01, 0x67, 0x03, 0x2d, 0x05, 0xf1, 0x06, 0xb2, 0x08, 0x70, 0x0a, 0x2b, 0x0c, 0xe4, 0x0d, 0x9a, 0x0f, + 0x00, 0x00, 0x00, 0x00, 0x4e, 0x01, 0xff, 0x02, 0xad, 0x04, 0x59, 0x06, 0x02, 0x08, 0xa9, 0x09, 0x4e, 0x0b, 0xf0, + 0x0c, 0x90, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0xc8, 0x01, 0x61, 0x03, 0xf7, 0x04, + 0x8c, 0x06, 0x1e, 0x08, 0xad, 0x09, 0x3b, 0x0b, 0xc7, 0x0c, 0x50, 0x0e, 0xd7, 0x0f, 0x5c, 0x01, 0xe0, 0x02, 0x61, + 0x04, 0xe0, 0x05, 0x5d, 0x07, 0xd8, 0x08, 0x51, 0x0a, 0xc8, 0x0b, 0x3d, 0x0d, 0xb1, 0x0e, 0x00, 0x00, 0x00, 0x00, + 0x22, 0x00, 0x92, 0x01, 0x00, 0x03, 0x6c, 0x04, 0xd6, 0x05, 0x3e, 0x07, 0xa5, 0x08, 0x0a, 0x0a, 0x6d, 0x0b, 0xcf, + 0x0c, 0x2e, 0x0e, 0x8c, 0x0f, 0xe9, 0x00, 0x43, 0x02, 0x9d, 0x03, 0xf4, 0x04, 0x4a, 0x06, 0x9e, 0x07, 0xf1, 0x08, + 0x42, 0x0a, 0x92, 0x0b, 0xe0, 0x0c, 0x2c, 0x0e, 0x77, 0x0f, 0xc1, 0x00, 0x09, 0x02, 0x50, 0x03, 0x95, 0x04, 0xd8, + 0x05, 0x1b, 0x07, 0x5c, 0x08, 0x9b, 0x09, 0xd9, 0x0a, 0x16, 0x0c, 0x51, 0x0d, 0x8b, 0x0e, 0xc4, 0x0f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x31, 0x02, 0x66, 0x03, 0x99, 0x04, 0xcb, 0x05, 0xfc, 0x06, 0x2c, + 0x08, 0x5a, 0x09, 0x87, 0x0a, 0xb3, 0x0b, 0xdd, 0x0c, 0x07, 0x0e, 0x2f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, + 0x7c, 0x01, 0xa0, 0x02, 0xc4, 0x03, 0xe6, 0x04, 0x07, 0x06, 0x27, 0x07, 0x46, 0x08, 0x64, 0x09, 0x81, 0x0a, 0x9c, + 0x0b, 0xb7, 0x0c, 0xd0, 0x0d, 0xe8, 0x0e, 0x00, 0x10, 0x00, 0x00, 0x2a, 0xfd, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x06, 0x00, 0x08, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0e, 0x00, 0x10, + 0x00, 0x12, 0x00, 0x16, 0x00, 0x18, 0x00, 0x1a, 0x00, 0x1e, 0x00, 0x20, 0x00, 0x24, 0x00, 0x26, 0x00, 0x2a, 0x00, + 0x2e, 0x00, 0x32, 0x00, 0x36, 0x00, 0x3a, 0x00, 0x40, 0x00, 0x44, 0x00, 0x4a, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x5a, + 0x00, 0x62, 0x00, 0x68, 0x00, 0x70, 0x00, 0x78, 0x00, 0x80, 0x00, 0x88, 0x00, 0x92, 0x00, 0x9a, 0x00, 0xa6, 0x00, + 0xb0, 0x00, 0xbc, 0x00, 0xc8, 0x00, 0xd4, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x8a, 0xfd, 0xff, 0xff, 0x04, 0x00, 0x00, + 0x00, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x14, 0x00, 0x18, 0x00, + 0x1c, 0x00, 0x20, 0x00, 0x24, 0x00, 0x28, 0x00, 0x2c, 0x00, 0x30, 0x00, 0x34, 0x00, 0x38, 0x00, 0x3c, 0x00, 0x44, + 0x00, 0x4c, 0x00, 0x50, 0x00, 0x58, 0x00, 0x60, 0x00, 0x68, 0x00, 0x70, 0x00, 0x78, 0x00, 0x80, 0x00, 0x88, 0x00, + 0x90, 0x00, 0x98, 0x00, 0xa0, 0x00, 0xa8, 0x00, 0xb0, 0x00, 0xb8, 0x00, 0xc4, 0x00, 0xd0, 0x00, 0xdc, 0x00, 0xe8, + 0x00, 0xf4, 0x00, 0x00, 0x01, 0x0c, 0x01, 0x1c, 0x01, 0x2c, 0x01, 0x00, 0x00, 0xea, 0xfd, 0xff, 0xff, 0x04, 0x00, + 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, + 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x08, 0x00, + 0x08, 0x00, 0x04, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, + 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x4a, 0xfe, 0xff, 0xff, 0x04, + 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x7c, 0x7f, 0x79, 0x7f, 0x76, 0x7f, 0xfa, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x7f, 0xf4, 0xff, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7f, 0xe9, 0xff, 0xfe, 0xff, 0x00, 0x00, 0x4b, 0x7f, 0xd0, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x7f, 0xa0, 0xff, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x7e, 0x42, 0xff, 0x00, 0x00, + 0x00, 0x00, 0xfd, 0x7d, 0x86, 0xfe, 0x04, 0x00, 0x00, 0x00, 0x87, 0x7c, 0x1d, 0xfd, 0x12, 0x00, 0x00, 0x00, 0xb6, + 0x79, 0x7f, 0xfa, 0x3e, 0x00, 0x00, 0x00, 0x73, 0x74, 0xf9, 0xf5, 0xca, 0x00, 0x00, 0x00, 0x36, 0x6b, 0x33, 0xef, + 0x32, 0x02, 0x00, 0x00, 0x9b, 0x5c, 0x87, 0xe7, 0xce, 0x04, 0x00, 0x00, 0xf0, 0x48, 0xde, 0xe2, 0xa0, 0x07, 0x00, + 0x00, 0x6e, 0x33, 0x8a, 0xe4, 0xa4, 0x08, 0x00, 0x00, 0x9c, 0x20, 0x22, 0xeb, 0x4c, 0x07, 0x00, 0x00, 0x0a, 0x13, + 0x7d, 0xf2, 0x02, 0x05, 0x00, 0x00, 0x89, 0x0a, 0x17, 0xf8, 0x06, 0x03, 0x00, 0x00, 0xa6, 0x05, 0xa0, 0xfb, 0xb4, + 0x01, 0x00, 0x00, 0xfa, 0x02, 0xac, 0xfd, 0xe8, 0x00, 0x00, 0x00, 0x8e, 0x01, 0xc7, 0xfe, 0x7a, 0x00, 0x00, 0x00, + 0xcf, 0x00, 0x5c, 0xff, 0x40, 0x00, 0x00, 0x00, 0x6b, 0x00, 0xab, 0xff, 0x22, 0x00, 0x00, 0x00, 0x38, 0x00, 0xd3, + 0xff, 0x12, 0x00, 0x00, 0x00, 0x1d, 0x00, 0xea, 0xff, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xf3, 0xff, 0x06, 0x00, + 0x00, 0x00, 0x08, 0x00, 0xf8, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0xfd, 0xff, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xfd, 0xff, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x62, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x72, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x82, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, + 0x92, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb2, 0xff, 0xff, 0xff, 0x04, 0x00, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x02, 0x00, 0x04, 0x00, 0x05, 0x00, 0x07, 0x00, 0x0a, 0x00, 0x0d, 0x00, 0x10, 0x00, 0x13, 0x00, 0x17, 0x00, + 0x1b, 0x00, 0x20, 0x00, 0x25, 0x00, 0x2a, 0x00, 0x30, 0x00, 0x35, 0x00, 0x3c, 0x00, 0x42, 0x00, 0x49, 0x00, 0x51, + 0x00, 0x58, 0x00, 0x60, 0x00, 0x68, 0x00, 0x71, 0x00, 0x7a, 0x00, 0x83, 0x00, 0x8d, 0x00, 0x97, 0x00, 0xa1, 0x00, + 0xac, 0x00, 0xb7, 0x00, 0xc2, 0x00, 0xcd, 0x00, 0xd9, 0x00, 0xe5, 0x00, 0xf2, 0x00, 0xff, 0x00, 0x0c, 0x01, 0x19, + 0x01, 0x27, 0x01, 0x35, 0x01, 0x43, 0x01, 0x52, 0x01, 0x61, 0x01, 0x70, 0x01, 0x7f, 0x01, 0x8f, 0x01, 0x9f, 0x01, + 0xaf, 0x01, 0xc0, 0x01, 0xd1, 0x01, 0xe2, 0x01, 0xf3, 0x01, 0x05, 0x02, 0x17, 0x02, 0x29, 0x02, 0x3c, 0x02, 0x4e, + 0x02, 0x61, 0x02, 0x75, 0x02, 0x88, 0x02, 0x9c, 0x02, 0xb0, 0x02, 0xc4, 0x02, 0xd8, 0x02, 0xed, 0x02, 0x02, 0x03, + 0x17, 0x03, 0x2c, 0x03, 0x41, 0x03, 0x57, 0x03, 0x6d, 0x03, 0x83, 0x03, 0x99, 0x03, 0xb0, 0x03, 0xc7, 0x03, 0xdd, + 0x03, 0xf4, 0x03, 0x0c, 0x04, 0x23, 0x04, 0x3b, 0x04, 0x52, 0x04, 0x6a, 0x04, 0x82, 0x04, 0x9a, 0x04, 0xb3, 0x04, + 0xcb, 0x04, 0xe4, 0x04, 0xfd, 0x04, 0x16, 0x05, 0x2f, 0x05, 0x48, 0x05, 0x61, 0x05, 0x7a, 0x05, 0x94, 0x05, 0xad, + 0x05, 0xc7, 0x05, 0xe1, 0x05, 0xfb, 0x05, 0x15, 0x06, 0x2f, 0x06, 0x49, 0x06, 0x63, 0x06, 0x7e, 0x06, 0x98, 0x06, + 0xb2, 0x06, 0xcd, 0x06, 0xe7, 0x06, 0x02, 0x07, 0x1d, 0x07, 0x37, 0x07, 0x52, 0x07, 0x6d, 0x07, 0x87, 0x07, 0xa2, + 0x07, 0xbd, 0x07, 0xd8, 0x07, 0xf3, 0x07, 0x0d, 0x08, 0x28, 0x08, 0x43, 0x08, 0x5e, 0x08, 0x79, 0x08, 0x93, 0x08, + 0xae, 0x08, 0xc9, 0x08, 0xe3, 0x08, 0xfe, 0x08, 0x19, 0x09, 0x33, 0x09, 0x4e, 0x09, 0x68, 0x09, 0x82, 0x09, 0x9d, + 0x09, 0xb7, 0x09, 0xd1, 0x09, 0xeb, 0x09, 0x05, 0x0a, 0x1f, 0x0a, 0x39, 0x0a, 0x53, 0x0a, 0x6c, 0x0a, 0x86, 0x0a, + 0x9f, 0x0a, 0xb8, 0x0a, 0xd1, 0x0a, 0xea, 0x0a, 0x03, 0x0b, 0x1c, 0x0b, 0x35, 0x0b, 0x4d, 0x0b, 0x66, 0x0b, 0x7e, + 0x0b, 0x96, 0x0b, 0xae, 0x0b, 0xc5, 0x0b, 0xdd, 0x0b, 0xf4, 0x0b, 0x0c, 0x0c, 0x23, 0x0c, 0x39, 0x0c, 0x50, 0x0c, + 0x67, 0x0c, 0x7d, 0x0c, 0x93, 0x0c, 0xa9, 0x0c, 0xbf, 0x0c, 0xd4, 0x0c, 0xe9, 0x0c, 0xfe, 0x0c, 0x13, 0x0d, 0x28, + 0x0d, 0x3c, 0x0d, 0x50, 0x0d, 0x64, 0x0d, 0x78, 0x0d, 0x8b, 0x0d, 0x9f, 0x0d, 0xb2, 0x0d, 0xc4, 0x0d, 0xd7, 0x0d, + 0xe9, 0x0d, 0xfb, 0x0d, 0x0d, 0x0e, 0x1e, 0x0e, 0x2f, 0x0e, 0x40, 0x0e, 0x51, 0x0e, 0x61, 0x0e, 0x71, 0x0e, 0x81, + 0x0e, 0x90, 0x0e, 0x9f, 0x0e, 0xae, 0x0e, 0xbd, 0x0e, 0xcb, 0x0e, 0xd9, 0x0e, 0xe7, 0x0e, 0xf4, 0x0e, 0x01, 0x0f, + 0x0e, 0x0f, 0x1b, 0x0f, 0x27, 0x0f, 0x33, 0x0f, 0x3e, 0x0f, 0x49, 0x0f, 0x54, 0x0f, 0x5f, 0x0f, 0x69, 0x0f, 0x73, + 0x0f, 0x7d, 0x0f, 0x86, 0x0f, 0x8f, 0x0f, 0x98, 0x0f, 0xa0, 0x0f, 0xa8, 0x0f, 0xaf, 0x0f, 0xb7, 0x0f, 0xbe, 0x0f, + 0xc4, 0x0f, 0xcb, 0x0f, 0xd0, 0x0f, 0xd6, 0x0f, 0xdb, 0x0f, 0xe0, 0x0f, 0xe5, 0x0f, 0xe9, 0x0f, 0xed, 0x0f, 0xf0, + 0x0f, 0xf3, 0x0f, 0xf6, 0x0f, 0xf9, 0x0f, 0xfb, 0x0f, 0xfc, 0x0f, 0xfe, 0x0f, 0xff, 0x0f, 0x00, 0x10, 0x00, 0x10, + 0x00, 0x10, 0x00, 0x10, 0xff, 0x0f, 0xfe, 0x0f, 0xfc, 0x0f, 0xfb, 0x0f, 0xf9, 0x0f, 0xf6, 0x0f, 0xf3, 0x0f, 0xf0, + 0x0f, 0xed, 0x0f, 0xe9, 0x0f, 0xe5, 0x0f, 0xe0, 0x0f, 0xdb, 0x0f, 0xd6, 0x0f, 0xd0, 0x0f, 0xcb, 0x0f, 0xc4, 0x0f, + 0xbe, 0x0f, 0xb7, 0x0f, 0xaf, 0x0f, 0xa8, 0x0f, 0xa0, 0x0f, 0x98, 0x0f, 0x8f, 0x0f, 0x86, 0x0f, 0x7d, 0x0f, 0x73, + 0x0f, 0x69, 0x0f, 0x5f, 0x0f, 0x54, 0x0f, 0x49, 0x0f, 0x3e, 0x0f, 0x33, 0x0f, 0x27, 0x0f, 0x1b, 0x0f, 0x0e, 0x0f, + 0x01, 0x0f, 0xf4, 0x0e, 0xe7, 0x0e, 0xd9, 0x0e, 0xcb, 0x0e, 0xbd, 0x0e, 0xae, 0x0e, 0x9f, 0x0e, 0x90, 0x0e, 0x81, + 0x0e, 0x71, 0x0e, 0x61, 0x0e, 0x51, 0x0e, 0x40, 0x0e, 0x2f, 0x0e, 0x1e, 0x0e, 0x0d, 0x0e, 0xfb, 0x0d, 0xe9, 0x0d, + 0xd7, 0x0d, 0xc4, 0x0d, 0xb2, 0x0d, 0x9f, 0x0d, 0x8b, 0x0d, 0x78, 0x0d, 0x64, 0x0d, 0x50, 0x0d, 0x3c, 0x0d, 0x28, + 0x0d, 0x13, 0x0d, 0xfe, 0x0c, 0xe9, 0x0c, 0xd4, 0x0c, 0xbf, 0x0c, 0xa9, 0x0c, 0x93, 0x0c, 0x7d, 0x0c, 0x67, 0x0c, + 0x50, 0x0c, 0x39, 0x0c, 0x23, 0x0c, 0x0c, 0x0c, 0xf4, 0x0b, 0xdd, 0x0b, 0xc5, 0x0b, 0xae, 0x0b, 0x96, 0x0b, 0x7e, + 0x0b, 0x66, 0x0b, 0x4d, 0x0b, 0x35, 0x0b, 0x1c, 0x0b, 0x03, 0x0b, 0xea, 0x0a, 0xd1, 0x0a, 0xb8, 0x0a, 0x9f, 0x0a, + 0x86, 0x0a, 0x6c, 0x0a, 0x53, 0x0a, 0x39, 0x0a, 0x1f, 0x0a, 0x05, 0x0a, 0xeb, 0x09, 0xd1, 0x09, 0xb7, 0x09, 0x9d, + 0x09, 0x82, 0x09, 0x68, 0x09, 0x4e, 0x09, 0x33, 0x09, 0x19, 0x09, 0xfe, 0x08, 0xe3, 0x08, 0xc9, 0x08, 0xae, 0x08, + 0x93, 0x08, 0x79, 0x08, 0x5e, 0x08, 0x43, 0x08, 0x28, 0x08, 0x0d, 0x08, 0xf3, 0x07, 0xd8, 0x07, 0xbd, 0x07, 0xa2, + 0x07, 0x87, 0x07, 0x6d, 0x07, 0x52, 0x07, 0x37, 0x07, 0x1d, 0x07, 0x02, 0x07, 0xe7, 0x06, 0xcd, 0x06, 0xb2, 0x06, + 0x98, 0x06, 0x7e, 0x06, 0x63, 0x06, 0x49, 0x06, 0x2f, 0x06, 0x15, 0x06, 0xfb, 0x05, 0xe1, 0x05, 0xc7, 0x05, 0xad, + 0x05, 0x94, 0x05, 0x7a, 0x05, 0x61, 0x05, 0x48, 0x05, 0x2f, 0x05, 0x16, 0x05, 0xfd, 0x04, 0xe4, 0x04, 0xcb, 0x04, + 0xb3, 0x04, 0x9a, 0x04, 0x82, 0x04, 0x6a, 0x04, 0x52, 0x04, 0x3b, 0x04, 0x23, 0x04, 0x0c, 0x04, 0xf4, 0x03, 0xdd, + 0x03, 0xc7, 0x03, 0xb0, 0x03, 0x99, 0x03, 0x83, 0x03, 0x6d, 0x03, 0x57, 0x03, 0x41, 0x03, 0x2c, 0x03, 0x17, 0x03, + 0x02, 0x03, 0xed, 0x02, 0xd8, 0x02, 0xc4, 0x02, 0xb0, 0x02, 0x9c, 0x02, 0x88, 0x02, 0x75, 0x02, 0x61, 0x02, 0x4e, + 0x02, 0x3c, 0x02, 0x29, 0x02, 0x17, 0x02, 0x05, 0x02, 0xf3, 0x01, 0xe2, 0x01, 0xd1, 0x01, 0xc0, 0x01, 0xaf, 0x01, + 0x9f, 0x01, 0x8f, 0x01, 0x7f, 0x01, 0x70, 0x01, 0x61, 0x01, 0x52, 0x01, 0x43, 0x01, 0x35, 0x01, 0x27, 0x01, 0x19, + 0x01, 0x0c, 0x01, 0xff, 0x00, 0xf2, 0x00, 0xe5, 0x00, 0xd9, 0x00, 0xcd, 0x00, 0xc2, 0x00, 0xb7, 0x00, 0xac, 0x00, + 0xa1, 0x00, 0x97, 0x00, 0x8d, 0x00, 0x83, 0x00, 0x7a, 0x00, 0x71, 0x00, 0x68, 0x00, 0x60, 0x00, 0x58, 0x00, 0x51, + 0x00, 0x49, 0x00, 0x42, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x30, 0x00, 0x2a, 0x00, 0x25, 0x00, 0x20, 0x00, 0x1b, 0x00, + 0x17, 0x00, 0x13, 0x00, 0x10, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x07, 0x00, 0x05, 0x00, 0x04, 0x00, 0x02, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xee, 0xff, 0xff, 0x38, 0xee, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x4d, 0x4c, + 0x49, 0x52, 0x20, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x65, 0x64, 0x2e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xf4, 0x05, 0x00, 0x00, 0xf8, 0x05, 0x00, + 0x00, 0xfc, 0x05, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, + 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, 0x24, 0x05, 0x00, 0x00, 0xc8, 0x04, 0x00, 0x00, 0x70, + 0x04, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x00, + 0x4c, 0x03, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0x6c, 0x01, 0x00, + 0x00, 0x48, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x78, 0x00, + 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf6, 0xfa, 0xff, 0xff, 0x0c, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x16, 0xfb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x11, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x3a, 0xfb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0xa6, 0xfc, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x10, 0x00, 0x00, + 0x00, 0x14, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x68, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x27, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xd6, 0xfc, 0xff, 0xff, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0x98, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, + 0x00, 0x12, 0x00, 0x00, 0x00, 0x06, 0xfd, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x10, 0x00, + 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xc8, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x25, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x36, 0xfd, 0xff, 0xff, + 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, + 0x00, 0xf8, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x1e, 0xfc, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x84, 0xfc, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x73, 0x63, 0x61, 0x6c, + 0x65, 0x00, 0x02, 0x24, 0x0f, 0x02, 0x01, 0x02, 0x03, 0x40, 0x04, 0x04, 0x04, 0x24, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0xdc, 0xfc, 0xff, 0xff, 0x10, 0x00, 0x00, + 0x00, 0x24, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x73, 0x6e, + 0x72, 0x5f, 0x73, 0x68, 0x69, 0x66, 0x74, 0x00, 0x01, 0x0b, 0x01, 0x01, 0x01, 0x06, 0x04, 0x02, 0x24, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x20, 0xfd, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, + 0x00, 0x0a, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65, 0x5f, + 0x6f, 0x6e, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75, 0x73, 0x5f, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, + 0x00, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, + 0x67, 0x00, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x69, 0x6e, 0x67, 0x00, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x00, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x00, 0x6f, 0x6e, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75, 0x73, 0x5f, 0x73, 0x6d, + 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x00, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x00, 0x73, + 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x00, 0x73, 0x70, 0x65, 0x63, 0x74, + 0x72, 0x61, 0x6c, 0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x69, 0x74, + 0x73, 0x00, 0x09, 0xa5, 0x88, 0x75, 0x6d, 0x59, 0x4d, 0x3a, 0x31, 0x23, 0x09, 0x00, 0x01, 0x00, 0x09, 0x00, 0x29, + 0x3c, 0xd7, 0x03, 0x00, 0x00, 0x33, 0x03, 0x28, 0x00, 0x67, 0x3e, 0x99, 0x01, 0x0a, 0x00, 0x0e, 0x00, 0x05, 0x05, + 0x69, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x1b, 0x25, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, + 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x20, 0xfe, 0xff, 0xff, 0x10, 0x00, + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x1d, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x54, 0xfe, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, + 0x00, 0x2c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x00, 0x01, 0x0e, 0x01, 0x01, 0x01, 0x28, 0x04, 0x02, 0x24, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x62, 0xfe, 0xff, + 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xca, 0xff, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0a, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8c, 0xf2, 0xff, 0xff, + 0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x0b, 0x00, + 0x04, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0xf2, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, + 0x00, 0xfe, 0xfe, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x64, 0xff, 0xff, 0xff, 0x10, + 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x00, 0x02, 0x17, 0x0e, 0x00, 0x03, 0x00, 0x01, 0x00, 0x02, 0x00, 0xf1, 0x00, 0x05, 0x00, 0x05, 0x05, + 0x06, 0x25, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17, + 0x00, 0x00, 0x00, 0xb8, 0xff, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x54, 0x00, 0x66, 0x66, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, + 0x68, 0x00, 0x02, 0x0e, 0x0d, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x07, 0x00, 0x00, 0x02, 0x05, 0x05, 0x06, 0x25, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x16, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, + 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, + 0x68, 0x69, 0x66, 0x74, 0x00, 0x01, 0x07, 0x01, 0x01, 0x01, 0x0c, 0x04, 0x02, 0x24, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x2a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xc4, 0x0a, + 0x00, 0x00, 0x74, 0x0a, 0x00, 0x00, 0x3c, 0x0a, 0x00, 0x00, 0x04, 0x0a, 0x00, 0x00, 0xd0, 0x09, 0x00, 0x00, 0x88, + 0x09, 0x00, 0x00, 0x40, 0x09, 0x00, 0x00, 0xfc, 0x08, 0x00, 0x00, 0xb8, 0x08, 0x00, 0x00, 0x6c, 0x08, 0x00, 0x00, + 0x20, 0x08, 0x00, 0x00, 0xd4, 0x07, 0x00, 0x00, 0x88, 0x07, 0x00, 0x00, 0x3c, 0x07, 0x00, 0x00, 0xf8, 0x06, 0x00, + 0x00, 0xb8, 0x06, 0x00, 0x00, 0x84, 0x06, 0x00, 0x00, 0x50, 0x06, 0x00, 0x00, 0x1c, 0x06, 0x00, 0x00, 0xd8, 0x05, + 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0xd8, 0x04, 0x00, 0x00, 0x98, + 0x04, 0x00, 0x00, 0x60, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00, + 0x6c, 0x03, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x2c, 0x02, 0x00, + 0x00, 0xe4, 0x01, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x08, 0x01, + 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xfe, + 0xf5, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x09, 0x20, 0x00, 0x00, 0x00, 0x44, 0xf5, 0xff, 0xff, 0x11, 0x00, 0x00, 0x00, 0x50, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x61, 0x6c, 0x6c, 0x3a, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x3e, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x84, 0xf5, 0xff, 0xff, + 0x0d, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x7a, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, + 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0xc0, + 0xf5, 0xff, 0xff, 0x15, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x2f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, + 0x00, 0xbe, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x04, 0xf6, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x61, + 0x64, 0x64, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xf2, 0xf6, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x18, 0x00, 0x00, 0x00, 0x38, 0xf6, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, + 0x74, 0x65, 0x44, 0x69, 0x76, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x2a, 0xf7, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x10, 0x00, 0x00, 0x00, 0x70, 0xf6, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x61, 0x64, 0x64, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x28, 0x00, 0x00, 0x00, 0x5a, 0xf7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, + 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x00, 0xa0, 0xf6, 0xff, 0xff, 0x03, + 0x00, 0x00, 0x00, 0x6d, 0x75, 0x6c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x8a, 0xf7, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x14, 0x00, 0x00, 0x00, 0xd0, 0xf6, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x74, 0x5f, 0x32, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xbe, 0xf7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x24, 0x00, 0x00, 0x00, + 0x04, 0xf7, 0xff, 0xff, 0x16, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x6c, 0x6f, 0x67, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x02, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x22, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x18, 0x00, 0x00, 0x00, 0x48, 0xf7, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x63, 0x61, 0x6e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, + 0x00, 0x3a, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x21, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x38, 0x00, 0x00, 0x00, 0x80, 0xf7, 0xff, 0xff, 0x28, 0x00, 0x00, 0x00, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x73, + 0x70, 0x65, 0x63, 0x74, 0x72, 0x61, 0x6c, 0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x31, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x92, 0xf8, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x34, + 0x00, 0x00, 0x00, 0xd8, 0xf7, 0xff, 0xff, 0x27, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x74, 0x72, 0x61, 0x6c, + 0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0xe6, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x2c, 0x00, 0x00, 0x00, 0x2c, 0xf8, 0xff, 0xff, 0x1e, 0x00, 0x00, 0x00, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, + 0x73, 0x71, 0x75, 0x61, 0x72, 0x65, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x32, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1e, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x20, 0x00, 0x00, 0x00, 0x78, 0xf8, 0xff, 0xff, 0x12, 0x00, 0x00, 0x00, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x72, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, + 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x14, 0x00, 0x00, 0x00, 0xb8, + 0xf8, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x74, 0x5f, 0x31, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0xa6, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, + 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xec, 0xf8, 0xff, 0xff, 0x06, 0x00, + 0x00, 0x00, 0x63, 0x6f, 0x6e, 0x63, 0x61, 0x74, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xda, + 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x20, 0xf9, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, + 0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xec, 0x00, + 0x00, 0x00, 0x16, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1a, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x5c, 0xf9, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, + 0x43, 0x61, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x4a, 0xfa, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0x1c, 0x00, 0x00, 0x00, 0x90, 0xf9, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x6c, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x86, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0x18, 0x00, 0x00, 0x00, 0xcc, 0xf9, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x69, + 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x66, 0x66, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0xbe, + 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0x04, 0xfa, 0xff, 0xff, 0x16, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x66, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x31, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x24, 0x00, 0x00, 0x00, 0x44, 0xfa, 0xff, 0xff, + 0x15, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x66, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f, + 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x42, 0xfb, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x14, 0x00, 0x00, 0x00, 0x88, 0xfa, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x52, 0x65, 0x73, 0x68, + 0x61, 0x70, 0x65, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x76, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x1c, 0x00, + 0x00, 0x00, 0xbc, 0xfa, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x77, 0x69, + 0x6e, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, + 0xb6, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xfc, 0xfa, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x43, 0x6f, + 0x6e, 0x73, 0x74, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, + 0x2c, 0xfb, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x16, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x11, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x5c, 0xfb, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x43, + 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, + 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, + 0x00, 0x8c, 0xfb, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x52, 0x65, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2f, 0x73, 0x68, + 0x61, 0x70, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x82, 0xfc, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x24, 0x00, 0x00, 0x00, 0xc8, 0xfb, 0xff, 0xff, 0x17, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x2f, 0x79, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xc2, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0e, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x08, 0xfc, 0xff, 0xff, 0x18, 0x00, 0x00, 0x00, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, + 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x0a, 0xfd, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x50, 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, + 0x74, 0x5f, 0x31, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x52, 0xfd, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, + 0x00, 0x00, 0x00, 0x98, 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x32, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x9a, 0xfd, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, + 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0xe0, + 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x33, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x29, 0x00, 0x00, 0x00, 0xe2, 0xfd, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, + 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x28, 0xfd, 0xff, 0xff, 0x1a, + 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, + 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x34, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, + 0x00, 0x2a, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x20, 0x00, 0x00, 0x00, 0x70, 0xfd, 0xff, 0xff, 0x11, 0x00, 0x00, 0x00, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x63, 0x61, 0x6e, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x6a, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, + 0x00, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x00, 0xb0, 0xfd, + 0xff, 0xff, 0x13, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, + 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xaa, 0xfe, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x24, 0x00, 0x00, 0x00, 0xf0, 0xfd, 0xff, 0xff, 0x15, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65, + 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xee, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0x34, 0xfe, 0xff, + 0xff, 0x15, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x2f, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x32, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x78, 0xfe, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, + 0x74, 0x5f, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, + 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xa8, + 0xfe, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x96, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xdc, 0xfe, 0xff, 0xff, 0x07, 0x00, + 0x00, 0x00, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x5f, 0x31, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xca, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x14, 0x00, 0x00, 0x00, 0x10, 0xff, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6e, + 0x73, 0x74, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x1c, 0x00, + 0x18, 0x00, 0x17, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x16, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x2c, 0x00, 0x00, 0x00, 0x5c, 0xff, 0xff, 0xff, 0x1d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x5f, + 0x66, 0x72, 0x61, 0x6d, 0x65, 0x3a, 0x30, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, + 0x01, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, + 0xa4, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, + 0x00, 0x10, 0x01, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x50, 0x00, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x50, 0xfe, 0xff, 0xff, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x5c, 0xfe, 0xff, 0xff, + 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x68, 0xfe, 0xff, 0xff, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2a, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x7c, 0xfe, 0xff, 0xff, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x12, 0x70, 0xfe, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x13, + 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b, + 0x4c, 0x6f, 0x67, 0x00, 0x98, 0xfe, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x0a, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x50, 0x43, 0x41, 0x4e, 0x00, 0x00, 0xb8, 0xfe, + 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x23, 0x00, 0x00, 0x00, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b, 0x53, 0x70, 0x65, 0x63, + 0x74, 0x72, 0x61, 0x6c, 0x53, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0xf0, 0xfe, 0xff, + 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x1a, 0x00, 0x00, 0x00, 0x53, 0x69, + 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b, 0x53, 0x71, 0x75, 0x61, 0x72, + 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x42, 0x61, 0x6e, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x6c, 0xff, 0xff, 0xff, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x0c, 0x00, 0x10, 0x00, 0x0f, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x35, 0x7c, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x0c, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x45, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x00, 0x00, + 0x00, 0x00, 0xa0, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0a, + 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x52, 0x66, 0x66, 0x74, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff, + 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x12, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x46, 0x66, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x0c, 0x00, + 0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x16, 0x0c, 0x00, 0x10, 0x00, 0x0f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0c, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x00, 0x00}; + +} // namespace micro_wake_word +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp new file mode 100644 index 0000000000..8a443bc224 --- /dev/null +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -0,0 +1,503 @@ +#include "micro_wake_word.h" + +/** + * This is a workaround until we can figure out a way to get + * the tflite-micro idf component code available in CI + * + * */ +// +#ifndef CLANG_TIDY + +#ifdef USE_ESP_IDF + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "audio_preprocessor_int8_model_data.h" + +#include +#include +#include + +#include + +namespace esphome { +namespace micro_wake_word { + +static const char *const TAG = "micro_wake_word"; + +static const size_t SAMPLE_RATE_HZ = 16000; // 16 kHz +static const size_t BUFFER_LENGTH = 500; // 0.5 seconds +static const size_t BUFFER_SIZE = SAMPLE_RATE_HZ / 1000 * BUFFER_LENGTH; +static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms + +float MicroWakeWord::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +static const LogString *micro_wake_word_state_to_string(State state) { + switch (state) { + case State::IDLE: + return LOG_STR("IDLE"); + case State::START_MICROPHONE: + return LOG_STR("START_MICROPHONE"); + case State::STARTING_MICROPHONE: + return LOG_STR("STARTING_MICROPHONE"); + case State::DETECTING_WAKE_WORD: + return LOG_STR("DETECTING_WAKE_WORD"); + case State::STOP_MICROPHONE: + return LOG_STR("STOP_MICROPHONE"); + case State::STOPPING_MICROPHONE: + return LOG_STR("STOPPING_MICROPHONE"); + default: + return LOG_STR("UNKNOWN"); + } +} + +void MicroWakeWord::setup() { + ESP_LOGCONFIG(TAG, "Setting up Micro Wake Word..."); + + if (!this->initialize_models()) { + ESP_LOGE(TAG, "Failed to initialize models"); + this->mark_failed(); + return; + } + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->input_buffer_ = allocator.allocate(NEW_SAMPLES_TO_GET); + if (this->input_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate input buffer"); + this->mark_failed(); + return; + } + + this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t)); + if (this->ring_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate ring buffer"); + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, "Micro Wake Word initialized"); +} + +int MicroWakeWord::read_microphone_() { + size_t bytes_read = this->microphone_->read(this->input_buffer_, NEW_SAMPLES_TO_GET * sizeof(int16_t)); + if (bytes_read == 0) { + return 0; + } + + size_t bytes_written = this->ring_buffer_->write((void *) this->input_buffer_, bytes_read); + if (bytes_written != bytes_read) { + ESP_LOGW(TAG, "Failed to write some data to ring buffer (written=%d, expected=%d)", bytes_written, bytes_read); + } + return bytes_written; +} + +void MicroWakeWord::loop() { + switch (this->state_) { + case State::IDLE: + break; + case State::START_MICROPHONE: + ESP_LOGD(TAG, "Starting Microphone"); + this->microphone_->start(); + this->set_state_(State::STARTING_MICROPHONE); + this->high_freq_.start(); + break; + case State::STARTING_MICROPHONE: + if (this->microphone_->is_running()) { + this->set_state_(State::DETECTING_WAKE_WORD); + } + break; + case State::DETECTING_WAKE_WORD: + this->read_microphone_(); + if (this->detect_wake_word_()) { + ESP_LOGD(TAG, "Wake Word Detected"); + this->detected_ = true; + this->set_state_(State::STOP_MICROPHONE); + } + break; + case State::STOP_MICROPHONE: + ESP_LOGD(TAG, "Stopping Microphone"); + this->microphone_->stop(); + this->set_state_(State::STOPPING_MICROPHONE); + this->high_freq_.stop(); + break; + case State::STOPPING_MICROPHONE: + if (this->microphone_->is_stopped()) { + this->set_state_(State::IDLE); + if (this->detected_) { + this->detected_ = false; + this->wake_word_detected_trigger_->trigger(""); + } + } + break; + } +} + +void MicroWakeWord::start() { + if (this->is_failed()) { + ESP_LOGW(TAG, "Wake word component is marked as failed. Please check setup logs"); + return; + } + if (this->state_ != State::IDLE) { + ESP_LOGW(TAG, "Wake word is already running"); + return; + } + this->set_state_(State::START_MICROPHONE); +} + +void MicroWakeWord::stop() { + if (this->state_ == State::IDLE) { + ESP_LOGW(TAG, "Wake word is already stopped"); + return; + } + if (this->state_ == State::STOPPING_MICROPHONE) { + ESP_LOGW(TAG, "Wake word is already stopping"); + return; + } + this->set_state_(State::STOP_MICROPHONE); +} + +void MicroWakeWord::set_state_(State state) { + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(micro_wake_word_state_to_string(this->state_)), + LOG_STR_ARG(micro_wake_word_state_to_string(state))); + this->state_ = state; +} + +bool MicroWakeWord::initialize_models() { + ExternalRAMAllocator arena_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + ExternalRAMAllocator features_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + ExternalRAMAllocator audio_samples_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + + this->streaming_tensor_arena_ = arena_allocator.allocate(STREAMING_MODEL_ARENA_SIZE); + if (this->streaming_tensor_arena_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena."); + return false; + } + + this->streaming_var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE); + if (this->streaming_var_arena_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the streaming model variable's tensor arena."); + return false; + } + + this->preprocessor_tensor_arena_ = arena_allocator.allocate(PREPROCESSOR_ARENA_SIZE); + if (this->preprocessor_tensor_arena_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the audio preprocessor model's tensor arena."); + return false; + } + + this->new_features_data_ = features_allocator.allocate(PREPROCESSOR_FEATURE_SIZE); + if (this->new_features_data_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the audio features buffer."); + return false; + } + + this->preprocessor_audio_buffer_ = audio_samples_allocator.allocate(SAMPLE_DURATION_COUNT); + if (this->preprocessor_audio_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the audio preprocessor's buffer."); + return false; + } + + this->preprocessor_stride_buffer_ = audio_samples_allocator.allocate(HISTORY_SAMPLES_TO_KEEP); + if (this->preprocessor_stride_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the audio preprocessor's stride buffer."); + return false; + } + + this->preprocessor_model_ = tflite::GetModel(G_AUDIO_PREPROCESSOR_INT8_TFLITE); + if (this->preprocessor_model_->version() != TFLITE_SCHEMA_VERSION) { + ESP_LOGE(TAG, "Wake word's audio preprocessor model's schema is not supported"); + return false; + } + + this->streaming_model_ = tflite::GetModel(this->model_start_); + if (this->streaming_model_->version() != TFLITE_SCHEMA_VERSION) { + ESP_LOGE(TAG, "Wake word's streaming model's schema is not supported"); + return false; + } + + static tflite::MicroMutableOpResolver<18> preprocessor_op_resolver; + static tflite::MicroMutableOpResolver<14> streaming_op_resolver; + + if (!this->register_preprocessor_ops_(preprocessor_op_resolver)) + return false; + if (!this->register_streaming_ops_(streaming_op_resolver)) + return false; + + tflite::MicroAllocator *ma = + tflite::MicroAllocator::Create(this->streaming_var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(ma, 15); + + static tflite::MicroInterpreter static_preprocessor_interpreter( + this->preprocessor_model_, preprocessor_op_resolver, this->preprocessor_tensor_arena_, PREPROCESSOR_ARENA_SIZE); + + static tflite::MicroInterpreter static_streaming_interpreter(this->streaming_model_, streaming_op_resolver, + this->streaming_tensor_arena_, + STREAMING_MODEL_ARENA_SIZE, this->mrv_); + + this->preprocessor_interperter_ = &static_preprocessor_interpreter; + this->streaming_interpreter_ = &static_streaming_interpreter; + + // Allocate tensors for each models. + if (this->preprocessor_interperter_->AllocateTensors() != kTfLiteOk) { + ESP_LOGE(TAG, "Failed to allocate tensors for the audio preprocessor"); + return false; + } + if (this->streaming_interpreter_->AllocateTensors() != kTfLiteOk) { + ESP_LOGE(TAG, "Failed to allocate tensors for the streaming model"); + return false; + } + + // Verify input tensor matches expected values + TfLiteTensor *input = this->streaming_interpreter_->input(0); + if ((input->dims->size != 3) || (input->dims->data[0] != 1) || (input->dims->data[0] != 1) || + (input->dims->data[1] != 1) || (input->dims->data[2] != PREPROCESSOR_FEATURE_SIZE)) { + ESP_LOGE(TAG, "Wake word detection model tensor input dimensions is not 1x1x%u", input->dims->data[2]); + return false; + } + + if (input->type != kTfLiteInt8) { + ESP_LOGE(TAG, "Wake word detection model tensor input is not int8."); + return false; + } + + // Verify output tensor matches expected values + TfLiteTensor *output = this->streaming_interpreter_->output(0); + if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) { + ESP_LOGE(TAG, "Wake word detection model tensor output dimensions is not 1x1."); + } + + if (output->type != kTfLiteUInt8) { + ESP_LOGE(TAG, "Wake word detection model tensor input is not uint8."); + return false; + } + + this->recent_streaming_probabilities_.resize(this->sliding_window_average_size_, 0.0); + + return true; +} + +bool MicroWakeWord::update_features_() { + // Verify we have enough samples for a feature slice + if (!this->slice_available_()) { + return false; + } + + // Retrieve strided audio samples + int16_t *audio_samples = nullptr; + if (!this->stride_audio_samples_(&audio_samples)) { + return false; + } + + // Compute the features for the newest audio samples + if (!this->generate_single_feature_(audio_samples, SAMPLE_DURATION_COUNT, this->new_features_data_)) { + return false; + } + + return true; +} + +float MicroWakeWord::perform_streaming_inference_() { + TfLiteTensor *input = this->streaming_interpreter_->input(0); + + size_t bytes_to_copy = input->bytes; + + memcpy((void *) (tflite::GetTensorData(input)), (const void *) (this->new_features_data_), bytes_to_copy); + + uint32_t prior_invoke = millis(); + + TfLiteStatus invoke_status = this->streaming_interpreter_->Invoke(); + if (invoke_status != kTfLiteOk) { + ESP_LOGW(TAG, "Streaming Interpreter Invoke failed"); + return false; + } + + ESP_LOGV(TAG, "Streaming Inference Latency=%u ms", (millis() - prior_invoke)); + + TfLiteTensor *output = this->streaming_interpreter_->output(0); + + return static_cast(output->data.uint8[0]) / 255.0; +} + +bool MicroWakeWord::detect_wake_word_() { + // Preprocess the newest audio samples into features + if (!this->update_features_()) { + return false; + } + + // Perform inference + uint32_t streaming_size = micros(); + float streaming_prob = this->perform_streaming_inference_(); + + // Add the most recent probability to the sliding window + this->recent_streaming_probabilities_[this->last_n_index_] = streaming_prob; + ++this->last_n_index_; + if (this->last_n_index_ == this->sliding_window_average_size_) + this->last_n_index_ = 0; + + float sum = 0.0; + for (auto &prob : this->recent_streaming_probabilities_) { + sum += prob; + } + + float sliding_window_average = sum / static_cast(this->sliding_window_average_size_); + + // Ensure we have enough samples since the last positive detection + this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0); + if (this->ignore_windows_ < 0) { + return false; + } + + // Detect the wake word if the sliding window average is above the cutoff + if (sliding_window_average > this->probability_cutoff_) { + this->ignore_windows_ = -MIN_SLICES_BEFORE_DETECTION; + for (auto &prob : this->recent_streaming_probabilities_) { + prob = 0; + } + return true; + } + + return false; +} + +void MicroWakeWord::set_sliding_window_average_size(size_t size) { + this->sliding_window_average_size_ = size; + this->recent_streaming_probabilities_.resize(this->sliding_window_average_size_, 0.0); +} + +bool MicroWakeWord::slice_available_() { + size_t available = this->ring_buffer_->available(); + + return available > (NEW_SAMPLES_TO_GET * sizeof(int16_t)); +} + +bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) { + // Copy 320 bytes (160 samples over 10 ms) into preprocessor_audio_buffer_ from history in + // preprocessor_stride_buffer_ + memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_stride_buffer_), + HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t)); + + if (this->ring_buffer_->available() < NEW_SAMPLES_TO_GET * sizeof(int16_t)) { + ESP_LOGD(TAG, "Audio Buffer not full enough"); + return false; + } + + // Copy 640 bytes (320 samples over 20 ms) from the ring buffer + // The first 320 bytes (160 samples over 10 ms) will be from history + size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_ + HISTORY_SAMPLES_TO_KEEP), + NEW_SAMPLES_TO_GET * sizeof(int16_t), pdMS_TO_TICKS(200)); + + if (bytes_read == 0) { + ESP_LOGE(TAG, "Could not read data from Ring Buffer"); + } else if (bytes_read < NEW_SAMPLES_TO_GET * sizeof(int16_t)) { + ESP_LOGD(TAG, "Partial Read of Data by Model"); + ESP_LOGD(TAG, "Could only read %d bytes when required %d bytes ", bytes_read, + (int) (NEW_SAMPLES_TO_GET * sizeof(int16_t))); + return false; + } + + // Copy the last 320 bytes (160 samples over 10 ms) from the audio buffer into history stride buffer for the next + // iteration + memcpy((void *) (this->preprocessor_stride_buffer_), (void *) (this->preprocessor_audio_buffer_ + NEW_SAMPLES_TO_GET), + HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t)); + + *audio_samples = this->preprocessor_audio_buffer_; + return true; +} + +bool MicroWakeWord::generate_single_feature_(const int16_t *audio_data, const int audio_data_size, + int8_t feature_output[PREPROCESSOR_FEATURE_SIZE]) { + TfLiteTensor *input = this->preprocessor_interperter_->input(0); + TfLiteTensor *output = this->preprocessor_interperter_->output(0); + std::copy_n(audio_data, audio_data_size, tflite::GetTensorData(input)); + + if (this->preprocessor_interperter_->Invoke() != kTfLiteOk) { + ESP_LOGE(TAG, "Failed to preprocess audio for local wake word."); + return false; + } + std::memcpy(feature_output, tflite::GetTensorData(output), PREPROCESSOR_FEATURE_SIZE * sizeof(int8_t)); + + return true; +} + +bool MicroWakeWord::register_preprocessor_ops_(tflite::MicroMutableOpResolver<18> &op_resolver) { + if (op_resolver.AddReshape() != kTfLiteOk) + return false; + if (op_resolver.AddCast() != kTfLiteOk) + return false; + if (op_resolver.AddStridedSlice() != kTfLiteOk) + return false; + if (op_resolver.AddConcatenation() != kTfLiteOk) + return false; + if (op_resolver.AddMul() != kTfLiteOk) + return false; + if (op_resolver.AddAdd() != kTfLiteOk) + return false; + if (op_resolver.AddDiv() != kTfLiteOk) + return false; + if (op_resolver.AddMinimum() != kTfLiteOk) + return false; + if (op_resolver.AddMaximum() != kTfLiteOk) + return false; + if (op_resolver.AddWindow() != kTfLiteOk) + return false; + if (op_resolver.AddFftAutoScale() != kTfLiteOk) + return false; + if (op_resolver.AddRfft() != kTfLiteOk) + return false; + if (op_resolver.AddEnergy() != kTfLiteOk) + return false; + if (op_resolver.AddFilterBank() != kTfLiteOk) + return false; + if (op_resolver.AddFilterBankSquareRoot() != kTfLiteOk) + return false; + if (op_resolver.AddFilterBankSpectralSubtraction() != kTfLiteOk) + return false; + if (op_resolver.AddPCAN() != kTfLiteOk) + return false; + if (op_resolver.AddFilterBankLog() != kTfLiteOk) + return false; + + return true; +} + +bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver) { + if (op_resolver.AddCallOnce() != kTfLiteOk) + return false; + if (op_resolver.AddVarHandle() != kTfLiteOk) + return false; + if (op_resolver.AddReshape() != kTfLiteOk) + return false; + if (op_resolver.AddReadVariable() != kTfLiteOk) + return false; + if (op_resolver.AddStridedSlice() != kTfLiteOk) + return false; + if (op_resolver.AddConcatenation() != kTfLiteOk) + return false; + if (op_resolver.AddAssignVariable() != kTfLiteOk) + return false; + if (op_resolver.AddConv2D() != kTfLiteOk) + return false; + if (op_resolver.AddMul() != kTfLiteOk) + return false; + if (op_resolver.AddAdd() != kTfLiteOk) + return false; + if (op_resolver.AddMean() != kTfLiteOk) + return false; + if (op_resolver.AddFullyConnected() != kTfLiteOk) + return false; + if (op_resolver.AddLogistic() != kTfLiteOk) + return false; + if (op_resolver.AddQuantize() != kTfLiteOk) + return false; + + return true; +} + +} // namespace micro_wake_word +} // namespace esphome + +#endif // USE_ESP_IDF + +#endif // CLANG_TIDY diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h new file mode 100644 index 0000000000..82f28b2ebb --- /dev/null +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -0,0 +1,204 @@ +#pragma once + +/** + * This is a workaround until we can figure out a way to get + * the tflite-micro idf component code available in CI + * + * */ +// +#ifndef CLANG_TIDY + +#ifdef USE_ESP_IDF + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/ring_buffer.h" + +#include "esphome/components/microphone/microphone.h" + +#include +#include +#include + +namespace esphome { +namespace micro_wake_word { + +// The following are dictated by the preprocessor model +// +// The number of features the audio preprocessor generates per slice +static const uint8_t PREPROCESSOR_FEATURE_SIZE = 40; +// How frequently the preprocessor generates a new set of features +static const uint8_t FEATURE_STRIDE_MS = 20; +// Duration of each slice used as input into the preprocessor +static const uint8_t FEATURE_DURATION_MS = 30; +// Audio sample frequency in hertz +static const uint16_t AUDIO_SAMPLE_FREQUENCY = 16000; +// The number of old audio samples that are saved to be part of the next feature window +static const uint16_t HISTORY_SAMPLES_TO_KEEP = + ((FEATURE_DURATION_MS - FEATURE_STRIDE_MS) * (AUDIO_SAMPLE_FREQUENCY / 1000)); +// The number of new audio samples to receive to be included with the next feature window +static const uint16_t NEW_SAMPLES_TO_GET = (FEATURE_STRIDE_MS * (AUDIO_SAMPLE_FREQUENCY / 1000)); +// The total number of audio samples included in the feature window +static const uint16_t SAMPLE_DURATION_COUNT = FEATURE_DURATION_MS * AUDIO_SAMPLE_FREQUENCY / 1000; +// Number of bytes in memory needed for the preprocessor arena +static const uint32_t PREPROCESSOR_ARENA_SIZE = 9528; + +// The following configure the streaming wake word model +// +// The number of audio slices to process before accepting a positive detection +static const uint8_t MIN_SLICES_BEFORE_DETECTION = 74; + +// Number of bytes in memory needed for the streaming wake word model +static const uint32_t STREAMING_MODEL_ARENA_SIZE = 64000; +static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024; + +enum State { + IDLE, + START_MICROPHONE, + STARTING_MICROPHONE, + DETECTING_WAKE_WORD, + STOP_MICROPHONE, + STOPPING_MICROPHONE, +}; + +class MicroWakeWord : public Component { + public: + void setup() override; + void loop() override; + float get_setup_priority() const override; + + void start(); + void stop(); + + bool is_running() const { return this->state_ != State::IDLE; } + + bool initialize_models(); + + // Increasing either of these will reduce the rate of false acceptances while increasing the false rejection rate + void set_probability_cutoff(float probability_cutoff) { this->probability_cutoff_ = probability_cutoff; } + void set_sliding_window_average_size(size_t size); + + void set_microphone(microphone::Microphone *microphone) { this->microphone_ = microphone; } + + Trigger *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } + + void set_model_start(const uint8_t *model_start) { this->model_start_ = model_start; } + void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; } + + protected: + void set_state_(State state); + int read_microphone_(); + + const uint8_t *model_start_; + std::string wake_word_; + + microphone::Microphone *microphone_{nullptr}; + Trigger *wake_word_detected_trigger_ = new Trigger(); + State state_{State::IDLE}; + HighFrequencyLoopRequester high_freq_; + + std::unique_ptr ring_buffer_; + + int16_t *input_buffer_; + + const tflite::Model *preprocessor_model_{nullptr}; + const tflite::Model *streaming_model_{nullptr}; + tflite::MicroInterpreter *streaming_interpreter_{nullptr}; + tflite::MicroInterpreter *preprocessor_interperter_{nullptr}; + + std::vector recent_streaming_probabilities_; + size_t last_n_index_{0}; + + float probability_cutoff_{0.5}; + size_t sliding_window_average_size_{10}; + + // When the wake word detection first starts or after the word has been detected once, we ignore this many audio + // feature slices before accepting a positive detection again + int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION}; + + uint8_t *streaming_var_arena_{nullptr}; + uint8_t *streaming_tensor_arena_{nullptr}; + uint8_t *preprocessor_tensor_arena_{nullptr}; + int8_t *new_features_data_{nullptr}; + + tflite::MicroResourceVariables *mrv_{nullptr}; + + // Stores audio fed into feature generator preprocessor + int16_t *preprocessor_audio_buffer_; + int16_t *preprocessor_stride_buffer_; + + bool detected_{false}; + + /** Detects if wake word has been said + * + * If enough audio samples are available, it will generate one slice of new features. + * If the streaming model predicts the wake word, then the nonstreaming model confirms it. + * @param ring_Buffer Ring buffer containing raw audio samples + * @return True if the wake word is detected, false otherwise + */ + bool detect_wake_word_(); + + /// @brief Returns true if there are enough audio samples in the buffer to generate another slice of features + bool slice_available_(); + + /** Shifts previous feature slices over by one and generates a new slice of features + * + * @param ring_buffer ring buffer containing raw audio samples + * @return True if a new slice of features was generated, false otherwise + */ + bool update_features_(); + + /** Generates features from audio samples + * + * Adapted from TFLite micro speech example + * @param audio_data Pointer to array with the audio samples + * @param audio_data_size The number of samples to use as input to the preprocessor model + * @param feature_output Array that will store the features + * @return True if successful, false otherwise. + */ + bool generate_single_feature_(const int16_t *audio_data, int audio_data_size, + int8_t feature_output[PREPROCESSOR_FEATURE_SIZE]); + + /** Performs inference over the most recent feature slice with the streaming model + * + * @return Probability of the wake word between 0.0 and 1.0 + */ + float perform_streaming_inference_(); + + /** Strides the audio samples by keeping the last 10 ms of the previous slice + * + * Adapted from the TFLite micro speech example + * @param ring_buffer Ring buffer containing raw audio samples + * @param audio_samples Pointer to an array that will store the strided audio samples + * @return True if successful, false otherwise + */ + bool stride_audio_samples_(int16_t **audio_samples); + + /// @brief Returns true if successfully registered the preprocessor's TensorFlow operations + bool register_preprocessor_ops_(tflite::MicroMutableOpResolver<18> &op_resolver); + + /// @brief Returns true if successfully registered the streaming model's TensorFlow operations + bool register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver); +}; + +template class StartAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->start(); } +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class IsRunningCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_running(); } +}; + +} // namespace micro_wake_word +} // namespace esphome + +#endif // USE_ESP_IDF + +#endif // CLANG_TIDY diff --git a/tests/components/micro_wake_word/test.esp32-s3-idf.yaml b/tests/components/micro_wake_word/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..c0f3593cc6 --- /dev/null +++ b/tests/components/micro_wake_word/test.esp32-s3-idf.yaml @@ -0,0 +1,15 @@ +i2s_audio: + i2s_lrclk_pin: GPIO18 + i2s_bclk_pin: GPIO19 + +microphone: + - platform: i2s_audio + id: echo_microphone + i2s_din_pin: GPIO17 + adc_type: external + pdm: true + +micro_wake_word: + model: hey_jarvis + on_wake_word_detected: + - logger.log: "Wake word detected" From 47d1a648941260a3260c9bddd3932e426a5ba334 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:45:31 +1300 Subject: [PATCH 334/436] Bump version to 2024.3.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 8f9606c5fd..20ad564291 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.1.0-dev" +__version__ = "2024.3.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 7baf091d47cbfb426918a0a836bd4dda8c8eb93b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:29:54 +1300 Subject: [PATCH 335/436] Bump openssh-client to 1:9.2p1-2+deb12u2 (#6216) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b28ca2ba66..5d9ece16a1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -35,7 +35,7 @@ RUN \ iputils-ping=3:20221126-1 \ git=1:2.39.2-1.1 \ curl=7.88.1-10+deb12u5 \ - openssh-client=1:9.2p1-2+deb12u1 \ + openssh-client=1:9.2p1-2+deb12u2 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ libmagic1=1:5.44-3 \ From 27a3a081c3aa50c7591565cc9eb45b70e484e023 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 15 Feb 2024 15:47:42 -0600 Subject: [PATCH 336/436] AUTO_LOAD `sensor` for `shelly_dimmer` (#6223) --- esphome/components/shelly_dimmer/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index 5bdb54baf5..625784427f 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -29,7 +29,8 @@ from esphome.const import ( from esphome.core import HexInt, CORE DOMAIN = "shelly_dimmer" -DEPENDENCIES = ["sensor", "uart", "esp8266"] +AUTO_LOAD = ["sensor"] +DEPENDENCIES = ["uart", "esp8266"] shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer") ShellyDimmer = shelly_dimmer_ns.class_( From db9d837d296a03f83475a1ace1243914e0c37c86 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Sun, 18 Feb 2024 00:50:24 -0500 Subject: [PATCH 337/436] Add more debugging logs to microWakeWord (#6238) --- .../components/micro_wake_word/__init__.py | 2 +- .../micro_wake_word/micro_wake_word.cpp | 44 +++++++++++++------ .../micro_wake_word/micro_wake_word.h | 3 ++ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 2a84b7d74b..38202bdfb9 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -261,7 +261,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(MicroWakeWord), cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), - cv.Optional(CONF_PROBABILITY_CUTOFF): cv.float_, + cv.Optional(CONF_PROBABILITY_CUTOFF): cv.percentage, cv.Optional(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int, cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation( single=True diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 8a443bc224..f0b3d55a9d 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -53,8 +53,15 @@ static const LogString *micro_wake_word_state_to_string(State state) { } } +void MicroWakeWord::dump_config() { + ESP_LOGCONFIG(TAG, "microWakeWord:"); + ESP_LOGCONFIG(TAG, " Wake Word: %s", this->get_wake_word().c_str()); + ESP_LOGCONFIG(TAG, " Probability cutoff: %.3f", this->probability_cutoff_); + ESP_LOGCONFIG(TAG, " Sliding window size: %d", this->sliding_window_average_size_); +} + void MicroWakeWord::setup() { - ESP_LOGCONFIG(TAG, "Setting up Micro Wake Word..."); + ESP_LOGCONFIG(TAG, "Setting up microWakeWord..."); if (!this->initialize_models()) { ESP_LOGE(TAG, "Failed to initialize models"); @@ -63,7 +70,7 @@ void MicroWakeWord::setup() { } ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - this->input_buffer_ = allocator.allocate(NEW_SAMPLES_TO_GET); + this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE * sizeof(int16_t)); if (this->input_buffer_ == nullptr) { ESP_LOGW(TAG, "Could not allocate input buffer"); this->mark_failed(); @@ -81,7 +88,7 @@ void MicroWakeWord::setup() { } int MicroWakeWord::read_microphone_() { - size_t bytes_read = this->microphone_->read(this->input_buffer_, NEW_SAMPLES_TO_GET * sizeof(int16_t)); + size_t bytes_read = this->microphone_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t)); if (bytes_read == 0) { return 0; } @@ -279,11 +286,6 @@ bool MicroWakeWord::initialize_models() { } bool MicroWakeWord::update_features_() { - // Verify we have enough samples for a feature slice - if (!this->slice_available_()) { - return false; - } - // Retrieve strided audio samples int16_t *audio_samples = nullptr; if (!this->stride_audio_samples_(&audio_samples)) { @@ -369,20 +371,36 @@ void MicroWakeWord::set_sliding_window_average_size(size_t size) { bool MicroWakeWord::slice_available_() { size_t available = this->ring_buffer_->available(); + size_t free = this->ring_buffer_->free(); + + if (free < NEW_SAMPLES_TO_GET * sizeof(int16_t)) { + // If the ring buffer is within one audio slice of being full, then wake word detection will have issues. + // If this is constantly occuring, then some possibilities why are + // 1) there are too many other slow components configured + // 2) the ESP32 isn't fast enough; e.g., an ESP32 is much slower than an ESP32-S3 at inferences. + // 3) the model is too large + // 4) the model uses operations that are not optimized + ESP_LOGW(TAG, + "Audio buffer is nearly full. Wake word detection may be less accurate and have slower reponse times. " +#if !defined(USE_ESP32_VARIANT_ESP32S3) + "microWakeWord is designed for the ESP32-S3. The current platform is too slow for this model." +#endif + ); + } + return available > (NEW_SAMPLES_TO_GET * sizeof(int16_t)); } bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) { + if (!this->slice_available_()) { + return false; + } + // Copy 320 bytes (160 samples over 10 ms) into preprocessor_audio_buffer_ from history in // preprocessor_stride_buffer_ memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_stride_buffer_), HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t)); - if (this->ring_buffer_->available() < NEW_SAMPLES_TO_GET * sizeof(int16_t)) { - ESP_LOGD(TAG, "Audio Buffer not full enough"); - return false; - } - // Copy 640 bytes (320 samples over 20 ms) from the ring buffer // The first 320 bytes (160 samples over 10 ms) will be from history size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_ + HISTORY_SAMPLES_TO_KEEP), diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index 82f28b2ebb..27d05c3e09 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -66,6 +66,7 @@ class MicroWakeWord : public Component { void setup() override; void loop() override; float get_setup_priority() const override; + void dump_config() override; void start(); void stop(); @@ -74,6 +75,8 @@ class MicroWakeWord : public Component { bool initialize_models(); + std::string get_wake_word() { return this->wake_word_; } + // Increasing either of these will reduce the rate of false acceptances while increasing the false rejection rate void set_probability_cutoff(float probability_cutoff) { this->probability_cutoff_ = probability_cutoff; } void set_sliding_window_average_size(size_t size); From acbcb9d2be0ed468132faa572a40db3ee67a0466 Mon Sep 17 00:00:00 2001 From: marshn Date: Sun, 18 Feb 2024 18:38:32 +0000 Subject: [PATCH 338/436] Fix to RF receiver for Drayton Digistat heating controller (#6235) --- esphome/components/remote_base/drayton_protocol.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index 6c617f56c8..acfb7a0f16 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -14,7 +14,7 @@ static const uint8_t NBITS_ADDRESS = 16; static const uint8_t NBITS_CHANNEL = 5; static const uint8_t NBITS_COMMAND = 7; static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; -static const uint8_t MIN_RX_SRC = (NDATABITS * 2 + NBITS_SYNC / 2); +static const uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); static const uint8_t CMD_ON = 0x41; static const uint8_t CMD_OFF = 0x02; @@ -135,7 +135,7 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { .command = 0, }; - while (src.size() - src.get_index() > MIN_RX_SRC) { + while (src.size() - src.get_index() >= MIN_RX_SRC) { ESP_LOGVV(TAG, "Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 @@ -150,7 +150,7 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { } // Look for sync pulse, after. If sucessful index points to space of sync symbol - while (src.size() - src.get_index() >= NDATABITS) { + while (src.size() - src.get_index() >= MIN_RX_SRC) { ESP_LOGVV(TAG, "Decode Drayton: sync search %d, %" PRId32 " %" PRId32, src.size() - src.get_index(), src.peek(), src.peek(1)); if (src.peek_mark(2 * BIT_TIME_US) && From 8a52ba3ea3f873057a3d9a73a8a4388a87038544 Mon Sep 17 00:00:00 2001 From: Marcel Hetzendorfer Date: Sun, 18 Feb 2024 19:40:20 +0100 Subject: [PATCH 339/436] WRGB Use correct multiplier (#6237) --- esphome/components/esp32_rmt_led_strip/led_strip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index c5a7f7918d..7727b64f29 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -160,7 +160,7 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index b = 0; break; } - uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3; uint8_t white = this->is_wrgb_ ? 0 : 3; return {this->buf_ + (index * multiplier) + r + this->is_wrgb_, From 142b33fc90f1f52b6fa3a576b2c728e6cb7c98c4 Mon Sep 17 00:00:00 2001 From: bisbastuner <104585618+bisbastuner@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:44:24 +0100 Subject: [PATCH 340/436] Add support for 1.8V-powered devices (#6234) --- esphome/components/bme680_bsec/__init__.py | 11 ++++++ .../components/bme680_bsec/bme680_bsec.cpp | 35 ++++++++++++++----- esphome/components/bme680_bsec/bme680_bsec.h | 7 ++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 085d2a574b..15c17f4064 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -11,6 +11,7 @@ MULTI_CONF = True CONF_BME680_BSEC_ID = "bme680_bsec_id" CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_IAQ_MODE = "iaq_mode" +CONF_SUPPLY_VOLTAGE = "supply_voltage" CONF_SAMPLE_RATE = "sample_rate" CONF_STATE_SAVE_INTERVAL = "state_save_interval" @@ -22,6 +23,12 @@ IAQ_MODE_OPTIONS = { "MOBILE": IAQMode.IAQ_MODE_MOBILE, } +SupplyVoltage = bme680_bsec_ns.enum("SupplyVoltage") +SUPPLY_VOLTAGE_OPTIONS = { + "1.8V": SupplyVoltage.SUPPLY_VOLTAGE_1V8, + "3.3V": SupplyVoltage.SUPPLY_VOLTAGE_3V3, +} + SampleRate = bme680_bsec_ns.enum("SampleRate") SAMPLE_RATE_OPTIONS = { "LP": SampleRate.SAMPLE_RATE_LP, @@ -40,6 +47,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( IAQ_MODE_OPTIONS, upper=True ), + cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum( + SUPPLY_VOLTAGE_OPTIONS, upper=True + ), cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( SAMPLE_RATE_OPTIONS, upper=True ), @@ -67,6 +77,7 @@ async def to_code(config): cg.add(var.set_device_id(str(config[CONF_ID]))) cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) + cg.add(var.set_supply_voltage(config[CONF_SUPPLY_VOLTAGE])) cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) cg.add( var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 2b1b0dc948..17dae35b5c 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -52,17 +52,33 @@ void BME680BSECComponent::setup() { void BME680BSECComponent::set_config_() { if (this->sample_rate_ == SAMPLE_RATE_ULP) { - const uint8_t config[] = { + if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) { + const uint8_t config[] = { #include "config/generic_33v_300s_28d/bsec_iaq.txt" - }; - this->bsec_status_ = - bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); - } else { - const uint8_t config[] = { + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } else { // SUPPLY_VOLTAGE_1V8 + const uint8_t config[] = { +#include "config/generic_18v_300s_28d/bsec_iaq.txt" + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } + } else { // SAMPLE_RATE_LP + if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) { + const uint8_t config[] = { #include "config/generic_33v_3s_28d/bsec_iaq.txt" - }; - this->bsec_status_ = - bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } else { // SUPPLY_VOLTAGE_1V8 + const uint8_t config[] = { +#include "config/generic_18v_3s_28d/bsec_iaq.txt" + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } } } @@ -145,6 +161,7 @@ void BME680BSECComponent::dump_config() { ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_); ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile"); + ESP_LOGCONFIG(TAG, " Supply Voltage: %sV", this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8"); ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_)); ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_); diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index a97ad2f53e..e52dbe964b 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -21,6 +21,11 @@ enum IAQMode { IAQ_MODE_MOBILE = 1, }; +enum SupplyVoltage { + SUPPLY_VOLTAGE_3V3 = 0, + SUPPLY_VOLTAGE_1V8 = 1, +}; + enum SampleRate { SAMPLE_RATE_LP = 0, SAMPLE_RATE_ULP = 1, @@ -35,6 +40,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + void set_supply_voltage(SupplyVoltage supply_voltage) { this->supply_voltage_ = supply_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; } @@ -109,6 +115,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { std::string device_id_; float temperature_offset_{0}; IAQMode iaq_mode_{IAQ_MODE_STATIC}; + SupplyVoltage supply_voltage_; SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; From e3e670c084204dbf30505b90be1684bf54a514f5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:52:37 +1300 Subject: [PATCH 341/436] Add optional minimum esphome version to microWakeWord manifest (#6240) --- esphome/components/micro_wake_word/__init__.py | 4 ++++ esphome/config_validation.py | 11 +++++++++++ esphome/core/config.py | 12 +----------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 38202bdfb9..209a1412ca 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -103,6 +103,7 @@ KEY_AUTHOR = "author" KEY_WEBSITE = "website" KEY_VERSION = "version" KEY_MICRO = "micro" +KEY_MINIMUM_ESPHOME_VERSION = "minimum_esphome_version" MANIFEST_SCHEMA_V1 = cv.Schema( { @@ -116,6 +117,9 @@ MANIFEST_SCHEMA_V1 = cv.Schema( { cv.Required(CONF_PROBABILITY_CUTOFF): cv.float_, cv.Required(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int, + cv.Optional(KEY_MINIMUM_ESPHOME_VERSION): cv.All( + cv.version_number, cv.validate_esphome_version + ), } ), } diff --git a/esphome/config_validation.py b/esphome/config_validation.py index fa1170fb93..9f577773d4 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -57,6 +57,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, VALID_SUBSTITUTIONS_CHARACTERS, + __version__ as ESPHOME_VERSION, ) from esphome.core import ( CORE, @@ -1895,6 +1896,16 @@ def version_number(value): raise Invalid("Not a valid version number") from e +def validate_esphome_version(value: str): + min_version = Version.parse(value) + current_version = Version.parse(ESPHOME_VERSION) + if current_version < min_version: + raise Invalid( + f"Your ESPHome version is too old. Please update to at least {min_version}" + ) + return value + + def platformio_version_constraint(value): # for documentation on valid version constraints: # https://docs.platformio.org/en/latest/core/userguide/platforms/cmd_install.html#cmd-platform-install diff --git a/esphome/core/config.py b/esphome/core/config.py index e4a1fdcafa..f3d732a8fc 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -102,16 +102,6 @@ def valid_project_name(value: str): return value -def validate_version(value: str): - min_version = cv.Version.parse(value) - current_version = cv.Version.parse(ESPHOME_VERSION) - if current_version < min_version: - raise cv.Invalid( - f"Your ESPHome version is too old. Please update to at least {min_version}" - ) - return value - - if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: _compile_process_limit_default = min( int(os.environ["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"]), @@ -164,7 +154,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( - cv.version_number, validate_version + cv.version_number, cv.validate_esphome_version ), cv.Optional( CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default From 062db622f3ec0f6550f6dbe60a42f4e097a6d2f4 Mon Sep 17 00:00:00 2001 From: Ivan Kravets Date: Mon, 19 Feb 2024 00:55:46 +0200 Subject: [PATCH 342/436] Adjust HeatpumpIR dependency (#6222) --- esphome/components/heatpumpir/climate.py | 2 +- platformio.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index a043b4a61b..8af4ca590f 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -119,4 +119,4 @@ def to_code(config): cg.add_library("tonia/HeatpumpIR", "1.0.23") if CORE.is_esp8266 or CORE.is_esp32: - cg.add_library("crankyoldgit/IRremoteESP8266", "2.7.12") + cg.add_library("crankyoldgit/IRremoteESP8266", "2.8.4") diff --git a/platformio.ini b/platformio.ini index e47527fe98..8ab2fb6f48 100644 --- a/platformio.ini +++ b/platformio.ini @@ -93,7 +93,7 @@ lib_deps = ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir + crankyoldgit/IRremoteESP8266@~2.8.4 ; heatpumpir build_flags = ${common:arduino.build_flags} -Wno-nonnull-compare @@ -122,7 +122,7 @@ lib_deps = ESPmDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) esphome/ESP32-audioI2S@2.0.7 ; i2s_audio - crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir + crankyoldgit/IRremoteESP8266@~2.8.4 ; heatpumpir droscy/esp_wireguard@0.3.2 ; wireguard build_flags = ${common:arduino.build_flags} From e1345ae7e3ca1ff99c7a540bff0bf939efb054c4 Mon Sep 17 00:00:00 2001 From: Anton Viktorov Date: Mon, 19 Feb 2024 02:24:59 +0100 Subject: [PATCH 343/436] INA226 - fixed improper work with signed values, added configurable ADC parameters (#6172) --- CODEOWNERS | 1 + esphome/components/ina226/__init__.py | 1 + esphome/components/ina226/ina226.cpp | 46 ++++++++++++++++++++------- esphome/components/ina226/ina226.h | 41 ++++++++++++++++++++++++ esphome/components/ina226/sensor.py | 40 ++++++++++++++++++++++- 5 files changed, 116 insertions(+), 13 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 56ec0a93cb..fce4534d7b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,7 @@ esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core +esphome/components/ina226/* @Sergio303 @latonita esphome/components/ina260/* @mreditor97 esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate6/* @jesserockz diff --git a/esphome/components/ina226/__init__.py b/esphome/components/ina226/__init__.py index e69de29bb2..ca1f2caa31 100644 --- a/esphome/components/ina226/__init__.py +++ b/esphome/components/ina226/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Sergio303", "@latonita"] diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index 1fb859da66..94016ad302 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -33,31 +33,37 @@ static const uint8_t INA226_REGISTER_POWER = 0x03; static const uint8_t INA226_REGISTER_CURRENT = 0x04; static const uint8_t INA226_REGISTER_CALIBRATION = 0x05; +static const uint16_t INA226_ADC_TIMES[] = {140, 204, 332, 588, 1100, 2116, 4156, 8244}; +static const uint16_t INA226_ADC_AVG_SAMPLES[] = {1, 4, 16, 64, 128, 256, 512, 1024}; + void INA226Component::setup() { ESP_LOGCONFIG(TAG, "Setting up INA226..."); - // Config Register - // 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset) - if (!this->write_byte_16(INA226_REGISTER_CONFIG, 0x8000)) { + + ConfigurationRegister config; + + config.reset = 1; + if (!this->write_byte_16(INA226_REGISTER_CONFIG, config.raw)) { this->mark_failed(); return; } delay(1); - uint16_t config = 0x0000; + config.raw = 0; + config.reserved = 0b100; // as per datasheet // Averaging Mode AVG Bit Settings[11:9] (000 -> 1 sample, 001 -> 4 sample, 111 -> 1024 samples) - config |= 0b0000001000000000; + config.avg_samples = this->adc_avg_samples_; // Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms) - config |= 0b0000000100000000; + config.bus_voltage_conversion_time = this->adc_time_; // Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms) - config |= 0b0000000000100000; + config.shunt_voltage_conversion_time = this->adc_time_; // Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous) - config |= 0b0000000000000111; + config.mode = 0b111; - if (!this->write_byte_16(INA226_REGISTER_CONFIG, config)) { + if (!this->write_byte_16(INA226_REGISTER_CONFIG, config.raw)) { this->mark_failed(); return; } @@ -87,6 +93,9 @@ void INA226Component::dump_config() { } LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " ADC Conversion Time: %d", INA226_ADC_TIMES[this->adc_time_ & 0b111]); + ESP_LOGCONFIG(TAG, " ADC Averaging Samples: %d", INA226_ADC_AVG_SAMPLES[this->adc_avg_samples_ & 0b111]); + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); LOG_SENSOR(" ", "Shunt Voltage", this->shunt_voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); @@ -102,7 +111,9 @@ void INA226Component::update() { this->status_set_warning(); return; } - float bus_voltage_v = int16_t(raw_bus_voltage) * 0.00125f; + // Convert for 2's compliment and signed value (though always positive) + float bus_voltage_v = this->twos_complement_(raw_bus_voltage, 16); + bus_voltage_v *= 0.00125f; this->bus_voltage_sensor_->publish_state(bus_voltage_v); } @@ -112,7 +123,9 @@ void INA226Component::update() { this->status_set_warning(); return; } - float shunt_voltage_v = int16_t(raw_shunt_voltage) * 0.0000025f; + // Convert for 2's compliment and signed value + float shunt_voltage_v = this->twos_complement_(raw_shunt_voltage, 16); + shunt_voltage_v *= 0.0000025f; this->shunt_voltage_sensor_->publish_state(shunt_voltage_v); } @@ -122,7 +135,9 @@ void INA226Component::update() { this->status_set_warning(); return; } - float current_ma = int16_t(raw_current) * (this->calibration_lsb_ / 1000.0f); + // Convert for 2's compliment and signed value + float current_ma = this->twos_complement_(raw_current, 16); + current_ma *= (this->calibration_lsb_ / 1000.0f); this->current_sensor_->publish_state(current_ma / 1000.0f); } @@ -139,5 +154,12 @@ void INA226Component::update() { this->status_clear_warning(); } +int32_t INA226Component::twos_complement_(int32_t val, uint8_t bits) { + if (val & ((uint32_t) 1 << (bits - 1))) { + val -= (uint32_t) 1 << bits; + } + return val; +} + } // namespace ina226 } // namespace esphome diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h index a551cb3430..2af9c8c195 100644 --- a/esphome/components/ina226/ina226.h +++ b/esphome/components/ina226/ina226.h @@ -7,6 +7,40 @@ namespace esphome { namespace ina226 { +enum AdcTime : uint16_t { + ADC_TIME_140US = 0, + ADC_TIME_204US = 1, + ADC_TIME_332US = 2, + ADC_TIME_588US = 3, + ADC_TIME_1100US = 4, + ADC_TIME_2116US = 5, + ADC_TIME_4156US = 6, + ADC_TIME_8244US = 7 +}; + +enum AdcAvgSamples : uint16_t { + ADC_AVG_SAMPLES_1 = 0, + ADC_AVG_SAMPLES_4 = 1, + ADC_AVG_SAMPLES_16 = 2, + ADC_AVG_SAMPLES_64 = 3, + ADC_AVG_SAMPLES_128 = 4, + ADC_AVG_SAMPLES_256 = 5, + ADC_AVG_SAMPLES_512 = 6, + ADC_AVG_SAMPLES_1024 = 7 +}; + +union ConfigurationRegister { + uint16_t raw; + struct { + uint16_t mode : 3; + AdcTime shunt_voltage_conversion_time : 3; + AdcTime bus_voltage_conversion_time : 3; + AdcAvgSamples avg_samples : 3; + uint16_t reserved : 3; + uint16_t reset : 1; + } __attribute__((packed)); +}; + class INA226Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; @@ -16,6 +50,9 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } + void set_adc_time(AdcTime time) { adc_time_ = time; } + void set_adc_avg_samples(AdcAvgSamples samples) { adc_avg_samples_ = samples; } + void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; } void set_shunt_voltage_sensor(sensor::Sensor *shunt_voltage_sensor) { shunt_voltage_sensor_ = shunt_voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } @@ -24,11 +61,15 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { protected: float shunt_resistance_ohm_; float max_current_a_; + AdcTime adc_time_{AdcTime::ADC_TIME_1100US}; + AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_4}; uint32_t calibration_lsb_; sensor::Sensor *bus_voltage_sensor_{nullptr}; sensor::Sensor *shunt_voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; + + int32_t twos_complement_(int32_t val, uint8_t bits); }; } // namespace ina226 diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py index ee4036ce7e..32fca504a9 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -20,11 +20,44 @@ from esphome.const import ( DEPENDENCIES = ["i2c"] +CONF_ADC_AVERAGING = "adc_averaging" +CONF_ADC_TIME = "adc_time" + ina226_ns = cg.esphome_ns.namespace("ina226") INA226Component = ina226_ns.class_( "INA226Component", cg.PollingComponent, i2c.I2CDevice ) +AdcTime = ina226_ns.enum("AdcTime") +ADC_TIMES = { + 140: AdcTime.ADC_TIME_140US, + 204: AdcTime.ADC_TIME_204US, + 332: AdcTime.ADC_TIME_332US, + 588: AdcTime.ADC_TIME_588US, + 1100: AdcTime.ADC_TIME_1100US, + 2116: AdcTime.ADC_TIME_2116US, + 4156: AdcTime.ADC_TIME_4156US, + 8244: AdcTime.ADC_TIME_8244US, +} + +AdcAvgSamples = ina226_ns.enum("AdcAvgSamples") +ADC_AVG_SAMPLES = { + 1: AdcAvgSamples.ADC_AVG_SAMPLES_1, + 4: AdcAvgSamples.ADC_AVG_SAMPLES_4, + 16: AdcAvgSamples.ADC_AVG_SAMPLES_16, + 64: AdcAvgSamples.ADC_AVG_SAMPLES_64, + 128: AdcAvgSamples.ADC_AVG_SAMPLES_128, + 256: AdcAvgSamples.ADC_AVG_SAMPLES_256, + 512: AdcAvgSamples.ADC_AVG_SAMPLES_512, + 1024: AdcAvgSamples.ADC_AVG_SAMPLES_1024, +} + + +def validate_adc_time(value): + value = cv.positive_time_period_microseconds(value).total_microseconds + return cv.enum(ADC_TIMES, int=True)(value) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -59,6 +92,10 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All( cv.current, cv.Range(min=0.0) ), + cv.Optional(CONF_ADC_TIME, default="1100 us"): validate_adc_time, + cv.Optional(CONF_ADC_AVERAGING, default=4): cv.enum( + ADC_AVG_SAMPLES, int=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -72,8 +109,9 @@ async def to_code(config): await i2c.register_i2c_device(var, config) cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) - cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) + cg.add(var.set_adc_time(config[CONF_ADC_TIME])) + cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING])) if CONF_BUS_VOLTAGE in config: sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE]) From 342fb72b6a7305fcf18043f05764ccaac8d12675 Mon Sep 17 00:00:00 2001 From: Carlos Ortega Date: Mon, 19 Feb 2024 01:29:41 +0000 Subject: [PATCH 344/436] Prevent network config on rpipico board (#5832) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/wizard.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/wizard.py b/esphome/wizard.py index 1308338ad0..4ec366bbb9 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -51,10 +51,12 @@ BASE_CONFIG_FRIENDLY = """esphome: friendly_name: {friendly_name} """ -LOGGER_API_CONFIG = """ +LOGGER_CONFIG = """ # Enable logging logger: +""" +API_CONFIG = """ # Enable Home Assistant API api: """ @@ -136,7 +138,12 @@ def wizard_file(**kwargs): config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs) - config += LOGGER_API_CONFIG + config += LOGGER_CONFIG + + if kwargs["board"] == "rpipico": + return config + + config += API_CONFIG # Configure API if "password" in kwargs: From 967259a21289666d63fed33b351e7e166bde02a5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:44:18 +1300 Subject: [PATCH 345/436] Fix xl9535 pin reads (#6242) --- esphome/components/xl9535/xl9535.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp index 8f4f556b4a..93c65a4cb7 100644 --- a/esphome/components/xl9535/xl9535.cpp +++ b/esphome/components/xl9535/xl9535.cpp @@ -36,14 +36,14 @@ bool XL9535Component::digital_read(uint8_t pin) { return state; } - state = (port & (pin - 10)) != 0; + state = (port & (1 << (pin - 10))) != 0; } else { if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { this->status_set_warning(); return state; } - state = (port & pin) != 0; + state = (port & (1 << pin)) != 0; } this->status_clear_warning(); From c39f6d0738d97ecc11238220b493731ec70c701c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 08:13:52 -0600 Subject: [PATCH 346/436] Bump pytest-asyncio from 0.23.3 to 0.23.5 (#6201) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 74d66f5b25..8b7c4d28bc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==7.4.4 pytest-cov==4.1.0 pytest-mock==3.12.0 -pytest-asyncio==0.23.3 +pytest-asyncio==0.23.5 asyncmock==0.4.2 hypothesis==6.92.1 From 5d144cff02084118d0b0baff22e29ce1f84bedf1 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 19 Feb 2024 11:13:12 -0800 Subject: [PATCH 347/436] hold interrupt disable for dallas one-wire (#6244) Co-authored-by: Samuel Sieb --- esphome/components/dallas/dallas_component.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 302422d6c7..b1983fef72 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -168,10 +168,6 @@ bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { if (!wire->reset()) { return false; } - } - - { - InterruptLock lock; wire->select(this->address_); wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD); From edd1678463181d65868c3025b1a7353bc97c3e16 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Mon, 19 Feb 2024 18:24:44 -0500 Subject: [PATCH 348/436] New component: ADE7880 voltage/current/power/energy sensor (#5242) --- CODEOWNERS | 1 + esphome/components/ade7880/__init__.py | 1 + esphome/components/ade7880/ade7880.cpp | 302 ++++++++++++++++++ esphome/components/ade7880/ade7880.h | 131 ++++++++ esphome/components/ade7880/ade7880_i2c.cpp | 101 ++++++ .../components/ade7880/ade7880_registers.h | 243 ++++++++++++++ esphome/components/ade7880/sensor.py | 290 +++++++++++++++++ tests/components/ade7880/common.yaml | 56 ++++ .../components/ade7880/test.esp32-c3-idf.yaml | 8 + tests/components/ade7880/test.esp32-c3.yaml | 8 + tests/components/ade7880/test.esp32-idf.yaml | 8 + tests/components/ade7880/test.esp32.yaml | 8 + tests/components/ade7880/test.esp8266.yaml | 8 + tests/components/ade7880/test.rp2040.yaml | 8 + tests/test1.yaml | 56 ++++ tests/test3.1.yaml | 56 ++++ 16 files changed, 1285 insertions(+) create mode 100644 esphome/components/ade7880/__init__.py create mode 100644 esphome/components/ade7880/ade7880.cpp create mode 100644 esphome/components/ade7880/ade7880.h create mode 100644 esphome/components/ade7880/ade7880_i2c.cpp create mode 100644 esphome/components/ade7880/ade7880_registers.h create mode 100644 esphome/components/ade7880/sensor.py create mode 100644 tests/components/ade7880/common.yaml create mode 100644 tests/components/ade7880/test.esp32-c3-idf.yaml create mode 100644 tests/components/ade7880/test.esp32-c3.yaml create mode 100644 tests/components/ade7880/test.esp32-idf.yaml create mode 100644 tests/components/ade7880/test.esp32.yaml create mode 100644 tests/components/ade7880/test.esp8266.yaml create mode 100644 tests/components/ade7880/test.rp2040.yaml diff --git a/CODEOWNERS b/CODEOWNERS index fce4534d7b..9577d7df47 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -18,6 +18,7 @@ esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter +esphome/components/ade7880/* @kpfleming esphome/components/ade7953/* @angelnu esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_spi/* @angelnu diff --git a/esphome/components/ade7880/__init__.py b/esphome/components/ade7880/__init__.py new file mode 100644 index 0000000000..aed63c7dfa --- /dev/null +++ b/esphome/components/ade7880/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@kpfleming"] diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp new file mode 100644 index 0000000000..31b72d51a6 --- /dev/null +++ b/esphome/components/ade7880/ade7880.cpp @@ -0,0 +1,302 @@ +// This component was developed using knowledge gathered by a number +// of people who reverse-engineered the Shelly 3EM: +// +// @AndreKR on GitHub +// Axel (@Axel830 on GitHub) +// Marko (@goodkiller on GitHub) +// Michaël Piron (@michaelpiron on GitHub) +// Theo Arends (@arendst on GitHub) + +#include "ade7880.h" +#include "ade7880_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ade7880 { + +static const char *const TAG = "ade7880"; + +void IRAM_ATTR ADE7880Store::gpio_intr(ADE7880Store *arg) { arg->reset_done = true; } + +void ADE7880::setup() { + if (this->irq0_pin_ != nullptr) { + this->irq0_pin_->setup(); + } + this->irq1_pin_->setup(); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + } + this->store_.irq1_pin = this->irq1_pin_->to_isr(); + this->irq1_pin_->attach_interrupt(ADE7880Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + + // if IRQ1 is already asserted, the cause must be determined + if (this->irq1_pin_->digital_read() == 0) { + ESP_LOGD(TAG, "IRQ1 found asserted during setup()"); + auto status1 = read_u32_register16_(STATUS1); + if ((status1 & ~STATUS1_RSTDONE) != 0) { + // not safe to proceed, must initiate reset + ESP_LOGD(TAG, "IRQ1 asserted for !RSTDONE, resetting device"); + this->reset_device_(); + return; + } + if ((status1 & STATUS1_RSTDONE) == STATUS1_RSTDONE) { + // safe to proceed, device has just completed reset cycle + ESP_LOGD(TAG, "Acknowledging RSTDONE"); + this->write_u32_register16_(STATUS0, 0xFFFF); + this->write_u32_register16_(STATUS1, 0xFFFF); + this->init_device_(); + return; + } + } + + this->reset_device_(); +} + +void ADE7880::loop() { + // check for completion of a reset cycle + if (!this->store_.reset_done) { + return; + } + + ESP_LOGD(TAG, "Acknowledging RSTDONE"); + this->write_u32_register16_(STATUS0, 0xFFFF); + this->write_u32_register16_(STATUS1, 0xFFFF); + this->init_device_(); + this->store_.reset_done = false; + this->store_.reset_pending = false; +} + +template +void ADE7880::update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) { + if (sensor == nullptr) { + return; + } + + float val = this->read_s24zp_register16_(a_register); + sensor->publish_state(f(val)); +} + +template +void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) { + if (sensor == nullptr) { + return; + } + + float val = this->read_s16_register16_(a_register); + sensor->publish_state(f(val)); +} + +template +void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) { + if (sensor == nullptr) { + return; + } + + float val = this->read_s32_register16_(a_register); + sensor->publish_state(f(val)); +} + +void ADE7880::update() { + if (this->store_.reset_pending) { + return; + } + + auto start = millis(); + + if (this->channel_n_ != nullptr) { + auto *chan = this->channel_n_; + this->update_sensor_from_s24zp_register16_(chan->current, NIRMS, [](float val) { return val / 100000.0f; }); + } + + if (this->channel_a_ != nullptr) { + auto *chan = this->channel_a_; + this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s16_register16_(chan->power_factor, APF, + [](float val) { return std::abs(val / -327.68f); }); + this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { + return chan->forward_active_energy_total += val / 14400.0f; + }); + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { + return chan->reverse_active_energy_total += val / 14400.0f; + }); + } + + if (this->channel_b_ != nullptr) { + auto *chan = this->channel_b_; + this->update_sensor_from_s24zp_register16_(chan->current, BIRMS, [](float val) { return val / 100000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->active_power, BWATT, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s16_register16_(chan->power_factor, BPF, + [](float val) { return std::abs(val / -327.68f); }); + this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { + return chan->forward_active_energy_total += val / 14400.0f; + }); + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { + return chan->reverse_active_energy_total += val / 14400.0f; + }); + } + + if (this->channel_c_ != nullptr) { + auto *chan = this->channel_c_; + this->update_sensor_from_s24zp_register16_(chan->current, CIRMS, [](float val) { return val / 100000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->voltage, CVRMS, [](float val) { return val / 10000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->active_power, CWATT, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s16_register16_(chan->power_factor, CPF, + [](float val) { return std::abs(val / -327.68f); }); + this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { + return chan->forward_active_energy_total += val / 14400.0f; + }); + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { + return chan->reverse_active_energy_total += val / 14400.0f; + }); + } + + ESP_LOGD(TAG, "update took %u ms", millis() - start); +} + +void ADE7880::dump_config() { + ESP_LOGCONFIG(TAG, "ADE7880:"); + LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_); + LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_); + LOG_PIN(" RESET Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_); + + if (this->channel_a_ != nullptr) { + ESP_LOGCONFIG(TAG, " Phase A:"); + LOG_SENSOR(" ", "Current", this->channel_a_->current); + LOG_SENSOR(" ", "Voltage", this->channel_a_->voltage); + LOG_SENSOR(" ", "Active Power", this->channel_a_->active_power); + LOG_SENSOR(" ", "Apparent Power", this->channel_a_->apparent_power); + LOG_SENSOR(" ", "Power Factor", this->channel_a_->power_factor); + LOG_SENSOR(" ", "Forward Active Energy", this->channel_a_->forward_active_energy); + LOG_SENSOR(" ", "Reverse Active Energy", this->channel_a_->reverse_active_energy); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %u", this->channel_a_->current_gain_calibration); + ESP_LOGCONFIG(TAG, " Voltage: %d", this->channel_a_->voltage_gain_calibration); + ESP_LOGCONFIG(TAG, " Power: %d", this->channel_a_->power_gain_calibration); + ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_a_->phase_angle_calibration); + } + + if (this->channel_b_ != nullptr) { + ESP_LOGCONFIG(TAG, " Phase B:"); + LOG_SENSOR(" ", "Current", this->channel_b_->current); + LOG_SENSOR(" ", "Voltage", this->channel_b_->voltage); + LOG_SENSOR(" ", "Active Power", this->channel_b_->active_power); + LOG_SENSOR(" ", "Apparent Power", this->channel_b_->apparent_power); + LOG_SENSOR(" ", "Power Factor", this->channel_b_->power_factor); + LOG_SENSOR(" ", "Forward Active Energy", this->channel_b_->forward_active_energy); + LOG_SENSOR(" ", "Reverse Active Energy", this->channel_b_->reverse_active_energy); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %u", this->channel_b_->current_gain_calibration); + ESP_LOGCONFIG(TAG, " Voltage: %d", this->channel_b_->voltage_gain_calibration); + ESP_LOGCONFIG(TAG, " Power: %d", this->channel_b_->power_gain_calibration); + ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_b_->phase_angle_calibration); + } + + if (this->channel_c_ != nullptr) { + ESP_LOGCONFIG(TAG, " Phase C:"); + LOG_SENSOR(" ", "Current", this->channel_c_->current); + LOG_SENSOR(" ", "Voltage", this->channel_c_->voltage); + LOG_SENSOR(" ", "Active Power", this->channel_c_->active_power); + LOG_SENSOR(" ", "Apparent Power", this->channel_c_->apparent_power); + LOG_SENSOR(" ", "Power Factor", this->channel_c_->power_factor); + LOG_SENSOR(" ", "Forward Active Energy", this->channel_c_->forward_active_energy); + LOG_SENSOR(" ", "Reverse Active Energy", this->channel_c_->reverse_active_energy); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %u", this->channel_c_->current_gain_calibration); + ESP_LOGCONFIG(TAG, " Voltage: %d", this->channel_c_->voltage_gain_calibration); + ESP_LOGCONFIG(TAG, " Power: %d", this->channel_c_->power_gain_calibration); + ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_c_->phase_angle_calibration); + } + + if (this->channel_n_ != nullptr) { + ESP_LOGCONFIG(TAG, " Neutral:"); + LOG_SENSOR(" ", "Current", this->channel_n_->current); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %u", this->channel_n_->current_gain_calibration); + } + + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); +} + +void ADE7880::calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration) { + if (calibration == 0) { + return; + } + + this->write_s10zp_register16_(a_register, calibration); +} + +void ADE7880::calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration) { + if (calibration == 0) { + return; + } + + this->write_s24zpse_register16_(a_register, calibration); +} + +void ADE7880::init_device_() { + this->write_u8_register16_(CONFIG2, CONFIG2_I2C_LOCK); + + this->write_u16_register16_(GAIN, 0); + + if (this->frequency_ > 55) { + this->write_u16_register16_(COMPMODE, COMPMODE_DEFAULT | COMPMODE_SELFREQ); + } + + if (this->channel_n_ != nullptr) { + this->calibrate_s24zpse_reading_(NIGAIN, this->channel_n_->current_gain_calibration); + } + + if (this->channel_a_ != nullptr) { + this->calibrate_s24zpse_reading_(AIGAIN, this->channel_a_->current_gain_calibration); + this->calibrate_s24zpse_reading_(AVGAIN, this->channel_a_->voltage_gain_calibration); + this->calibrate_s24zpse_reading_(APGAIN, this->channel_a_->power_gain_calibration); + this->calibrate_s10zp_reading_(APHCAL, this->channel_a_->phase_angle_calibration); + } + + if (this->channel_b_ != nullptr) { + this->calibrate_s24zpse_reading_(BIGAIN, this->channel_b_->current_gain_calibration); + this->calibrate_s24zpse_reading_(BVGAIN, this->channel_b_->voltage_gain_calibration); + this->calibrate_s24zpse_reading_(BPGAIN, this->channel_b_->power_gain_calibration); + this->calibrate_s10zp_reading_(BPHCAL, this->channel_b_->phase_angle_calibration); + } + + if (this->channel_c_ != nullptr) { + this->calibrate_s24zpse_reading_(CIGAIN, this->channel_c_->current_gain_calibration); + this->calibrate_s24zpse_reading_(CVGAIN, this->channel_c_->voltage_gain_calibration); + this->calibrate_s24zpse_reading_(CPGAIN, this->channel_c_->power_gain_calibration); + this->calibrate_s10zp_reading_(CPHCAL, this->channel_c_->phase_angle_calibration); + } + + // write three default values to data memory RAM to flush the I2C write queue + this->write_s32_register16_(VLEVEL, 0); + this->write_s32_register16_(VLEVEL, 0); + this->write_s32_register16_(VLEVEL, 0); + + this->write_u8_register16_(DSPWP_SEL, DSPWP_SEL_SET); + this->write_u8_register16_(DSPWP_SET, DSPWP_SET_RO); + this->write_u16_register16_(RUN, RUN_ENABLE); +} + +void ADE7880::reset_device_() { + if (this->reset_pin_ != nullptr) { + ESP_LOGD(TAG, "Reset device using RESET pin"); + this->reset_pin_->digital_write(false); + delay(1); + this->reset_pin_->digital_write(true); + } else { + ESP_LOGD(TAG, "Reset device using SWRST command"); + this->write_u16_register16_(CONFIG, CONFIG_SWRST); + } + this->store_.reset_pending = true; +} + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h new file mode 100644 index 0000000000..a565357dc5 --- /dev/null +++ b/esphome/components/ade7880/ade7880.h @@ -0,0 +1,131 @@ +#pragma once + +// This component was developed using knowledge gathered by a number +// of people who reverse-engineered the Shelly 3EM: +// +// @AndreKR on GitHub +// Axel (@Axel830 on GitHub) +// Marko (@goodkiller on GitHub) +// Michaël Piron (@michaelpiron on GitHub) +// Theo Arends (@arendst on GitHub) + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +#include "ade7880_registers.h" + +namespace esphome { +namespace ade7880 { + +struct NeutralChannel { + void set_current(sensor::Sensor *sens) { this->current = sens; } + + void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; } + + sensor::Sensor *current{nullptr}; + int32_t current_gain_calibration{0}; +}; + +struct PowerChannel { + void set_current(sensor::Sensor *sens) { this->current = sens; } + void set_voltage(sensor::Sensor *sens) { this->voltage = sens; } + void set_active_power(sensor::Sensor *sens) { this->active_power = sens; } + void set_apparent_power(sensor::Sensor *sens) { this->apparent_power = sens; } + void set_power_factor(sensor::Sensor *sens) { this->power_factor = sens; } + void set_forward_active_energy(sensor::Sensor *sens) { this->forward_active_energy = sens; } + void set_reverse_active_energy(sensor::Sensor *sens) { this->reverse_active_energy = sens; } + + void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; } + void set_voltage_gain_calibration(int32_t val) { this->voltage_gain_calibration = val; } + void set_power_gain_calibration(int32_t val) { this->power_gain_calibration = val; } + void set_phase_angle_calibration(int32_t val) { this->phase_angle_calibration = val; } + + sensor::Sensor *current{nullptr}; + sensor::Sensor *voltage{nullptr}; + sensor::Sensor *active_power{nullptr}; + sensor::Sensor *apparent_power{nullptr}; + sensor::Sensor *power_factor{nullptr}; + sensor::Sensor *forward_active_energy{nullptr}; + sensor::Sensor *reverse_active_energy{nullptr}; + int32_t current_gain_calibration{0}; + int32_t voltage_gain_calibration{0}; + int32_t power_gain_calibration{0}; + uint16_t phase_angle_calibration{0}; + float forward_active_energy_total{0}; + float reverse_active_energy_total{0}; +}; + +// Store data in a class that doesn't use multiple-inheritance (no vtables in flash!) +struct ADE7880Store { + volatile bool reset_done{false}; + bool reset_pending{false}; + ISRInternalGPIOPin irq1_pin; + + static void gpio_intr(ADE7880Store *arg); +}; + +class ADE7880 : public i2c::I2CDevice, public PollingComponent { + public: + void set_irq0_pin(InternalGPIOPin *pin) { this->irq0_pin_ = pin; } + void set_irq1_pin(InternalGPIOPin *pin) { this->irq1_pin_ = pin; } + void set_reset_pin(InternalGPIOPin *pin) { this->reset_pin_ = pin; } + void set_frequency(float frequency) { this->frequency_ = frequency; } + void set_channel_n(NeutralChannel *channel) { this->channel_n_ = channel; } + void set_channel_a(PowerChannel *channel) { this->channel_a_ = channel; } + void set_channel_b(PowerChannel *channel) { this->channel_b_ = channel; } + void set_channel_c(PowerChannel *channel) { this->channel_c_ = channel; } + + void setup() override; + + void loop() override; + + void update() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + ADE7880Store store_{}; + InternalGPIOPin *irq0_pin_{nullptr}; + InternalGPIOPin *irq1_pin_{nullptr}; + InternalGPIOPin *reset_pin_{nullptr}; + float frequency_; + NeutralChannel *channel_n_{nullptr}; + PowerChannel *channel_a_{nullptr}; + PowerChannel *channel_b_{nullptr}; + PowerChannel *channel_c_{nullptr}; + + void calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration); + void calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration); + + void init_device_(); + + // each of these functions allow the caller to pass in a lambda (or any other callable) + // which modifies the value read from the register before it is passed to the sensor + // the callable will be passed a 'float' value and is expected to return a 'float' + template void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f); + template void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f); + template void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f); + + void reset_device_(); + + uint8_t read_u8_register16_(uint16_t a_register); + int16_t read_s16_register16_(uint16_t a_register); + uint16_t read_u16_register16_(uint16_t a_register); + int32_t read_s24zp_register16_(uint16_t a_register); + int32_t read_s32_register16_(uint16_t a_register); + uint32_t read_u32_register16_(uint16_t a_register); + + void write_u8_register16_(uint16_t a_register, uint8_t value); + void write_s10zp_register16_(uint16_t a_register, int16_t value); + void write_u16_register16_(uint16_t a_register, uint16_t value); + void write_s24zpse_register16_(uint16_t a_register, int32_t value); + void write_s32_register16_(uint16_t a_register, int32_t value); + void write_u32_register16_(uint16_t a_register, uint32_t value); +}; + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/ade7880_i2c.cpp b/esphome/components/ade7880/ade7880_i2c.cpp new file mode 100644 index 0000000000..fae20f175d --- /dev/null +++ b/esphome/components/ade7880/ade7880_i2c.cpp @@ -0,0 +1,101 @@ +// This component was developed using knowledge gathered by a number +// of people who reverse-engineered the Shelly 3EM: +// +// @AndreKR on GitHub +// Axel (@Axel830 on GitHub) +// Marko (@goodkiller on GitHub) +// Michaël Piron (@michaelpiron on GitHub) +// Theo Arends (@arendst on GitHub) + +#include "ade7880.h" + +namespace esphome { +namespace ade7880 { + +// adapted from https://stackoverflow.com/a/55912127/1886371 +template inline T sign_extend(const T &v) noexcept { + using S = struct { signed Val : Bits; }; + return reinterpret_cast(&v)->Val; +} + +// Register types +// unsigned 8-bit (uint8_t) +// signed 10-bit - 16-bit ZP on wire (int16_t, needs sign extension) +// unsigned 16-bit (uint16_t) +// unsigned 20-bit - 32-bit ZP on wire (uint32_t) +// signed 24-bit - 32-bit ZPSE on wire (int32_t, needs sign extension) +// signed 24-bit - 32-bit ZP on wire (int32_t, needs sign extension) +// signed 24-bit - 32-bit SE on wire (int32_t) +// signed 28-bit - 32-bit ZP on wire (int32_t, needs sign extension) +// unsigned 32-bit (uint32_t) +// signed 32-bit (int32_t) + +uint8_t ADE7880::read_u8_register16_(uint16_t a_register) { + uint8_t in; + this->read_register16(a_register, &in, sizeof(in)); + return in; +} + +int16_t ADE7880::read_s16_register16_(uint16_t a_register) { + int16_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +uint16_t ADE7880::read_u16_register16_(uint16_t a_register) { + uint16_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +int32_t ADE7880::read_s24zp_register16_(uint16_t a_register) { + // s24zp means 24 bit signed value in the lower 24 bits of a 32-bit register + int32_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return sign_extend<24>(convert_big_endian(in)); +} + +int32_t ADE7880::read_s32_register16_(uint16_t a_register) { + int32_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +uint32_t ADE7880::read_u32_register16_(uint16_t a_register) { + uint32_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +void ADE7880::write_u8_register16_(uint16_t a_register, uint8_t value) { + this->write_register16(a_register, &value, sizeof(value)); +} + +void ADE7880::write_s10zp_register16_(uint16_t a_register, int16_t value) { + int16_t out = convert_big_endian(value & 0x03FF); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_u16_register16_(uint16_t a_register, uint16_t value) { + uint16_t out = convert_big_endian(value); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_s24zpse_register16_(uint16_t a_register, int32_t value) { + // s24zpse means a 24-bit signed value, sign-extended to 28 bits, in the lower 28 bits of a 32-bit register + int32_t out = convert_big_endian(value & 0x0FFFFFFF); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_s32_register16_(uint16_t a_register, int32_t value) { + int32_t out = convert_big_endian(value); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_u32_register16_(uint16_t a_register, uint32_t value) { + uint32_t out = convert_big_endian(value); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h new file mode 100644 index 0000000000..8b5b68abb0 --- /dev/null +++ b/esphome/components/ade7880/ade7880_registers.h @@ -0,0 +1,243 @@ +#pragma once + +// This file is a modified version of the one created by Michaël Piron (@michaelpiron on GitHub) + +// Source: https://www.analog.com/media/en/technical-documentation/application-notes/AN-1127.pdf + +namespace esphome { +namespace ade7880 { + +// DSP Data Memory RAM registers +constexpr uint16_t AIGAIN = 0x4380; +constexpr uint16_t AVGAIN = 0x4381; +constexpr uint16_t BIGAIN = 0x4382; +constexpr uint16_t BVGAIN = 0x4383; +constexpr uint16_t CIGAIN = 0x4384; +constexpr uint16_t CVGAIN = 0x4385; +constexpr uint16_t NIGAIN = 0x4386; + +constexpr uint16_t DICOEFF = 0x4388; + +constexpr uint16_t APGAIN = 0x4389; +constexpr uint16_t AWATTOS = 0x438A; +constexpr uint16_t BPGAIN = 0x438B; +constexpr uint16_t BWATTOS = 0x438C; +constexpr uint16_t CPGAIN = 0x438D; +constexpr uint16_t CWATTOS = 0x438E; +constexpr uint16_t AIRMSOS = 0x438F; +constexpr uint16_t AVRMSOS = 0x4390; +constexpr uint16_t BIRMSOS = 0x4391; +constexpr uint16_t BVRMSOS = 0x4392; +constexpr uint16_t CIRMSOS = 0x4393; +constexpr uint16_t CVRMSOS = 0x4394; +constexpr uint16_t NIRMSOS = 0x4395; +constexpr uint16_t HPGAIN = 0x4398; +constexpr uint16_t ISUMLVL = 0x4399; + +constexpr uint16_t VLEVEL = 0x439F; + +constexpr uint16_t AFWATTOS = 0x43A2; +constexpr uint16_t BFWATTOS = 0x43A3; +constexpr uint16_t CFWATTOS = 0x43A4; + +constexpr uint16_t AFVAROS = 0x43A5; +constexpr uint16_t BFVAROS = 0x43A6; +constexpr uint16_t CFVAROS = 0x43A7; + +constexpr uint16_t AFIRMSOS = 0x43A8; +constexpr uint16_t BFIRMSOS = 0x43A9; +constexpr uint16_t CFIRMSOS = 0x43AA; + +constexpr uint16_t AFVRMSOS = 0x43AB; +constexpr uint16_t BFVRMSOS = 0x43AC; +constexpr uint16_t CFVRMSOS = 0x43AD; + +constexpr uint16_t HXWATTOS = 0x43AE; +constexpr uint16_t HYWATTOS = 0x43AF; +constexpr uint16_t HZWATTOS = 0x43B0; +constexpr uint16_t HXVAROS = 0x43B1; +constexpr uint16_t HYVAROS = 0x43B2; +constexpr uint16_t HZVAROS = 0x43B3; + +constexpr uint16_t HXIRMSOS = 0x43B4; +constexpr uint16_t HYIRMSOS = 0x43B5; +constexpr uint16_t HZIRMSOS = 0x43B6; +constexpr uint16_t HXVRMSOS = 0x43B7; +constexpr uint16_t HYVRMSOS = 0x43B8; +constexpr uint16_t HZVRMSOS = 0x43B9; + +constexpr uint16_t AIRMS = 0x43C0; +constexpr uint16_t AVRMS = 0x43C1; +constexpr uint16_t BIRMS = 0x43C2; +constexpr uint16_t BVRMS = 0x43C3; +constexpr uint16_t CIRMS = 0x43C4; +constexpr uint16_t CVRMS = 0x43C5; +constexpr uint16_t NIRMS = 0x43C6; + +constexpr uint16_t ISUM = 0x43C7; + +// Internal DSP Memory RAM registers +constexpr uint16_t RUN = 0xE228; + +constexpr uint16_t AWATTHR = 0xE400; +constexpr uint16_t BWATTHR = 0xE401; +constexpr uint16_t CWATTHR = 0xE402; +constexpr uint16_t AFWATTHR = 0xE403; +constexpr uint16_t BFWATTHR = 0xE404; +constexpr uint16_t CFWATTHR = 0xE405; +constexpr uint16_t AFVARHR = 0xE409; +constexpr uint16_t BFVARHR = 0xE40A; +constexpr uint16_t CFVARHR = 0xE40B; + +constexpr uint16_t AVAHR = 0xE40C; +constexpr uint16_t BVAHR = 0xE40D; +constexpr uint16_t CVAHR = 0xE40E; + +constexpr uint16_t IPEAK = 0xE500; +constexpr uint16_t VPEAK = 0xE501; + +constexpr uint16_t STATUS0 = 0xE502; +constexpr uint16_t STATUS1 = 0xE503; + +constexpr uint16_t AIMAV = 0xE504; +constexpr uint16_t BIMAV = 0xE505; +constexpr uint16_t CIMAV = 0xE506; + +constexpr uint16_t OILVL = 0xE507; +constexpr uint16_t OVLVL = 0xE508; +constexpr uint16_t SAGLVL = 0xE509; +constexpr uint16_t MASK0 = 0xE50A; +constexpr uint16_t MASK1 = 0xE50B; + +constexpr uint16_t IAWV = 0xE50C; +constexpr uint16_t IBWV = 0xE50D; +constexpr uint16_t ICWV = 0xE50E; +constexpr uint16_t INWV = 0xE50F; +constexpr uint16_t VAWV = 0xE510; +constexpr uint16_t VBWV = 0xE511; +constexpr uint16_t VCWV = 0xE512; + +constexpr uint16_t AWATT = 0xE513; +constexpr uint16_t BWATT = 0xE514; +constexpr uint16_t CWATT = 0xE515; + +constexpr uint16_t AFVAR = 0xE516; +constexpr uint16_t BFVAR = 0xE517; +constexpr uint16_t CFVAR = 0xE518; + +constexpr uint16_t AVA = 0xE519; +constexpr uint16_t BVA = 0xE51A; +constexpr uint16_t CVA = 0xE51B; + +constexpr uint16_t CHECKSUM = 0xE51F; +constexpr uint16_t VNOM = 0xE520; +constexpr uint16_t LAST_RWDATA_24BIT = 0xE5FF; +constexpr uint16_t PHSTATUS = 0xE600; +constexpr uint16_t ANGLE0 = 0xE601; +constexpr uint16_t ANGLE1 = 0xE602; +constexpr uint16_t ANGLE2 = 0xE603; +constexpr uint16_t PHNOLOAD = 0xE608; +constexpr uint16_t LINECYC = 0xE60C; +constexpr uint16_t ZXTOUT = 0xE60D; +constexpr uint16_t COMPMODE = 0xE60E; +constexpr uint16_t GAIN = 0xE60F; +constexpr uint16_t CFMODE = 0xE610; +constexpr uint16_t CF1DEN = 0xE611; +constexpr uint16_t CF2DEN = 0xE612; +constexpr uint16_t CF3DEN = 0xE613; +constexpr uint16_t APHCAL = 0xE614; +constexpr uint16_t BPHCAL = 0xE615; +constexpr uint16_t CPHCAL = 0xE616; +constexpr uint16_t PHSIGN = 0xE617; +constexpr uint16_t CONFIG = 0xE618; +constexpr uint16_t MMODE = 0xE700; +constexpr uint16_t ACCMODE = 0xE701; +constexpr uint16_t LCYCMODE = 0xE702; +constexpr uint16_t PEAKCYC = 0xE703; +constexpr uint16_t SAGCYC = 0xE704; +constexpr uint16_t CFCYC = 0xE705; +constexpr uint16_t HSDC_CFG = 0xE706; +constexpr uint16_t VERSION = 0xE707; +constexpr uint16_t DSPWP_SET = 0xE7E3; +constexpr uint16_t LAST_RWDATA_8BIT = 0xE7FD; +constexpr uint16_t DSPWP_SEL = 0xE7FE; +constexpr uint16_t FVRMS = 0xE880; +constexpr uint16_t FIRMS = 0xE881; +constexpr uint16_t FWATT = 0xE882; +constexpr uint16_t FVAR = 0xE883; +constexpr uint16_t FVA = 0xE884; +constexpr uint16_t FPF = 0xE885; +constexpr uint16_t VTHDN = 0xE886; +constexpr uint16_t ITHDN = 0xE887; +constexpr uint16_t HXVRMS = 0xE888; +constexpr uint16_t HXIRMS = 0xE889; +constexpr uint16_t HXWATT = 0xE88A; +constexpr uint16_t HXVAR = 0xE88B; +constexpr uint16_t HXVA = 0xE88C; +constexpr uint16_t HXPF = 0xE88D; +constexpr uint16_t HXVHD = 0xE88E; +constexpr uint16_t HXIHD = 0xE88F; +constexpr uint16_t HYVRMS = 0xE890; +constexpr uint16_t HYIRMS = 0xE891; +constexpr uint16_t HYWATT = 0xE892; +constexpr uint16_t HYVAR = 0xE893; +constexpr uint16_t HYVA = 0xE894; +constexpr uint16_t HYPF = 0xE895; +constexpr uint16_t HYVHD = 0xE896; +constexpr uint16_t HYIHD = 0xE897; +constexpr uint16_t HZVRMS = 0xE898; +constexpr uint16_t HZIRMS = 0xE899; +constexpr uint16_t HZWATT = 0xE89A; +constexpr uint16_t HZVAR = 0xE89B; +constexpr uint16_t HZVA = 0xE89C; +constexpr uint16_t HZPF = 0xE89D; +constexpr uint16_t HZVHD = 0xE89E; +constexpr uint16_t HZIHD = 0xE89F; +constexpr uint16_t HCONFIG = 0xE900; +constexpr uint16_t APF = 0xE902; +constexpr uint16_t BPF = 0xE903; +constexpr uint16_t CPF = 0xE904; +constexpr uint16_t APERIOD = 0xE905; +constexpr uint16_t BPERIOD = 0xE906; +constexpr uint16_t CPERIOD = 0xE907; +constexpr uint16_t APNOLOAD = 0xE908; +constexpr uint16_t VARNOLOAD = 0xE909; +constexpr uint16_t VANOLOAD = 0xE90A; +constexpr uint16_t LAST_ADD = 0xE9FE; +constexpr uint16_t LAST_RWDATA_16BIT = 0xE9FF; +constexpr uint16_t CONFIG3 = 0xEA00; +constexpr uint16_t LAST_OP = 0xEA01; +constexpr uint16_t WTHR = 0xEA02; +constexpr uint16_t VARTHR = 0xEA03; +constexpr uint16_t VATHR = 0xEA04; + +constexpr uint16_t HX_REG = 0xEA08; +constexpr uint16_t HY_REG = 0xEA09; +constexpr uint16_t HZ_REG = 0xEA0A; +constexpr uint16_t LPOILVL = 0xEC00; +constexpr uint16_t CONFIG2 = 0xEC01; + +// STATUS1 Register Bits +constexpr uint32_t STATUS1_RSTDONE = (1 << 15); + +// CONFIG Register Bits +constexpr uint16_t CONFIG_SWRST = (1 << 7); + +// CONFIG2 Register Bits +constexpr uint8_t CONFIG2_I2C_LOCK = (1 << 1); + +// COMPMODE Register Bits +constexpr uint16_t COMPMODE_DEFAULT = 0x01FF; +constexpr uint16_t COMPMODE_SELFREQ = (1 << 14); + +// RUN Register Bits +constexpr uint16_t RUN_ENABLE = (1 << 0); + +// DSPWP_SET Register Bits +constexpr uint8_t DSPWP_SET_RO = (1 << 7); + +// DSPWP_SEL Register Bits +constexpr uint8_t DSPWP_SEL_SET = 0xAD; + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py new file mode 100644 index 0000000000..42a2b0d3fc --- /dev/null +++ b/esphome/components/ade7880/sensor.py @@ -0,0 +1,290 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, i2c +from esphome import pins +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_CALIBRATION, + CONF_CURRENT, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_NAME, + CONF_PHASE_A, + CONF_PHASE_ANGLE, + CONF_PHASE_B, + CONF_PHASE_C, + CONF_POWER_FACTOR, + CONF_RESET_PIN, + CONF_REVERSE_ACTIVE_ENERGY, + CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_PERCENT, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE_HOURS, + UNIT_WATT, + UNIT_WATT_HOURS, +) + +DEPENDENCIES = ["i2c"] + +ade7880_ns = cg.esphome_ns.namespace("ade7880") +ADE7880 = ade7880_ns.class_("ADE7880", cg.PollingComponent, i2c.I2CDevice) +NeutralChannel = ade7880_ns.struct("NeutralChannel") +PowerChannel = ade7880_ns.struct("PowerChannel") + +CONF_CURRENT_GAIN = "current_gain" +CONF_IRQ0_PIN = "irq0_pin" +CONF_IRQ1_PIN = "irq1_pin" +CONF_POWER_GAIN = "power_gain" +CONF_VOLTAGE_GAIN = "voltage_gain" + +CONF_NEUTRAL = "neutral" + +NEUTRAL_CHANNEL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(NeutralChannel), + cv.Optional(CONF_NAME): cv.string_strict, + cv.Required(CONF_CURRENT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Required(CONF_CALIBRATION): cv.Schema( + { + cv.Required(CONF_CURRENT_GAIN): cv.int_, + }, + ), + } +) + +POWER_CHANNEL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PowerChannel), + cv.Optional(CONF_NAME): cv.string_strict, + cv.Optional(CONF_VOLTAGE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_CURRENT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTIVE_POWER): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_APPARENT_POWER): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_POWER_FACTOR): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + key=CONF_NAME, + ), + cv.Required(CONF_CALIBRATION): cv.Schema( + { + cv.Required(CONF_CURRENT_GAIN): cv.int_, + cv.Required(CONF_VOLTAGE_GAIN): cv.int_, + cv.Required(CONF_POWER_GAIN): cv.int_, + cv.Required(CONF_PHASE_ANGLE): cv.int_, + }, + ), + } +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADE7880), + cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( + cv.frequency, cv.Range(min=45.0, max=66.0) + ), + cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_PHASE_A): POWER_CHANNEL_SCHEMA, + cv.Optional(CONF_PHASE_B): POWER_CHANNEL_SCHEMA, + cv.Optional(CONF_PHASE_C): POWER_CHANNEL_SCHEMA, + cv.Optional(CONF_NEUTRAL): NEUTRAL_CHANNEL_SCHEMA, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x38)) +) + + +async def neutral_channel(config): + var = cg.new_Pvariable(config[CONF_ID]) + + current = config[CONF_CURRENT] + sens = await sensor.new_sensor(current) + cg.add(var.set_current(sens)) + + cg.add( + var.set_current_gain_calibration(config[CONF_CALIBRATION][CONF_CURRENT_GAIN]) + ) + + return var + + +async def power_channel(config): + var = cg.new_Pvariable(config[CONF_ID]) + + for sensor_type in [ + CONF_CURRENT, + CONF_VOLTAGE, + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_POWER_FACTOR, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, + ]: + if conf := config.get(sensor_type): + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{sensor_type}")(sens)) + + for calib_type in [ + CONF_CURRENT_GAIN, + CONF_VOLTAGE_GAIN, + CONF_POWER_GAIN, + CONF_PHASE_ANGLE, + ]: + cg.add( + getattr(var, f"set_{calib_type}_calibration")( + config[CONF_CALIBRATION][calib_type] + ) + ) + + return var + + +def final_validate(config): + for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]: + if channel := config.get(channel): + channel_name = channel.get(CONF_NAME) + + for sensor_type in [ + CONF_CURRENT, + CONF_VOLTAGE, + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_POWER_FACTOR, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, + ]: + if conf := channel.get(sensor_type): + sensor_name = conf.get(CONF_NAME) + if ( + sensor_name + and channel_name + and not sensor_name.startswith(channel_name) + ): + conf[CONF_NAME] = f"{channel_name} {sensor_name}" + + if channel := config.get(CONF_NEUTRAL): + channel_name = channel.get(CONF_NAME) + if conf := channel.get(CONF_CURRENT): + sensor_name = conf.get(CONF_NAME) + if ( + sensor_name + and channel_name + and not sensor_name.startswith(channel_name) + ): + conf[CONF_NAME] = f"{channel_name} {sensor_name}" + + +FINAL_VALIDATE_SCHEMA = final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if irq0_pin := config.get(CONF_IRQ0_PIN): + pin = await cg.gpio_pin_expression(irq0_pin) + cg.add(var.set_irq0_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ1_PIN]) + cg.add(var.set_irq1_pin(pin)) + + if reset_pin := config.get(CONF_RESET_PIN): + pin = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(pin)) + + if frequency := config.get(CONF_FREQUENCY): + cg.add(var.set_frequency(frequency)) + + if channel := config.get(CONF_PHASE_A): + chan = await power_channel(channel) + cg.add(var.set_channel_a(chan)) + + if channel := config.get(CONF_PHASE_B): + chan = await power_channel(channel) + cg.add(var.set_channel_b(chan)) + + if channel := config.get(CONF_PHASE_C): + chan = await power_channel(channel) + cg.add(var.set_channel_c(chan)) + + if channel := config.get(CONF_NEUTRAL): + chan = await neutral_channel(channel) + cg.add(var.set_channel_n(chan)) diff --git a/tests/components/ade7880/common.yaml b/tests/components/ade7880/common.yaml new file mode 100644 index 0000000000..0aa388a325 --- /dev/null +++ b/tests/components/ade7880/common.yaml @@ -0,0 +1,56 @@ +i2c: + - id: i2c_ade7880 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: ade7880 + i2c_id: i2c_ade7880 + irq0_pin: ${irq0_pin} + irq1_pin: ${irq1_pin} + reset_pin: ${reset_pin} + frequency: 60Hz + phase_a: + name: Channel A + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3116628 + voltage_gain: -757178 + power_gain: -1344457 + phase_angle: 188 + phase_b: + name: Channel B + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3133655 + voltage_gain: -755235 + power_gain: -1345638 + phase_angle: 188 + phase_c: + name: Channel C + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3111158 + voltage_gain: -743813 + power_gain: -1351437 + phase_angle: 180 + neutral: + name: Neutral + current: Current + calibration: + current_gain: 3189 diff --git a/tests/components/ade7880/test.esp32-c3-idf.yaml b/tests/components/ade7880/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..87db3e9427 --- /dev/null +++ b/tests/components/ade7880/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO6 + irq1_pin: GPIO7 + reset_pin: GPIO10 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-c3.yaml b/tests/components/ade7880/test.esp32-c3.yaml new file mode 100644 index 0000000000..87db3e9427 --- /dev/null +++ b/tests/components/ade7880/test.esp32-c3.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO6 + irq1_pin: GPIO7 + reset_pin: GPIO10 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-idf.yaml b/tests/components/ade7880/test.esp32-idf.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32.yaml b/tests/components/ade7880/test.esp32.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.esp32.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp8266.yaml b/tests/components/ade7880/test.esp8266.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.esp8266.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.rp2040.yaml b/tests/components/ade7880/test.rp2040.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.rp2040.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/test1.yaml b/tests/test1.yaml index 3558fa328e..c2b92c2b21 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1758,6 +1758,62 @@ sensor: memory_location: 0x20 memory_address: 0x7d name: Adres sensor + - platform: ade7880 + i2c_id: i2c_bus + irq0_pin: + number: GPIO13 + allow_other_uses: true + irq1_pin: + number: GPIO5 + allow_other_uses: true + reset_pin: + number: GPIO16 + allow_other_uses: true + frequency: 60Hz + phase_a: + name: Channel A + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3116628 + voltage_gain: -757178 + power_gain: -1344457 + phase_angle: 188 + phase_b: + name: Channel B + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3133655 + voltage_gain: -755235 + power_gain: -1345638 + phase_angle: 188 + phase_c: + name: Channel C + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3111158 + voltage_gain: -743813 + power_gain: -1351437 + phase_angle: 180 + neutral: + name: Neutral + current: Current + calibration: + current_gain: 3189 psram: diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 5cbdca91c1..fca37054a7 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -290,6 +290,62 @@ sensor: id: adc128s102_channel_0 channel: 0 + - platform: ade7880 + irq0_pin: + number: GPIO13 + allow_other_uses: true + irq1_pin: + number: GPIO5 + allow_other_uses: true + reset_pin: + number: GPIO16 + allow_other_uses: true + frequency: 60Hz + phase_a: + name: Channel A + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3116628 + voltage_gain: -757178 + power_gain: -1344457 + phase_angle: 188 + phase_b: + name: Channel B + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3133655 + voltage_gain: -755235 + power_gain: -1345638 + phase_angle: 188 + phase_c: + name: Channel C + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3111158 + voltage_gain: -743813 + power_gain: -1351437 + phase_angle: 180 + neutral: + name: Neutral + current: Current + calibration: + current_gain: 3189 + apds9960: address: 0x20 update_interval: 60s From 5ef1bab23e25e5b5ad81cb2ee203a82c4a2650de Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 20 Feb 2024 13:12:08 -0600 Subject: [PATCH 349/436] Fix tm1651 enum (#6248) --- esphome/components/tm1651/__init__.py | 7 ++++--- esphome/components/tm1651/tm1651.cpp | 14 +++++++------- esphome/components/tm1651/tm1651.h | 7 +++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index a6b2189eb6..4ef8842571 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CODEOWNERS = ["@freekode"] tm1651_ns = cg.esphome_ns.namespace("tm1651") +TM1651Brightness = tm1651_ns.enum("TM1651Brightness") TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component) SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) @@ -24,9 +25,9 @@ TurnOffAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) CONF_LEVEL_PERCENT = "level_percent" TM1651_BRIGHTNESS_OPTIONS = { - 1: TM1651Display.TM1651_BRIGHTNESS_LOW, - 2: TM1651Display.TM1651_BRIGHTNESS_MEDIUM, - 3: TM1651Display.TM1651_BRIGHTNESS_HIGH, + 1: TM1651Brightness.TM1651_BRIGHTNESS_LOW, + 2: TM1651Brightness.TM1651_BRIGHTNESS_MEDIUM, + 3: TM1651Brightness.TM1651_BRIGHTNESS_HIGH, } CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index c6bb1bc025..89807f5565 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -12,9 +12,9 @@ static const char *const TAG = "tm1651.display"; static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100; static const uint8_t TM1651_MAX_LEVEL = 7; -static const uint8_t TM1651_BRIGHTNESS_LOW = 0; -static const uint8_t TM1651_BRIGHTNESS_MEDIUM = 2; -static const uint8_t TM1651_BRIGHTNESS_HIGH = 7; +static const uint8_t TM1651_BRIGHTNESS_LOW_HW = 0; +static const uint8_t TM1651_BRIGHTNESS_MEDIUM_HW = 2; +static const uint8_t TM1651_BRIGHTNESS_HIGH_HW = 7; void TM1651Display::setup() { ESP_LOGCONFIG(TAG, "Setting up TM1651..."); @@ -78,14 +78,14 @@ uint8_t TM1651Display::calculate_level_(uint8_t new_level) { uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) { if (new_brightness <= 1) { - return TM1651_BRIGHTNESS_LOW; + return TM1651_BRIGHTNESS_LOW_HW; } else if (new_brightness == 2) { - return TM1651_BRIGHTNESS_MEDIUM; + return TM1651_BRIGHTNESS_MEDIUM_HW; } else if (new_brightness >= 3) { - return TM1651_BRIGHTNESS_HIGH; + return TM1651_BRIGHTNESS_HIGH_HW; } - return TM1651_BRIGHTNESS_LOW; + return TM1651_BRIGHTNESS_LOW_HW; } } // namespace tm1651 diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index eb65ed186d..fe7b7d9c6f 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -13,6 +13,12 @@ namespace esphome { namespace tm1651 { +enum TM1651Brightness : uint8_t { + TM1651_BRIGHTNESS_LOW = 1, + TM1651_BRIGHTNESS_MEDIUM = 2, + TM1651_BRIGHTNESS_HIGH = 3, +}; + class TM1651Display : public Component { public: void set_clk_pin(InternalGPIOPin *pin) { clk_pin_ = pin; } @@ -24,6 +30,7 @@ class TM1651Display : public Component { void set_level_percent(uint8_t new_level); void set_level(uint8_t new_level); void set_brightness(uint8_t new_brightness); + void set_brightness(TM1651Brightness new_brightness) { this->set_brightness(static_cast(new_brightness)); } void turn_on(); void turn_off(); From 2948d87a66e1a3388bbaca7a76767a254a2595bd Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 20 Feb 2024 13:40:13 -0600 Subject: [PATCH 350/436] Add some components to the new testing framework (D) (#6175) --- .../components/dac7678/test.esp32-c3-idf.yaml | 43 +++++++++++++++ tests/components/dac7678/test.esp32-c3.yaml | 43 +++++++++++++++ tests/components/dac7678/test.esp32-idf.yaml | 43 +++++++++++++++ tests/components/dac7678/test.esp32.yaml | 43 +++++++++++++++ tests/components/dac7678/test.esp8266.yaml | 43 +++++++++++++++ tests/components/dac7678/test.rp2040.yaml | 43 +++++++++++++++ tests/components/daikin/test.esp32.yaml | 12 ++++ tests/components/daikin/test.esp8266.yaml | 12 ++++ .../daikin_brc/test.esp32-c3-idf.yaml | 7 +++ .../components/daikin_brc/test.esp32-c3.yaml | 7 +++ .../components/daikin_brc/test.esp32-idf.yaml | 7 +++ tests/components/daikin_brc/test.esp32.yaml | 7 +++ tests/components/daikin_brc/test.esp8266.yaml | 7 +++ .../components/dallas/test.esp32-c3-idf.yaml | 11 ++++ tests/components/dallas/test.esp32-c3.yaml | 11 ++++ tests/components/dallas/test.esp32-idf.yaml | 11 ++++ tests/components/dallas/test.esp32.yaml | 11 ++++ tests/components/dallas/test.esp8266.yaml | 11 ++++ tests/components/dallas/test.rp2040.yaml | 11 ++++ .../daly_bms/test.esp32-c3-idf.yaml | 55 +++++++++++++++++++ tests/components/daly_bms/test.esp32-c3.yaml | 55 +++++++++++++++++++ tests/components/daly_bms/test.esp32-idf.yaml | 55 +++++++++++++++++++ tests/components/daly_bms/test.esp32.yaml | 55 +++++++++++++++++++ tests/components/daly_bms/test.esp8266.yaml | 55 +++++++++++++++++++ tests/components/daly_bms/test.rp2040.yaml | 55 +++++++++++++++++++ tests/components/debug/test.esp32-c3-idf.yaml | 1 + tests/components/debug/test.esp32-c3.yaml | 1 + tests/components/debug/test.esp32-idf.yaml | 1 + tests/components/debug/test.esp32.yaml | 1 + tests/components/debug/test.esp8266.yaml | 1 + tests/components/debug/test.rp2040.yaml | 1 + .../deep_sleep/test.esp32-c3-idf.yaml | 14 +++++ .../components/deep_sleep/test.esp32-c3.yaml | 14 +++++ .../components/deep_sleep/test.esp32-idf.yaml | 14 +++++ tests/components/deep_sleep/test.esp32.yaml | 14 +++++ tests/components/deep_sleep/test.esp8266.yaml | 10 ++++ .../delonghi/test.esp32-c3-idf.yaml | 7 +++ tests/components/delonghi/test.esp32-c3.yaml | 7 +++ tests/components/delonghi/test.esp32-idf.yaml | 7 +++ tests/components/delonghi/test.esp32.yaml | 7 +++ tests/components/delonghi/test.esp8266.yaml | 7 +++ .../dfplayer/test.esp32-c3-idf.yaml | 42 ++++++++++++++ tests/components/dfplayer/test.esp32-c3.yaml | 42 ++++++++++++++ tests/components/dfplayer/test.esp32-idf.yaml | 42 ++++++++++++++ tests/components/dfplayer/test.esp32.yaml | 42 ++++++++++++++ tests/components/dfplayer/test.esp8266.yaml | 42 ++++++++++++++ tests/components/dfplayer/test.rp2040.yaml | 42 ++++++++++++++ .../dfrobot_sen0395/test.esp32-c3-idf.yaml | 28 ++++++++++ .../dfrobot_sen0395/test.esp32-c3.yaml | 28 ++++++++++ .../dfrobot_sen0395/test.esp32-idf.yaml | 28 ++++++++++ .../dfrobot_sen0395/test.esp32.yaml | 28 ++++++++++ .../dfrobot_sen0395/test.esp8266.yaml | 28 ++++++++++ .../dfrobot_sen0395/test.rp2040.yaml | 28 ++++++++++ tests/components/dht/test.esp32-c3-idf.yaml | 11 ++++ tests/components/dht/test.esp32-c3.yaml | 11 ++++ tests/components/dht/test.esp32-idf.yaml | 11 ++++ tests/components/dht/test.esp32.yaml | 11 ++++ tests/components/dht/test.esp8266.yaml | 11 ++++ tests/components/dht/test.rp2040.yaml | 11 ++++ tests/components/dht12/test.esp32-c3-idf.yaml | 12 ++++ tests/components/dht12/test.esp32-c3.yaml | 12 ++++ tests/components/dht12/test.esp32-idf.yaml | 12 ++++ tests/components/dht12/test.esp32.yaml | 12 ++++ tests/components/dht12/test.esp8266.yaml | 12 ++++ tests/components/dht12/test.rp2040.yaml | 12 ++++ .../components/dps310/test.esp32-c3-idf.yaml | 12 ++++ tests/components/dps310/test.esp32-c3.yaml | 12 ++++ tests/components/dps310/test.esp32-idf.yaml | 12 ++++ tests/components/dps310/test.esp32.yaml | 12 ++++ tests/components/dps310/test.esp8266.yaml | 12 ++++ tests/components/dps310/test.rp2040.yaml | 12 ++++ .../components/ds1307/test.esp32-c3-idf.yaml | 9 +++ tests/components/ds1307/test.esp32-c3.yaml | 9 +++ tests/components/ds1307/test.esp32-idf.yaml | 9 +++ tests/components/ds1307/test.esp32.yaml | 9 +++ tests/components/ds1307/test.esp8266.yaml | 9 +++ tests/components/ds1307/test.rp2040.yaml | 9 +++ tests/components/dsmr/test.esp32-c3.yaml | 12 ++++ tests/components/dsmr/test.esp32.yaml | 12 ++++ tests/components/dsmr/test.esp8266.yaml | 12 ++++ tests/components/dsmr/test.rp2040.yaml | 12 ++++ .../duty_cycle/test.esp32-c3-idf.yaml | 4 ++ .../components/duty_cycle/test.esp32-c3.yaml | 4 ++ .../components/duty_cycle/test.esp32-idf.yaml | 4 ++ tests/components/duty_cycle/test.esp32.yaml | 4 ++ tests/components/duty_cycle/test.esp8266.yaml | 4 ++ tests/components/duty_cycle/test.rp2040.yaml | 4 ++ .../duty_time/test.esp32-c3-idf.yaml | 14 +++++ tests/components/duty_time/test.esp32-c3.yaml | 14 +++++ .../components/duty_time/test.esp32-idf.yaml | 14 +++++ tests/components/duty_time/test.esp32.yaml | 14 +++++ tests/components/duty_time/test.esp8266.yaml | 14 +++++ tests/components/duty_time/test.rp2040.yaml | 14 +++++ 93 files changed, 1660 insertions(+) create mode 100644 tests/components/dac7678/test.esp32-c3-idf.yaml create mode 100644 tests/components/dac7678/test.esp32-c3.yaml create mode 100644 tests/components/dac7678/test.esp32-idf.yaml create mode 100644 tests/components/dac7678/test.esp32.yaml create mode 100644 tests/components/dac7678/test.esp8266.yaml create mode 100644 tests/components/dac7678/test.rp2040.yaml create mode 100644 tests/components/daikin/test.esp32.yaml create mode 100644 tests/components/daikin/test.esp8266.yaml create mode 100644 tests/components/daikin_brc/test.esp32-c3-idf.yaml create mode 100644 tests/components/daikin_brc/test.esp32-c3.yaml create mode 100644 tests/components/daikin_brc/test.esp32-idf.yaml create mode 100644 tests/components/daikin_brc/test.esp32.yaml create mode 100644 tests/components/daikin_brc/test.esp8266.yaml create mode 100644 tests/components/dallas/test.esp32-c3-idf.yaml create mode 100644 tests/components/dallas/test.esp32-c3.yaml create mode 100644 tests/components/dallas/test.esp32-idf.yaml create mode 100644 tests/components/dallas/test.esp32.yaml create mode 100644 tests/components/dallas/test.esp8266.yaml create mode 100644 tests/components/dallas/test.rp2040.yaml create mode 100644 tests/components/daly_bms/test.esp32-c3-idf.yaml create mode 100644 tests/components/daly_bms/test.esp32-c3.yaml create mode 100644 tests/components/daly_bms/test.esp32-idf.yaml create mode 100644 tests/components/daly_bms/test.esp32.yaml create mode 100644 tests/components/daly_bms/test.esp8266.yaml create mode 100644 tests/components/daly_bms/test.rp2040.yaml create mode 100644 tests/components/debug/test.esp32-c3-idf.yaml create mode 100644 tests/components/debug/test.esp32-c3.yaml create mode 100644 tests/components/debug/test.esp32-idf.yaml create mode 100644 tests/components/debug/test.esp32.yaml create mode 100644 tests/components/debug/test.esp8266.yaml create mode 100644 tests/components/debug/test.rp2040.yaml create mode 100644 tests/components/deep_sleep/test.esp32-c3-idf.yaml create mode 100644 tests/components/deep_sleep/test.esp32-c3.yaml create mode 100644 tests/components/deep_sleep/test.esp32-idf.yaml create mode 100644 tests/components/deep_sleep/test.esp32.yaml create mode 100644 tests/components/deep_sleep/test.esp8266.yaml create mode 100644 tests/components/delonghi/test.esp32-c3-idf.yaml create mode 100644 tests/components/delonghi/test.esp32-c3.yaml create mode 100644 tests/components/delonghi/test.esp32-idf.yaml create mode 100644 tests/components/delonghi/test.esp32.yaml create mode 100644 tests/components/delonghi/test.esp8266.yaml create mode 100644 tests/components/dfplayer/test.esp32-c3-idf.yaml create mode 100644 tests/components/dfplayer/test.esp32-c3.yaml create mode 100644 tests/components/dfplayer/test.esp32-idf.yaml create mode 100644 tests/components/dfplayer/test.esp32.yaml create mode 100644 tests/components/dfplayer/test.esp8266.yaml create mode 100644 tests/components/dfplayer/test.rp2040.yaml create mode 100644 tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml create mode 100644 tests/components/dfrobot_sen0395/test.esp32-c3.yaml create mode 100644 tests/components/dfrobot_sen0395/test.esp32-idf.yaml create mode 100644 tests/components/dfrobot_sen0395/test.esp32.yaml create mode 100644 tests/components/dfrobot_sen0395/test.esp8266.yaml create mode 100644 tests/components/dfrobot_sen0395/test.rp2040.yaml create mode 100644 tests/components/dht/test.esp32-c3-idf.yaml create mode 100644 tests/components/dht/test.esp32-c3.yaml create mode 100644 tests/components/dht/test.esp32-idf.yaml create mode 100644 tests/components/dht/test.esp32.yaml create mode 100644 tests/components/dht/test.esp8266.yaml create mode 100644 tests/components/dht/test.rp2040.yaml create mode 100644 tests/components/dht12/test.esp32-c3-idf.yaml create mode 100644 tests/components/dht12/test.esp32-c3.yaml create mode 100644 tests/components/dht12/test.esp32-idf.yaml create mode 100644 tests/components/dht12/test.esp32.yaml create mode 100644 tests/components/dht12/test.esp8266.yaml create mode 100644 tests/components/dht12/test.rp2040.yaml create mode 100644 tests/components/dps310/test.esp32-c3-idf.yaml create mode 100644 tests/components/dps310/test.esp32-c3.yaml create mode 100644 tests/components/dps310/test.esp32-idf.yaml create mode 100644 tests/components/dps310/test.esp32.yaml create mode 100644 tests/components/dps310/test.esp8266.yaml create mode 100644 tests/components/dps310/test.rp2040.yaml create mode 100644 tests/components/ds1307/test.esp32-c3-idf.yaml create mode 100644 tests/components/ds1307/test.esp32-c3.yaml create mode 100644 tests/components/ds1307/test.esp32-idf.yaml create mode 100644 tests/components/ds1307/test.esp32.yaml create mode 100644 tests/components/ds1307/test.esp8266.yaml create mode 100644 tests/components/ds1307/test.rp2040.yaml create mode 100644 tests/components/dsmr/test.esp32-c3.yaml create mode 100644 tests/components/dsmr/test.esp32.yaml create mode 100644 tests/components/dsmr/test.esp8266.yaml create mode 100644 tests/components/dsmr/test.rp2040.yaml create mode 100644 tests/components/duty_cycle/test.esp32-c3-idf.yaml create mode 100644 tests/components/duty_cycle/test.esp32-c3.yaml create mode 100644 tests/components/duty_cycle/test.esp32-idf.yaml create mode 100644 tests/components/duty_cycle/test.esp32.yaml create mode 100644 tests/components/duty_cycle/test.esp8266.yaml create mode 100644 tests/components/duty_cycle/test.rp2040.yaml create mode 100644 tests/components/duty_time/test.esp32-c3-idf.yaml create mode 100644 tests/components/duty_time/test.esp32-c3.yaml create mode 100644 tests/components/duty_time/test.esp32-idf.yaml create mode 100644 tests/components/duty_time/test.esp32.yaml create mode 100644 tests/components/duty_time/test.esp8266.yaml create mode 100644 tests/components/duty_time/test.rp2040.yaml diff --git a/tests/components/dac7678/test.esp32-c3-idf.yaml b/tests/components/dac7678/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.esp32-c3-idf.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp32-c3.yaml b/tests/components/dac7678/test.esp32-c3.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.esp32-c3.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp32-idf.yaml b/tests/components/dac7678/test.esp32-idf.yaml new file mode 100644 index 0000000000..946a7ca58d --- /dev/null +++ b/tests/components/dac7678/test.esp32-idf.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 16 + sda: 17 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp32.yaml b/tests/components/dac7678/test.esp32.yaml new file mode 100644 index 0000000000..946a7ca58d --- /dev/null +++ b/tests/components/dac7678/test.esp32.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 16 + sda: 17 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp8266.yaml b/tests/components/dac7678/test.esp8266.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.esp8266.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.rp2040.yaml b/tests/components/dac7678/test.rp2040.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.rp2040.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/daikin/test.esp32.yaml b/tests/components/daikin/test.esp32.yaml new file mode 100644 index 0000000000..6672fe3e45 --- /dev/null +++ b/tests/components/daikin/test.esp32.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: daikin + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/daikin/test.esp8266.yaml b/tests/components/daikin/test.esp8266.yaml new file mode 100644 index 0000000000..47f02bbad9 --- /dev/null +++ b/tests/components/daikin/test.esp8266.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: daikin + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/daikin_brc/test.esp32-c3-idf.yaml b/tests/components/daikin_brc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp32-c3.yaml b/tests/components/daikin_brc/test.esp32-c3.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32-c3.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp32-idf.yaml b/tests/components/daikin_brc/test.esp32-idf.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp32.yaml b/tests/components/daikin_brc/test.esp32.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp8266.yaml b/tests/components/daikin_brc/test.esp8266.yaml new file mode 100644 index 0000000000..b8c74803a2 --- /dev/null +++ b/tests/components/daikin_brc/test.esp8266.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/dallas/test.esp32-c3-idf.yaml b/tests/components/dallas/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7975977107 --- /dev/null +++ b/tests/components/dallas/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +dallas: + pin: 4 + +sensor: + - platform: dallas + address: 0x1C0000031EDD2A28 + name: Dallas Temperature + resolution: 9 + - platform: dallas + index: 1 + name: Dallas Temperature diff --git a/tests/components/dallas/test.esp32-c3.yaml b/tests/components/dallas/test.esp32-c3.yaml new file mode 100644 index 0000000000..7975977107 --- /dev/null +++ b/tests/components/dallas/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +dallas: + pin: 4 + +sensor: + - platform: dallas + address: 0x1C0000031EDD2A28 + name: Dallas Temperature + resolution: 9 + - platform: dallas + index: 1 + name: Dallas Temperature diff --git a/tests/components/dallas/test.esp32-idf.yaml b/tests/components/dallas/test.esp32-idf.yaml new file mode 100644 index 0000000000..7975977107 --- /dev/null +++ b/tests/components/dallas/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +dallas: + pin: 4 + +sensor: + - platform: dallas + address: 0x1C0000031EDD2A28 + name: Dallas Temperature + resolution: 9 + - platform: dallas + index: 1 + name: Dallas Temperature diff --git a/tests/components/dallas/test.esp32.yaml b/tests/components/dallas/test.esp32.yaml new file mode 100644 index 0000000000..7975977107 --- /dev/null +++ b/tests/components/dallas/test.esp32.yaml @@ -0,0 +1,11 @@ +dallas: + pin: 4 + +sensor: + - platform: dallas + address: 0x1C0000031EDD2A28 + name: Dallas Temperature + resolution: 9 + - platform: dallas + index: 1 + name: Dallas Temperature diff --git a/tests/components/dallas/test.esp8266.yaml b/tests/components/dallas/test.esp8266.yaml new file mode 100644 index 0000000000..7975977107 --- /dev/null +++ b/tests/components/dallas/test.esp8266.yaml @@ -0,0 +1,11 @@ +dallas: + pin: 4 + +sensor: + - platform: dallas + address: 0x1C0000031EDD2A28 + name: Dallas Temperature + resolution: 9 + - platform: dallas + index: 1 + name: Dallas Temperature diff --git a/tests/components/dallas/test.rp2040.yaml b/tests/components/dallas/test.rp2040.yaml new file mode 100644 index 0000000000..7975977107 --- /dev/null +++ b/tests/components/dallas/test.rp2040.yaml @@ -0,0 +1,11 @@ +dallas: + pin: 4 + +sensor: + - platform: dallas + address: 0x1C0000031EDD2A28 + name: Dallas Temperature + resolution: 9 + - platform: dallas + index: 1 + name: Dallas Temperature diff --git a/tests/components/daly_bms/test.esp32-c3-idf.yaml b/tests/components/daly_bms/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.esp32-c3-idf.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp32-c3.yaml b/tests/components/daly_bms/test.esp32-c3.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.esp32-c3.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp32-idf.yaml b/tests/components/daly_bms/test.esp32-idf.yaml new file mode 100644 index 0000000000..ec9607334d --- /dev/null +++ b/tests/components/daly_bms/test.esp32-idf.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp32.yaml b/tests/components/daly_bms/test.esp32.yaml new file mode 100644 index 0000000000..ec9607334d --- /dev/null +++ b/tests/components/daly_bms/test.esp32.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp8266.yaml b/tests/components/daly_bms/test.esp8266.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.esp8266.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.rp2040.yaml b/tests/components/daly_bms/test.rp2040.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.rp2040.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/debug/test.esp32-c3-idf.yaml b/tests/components/debug/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/components/debug/test.esp32-c3.yaml b/tests/components/debug/test.esp32-c3.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/test.esp32-c3.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/components/debug/test.esp32-idf.yaml b/tests/components/debug/test.esp32-idf.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/test.esp32-idf.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/components/debug/test.esp32.yaml b/tests/components/debug/test.esp32.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/test.esp32.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/components/debug/test.esp8266.yaml b/tests/components/debug/test.esp8266.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/test.esp8266.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/components/debug/test.rp2040.yaml b/tests/components/debug/test.rp2040.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/test.rp2040.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/components/deep_sleep/test.esp32-c3-idf.yaml b/tests/components/deep_sleep/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp32-c3.yaml b/tests/components/deep_sleep/test.esp32-c3.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-c3.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp32-idf.yaml b/tests/components/deep_sleep/test.esp32-idf.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp32.yaml b/tests/components/deep_sleep/test.esp32.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp8266.yaml b/tests/components/deep_sleep/test.esp8266.yaml new file mode 100644 index 0000000000..0992fa9696 --- /dev/null +++ b/tests/components/deep_sleep/test.esp8266.yaml @@ -0,0 +1,10 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: 10s + sleep_duration: 50s diff --git a/tests/components/delonghi/test.esp32-c3-idf.yaml b/tests/components/delonghi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp32-c3.yaml b/tests/components/delonghi/test.esp32-c3.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32-c3.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp32-idf.yaml b/tests/components/delonghi/test.esp32-idf.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp32.yaml b/tests/components/delonghi/test.esp32.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp8266.yaml b/tests/components/delonghi/test.esp8266.yaml new file mode 100644 index 0000000000..adc478a6e6 --- /dev/null +++ b/tests/components/delonghi/test.esp8266.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/dfplayer/test.esp32-c3-idf.yaml b/tests/components/dfplayer/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.esp32-c3-idf.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp32-c3.yaml b/tests/components/dfplayer/test.esp32-c3.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.esp32-c3.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp32-idf.yaml b/tests/components/dfplayer/test.esp32-idf.yaml new file mode 100644 index 0000000000..03b44b8ca9 --- /dev/null +++ b/tests/components/dfplayer/test.esp32-idf.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp32.yaml b/tests/components/dfplayer/test.esp32.yaml new file mode 100644 index 0000000000..03b44b8ca9 --- /dev/null +++ b/tests/components/dfplayer/test.esp32.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp8266.yaml b/tests/components/dfplayer/test.esp8266.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.esp8266.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.rp2040.yaml b/tests/components/dfplayer/test.rp2040.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.rp2040.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32-c3.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp32-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-idf.yaml new file mode 100644 index 0000000000..5c06fc6660 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp32.yaml b/tests/components/dfrobot_sen0395/test.esp32.yaml new file mode 100644 index 0000000000..5c06fc6660 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp8266.yaml b/tests/components/dfrobot_sen0395/test.esp8266.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp8266.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.rp2040.yaml b/tests/components/dfrobot_sen0395/test.rp2040.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.rp2040.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dht/test.esp32-c3-idf.yaml b/tests/components/dht/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f134a324ca --- /dev/null +++ b/tests/components/dht/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: dht + pin: 4 + model: AM2302 + update_interval: 15s + temperature: + id: dht_temperature + name: DHT Temperature + humidity: + id: dht_humidity + name: DHT Humidity diff --git a/tests/components/dht/test.esp32-c3.yaml b/tests/components/dht/test.esp32-c3.yaml new file mode 100644 index 0000000000..f134a324ca --- /dev/null +++ b/tests/components/dht/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: dht + pin: 4 + model: AM2302 + update_interval: 15s + temperature: + id: dht_temperature + name: DHT Temperature + humidity: + id: dht_humidity + name: DHT Humidity diff --git a/tests/components/dht/test.esp32-idf.yaml b/tests/components/dht/test.esp32-idf.yaml new file mode 100644 index 0000000000..f134a324ca --- /dev/null +++ b/tests/components/dht/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: dht + pin: 4 + model: AM2302 + update_interval: 15s + temperature: + id: dht_temperature + name: DHT Temperature + humidity: + id: dht_humidity + name: DHT Humidity diff --git a/tests/components/dht/test.esp32.yaml b/tests/components/dht/test.esp32.yaml new file mode 100644 index 0000000000..f134a324ca --- /dev/null +++ b/tests/components/dht/test.esp32.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: dht + pin: 4 + model: AM2302 + update_interval: 15s + temperature: + id: dht_temperature + name: DHT Temperature + humidity: + id: dht_humidity + name: DHT Humidity diff --git a/tests/components/dht/test.esp8266.yaml b/tests/components/dht/test.esp8266.yaml new file mode 100644 index 0000000000..f134a324ca --- /dev/null +++ b/tests/components/dht/test.esp8266.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: dht + pin: 4 + model: AM2302 + update_interval: 15s + temperature: + id: dht_temperature + name: DHT Temperature + humidity: + id: dht_humidity + name: DHT Humidity diff --git a/tests/components/dht/test.rp2040.yaml b/tests/components/dht/test.rp2040.yaml new file mode 100644 index 0000000000..f134a324ca --- /dev/null +++ b/tests/components/dht/test.rp2040.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: dht + pin: 4 + model: AM2302 + update_interval: 15s + temperature: + id: dht_temperature + name: DHT Temperature + humidity: + id: dht_humidity + name: DHT Humidity diff --git a/tests/components/dht12/test.esp32-c3-idf.yaml b/tests/components/dht12/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp32-c3.yaml b/tests/components/dht12/test.esp32-c3.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.esp32-c3.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp32-idf.yaml b/tests/components/dht12/test.esp32-idf.yaml new file mode 100644 index 0000000000..02a00f5df7 --- /dev/null +++ b/tests/components/dht12/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 16 + sda: 17 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp32.yaml b/tests/components/dht12/test.esp32.yaml new file mode 100644 index 0000000000..02a00f5df7 --- /dev/null +++ b/tests/components/dht12/test.esp32.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 16 + sda: 17 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp8266.yaml b/tests/components/dht12/test.esp8266.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.esp8266.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.rp2040.yaml b/tests/components/dht12/test.rp2040.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.rp2040.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dps310/test.esp32-c3-idf.yaml b/tests/components/dps310/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp32-c3.yaml b/tests/components/dps310/test.esp32-c3.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.esp32-c3.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp32-idf.yaml b/tests/components/dps310/test.esp32-idf.yaml new file mode 100644 index 0000000000..417cab5c40 --- /dev/null +++ b/tests/components/dps310/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 16 + sda: 17 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp32.yaml b/tests/components/dps310/test.esp32.yaml new file mode 100644 index 0000000000..417cab5c40 --- /dev/null +++ b/tests/components/dps310/test.esp32.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 16 + sda: 17 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp8266.yaml b/tests/components/dps310/test.esp8266.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.esp8266.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.rp2040.yaml b/tests/components/dps310/test.rp2040.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.rp2040.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/ds1307/test.esp32-c3-idf.yaml b/tests/components/ds1307/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp32-c3.yaml b/tests/components/ds1307/test.esp32-c3.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.esp32-c3.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp32-idf.yaml b/tests/components/ds1307/test.esp32-idf.yaml new file mode 100644 index 0000000000..017c7aac92 --- /dev/null +++ b/tests/components/ds1307/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 16 + sda: 17 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp32.yaml b/tests/components/ds1307/test.esp32.yaml new file mode 100644 index 0000000000..017c7aac92 --- /dev/null +++ b/tests/components/ds1307/test.esp32.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 16 + sda: 17 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp8266.yaml b/tests/components/ds1307/test.esp8266.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.esp8266.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.rp2040.yaml b/tests/components/ds1307/test.rp2040.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.rp2040.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/dsmr/test.esp32-c3.yaml b/tests/components/dsmr/test.esp32-c3.yaml new file mode 100644 index 0000000000..e813556be8 --- /dev/null +++ b/tests/components/dsmr/test.esp32-c3.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 6 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/dsmr/test.esp32.yaml b/tests/components/dsmr/test.esp32.yaml new file mode 100644 index 0000000000..1fd0448ab3 --- /dev/null +++ b/tests/components/dsmr/test.esp32.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 15 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/dsmr/test.esp8266.yaml b/tests/components/dsmr/test.esp8266.yaml new file mode 100644 index 0000000000..8037fb4b1a --- /dev/null +++ b/tests/components/dsmr/test.esp8266.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 15 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/dsmr/test.rp2040.yaml b/tests/components/dsmr/test.rp2040.yaml new file mode 100644 index 0000000000..e813556be8 --- /dev/null +++ b/tests/components/dsmr/test.rp2040.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 6 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/duty_cycle/test.esp32-c3-idf.yaml b/tests/components/duty_cycle/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2b7f31efbd --- /dev/null +++ b/tests/components/duty_cycle/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: duty_cycle + pin: 4 + name: Duty Cycle Sensor diff --git a/tests/components/duty_cycle/test.esp32-c3.yaml b/tests/components/duty_cycle/test.esp32-c3.yaml new file mode 100644 index 0000000000..2b7f31efbd --- /dev/null +++ b/tests/components/duty_cycle/test.esp32-c3.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: duty_cycle + pin: 4 + name: Duty Cycle Sensor diff --git a/tests/components/duty_cycle/test.esp32-idf.yaml b/tests/components/duty_cycle/test.esp32-idf.yaml new file mode 100644 index 0000000000..2b7f31efbd --- /dev/null +++ b/tests/components/duty_cycle/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: duty_cycle + pin: 4 + name: Duty Cycle Sensor diff --git a/tests/components/duty_cycle/test.esp32.yaml b/tests/components/duty_cycle/test.esp32.yaml new file mode 100644 index 0000000000..2b7f31efbd --- /dev/null +++ b/tests/components/duty_cycle/test.esp32.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: duty_cycle + pin: 4 + name: Duty Cycle Sensor diff --git a/tests/components/duty_cycle/test.esp8266.yaml b/tests/components/duty_cycle/test.esp8266.yaml new file mode 100644 index 0000000000..2b7f31efbd --- /dev/null +++ b/tests/components/duty_cycle/test.esp8266.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: duty_cycle + pin: 4 + name: Duty Cycle Sensor diff --git a/tests/components/duty_cycle/test.rp2040.yaml b/tests/components/duty_cycle/test.rp2040.yaml new file mode 100644 index 0000000000..2b7f31efbd --- /dev/null +++ b/tests/components/duty_cycle/test.rp2040.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: duty_cycle + pin: 4 + name: Duty Cycle Sensor diff --git a/tests/components/duty_time/test.esp32-c3-idf.yaml b/tests/components/duty_time/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..28fa4afd1c --- /dev/null +++ b/tests/components/duty_time/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +sensor: + - platform: duty_time + name: Duty Time + sensor: bin1 diff --git a/tests/components/duty_time/test.esp32-c3.yaml b/tests/components/duty_time/test.esp32-c3.yaml new file mode 100644 index 0000000000..28fa4afd1c --- /dev/null +++ b/tests/components/duty_time/test.esp32-c3.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +sensor: + - platform: duty_time + name: Duty Time + sensor: bin1 diff --git a/tests/components/duty_time/test.esp32-idf.yaml b/tests/components/duty_time/test.esp32-idf.yaml new file mode 100644 index 0000000000..28fa4afd1c --- /dev/null +++ b/tests/components/duty_time/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +sensor: + - platform: duty_time + name: Duty Time + sensor: bin1 diff --git a/tests/components/duty_time/test.esp32.yaml b/tests/components/duty_time/test.esp32.yaml new file mode 100644 index 0000000000..28fa4afd1c --- /dev/null +++ b/tests/components/duty_time/test.esp32.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +sensor: + - platform: duty_time + name: Duty Time + sensor: bin1 diff --git a/tests/components/duty_time/test.esp8266.yaml b/tests/components/duty_time/test.esp8266.yaml new file mode 100644 index 0000000000..28fa4afd1c --- /dev/null +++ b/tests/components/duty_time/test.esp8266.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +sensor: + - platform: duty_time + name: Duty Time + sensor: bin1 diff --git a/tests/components/duty_time/test.rp2040.yaml b/tests/components/duty_time/test.rp2040.yaml new file mode 100644 index 0000000000..28fa4afd1c --- /dev/null +++ b/tests/components/duty_time/test.rp2040.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +sensor: + - platform: duty_time + name: Duty Time + sensor: bin1 From 1f432ec7de6f806d0a329254a97b3dd2997adb37 Mon Sep 17 00:00:00 2001 From: SmartShackMaster Date: Tue, 20 Feb 2024 23:27:17 +0200 Subject: [PATCH 351/436] Clear UART read buffer before sending next command (#6200) --- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 0a46755bd3..e8c6b2537f 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -336,6 +336,8 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } uint8_t FingerprintGrowComponent::send_command_() { + while (this->available()) + this->read(); this->write((uint8_t) (START_CODE >> 8)); this->write((uint8_t) (START_CODE & 0xFF)); this->write(this->address_[0]); From 4b04df2f6b3c33831be11ef45d1a8b737f95c223 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 20 Feb 2024 15:38:33 -0600 Subject: [PATCH 352/436] Voice Assistant: add on_idle trigger and fix nevermind (#6141) --- esphome/components/voice_assistant/__init__.py | 9 +++++++++ esphome/components/voice_assistant/voice_assistant.cpp | 5 +++++ esphome/components/voice_assistant/voice_assistant.h | 2 ++ 3 files changed, 16 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 59aef901f2..b21a5b27da 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -32,6 +32,7 @@ CONF_ON_TTS_START = "on_tts_start" CONF_ON_TTS_STREAM_START = "on_tts_stream_start" CONF_ON_TTS_STREAM_END = "on_tts_stream_end" CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" +CONF_ON_IDLE = "on_idle" CONF_SILENCE_DETECTION = "silence_detection" CONF_USE_WAKE_WORD = "use_wake_word" @@ -127,6 +128,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_IDLE): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA), tts_stream_validate, @@ -259,6 +261,13 @@ async def to_code(config): config[CONF_ON_TTS_STREAM_END], ) + if CONF_ON_IDLE in config: + await automation.build_automation( + var.get_idle_trigger(), + [], + config[CONF_ON_IDLE], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 9094b93c02..260605c0b4 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -135,6 +135,8 @@ void VoiceAssistant::loop() { switch (this->state_) { case State::IDLE: { if (this->continuous_ && this->desired_state_ == State::IDLE) { + this->idle_trigger_->trigger(); + this->ring_buffer_->reset(); #ifdef USE_ESP_ADF if (this->use_wake_word_) { @@ -618,6 +620,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { { this->set_state_(State::IDLE, State::IDLE); } + } else if (this->state_ == State::AWAITING_RESPONSE) { + // No TTS start event ("nevermind") + this->set_state_(State::IDLE, State::IDLE); } this->defer([this]() { this->end_trigger_->trigger(); }); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index d996efe08e..f0ee793f53 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -116,6 +116,7 @@ class VoiceAssistant : public Component { Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } Trigger<> *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } @@ -148,6 +149,7 @@ class VoiceAssistant : public Component { Trigger *tts_end_trigger_ = new Trigger(); Trigger *tts_start_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); + Trigger<> *idle_trigger_ = new Trigger<>(); Trigger<> *client_connected_trigger_ = new Trigger<>(); Trigger<> *client_disconnected_trigger_ = new Trigger<>(); From 924389ba74a977f62c1ed61731dfd5fad0972cd0 Mon Sep 17 00:00:00 2001 From: sibowler Date: Wed, 21 Feb 2024 08:40:17 +1100 Subject: [PATCH 353/436] Tuya Fan component fix to handle enum datapoint type (#6135) --- esphome/components/tuya/fan/tuya_fan.cpp | 10 +++++++++- esphome/components/tuya/fan/tuya_fan.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 481c931f2e..8a613d0bae 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -34,9 +34,13 @@ void TuyaFan::setup() { } if (this->oscillation_id_.has_value()) { this->parent_->register_listener(*this->oscillation_id_, [this](const TuyaDatapoint &datapoint) { + // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both + // scenarios ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); this->oscillating = datapoint.value_bool; this->publish_state(); + + this->oscillation_type_ = datapoint.type; }); } if (this->direction_id_.has_value()) { @@ -80,7 +84,11 @@ void TuyaFan::control(const fan::FanCall &call) { this->parent_->set_boolean_datapoint_value(*this->switch_id_, *call.get_state()); } if (this->oscillation_id_.has_value() && call.get_oscillating().has_value()) { - this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + if (this->oscillation_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + } else if (this->speed_type_ == TuyaDatapointType::BOOLEAN) { + this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + } } if (this->direction_id_.has_value() && call.get_direction().has_value()) { bool enable = *call.get_direction() == fan::FanDirection::REVERSE; diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 77b2cc6383..527efa8246 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -29,6 +29,7 @@ class TuyaFan : public Component, public fan::Fan { optional direction_id_{}; int speed_count_{}; TuyaDatapointType speed_type_{}; + TuyaDatapointType oscillation_type_{}; }; } // namespace tuya From 4d8b5edb1c9834b3f4340922e2168f5e79509676 Mon Sep 17 00:00:00 2001 From: Stephen Cox <69124219+linkedupbits@users.noreply.github.com> Date: Wed, 21 Feb 2024 10:49:59 +1300 Subject: [PATCH 354/436] Provide example devcontainer config for mdns and USB passthrough (#6094) --- .devcontainer/devcontainer.json | 15 ++++++++++++++- script/devcontainer-post-create | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7abcb43417..4596b59200 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,8 +7,21 @@ "PIP_BREAK_SYSTEM_PACKAGES": "1", "PIP_ROOT_USER_ACTION": "ignore" }, - "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"], + "runArgs": [ + "--privileged", + "-e", + "ESPHOME_DASHBOARD_USE_PING=1" + // uncomment and edit the path in order to pass though local USB serial to the conatiner + // , "--device=/dev/ttyACM0" + ], "appPort": 6052, + // if you are using avahi in the host device, uncomment these to allow the + // devcontainer to find devices via mdns + //"mounts": [ + // "type=bind,source=/dev/bus/usb,target=/dev/bus/usb", + // "type=bind,source=/var/run/dbus,target=/var/run/dbus", + // "type=bind,source=/var/run/avahi-daemon/socket,target=/var/run/avahi-daemon/socket" + //], "customizations": { "vscode": { "extensions": [ diff --git a/script/devcontainer-post-create b/script/devcontainer-post-create index 120ab3307d..272d350519 100755 --- a/script/devcontainer-post-create +++ b/script/devcontainer-post-create @@ -3,6 +3,9 @@ set -e # set -x +apt update +apt-get install avahi-utils -y + mkdir -p config script/setup From 07c3ee75e522c28921612c0572d00e3c158001fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:53:50 +1300 Subject: [PATCH 355/436] Bump black from 23.12.1 to 24.2.0 (#6221) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- esphome/components/honeywell_hih_i2c/__init__.py | 1 + esphome/components/honeywellabp2_i2c/__init__.py | 1 + esphome/components/tmp102/sensor.py | 1 + esphome/components/wifi/wpa2_eap.py | 1 + esphome/config.py | 3 +-- esphome/dashboard/enum.py | 1 + esphome/schema_extractors.py | 1 - esphome/types.py | 1 + requirements_test.txt | 2 +- script/build_language_schema.py | 8 +++++--- tests/dashboard/util/test_file.py | 13 ++++++++----- tests/unit_tests/conftest.py | 1 + tests/unit_tests/test_wizard.py | 1 + 14 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2f44d088f..ea0eb80f86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.2.0 hooks: - id: black args: diff --git a/esphome/components/honeywell_hih_i2c/__init__.py b/esphome/components/honeywell_hih_i2c/__init__.py index fbf67230f7..8d13fcb152 100644 --- a/esphome/components/honeywell_hih_i2c/__init__.py +++ b/esphome/components/honeywell_hih_i2c/__init__.py @@ -1,2 +1,3 @@ """Support for Honeywell HumidIcon HIH""" + CODEOWNERS = ["@Benichou34"] diff --git a/esphome/components/honeywellabp2_i2c/__init__.py b/esphome/components/honeywellabp2_i2c/__init__.py index e748df3c98..29a910eca9 100644 --- a/esphome/components/honeywellabp2_i2c/__init__.py +++ b/esphome/components/honeywellabp2_i2c/__init__.py @@ -1,2 +1,3 @@ """Support for Honeywell ABP2""" + CODEOWNERS = ["@jpfaff"] diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py index 57d0afd5a1..2cb1a6d1f5 100644 --- a/esphome/components/tmp102/sensor.py +++ b/esphome/components/tmp102/sensor.py @@ -7,6 +7,7 @@ reading temperatures to a resolution of 0.0625°C. https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf """ + import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 3cb60e6175..3985dfef18 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -3,6 +3,7 @@ The cryptography package is loaded lazily in the functions so that it doesn't crash if it's not installed. """ + import logging from pathlib import Path diff --git a/esphome/config.py b/esphome/config.py index 4aca0d6056..f5a1ebb8d7 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -292,8 +292,7 @@ class ConfigValidationStep(abc.ABC): priority: float = 0.0 @abc.abstractmethod - def run(self, result: Config) -> None: - ... + def run(self, result: Config) -> None: ... # noqa: E704 class LoadValidationStep(ConfigValidationStep): diff --git a/esphome/dashboard/enum.py b/esphome/dashboard/enum.py index 6aff21620e..0fe30cf92a 100644 --- a/esphome/dashboard/enum.py +++ b/esphome/dashboard/enum.py @@ -1,4 +1,5 @@ """Enum backports from standard lib.""" + from __future__ import annotations from enum import Enum diff --git a/esphome/schema_extractors.py b/esphome/schema_extractors.py index 2280a84849..5491bc88c4 100644 --- a/esphome/schema_extractors.py +++ b/esphome/schema_extractors.py @@ -8,7 +8,6 @@ originally do. However there is a property to further disable decorator impact.""" - # This is set to true by script/build_language_schema.py # only, so data is collected (again functionality is not modified) EnableSchemaExtraction = False diff --git a/esphome/types.py b/esphome/types.py index adb16fa91b..27ec61ceff 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -1,4 +1,5 @@ """This helper module tracks commonly used types in the esphome python codebase.""" + from typing import Union from esphome.core import ID, Lambda, EsphomeCore diff --git a/requirements_test.txt b/requirements_test.txt index 8b7c4d28bc..c55e7751a2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.0.3 flake8==7.0.0 # also change in .pre-commit-config.yaml when updating -black==23.12.1 # also change in .pre-commit-config.yaml when updating +black==24.2.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/build_language_schema.py b/script/build_language_schema.py index fc6ccadc5f..cb3dc1832d 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -849,9 +849,11 @@ def convert(schema, config_var, path): config_var["id_type"] = { "class": str(data.base), - "parents": [str(x.base) for x in parents] - if isinstance(parents, list) - else None, + "parents": ( + [str(x.base) for x in parents] + if isinstance(parents, list) + else None + ), } elif schema_type == "use_id": if inspect.ismodule(data): diff --git a/tests/dashboard/util/test_file.py b/tests/dashboard/util/test_file.py index 270ab565f1..51ba10b328 100644 --- a/tests/dashboard/util/test_file.py +++ b/tests/dashboard/util/test_file.py @@ -28,8 +28,9 @@ def test_write_utf8_file_fails_at_rename( test_dir = tmpdir.mkdir("files") test_file = Path(test_dir / "test.json") - with pytest.raises(OSError), patch( - "esphome.dashboard.util.file.os.replace", side_effect=OSError + with ( + pytest.raises(OSError), + patch("esphome.dashboard.util.file.os.replace", side_effect=OSError), ): write_utf8_file(test_file, '{"some":"data"}', False) @@ -45,9 +46,11 @@ def test_write_utf8_file_fails_at_rename_and_remove( test_dir = tmpdir.mkdir("files") test_file = Path(test_dir / "test.json") - with pytest.raises(OSError), patch( - "esphome.dashboard.util.file.os.remove", side_effect=OSError - ), patch("esphome.dashboard.util.file.os.replace", side_effect=OSError): + with ( + pytest.raises(OSError), + patch("esphome.dashboard.util.file.os.remove", side_effect=OSError), + patch("esphome.dashboard.util.file.os.replace", side_effect=OSError), + ): write_utf8_file(test_file, '{"some":"data"}', False) assert "File replacement cleanup failed" in caplog.text diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 41d0f3dadb..d61c4a442a 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -8,6 +8,7 @@ If adding unit tests ensure that they are fast. Slower integration tests should not be part of a unit test suite. """ + import sys import pytest diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 46700a3ba8..9260629ec3 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -1,4 +1,5 @@ """Tests for the wizard.py file.""" + import os import esphome.wizard as wz From b75caf5ea752cfc2ecc1ce2ecc6e95618ea96b47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:54:32 +1300 Subject: [PATCH 356/436] Bump pytest from 7.4.4 to 8.0.1 (#6246) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c55e7751a2..009be65455 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.4.4 +pytest==8.0.1 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-asyncio==0.23.5 From 57f53a0f168fa1b062bd0748a289b8675f9e0de1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:56:28 +1300 Subject: [PATCH 357/436] Bump codecov/codecov-action from 3 to 4 (#6160) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a108b34dd..6e8f892465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} From f4552f50627b82c1b8f104b6555966e9d88f70d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:58:10 +1300 Subject: [PATCH 358/436] Bump peter-evans/create-pull-request from 5.0.2 to 6.0.0 (#6159) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index d45784bf7f..a5bd178e33 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -36,7 +36,7 @@ jobs: python ./script/sync-device_class.py - name: Commit changes - uses: peter-evans/create-pull-request@v5.0.2 + uses: peter-evans/create-pull-request@v6.0.0 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot From f4eb525c97c0d8cbbd7f304abb4c94fec1ac9ba5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:46:35 +1300 Subject: [PATCH 359/436] Bump frenck/action-yamllint from 1.4.2 to 1.5.0 (#6236) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/yaml-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml index c9f056b18c..1bfa70c455 100644 --- a/.github/workflows/yaml-lint.yml +++ b/.github/workflows/yaml-lint.yml @@ -19,4 +19,4 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.1 - name: Run yamllint - uses: frenck/action-yamllint@v1.4.2 + uses: frenck/action-yamllint@v1.5.0 From 256d886d77fbff37e803593fdc6fce7be0b49487 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:57:38 +1300 Subject: [PATCH 360/436] Bump voluptuous from 0.14.1 to 0.14.2 (#6181) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/voluptuous_schema.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index e2171cabed..9af6cb717c 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -64,7 +64,7 @@ class _Schema(vol.Schema): # Recursively compile schema _compiled_schema = {} - for skey, svalue in vol.iteritems(schema): + for skey, svalue in schema.items(): new_key = self._compile(skey) new_value = self._compile(svalue) _compiled_schema[skey] = (new_key, new_value) diff --git a/requirements.txt b/requirements.txt index 878d2e8a50..aa97e23f9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ async_timeout==4.0.3; python_version <= "3.10" cryptography==42.0.2 -voluptuous==0.14.1 +voluptuous==0.14.2 PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 From d96090095ae0433bdfb8098ba2d64a0d319ec875 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:57:51 +1300 Subject: [PATCH 361/436] Bump pyupgrade from 3.15.0 to 3.15.1 (#6247) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea0eb80f86..7865c52abd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test.txt b/requirements_test.txt index 009be65455..eef49e5ced 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==3.0.3 flake8==7.0.0 # also change in .pre-commit-config.yaml when updating black==24.2.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.15.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests From e847039ffd9c9798fed95a59d4bcb29f3282cf72 Mon Sep 17 00:00:00 2001 From: Stephen Tierney Date: Wed, 21 Feb 2024 15:10:04 +1100 Subject: [PATCH 362/436] LTR390 - Multiple bugfixes (#6161) --- esphome/components/ltr390/ltr390.cpp | 30 ++++++++++++++++++++-------- esphome/components/ltr390/ltr390.h | 9 +-------- esphome/components/ltr390/sensor.py | 11 +++++----- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index 959af68235..65c08ab614 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -8,10 +8,23 @@ namespace ltr390 { static const char *const TAG = "ltr390"; +static const uint8_t LTR390_MAIN_CTRL = 0x00; +static const uint8_t LTR390_MEAS_RATE = 0x04; +static const uint8_t LTR390_GAIN = 0x05; +static const uint8_t LTR390_PART_ID = 0x06; +static const uint8_t LTR390_MAIN_STATUS = 0x07; + static const float GAINVALUES[5] = {1.0, 3.0, 6.0, 9.0, 18.0}; static const float RESOLUTIONVALUE[6] = {4.0, 2.0, 1.0, 0.5, 0.25, 0.125}; + +// Request fastest measurement rate - will be slowed by device if conversion rate is slower. +static const float RESOLUTION_SETTING[6] = {0x00, 0x10, 0x20, 0x30, 0x40, 0x50}; static const uint32_t MODEADDRESSES[2] = {0x0D, 0x10}; +static const float SENSITIVITY_MAX = 2300; +static const float INTG_MAX = RESOLUTIONVALUE[0] * 100; +static const int GAIN_MAX = GAINVALUES[4]; + uint32_t little_endian_bytes_to_int(const uint8_t *buffer, uint8_t num_bytes) { uint32_t value = 0; @@ -58,7 +71,7 @@ void LTR390Component::read_als_() { uint32_t als = *val; if (this->light_sensor_ != nullptr) { - float lux = (0.6 * als) / (GAINVALUES[this->gain_] * RESOLUTIONVALUE[this->res_]) * this->wfac_; + float lux = ((0.6 * als) / (GAINVALUES[this->gain_] * RESOLUTIONVALUE[this->res_])) * this->wfac_; this->light_sensor_->publish_state(lux); } @@ -74,7 +87,7 @@ void LTR390Component::read_uvs_() { uint32_t uv = *val; if (this->uvi_sensor_ != nullptr) { - this->uvi_sensor_->publish_state(uv / LTR390_SENSITIVITY * this->wfac_); + this->uvi_sensor_->publish_state((uv / this->sensitivity_) * this->wfac_); } if (this->uv_sensor_ != nullptr) { @@ -132,12 +145,13 @@ void LTR390Component::setup() { // Set gain this->reg(LTR390_GAIN) = gain_; - // Set resolution - uint8_t res = this->reg(LTR390_MEAS_RATE).get(); - // resolution is in bits 5-7 - res &= ~0b01110000; - res |= res << 4; - this->reg(LTR390_MEAS_RATE) = res; + // Set resolution and measurement rate + this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_]; + + // Set sensitivity by linearly scaling against known value in the datasheet + float gain_scale = GAINVALUES[this->gain_] / GAIN_MAX; + float intg_scale = (RESOLUTIONVALUE[this->res_] * 100) / INTG_MAX; + this->sensitivity_ = SENSITIVITY_MAX * gain_scale * intg_scale; // Set sensor read state this->reading_ = false; diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 1bb7a8fa22..bc98518fe9 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -17,14 +17,6 @@ enum LTR390CTRL { }; // enums from https://github.com/adafruit/Adafruit_LTR390/ - -static const uint8_t LTR390_MAIN_CTRL = 0x00; -static const uint8_t LTR390_MEAS_RATE = 0x04; -static const uint8_t LTR390_GAIN = 0x05; -static const uint8_t LTR390_PART_ID = 0x06; -static const uint8_t LTR390_MAIN_STATUS = 0x07; -static const float LTR390_SENSITIVITY = 2300.0; - // Sensing modes enum LTR390MODE { LTR390_MODE_ALS, @@ -81,6 +73,7 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { LTR390GAIN gain_; LTR390RESOLUTION res_; + float sensitivity_; float wfac_; sensor::Sensor *light_sensor_{nullptr}; diff --git a/esphome/components/ltr390/sensor.py b/esphome/components/ltr390/sensor.py index 0a765dbe3d..fe8cad00b6 100644 --- a/esphome/components/ltr390/sensor.py +++ b/esphome/components/ltr390/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_RESOLUTION, UNIT_LUX, ICON_BRIGHTNESS_5, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, ) @@ -61,22 +62,22 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, ), cv.Optional(CONF_UV_INDEX): sensor.sensor_schema( unit_of_measurement=UNIT_UVI, icon=ICON_BRIGHTNESS_5, accuracy_decimals=5, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, ), cv.Optional(CONF_UV): sensor.sensor_schema( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, ), - cv.Optional(CONF_GAIN, default="X3"): cv.enum(GAIN_OPTIONS), - cv.Optional(CONF_RESOLUTION, default=18): cv.enum(RES_OPTIONS), + cv.Optional(CONF_GAIN, default="X18"): cv.enum(GAIN_OPTIONS), + cv.Optional(CONF_RESOLUTION, default=20): cv.enum(RES_OPTIONS), cv.Optional(CONF_WINDOW_CORRECTION_FACTOR, default=1.0): cv.float_range( min=1.0 ), From 75af4c3d629768ba257d55a89fe40a266ef2fee2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:14:30 +1300 Subject: [PATCH 363/436] Fix yamllint (#6253) --- .github/workflows/ci.yml | 1 + .github/workflows/needs-docs.yml | 1 + .github/workflows/sync-device-classes.yml | 1 + .github/workflows/yaml-lint.yml | 4 +++ .yamllint | 19 ++++++++-- .../animation/test.esp32-c3-idf.yaml | 2 +- tests/components/animation/test.esp32-c3.yaml | 2 +- .../components/animation/test.esp32-idf.yaml | 2 +- tests/components/animation/test.esp32.yaml | 2 +- tests/components/animation/test.esp8266.yaml | 2 +- tests/components/animation/test.rp2040.yaml | 2 +- .../mopeka_std_check/test.esp32.yaml | 9 +++-- tests/test1.yaml | 10 +++--- tests/test11.5.yaml | 8 ++--- tests/test2.yaml | 6 ++-- tests/test3.1.yaml | 2 +- tests/test3.yaml | 36 +++++++++---------- tests/test4.yaml | 23 +++++------- tests/test5.yaml | 8 ++--- tests/test6.yaml | 4 +-- tests/test8.1.yaml | 5 ++- tests/test8.2.yaml | 2 +- tests/test8.yaml | 2 +- 23 files changed, 83 insertions(+), 70 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e8f892465..8b4aafb1dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ on: - "**" - "!.github/workflows/*.yml" - ".github/workflows/ci.yml" + - "!.yamllint" merge_group: permissions: diff --git a/.github/workflows/needs-docs.yml b/.github/workflows/needs-docs.yml index 628b5cc5e3..6a66e5769c 100644 --- a/.github/workflows/needs-docs.yml +++ b/.github/workflows/needs-docs.yml @@ -1,5 +1,6 @@ name: Needs Docs +# yamllint disable-line rule:truthy on: pull_request: types: [labeled, unlabeled] diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index a5bd178e33..efa1aefae5 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -1,6 +1,7 @@ --- name: Synchronise Device Classes from Home Assistant +# yamllint disable-line rule:truthy on: workflow_dispatch: schedule: diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml index 1bfa70c455..3694436866 100644 --- a/.github/workflows/yaml-lint.yml +++ b/.github/workflows/yaml-lint.yml @@ -1,5 +1,7 @@ +--- name: YAML lint +# yamllint disable-line rule:truthy on: push: branches: [dev, beta, release] @@ -20,3 +22,5 @@ jobs: uses: actions/checkout@v4.1.1 - name: Run yamllint uses: frenck/action-yamllint@v1.5.0 + with: + strict: true diff --git a/.yamllint b/.yamllint index 4fea263214..9cd1482869 100644 --- a/.yamllint +++ b/.yamllint @@ -1,3 +1,18 @@ --- -ignore: | - venv/ +extends: default + +ignore-from-file: .gitignore + +rules: + document-start: disable + empty-lines: + level: error + max: 1 + max-start: 0 + max-end: 1 + indentation: + level: error + spaces: 2 + indent-sequences: true + check-multi-line-strings: false + line-length: disable diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml index 9a415255ae..9bcfbdb118 100644 --- a/tests/components/animation/test.esp32-c3-idf.yaml +++ b/tests/components/animation/test.esp32-c3-idf.yaml @@ -20,4 +20,4 @@ animation: - id: rgb565_animation file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false diff --git a/tests/components/animation/test.esp32-c3.yaml b/tests/components/animation/test.esp32-c3.yaml index 9a415255ae..9bcfbdb118 100644 --- a/tests/components/animation/test.esp32-c3.yaml +++ b/tests/components/animation/test.esp32-c3.yaml @@ -20,4 +20,4 @@ animation: - id: rgb565_animation file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false diff --git a/tests/components/animation/test.esp32-idf.yaml b/tests/components/animation/test.esp32-idf.yaml index 31b78eb980..5dc132eb2d 100644 --- a/tests/components/animation/test.esp32-idf.yaml +++ b/tests/components/animation/test.esp32-idf.yaml @@ -20,4 +20,4 @@ animation: - id: rgb565_animation file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false diff --git a/tests/components/animation/test.esp32.yaml b/tests/components/animation/test.esp32.yaml index 31b78eb980..5dc132eb2d 100644 --- a/tests/components/animation/test.esp32.yaml +++ b/tests/components/animation/test.esp32.yaml @@ -20,4 +20,4 @@ animation: - id: rgb565_animation file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false diff --git a/tests/components/animation/test.esp8266.yaml b/tests/components/animation/test.esp8266.yaml index 2bd441de99..ef0f483a79 100644 --- a/tests/components/animation/test.esp8266.yaml +++ b/tests/components/animation/test.esp8266.yaml @@ -20,4 +20,4 @@ animation: - id: rgb565_animation file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false diff --git a/tests/components/animation/test.rp2040.yaml b/tests/components/animation/test.rp2040.yaml index 0f42c33687..6ee29a3347 100644 --- a/tests/components/animation/test.rp2040.yaml +++ b/tests/components/animation/test.rp2040.yaml @@ -20,4 +20,4 @@ animation: - id: rgb565_animation file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false diff --git a/tests/components/mopeka_std_check/test.esp32.yaml b/tests/components/mopeka_std_check/test.esp32.yaml index 830adf952f..383e2e2a19 100644 --- a/tests/components/mopeka_std_check/test.esp32.yaml +++ b/tests/components/mopeka_std_check/test.esp32.yaml @@ -6,11 +6,10 @@ sensor: mac_address: D3:75:F2:DC:16:91 tank_type: Europe_11kg temperature: - name: "Propane test temp" + name: "Propane test temp" level: - name: "Propane test level" + name: "Propane test level" distance: - name: "Propane test distance" + name: "Propane test distance" battery_level: - name: "Propane test battery level" - + name: "Propane test battery level" diff --git a/tests/test1.yaml b/tests/test1.yaml index c2b92c2b21..57456e7d6d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -380,7 +380,7 @@ ble_client: then: - ble_client.numeric_comparison_reply: id: ble_blah - accept: True + accept: true - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client @@ -2053,7 +2053,7 @@ binary_sensor: on_press: - fan.cycle_speed: id: fan_speed - off_speed_cycle: False + off_speed_cycle: false - logger.log: "Cycle speed clicked" - platform: remote_receiver name: Raw Remote Receiver Test @@ -2312,7 +2312,7 @@ output: pin: pcf8574: pcf8574_hub number: 0 - #allow_other_uses: true + # allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2320,7 +2320,7 @@ output: pin: pca9554: pca9554_hub number: 0 - #allow_other_uses: true + # allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -3411,7 +3411,7 @@ display: reset_pin: allow_other_uses: true number: GPIO23 - backlight_pin: no + backlight_pin: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7920 diff --git a/tests/test11.5.yaml b/tests/test11.5.yaml index 2a9b40c5c3..ef260d79c0 100644 --- a/tests/test11.5.yaml +++ b/tests/test11.5.yaml @@ -116,7 +116,7 @@ binary_sensor: id: modbus_binsensortest register_type: read address: 0x3200 - bitmask: 0x80 # (bit 8) + bitmask: 0x80 # (bit 8) lambda: "return x;" - platform: tm1638 @@ -714,9 +714,9 @@ display: id: primarydisplay stb_pin: allow_other_uses: true - number: 5 #TM1638 STB - clk_pin: 18 #TM1638 CLK - dio_pin: 23 #TM1638 DIO + number: 5 # TM1638 STB + clk_pin: 18 # TM1638 CLK + dio_pin: 23 # TM1638 DIO update_interval: 5s intensity: 5 lambda: |- diff --git a/tests/test2.yaml b/tests/test2.yaml index e5358781df..f7a690709a 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -8,7 +8,7 @@ esphome: globals: - id: my_global_string type: std::string - restore_value: yes + restore_value: true max_restore_data_length: 70 initial_value: '"DefaultValue"' @@ -786,11 +786,11 @@ image: - id: rgb24_image file: pnglogo.png type: RGB24 - use_transparency: yes + use_transparency: true - id: rgb565_image file: pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false - id: web_svg_image file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg resize: 256x48 diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index fca37054a7..2bddd6f4d7 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -503,7 +503,7 @@ switch: - platform: template name: open_vent id: open_vent - optimistic: True + optimistic: true on_turn_on: then: - grove_tb6612fng.run: diff --git a/tests/test3.yaml b/tests/test3.yaml index cbd3d15b8a..68b9a544e1 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1035,34 +1035,34 @@ climate: target_temperature: 1 current_temperature: 0.5 supported_modes: - - 'OFF' - - HEAT_COOL - - COOL - - HEAT - - DRY - - FAN_ONLY + - "OFF" + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY supported_swing_modes: - - 'OFF' - - VERTICAL - - HORIZONTAL - - BOTH + - "OFF" + - VERTICAL + - HORIZONTAL + - BOTH supported_presets: - - AWAY - - BOOST - - ECO - - SLEEP + - AWAY + - BOOST + - ECO + - SLEEP on_alarm_start: then: - logger.log: level: DEBUG - format: "Alarm activated. Code: %d. Message: \"%s\"" - args: [ code, message] + format: 'Alarm activated. Code: %d. Message: "%s"' + args: [code, message] on_alarm_end: then: - logger.log: level: DEBUG - format: "Alarm deactivated. Code: %d. Message: \"%s\"" - args: [ code, message] + format: 'Alarm deactivated. Code: %d. Message: "%s"' + args: [code, message] sprinkler: - id: yard_sprinkler_ctrlr diff --git a/tests/test4.yaml b/tests/test4.yaml index f68406298e..e46102e88a 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -477,7 +477,6 @@ binary_sensor: sx1509: sx1509_hub number: 3 - - platform: touchscreen touchscreen_id: lilygo_touchscreen id: touch_key1 @@ -491,7 +490,6 @@ binary_sensor: id: touch_key_911 index: 0 - - platform: gpio name: MaxIn Pin 4 pin: @@ -521,7 +519,6 @@ binary_sensor: input: true inverted: false - climate: - platform: tuya id: tuya_climate @@ -591,7 +588,6 @@ cover: open_duration: 14s close_duration: 14s - display: - platform: addressable_light id: led_matrix_32x8_display @@ -885,16 +881,16 @@ esp32_camera: allow_other_uses: true - number: GPIO35 allow_other_uses: true - - number: GPIO34 - - number: GPIO5 + - number: GPIO34 + - number: GPIO5 allow_other_uses: true - - number: GPIO39 + - number: GPIO39 allow_other_uses: true - - number: GPIO18 + - number: GPIO18 allow_other_uses: true - - number: GPIO36 + - number: GPIO36 allow_other_uses: true - - number: GPIO19 + - number: GPIO19 allow_other_uses: true vsync_pin: allow_other_uses: true @@ -927,8 +923,8 @@ esp32_camera: jpeg_quality: 10 on_image: then: - - lambda: |- - ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); + - lambda: |- + ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); esp32_camera_web_server: - port: 8080 @@ -1004,7 +1000,6 @@ touchscreen: number: GPIO3 display: inkplate_display - - platform: ft63x6 id: ft63_touchscreen interrupt_pin: @@ -1012,7 +1007,7 @@ touchscreen: number: GPIO39 reset_pin: allow_other_uses: true - number: GPIO5 + number: GPIO5 display: inkplate_display on_touch: - logger.log: diff --git a/tests/test5.yaml b/tests/test5.yaml index bf4247fb92..55efba57d1 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -100,7 +100,7 @@ binary_sensor: id: modbus_binsensortest register_type: read address: 0x3200 - bitmask: 0x80 # (bit 8) + bitmask: 0x80 # (bit 8) lambda: "return x;" - platform: tm1638 @@ -632,9 +632,9 @@ display: id: primarydisplay stb_pin: allow_other_uses: true - number: 5 #TM1638 STB - clk_pin: 18 #TM1638 CLK - dio_pin: 23 #TM1638 DIO + number: 5 # TM1638 STB + clk_pin: 18 # TM1638 CLK + dio_pin: 23 # TM1638 DIO update_interval: 5s intensity: 5 lambda: |- diff --git a/tests/test6.yaml b/tests/test6.yaml index b0ec04eb6a..2c5aa30aad 100644 --- a/tests/test6.yaml +++ b/tests/test6.yaml @@ -42,7 +42,6 @@ switch: output: pin_4 id: pin_4_switch - spi: # Pins are for SPI1 on the RP2040 Pico-W miso_pin: 8 clk_pin: 10 @@ -50,7 +49,7 @@ spi: # Pins are for SPI1 on the RP2040 Pico-W id: spi_0 interface: hardware -#light: +# light: # - platform: rp2040_pio_led_strip # id: led_strip # pin: GPIO13 @@ -69,7 +68,6 @@ spi: # Pins are for SPI1 on the RP2040 Pico-W # bit1_high: .69us # bit1_low: .4us - sensor: - platform: internal_temperature name: Internal Temperature diff --git a/tests/test8.1.yaml b/tests/test8.1.yaml index 839b1f3e6e..fdfa8bc786 100644 --- a/tests/test8.1.yaml +++ b/tests/test8.1.yaml @@ -24,15 +24,14 @@ psram: spi: - id: spi_id_1 clk_pin: - number: GPIO7 + number: GPIO7 allow_other_uses: false mosi_pin: GPIO6 interface: any - id: quad_spi clk_pin: 47 data_pins: - - - number: 40 + - number: 40 allow_other_uses: false - 41 - 42 diff --git a/tests/test8.2.yaml b/tests/test8.2.yaml index 69525b333b..ae892559e5 100644 --- a/tests/test8.2.yaml +++ b/tests/test8.2.yaml @@ -24,7 +24,7 @@ psram: spi: - id: spi_id_1 clk_pin: - number: GPIO7 + number: GPIO7 allow_other_uses: false mosi_pin: GPIO6 interface: any diff --git a/tests/test8.yaml b/tests/test8.yaml index fafdb76e12..5618e23e25 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -101,4 +101,4 @@ animation: - id: rgb565_animation file: pnglogo.png type: RGB565 - use_transparency: no + use_transparency: false From 127cbde2a28e7774ceab7ebdefa7a2ca1af682b3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 22 Feb 2024 12:51:06 +1300 Subject: [PATCH 364/436] Add missing timeout to "async_request" (#6267) --- esphome/zeroconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 72cc4c00c6..77044d4a11 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -110,7 +110,7 @@ class DashboardImportDiscovery: self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str ) -> None: """Process a service info.""" - if await info.async_request(zeroconf): + if await info.async_request(zeroconf, timeout=5): self._process_service_info(name, info) def _process_service_info(self, name: str, info: ServiceInfo) -> None: From a3fa1e6c52d1bfe07b0df341916000f1d312c7bc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:26:00 +1300 Subject: [PATCH 365/436] Bump zeroconf timeout to 3000 (#6270) --- esphome/zeroconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 77044d4a11..b67ea41323 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -110,7 +110,7 @@ class DashboardImportDiscovery: self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str ) -> None: """Process a service info.""" - if await info.async_request(zeroconf, timeout=5): + if await info.async_request(zeroconf, timeout=3000): self._process_service_info(name, info) def _process_service_info(self, name: str, info: ServiceInfo) -> None: From 481f06762521de4136562aad0ea4ba38e26b76e2 Mon Sep 17 00:00:00 2001 From: Daniel Baulig Date: Wed, 21 Feb 2024 17:33:28 -0800 Subject: [PATCH 366/436] web_server: Add a position property for cover entities that have the supports position trait (#6269) --- esphome/components/web_server/web_server.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 54e9e6ebcc..f87e920f13 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -785,6 +785,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { obj->position, start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + if (obj->get_traits().get_supports_position()) + root["position"] = obj->position; if (obj->get_traits().get_supports_tilt()) root["tilt"] = obj->tilt; }); From ea44166814fb7c73d41f532cdcb10b8b81e58b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 22 Feb 2024 02:35:21 +0100 Subject: [PATCH 367/436] Improve the error message on OTA version mismatch (#6259) --- esphome/espota2.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/espota2.py b/esphome/espota2.py index cdf6d7df32..580536153a 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -206,8 +206,11 @@ def perform_ota( _, version = receive_exactly(sock, 2, "version", RESPONSE_OK) _LOGGER.debug("Device support OTA version: %s", version) - if version not in (OTA_VERSION_1_0, OTA_VERSION_2_0): - raise OTAError(f"Unsupported OTA version {version}") + supported_versions = (OTA_VERSION_1_0, OTA_VERSION_2_0) + if version not in supported_versions: + raise OTAError( + f"Device uses unsupported OTA version {version}, this ESPHome supports {supported_versions}" + ) # Features send_check(sock, FEATURE_SUPPORTS_COMPRESSION, "features") From fd03d875e8f457584e3ee65a3fb222764c8ad848 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:48:12 +1300 Subject: [PATCH 368/436] Bump aioesphomeapi from 21.0.2 to 22.0.0 (#6263) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aa97e23f9e..2def1f4edc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.13 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==21.0.2 +aioesphomeapi==22.0.0 zeroconf==0.131.0 python-magic==0.4.27 From 76a3ffc8a9961bab029a33f91b833a92b014c274 Mon Sep 17 00:00:00 2001 From: LouDou Date: Thu, 22 Feb 2024 01:51:05 +0000 Subject: [PATCH 369/436] Allow ESP8266 to use multiple i2c busses (#6145) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/i2c/i2c_bus_arduino.cpp | 28 ++++++++++++++++------ esphome/components/i2c/i2c_bus_arduino.h | 1 + 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index d80ab1fd1d..0966bd4d97 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -24,7 +24,7 @@ void ArduinoI2CBus::setup() { } next_bus_num++; #elif defined(USE_ESP8266) - wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer) + wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) static bool first = true; if (first) { @@ -35,6 +35,16 @@ void ArduinoI2CBus::setup() { } #endif + this->set_pins_and_clock_(); + + this->initialized_ = true; + if (this->scan_) { + ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); + this->i2c_scan_(); + } +} + +void ArduinoI2CBus::set_pins_and_clock_() { #ifdef USE_RP2040 wire_->setSDA(this->sda_pin_); wire_->setSCL(this->scl_pin_); @@ -43,12 +53,8 @@ void ArduinoI2CBus::setup() { wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); #endif wire_->setClock(frequency_); - initialized_ = true; - if (this->scan_) { - ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); - this->i2c_scan_(); - } } + void ArduinoI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u", this->sda_pin_); @@ -82,6 +88,10 @@ void ArduinoI2CBus::dump_config() { } ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { +#if defined(USE_ESP8266) + this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances +#endif + // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { @@ -120,6 +130,10 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) return ERROR_OK; } ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { +#if defined(USE_ESP8266) + this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances +#endif + // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { @@ -164,7 +178,7 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn return ERROR_UNKNOWN; case 2: case 3: - ESP_LOGVV(TAG, "TX failed: not acknowledged"); + ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status); return ERROR_NOT_ACKNOWLEDGED; case 5: ESP_LOGVV(TAG, "TX failed: timeout"); diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 7298c3a1c9..6304c2b039 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -30,6 +30,7 @@ class ArduinoI2CBus : public I2CBus, public Component { private: void recover_(); + void set_pins_and_clock_(); RecoveryCode recovery_result_; protected: From 58c0d8c267e8e7607b710a0c52669c5f12667845 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Thu, 22 Feb 2024 04:03:14 +0100 Subject: [PATCH 370/436] Add Uponor Smatrix component (#5769) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/bl0940/sensor.py | 2 +- esphome/components/emc2101/sensor/__init__.py | 2 +- .../components/inkbird_ibsth1_mini/sensor.py | 3 +- esphome/components/uponor_smatrix/__init__.py | 78 ++++++ .../uponor_smatrix/climate/__init__.py | 33 +++ .../climate/uponor_smatrix_climate.cpp | 101 ++++++++ .../climate/uponor_smatrix_climate.h | 28 +++ .../uponor_smatrix/sensor/__init__.py | 70 ++++++ .../sensor/uponor_smatrix_sensor.cpp | 37 +++ .../sensor/uponor_smatrix_sensor.h | 23 ++ .../uponor_smatrix/uponor_smatrix.cpp | 225 ++++++++++++++++++ .../uponor_smatrix/uponor_smatrix.h | 128 ++++++++++ esphome/const.py | 1 + tests/components/uponor_smatrix/common.yaml | 38 +++ .../uponor_smatrix/test.esp32-c3-idf.yaml | 5 + .../uponor_smatrix/test.esp32-c3.yaml | 5 + .../uponor_smatrix/test.esp32-idf.yaml | 5 + .../components/uponor_smatrix/test.esp32.yaml | 5 + .../uponor_smatrix/test.esp8266.yaml | 5 + .../uponor_smatrix/test.rp2040.yaml | 5 + 21 files changed, 796 insertions(+), 4 deletions(-) create mode 100644 esphome/components/uponor_smatrix/__init__.py create mode 100644 esphome/components/uponor_smatrix/climate/__init__.py create mode 100644 esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp create mode 100644 esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h create mode 100644 esphome/components/uponor_smatrix/sensor/__init__.py create mode 100644 esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp create mode 100644 esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h create mode 100644 esphome/components/uponor_smatrix/uponor_smatrix.cpp create mode 100644 esphome/components/uponor_smatrix/uponor_smatrix.h create mode 100644 tests/components/uponor_smatrix/common.yaml create mode 100644 tests/components/uponor_smatrix/test.esp32-c3-idf.yaml create mode 100644 tests/components/uponor_smatrix/test.esp32-c3.yaml create mode 100644 tests/components/uponor_smatrix/test.esp32-idf.yaml create mode 100644 tests/components/uponor_smatrix/test.esp32.yaml create mode 100644 tests/components/uponor_smatrix/test.esp8266.yaml create mode 100644 tests/components/uponor_smatrix/test.rp2040.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 9577d7df47..8d0ac7983f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -365,6 +365,7 @@ esphome/components/uart/button/* @ssieb esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter +esphome/components/uponor_smatrix/* @kroimon esphome/components/vbus/* @ssieb esphome/components/veml3235/* @kbx81 esphome/components/version/* @esphome/core diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index a197becef8..fc2b04f976 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -4,6 +4,7 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_CURRENT, CONF_ENERGY, + CONF_EXTERNAL_TEMPERATURE, CONF_ID, CONF_POWER, CONF_VOLTAGE, @@ -24,7 +25,6 @@ from esphome.const import ( DEPENDENCIES = ["uart"] CONF_INTERNAL_TEMPERATURE = "internal_temperature" -CONF_EXTERNAL_TEMPERATURE = "external_temperature" bl0940_ns = cg.esphome_ns.namespace("bl0940") BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) diff --git a/esphome/components/emc2101/sensor/__init__.py b/esphome/components/emc2101/sensor/__init__.py index 03d3d0314e..9f3fbdce00 100644 --- a/esphome/components/emc2101/sensor/__init__.py +++ b/esphome/components/emc2101/sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( + CONF_EXTERNAL_TEMPERATURE, CONF_ID, CONF_SPEED, DEVICE_CLASS_TEMPERATURE, @@ -16,7 +17,6 @@ from .. import EMC2101_COMPONENT_SCHEMA, CONF_EMC2101_ID, emc2101_ns DEPENDENCIES = ["emc2101"] CONF_INTERNAL_TEMPERATURE = "internal_temperature" -CONF_EXTERNAL_TEMPERATURE = "external_temperature" CONF_DUTY_CYCLE = "duty_cycle" EMC2101Sensor = emc2101_ns.class_("EMC2101Sensor", cg.PollingComponent) diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index aa11fb3172..cdd0b5ade5 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_BATTERY_LEVEL, + CONF_EXTERNAL_TEMPERATURE, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, @@ -19,8 +20,6 @@ from esphome.const import ( CODEOWNERS = ["@fkirill"] DEPENDENCIES = ["esp32_ble_tracker"] -CONF_EXTERNAL_TEMPERATURE = "external_temperature" - inkbird_ibsth1_mini_ns = cg.esphome_ns.namespace("inkbird_ibsth1_mini") InkbirdIbstH1Mini = inkbird_ibsth1_mini_ns.class_( "InkbirdIbstH1Mini", esp32_ble_tracker.ESPBTDeviceListener, cg.Component diff --git a/esphome/components/uponor_smatrix/__init__.py b/esphome/components/uponor_smatrix/__init__.py new file mode 100644 index 0000000000..35c4c4cecd --- /dev/null +++ b/esphome/components/uponor_smatrix/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, time +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_TIME_ID, +) + +CODEOWNERS = ["@kroimon"] + +DEPENDENCIES = ["uart"] + +MULTI_CONF = True + +uponor_smatrix_ns = cg.esphome_ns.namespace("uponor_smatrix") +UponorSmatrixComponent = uponor_smatrix_ns.class_( + "UponorSmatrixComponent", cg.Component, uart.UARTDevice +) +UponorSmatrixDevice = uponor_smatrix_ns.class_( + "UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent) +) + +CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id" +CONF_TIME_DEVICE_ADDRESS = "time_device_address" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UponorSmatrixComponent), + cv.Optional(CONF_ADDRESS): cv.hex_uint16_t, + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "uponor_smatrix", + baud_rate=19200, + require_tx=True, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + +# A schema to use for all Uponor Smatrix devices +UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent), + cv.Required(CONF_ADDRESS): cv.hex_uint16_t, + } +) + + +async def to_code(config): + cg.add_global(uponor_smatrix_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if address := config.get(CONF_ADDRESS): + cg.add(var.set_system_address(address)) + if time_id := config.get(CONF_TIME_ID): + time_ = await cg.get_variable(time_id) + cg.add(var.set_time_id(time_)) + if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS): + cg.add(var.set_time_device_address(time_device_address)) + + +async def register_uponor_smatrix_device(var, config): + parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_device_address(config[CONF_ADDRESS])) + cg.add(parent.register_device(var)) diff --git a/esphome/components/uponor_smatrix/climate/__init__.py b/esphome/components/uponor_smatrix/climate/__init__.py new file mode 100644 index 0000000000..0becec2624 --- /dev/null +++ b/esphome/components/uponor_smatrix/climate/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate +from esphome.const import CONF_ID + +from .. import ( + uponor_smatrix_ns, + UponorSmatrixDevice, + UPONOR_SMATRIX_DEVICE_SCHEMA, + register_uponor_smatrix_device, +) + +DEPENDENCIES = ["uponor_smatrix"] + +UponorSmatrixClimate = uponor_smatrix_ns.class_( + "UponorSmatrixClimate", + climate.Climate, + cg.Component, + UponorSmatrixDevice, +) + +CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UponorSmatrixClimate), + } +).extend(UPONOR_SMATRIX_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, config) + await register_uponor_smatrix_device(var, config) diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp new file mode 100644 index 0000000000..5afc628db3 --- /dev/null +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -0,0 +1,101 @@ +#include "uponor_smatrix_climate.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uponor_smatrix { + +static const char *const TAG = "uponor_smatrix.climate"; + +void UponorSmatrixClimate::dump_config() { + LOG_CLIMATE("", "Uponor Smatrix Climate", this); + ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_); +} + +void UponorSmatrixClimate::loop() { + const uint32_t now = millis(); + + // Publish state after all update packets are processed + if (this->last_data_ != 0 && (now - this->last_data_ > 100) && this->target_temperature_raw_ != 0) { + float temp = raw_to_celsius((this->preset == climate::CLIMATE_PRESET_ECO) + ? (this->target_temperature_raw_ - this->eco_setback_value_raw_) + : this->target_temperature_raw_); + float step = this->get_traits().get_visual_target_temperature_step(); + this->target_temperature = roundf(temp / step) * step; + this->publish_state(); + this->last_data_ = 0; + } +} + +climate::ClimateTraits UponorSmatrixClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_current_humidity(true); + traits.set_supported_modes({climate::CLIMATE_MODE_HEAT}); + traits.set_supports_action(true); + traits.set_supported_presets({climate::CLIMATE_PRESET_ECO}); + traits.set_visual_min_temperature(this->min_temperature_); + traits.set_visual_max_temperature(this->max_temperature_); + traits.set_visual_current_temperature_step(0.1f); + traits.set_visual_target_temperature_step(0.5f); + return traits; +} + +void UponorSmatrixClimate::control(const climate::ClimateCall &call) { + if (call.get_target_temperature().has_value()) { + uint16_t temp = celsius_to_raw(*call.get_target_temperature()); + if (this->preset == climate::CLIMATE_PRESET_ECO) { + // During ECO mode, the thermostat automatically substracts the setback value from the setpoint, + // so we need to add it here first + temp += this->eco_setback_value_raw_; + } + + // For unknown reasons, we need to send a null setpoint first for the thermostat to react + UponorSmatrixData data[] = {{UPONOR_ID_TARGET_TEMP, 0}, {UPONOR_ID_TARGET_TEMP, temp}}; + this->send(data, sizeof(data) / sizeof(data[0])); + } +} + +void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) { + for (int i = 0; i < data_len; i++) { + switch (data[i].id) { + case UPONOR_ID_TARGET_TEMP_MIN: + this->min_temperature_ = raw_to_celsius(data[i].value); + break; + case UPONOR_ID_TARGET_TEMP_MAX: + this->max_temperature_ = raw_to_celsius(data[i].value); + break; + case UPONOR_ID_TARGET_TEMP: + // Ignore invalid values here as they are used by the controller to explicitely request the setpoint from a + // thermostat + if (data[i].value != UPONOR_INVALID_VALUE) + this->target_temperature_raw_ = data[i].value; + break; + case UPONOR_ID_ECO_SETBACK: + this->eco_setback_value_raw_ = data[i].value; + break; + case UPONOR_ID_DEMAND: + if (data[i].value & 0x1000) { + this->mode = climate::CLIMATE_MODE_COOL; + this->action = (data[i].value & 0x0040) ? climate::CLIMATE_ACTION_COOLING : climate::CLIMATE_ACTION_IDLE; + } else { + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = (data[i].value & 0x0040) ? climate::CLIMATE_ACTION_HEATING : climate::CLIMATE_ACTION_IDLE; + } + break; + case UPONOR_ID_MODE1: + this->set_preset_((data[i].value & 0x0008) ? climate::CLIMATE_PRESET_ECO : climate::CLIMATE_PRESET_NONE); + break; + case UPONOR_ID_ROOM_TEMP: + this->current_temperature = raw_to_celsius(data[i].value); + break; + case UPONOR_ID_HUMIDITY: + this->current_humidity = data[i].value & 0x00FF; + } + } + + this->last_data_ = millis(); +} + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h new file mode 100644 index 0000000000..b8458045c6 --- /dev/null +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/climate/climate.h" +#include "esphome/components/uponor_smatrix/uponor_smatrix.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uponor_smatrix { + +class UponorSmatrixClimate : public climate::Climate, public Component, public UponorSmatrixDevice { + public: + void dump_config() override; + void loop() override; + + protected: + climate::ClimateTraits traits() override; + void control(const climate::ClimateCall &call) override; + void on_device_data(const UponorSmatrixData *data, size_t data_len) override; + + uint32_t last_data_; + float min_temperature_{5.0f}; + float max_temperature_{35.0f}; + uint16_t eco_setback_value_raw_{0x0048}; + uint16_t target_temperature_raw_; +}; + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/sensor/__init__.py b/esphome/components/uponor_smatrix/sensor/__init__.py new file mode 100644 index 0000000000..89097aef18 --- /dev/null +++ b/esphome/components/uponor_smatrix/sensor/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_EXTERNAL_TEMPERATURE, + CONF_HUMIDITY, + CONF_TEMPERATURE, + CONF_ID, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +from .. import ( + uponor_smatrix_ns, + UponorSmatrixDevice, + UPONOR_SMATRIX_DEVICE_SCHEMA, + register_uponor_smatrix_device, +) + +DEPENDENCIES = ["uponor_smatrix"] + +UponorSmatrixSensor = uponor_smatrix_ns.class_( + "UponorSmatrixSensor", + sensor.Sensor, + cg.Component, + UponorSmatrixDevice, +) + +CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UponorSmatrixSensor), + 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_EXTERNAL_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, + ), + } +).extend(UPONOR_SMATRIX_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await register_uponor_smatrix_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(external_temperature_config) + cg.add(var.set_external_temperature_sensor(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp new file mode 100644 index 0000000000..2fd2a36efc --- /dev/null +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -0,0 +1,37 @@ +#include "uponor_smatrix_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uponor_smatrix { + +static const char *const TAG = "uponor_smatrix.sensor"; + +void UponorSmatrixSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Uponor Smatrix Sensor"); + ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) { + for (int i = 0; i < data_len; i++) { + switch (data[i].id) { + case UPONOR_ID_ROOM_TEMP: + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(raw_to_celsius(data[i].value)); + break; + case UPONOR_ID_EXTERNAL_TEMP: + if (this->external_temperature_sensor_ != nullptr) + this->external_temperature_sensor_->publish_state(raw_to_celsius(data[i].value)); + break; + case UPONOR_ID_HUMIDITY: + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(data[i].value & 0x00FF); + break; + } + } +} + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h new file mode 100644 index 0000000000..5e38117a21 --- /dev/null +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uponor_smatrix/uponor_smatrix.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uponor_smatrix { + +class UponorSmatrixSensor : public sensor::Sensor, public Component, public UponorSmatrixDevice { + SUB_SENSOR(temperature) + SUB_SENSOR(external_temperature) + SUB_SENSOR(humidity) + + public: + void dump_config() override; + + protected: + void on_device_data(const UponorSmatrixData *data, size_t data_len) override; +}; + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp new file mode 100644 index 0000000000..10cd787c7f --- /dev/null +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -0,0 +1,225 @@ +#include "uponor_smatrix.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uponor_smatrix { + +static const char *const TAG = "uponor_smatrix"; + +void UponorSmatrixComponent::setup() { +#ifdef USE_TIME + if (this->time_id_ != nullptr) { + this->time_id_->add_on_time_sync_callback([this] { this->send_time(); }); + } +#endif +} + +void UponorSmatrixComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Uponor Smatrix"); + ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_); +#ifdef USE_TIME + if (this->time_id_ != nullptr) { + ESP_LOGCONFIG(TAG, " Time synchronization: YES"); + ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_); + } +#endif + + this->check_uart_settings(19200); + + if (!this->unknown_devices_.empty()) { + ESP_LOGCONFIG(TAG, " Detected unknown device addresses:"); + for (auto device_address : this->unknown_devices_) { + ESP_LOGCONFIG(TAG, " 0x%04X", device_address); + } + } +} + +void UponorSmatrixComponent::loop() { + const uint32_t now = millis(); + + // Discard stale data + if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) { + ESP_LOGD(TAG, "Discarding %d bytes of unparsed data", this->rx_buffer_.size()); + this->rx_buffer_.clear(); + } + + // Read incoming data + while (this->available()) { + // The controller polls devices every 10 seconds, with around 200 ms between devices. + // Remember timestamps so we can send our own packets when the bus is expected to be silent. + if (now - this->last_rx_ > 500) { + this->last_poll_start_ = now; + } + this->last_rx_ = now; + + uint8_t byte; + this->read_byte(&byte); + if (this->parse_byte_(byte)) { + this->rx_buffer_.clear(); + } + } + + // Send packets during bus silence + if ((now - this->last_rx_ > 300) && (now - this->last_poll_start_ < 9500) && (now - this->last_tx_ > 200)) { + // Only build time packet when bus is silent and queue is empty to make sure we can send it right away + if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_()) + this->send_time_requested_ = false; + // Send the next packet in the queue + if (!this->tx_queue_.empty()) { + auto packet = std::move(this->tx_queue_.front()); + this->tx_queue_.pop(); + + this->write_array(packet); + this->flush(); + + this->last_tx_ = now; + } + } +} + +bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { + this->rx_buffer_.push_back(byte); + const uint8_t *packet = this->rx_buffer_.data(); + size_t packet_len = this->rx_buffer_.size(); + + if (packet_len < 7) { + // Minimum packet size is 7 bytes, wait for more + return false; + } + + uint16_t system_address = encode_uint16(packet[0], packet[1]); + uint16_t device_address = encode_uint16(packet[2], packet[3]); + uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]); + + uint16_t computed_crc = crc16(packet, packet_len - 2); + if (crc != computed_crc) { + // CRC did not match, more data might be coming + return false; + } + + ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address, + format_hex(&packet[4], packet_len - 6).c_str(), crc); + + // Detect or check system address + if (this->address_ == 0) { + ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address); + this->address_ = system_address; + } else if (this->address_ != system_address) { + // This should never happen except if the system address was set or detected incorrectly, so warn the user. + ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address); + return true; + } + + // Handle packet + size_t data_len = (packet_len - 6) / 3; + if (data_len == 0) { + if (packet[4] == UPONOR_ID_REQUEST) + ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address); + return true; + } + + // Decode packet payload data for easy access + UponorSmatrixData data[data_len]; + for (int i = 0; i < data_len; i++) { + data[i].id = packet[(i * 3) + 4]; + data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]); + } + +#ifdef USE_TIME + // Detect device that acts as time master if not set explicitely + if (this->time_device_address_ == 0 && data_len >= 2) { + // The first thermostat paired to the controller will act as the time master. Time can only be manually adjusted at + // this first thermostat. To synchronize time, we need to know its address, so we search for packets coming from a + // thermostat sending both room temperature and time information. + bool found_temperature = false; + bool found_time = false; + for (int i = 0; i < data_len; i++) { + if (data[i].id == UPONOR_ID_ROOM_TEMP) + found_temperature = true; + if (data[i].id == UPONOR_ID_DATETIME1) + found_time = true; + if (found_temperature && found_time) { + ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address); + this->time_device_address_ = device_address; + break; + } + } + } +#endif + + // Forward data to device components + bool found = false; + for (auto *device : this->devices_) { + if (device->address_ == device_address) { + found = true; + device->on_device_data(data, data_len); + } + } + + // Log unknown device addresses + if (!found && !this->unknown_devices_.count(device_address)) { + ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address); + this->unknown_devices_.insert(device_address); + } + + // Return true to reset buffer + return true; +} + +bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) { + if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0) + return false; + + // Assemble packet for send queue. All fields are big-endian except for the little-endian checksum. + std::vector packet(6 + 3 * data_len); + packet.push_back(this->address_ >> 8); + packet.push_back(this->address_ >> 0); + packet.push_back(device_address >> 8); + packet.push_back(device_address >> 0); + + for (int i = 0; i < data_len; i++) { + packet.push_back(data[i].id); + packet.push_back(data[i].value >> 8); + packet.push_back(data[i].value >> 0); + } + + auto crc = crc16(packet.data(), packet.size()); + packet.push_back(crc >> 0); + packet.push_back(crc >> 8); + + this->tx_queue_.push(packet); + return true; +} + +#ifdef USE_TIME +bool UponorSmatrixComponent::do_send_time_() { + if (this->time_device_address_ == 0 || this->time_id_ == nullptr) + return false; + + ESPTime now = this->time_id_->now(); + if (!now.is_valid()) + return false; + + uint8_t year = now.year - 2000; + uint8_t month = now.month; + // ESPHome days are [1-7] starting with Sunday, Uponor days are [0-6] starting with Monday + uint8_t day_of_week = (now.day_of_week == 1) ? 6 : (now.day_of_week - 2); + uint8_t day_of_month = now.day_of_month; + uint8_t hour = now.hour; + uint8_t minute = now.minute; + uint8_t second = now.second; + + uint16_t time1 = (year & 0x7F) << 7 | (month & 0x0F) << 3 | (day_of_week & 0x07); + uint16_t time2 = (day_of_month & 0x1F) << 11 | (hour & 0x1F) << 6 | (minute & 0x3F); + uint16_t time3 = second; + + ESP_LOGI(TAG, "Sending local time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, + now.minute, now.second); + + UponorSmatrixData data[] = {{UPONOR_ID_DATETIME1, time1}, {UPONOR_ID_DATETIME2, time2}, {UPONOR_ID_DATETIME3, time3}}; + return this->send(this->time_device_address_, data, sizeof(data) / sizeof(data[0])); +} +#endif + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h new file mode 100644 index 0000000000..b6675199b5 --- /dev/null +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -0,0 +1,128 @@ +#pragma once + +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" +#endif + +#include +#include +#include + +namespace esphome { +namespace uponor_smatrix { + +/// Date/Time Part 1 (year, month, day of week) +static const uint8_t UPONOR_ID_DATETIME1 = 0x08; +/// Date/Time Part 2 (day of month, hour, minute) +static const uint8_t UPONOR_ID_DATETIME2 = 0x09; +/// Date/Time Part 3 (seconds) +static const uint8_t UPONOR_ID_DATETIME3 = 0x0A; +/// Unknown (observed values: 0x0342, 0x0024) +static const uint8_t UPONOR_ID_UNKNOWN1 = 0x0C; +/// Outdoor Temperature? (sent by controller) +static const uint8_t UPONOR_ID_OUTDOOR_TEMP = 0x2D; +/// Unknown (observed values: 0x8000) +static const uint8_t UPONOR_ID_UNKNOWN2 = 0x35; +/// Room Temperature Setpoint Minimum +static const uint8_t UPONOR_ID_TARGET_TEMP_MIN = 0x37; +/// Room Temperature Setpoint Maximum +static const uint8_t UPONOR_ID_TARGET_TEMP_MAX = 0x38; +/// Room Temperature Setpoint +static const uint8_t UPONOR_ID_TARGET_TEMP = 0x3B; +/// Room Temperature Setpoint Setback for ECO Mode +static const uint8_t UPONOR_ID_ECO_SETBACK = 0x3C; +/// Heating/Cooling Demand +static const uint8_t UPONOR_ID_DEMAND = 0x3D; +/// Thermostat Operating Mode 1 (ECO state, program schedule state) +static const uint8_t UPONOR_ID_MODE1 = 0x3E; +/// Thermostat Operating Mode 2 (sensor configuration, heating/cooling allowed) +static const uint8_t UPONOR_ID_MODE2 = 0x3F; +/// Current Room Temperature +static const uint8_t UPONOR_ID_ROOM_TEMP = 0x40; +/// Current External (Floor/Outdoor) Sensor Temperature +static const uint8_t UPONOR_ID_EXTERNAL_TEMP = 0x41; +/// Current Room Humidity +static const uint8_t UPONOR_ID_HUMIDITY = 0x42; +/// Data Request (sent by controller) +static const uint8_t UPONOR_ID_REQUEST = 0xFF; + +/// Indicating an invalid/missing value +static const uint16_t UPONOR_INVALID_VALUE = 0x7FFF; + +struct UponorSmatrixData { + uint8_t id; + uint16_t value; +}; + +class UponorSmatrixDevice; + +class UponorSmatrixComponent : public uart::UARTDevice, public Component { + public: + UponorSmatrixComponent() = default; + + void setup() override; + void dump_config() override; + void loop() override; + + void set_system_address(uint16_t address) { this->address_ = address; } + void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); } + + bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len); + +#ifdef USE_TIME + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } + void set_time_device_address(uint16_t address) { this->time_device_address_ = address; } + void send_time() { this->send_time_requested_ = true; } +#endif + + protected: + bool parse_byte_(uint8_t byte); + + uint16_t address_; + std::vector devices_; + std::set unknown_devices_; + + std::vector rx_buffer_; + std::queue> tx_queue_; + uint32_t last_rx_; + uint32_t last_tx_; + uint32_t last_poll_start_; + +#ifdef USE_TIME + time::RealTimeClock *time_id_{nullptr}; + uint16_t time_device_address_; + bool send_time_requested_; + bool do_send_time_(); +#endif +}; + +class UponorSmatrixDevice : public Parented { + public: + void set_device_address(uint16_t address) { this->address_ = address; } + + virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0; + bool send(const UponorSmatrixData *data, size_t data_len) { + return this->parent_->send(this->address_, data, data_len); + } + + protected: + friend UponorSmatrixComponent; + uint16_t address_; +}; + +inline float raw_to_celsius(uint16_t raw) { + return (raw == UPONOR_INVALID_VALUE) ? NAN : fahrenheit_to_celsius(raw / 10.0f); +} + +inline uint16_t celsius_to_raw(float celsius) { + return std::isnan(celsius) ? UPONOR_INVALID_VALUE + : static_cast(lroundf(celsius_to_fahrenheit(celsius) * 10.0f)); +} + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 20ad564291..1d7f43d841 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -251,6 +251,7 @@ CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy" CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy" CONF_EXTERNAL_CLOCK_INPUT = "external_clock_input" CONF_EXTERNAL_COMPONENTS = "external_components" +CONF_EXTERNAL_TEMPERATURE = "external_temperature" CONF_EXTERNAL_VCC = "external_vcc" CONF_FALLING_EDGE = "falling_edge" CONF_FAMILY = "family" diff --git a/tests/components/uponor_smatrix/common.yaml b/tests/components/uponor_smatrix/common.yaml new file mode 100644 index 0000000000..cfdbacaa4c --- /dev/null +++ b/tests/components/uponor_smatrix/common.yaml @@ -0,0 +1,38 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uponor_uart + baud_rate: 19200 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + +uponor_smatrix: + uart_id: uponor_uart + address: 0x110B + time_id: sntp_time + time_device_address: 0xDE13 + +climate: + - platform: uponor_smatrix + address: 0xDE13 + name: Thermostat Living Room + +sensor: + - platform: uponor_smatrix + address: 0xDE13 + humidity: + name: Thermostat Humidity Living Room + temperature: + name: Thermostat Temperature Living Room + external_temperature: + name: Thermostat Floor Temperature Living Room diff --git a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml b/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-c3.yaml b/tests/components/uponor_smatrix/test.esp32-c3.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32-c3.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-idf.yaml b/tests/components/uponor_smatrix/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32.yaml b/tests/components/uponor_smatrix/test.esp32.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp8266.yaml b/tests/components/uponor_smatrix/test.esp8266.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp8266.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.rp2040.yaml b/tests/components/uponor_smatrix/test.rp2040.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.rp2040.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml From a7486100713d8bc39d6ef2a301d8507367ab45f5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 23 Feb 2024 07:38:24 +1300 Subject: [PATCH 371/436] Merge pull request from GHSA-8p25-3q46-8q2p --- esphome/dashboard/web_server.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index c16461d174..aca3a7a3a8 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -808,8 +808,16 @@ class EditRequestHandler(BaseHandler): @bind_config async def get(self, configuration: str | None = None) -> None: """Get the content of a file.""" - loop = asyncio.get_running_loop() + if not configuration.endswith((".yaml", ".yml")): + self.send_error(404) + return + filename = settings.rel_path(configuration) + if Path(filename).resolve().parent != settings.absolute_config_dir: + self.send_error(404) + return + + loop = asyncio.get_running_loop() content = await loop.run_in_executor( None, self._read_file, filename, configuration ) @@ -835,14 +843,20 @@ class EditRequestHandler(BaseHandler): @bind_config async def post(self, configuration: str | None = None) -> None: """Write the content of a file.""" + if not configuration.endswith((".yaml", ".yml")): + self.send_error(404) + return + + filename = settings.rel_path(configuration) + if Path(filename).resolve().parent != settings.absolute_config_dir: + self.send_error(404) + return + loop = asyncio.get_running_loop() - config_file = settings.rel_path(configuration) - await loop.run_in_executor( - None, self._write_file, config_file, self.request.body - ) + await loop.run_in_executor(None, self._write_file, filename, self.request.body) # Ensure the StorageJSON is updated as well await async_run_system_command( - [*DASHBOARD_COMMAND, "compile", "--only-generate", config_file] + [*DASHBOARD_COMMAND, "compile", "--only-generate", filename] ) self.set_status(200) From 15af08f6b753212b6d8260670b3cb1aa2ce91c50 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Thu, 22 Feb 2024 18:17:10 -0800 Subject: [PATCH 372/436] allow multiple emc2101 (#6272) --- esphome/components/emc2101/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/emc2101/__init__.py b/esphome/components/emc2101/__init__.py index 7a7b31cf14..8012d3e897 100644 --- a/esphome/components/emc2101/__init__.py +++ b/esphome/components/emc2101/__init__.py @@ -7,6 +7,8 @@ CODEOWNERS = ["@ellull"] DEPENDENCIES = ["i2c"] +MULTI_CONF = True + CONF_PWM = "pwm" CONF_DIVIDER = "divider" CONF_DAC = "dac" From 4a54af0d57787d9cfe4cbf6d55d6b6841fa3e296 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 24 Feb 2024 00:31:20 -0600 Subject: [PATCH 373/436] Fix RP2040 SPI pin validation (#6277) --- esphome/components/spi/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 10ea906a92..d45362435e 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -74,7 +74,8 @@ CONF_FORCE_SW = "force_sw" CONF_INTERFACE = "interface" CONF_INTERFACE_INDEX = "interface_index" -# RP2040 SPI pin assignments are complicated. Refer to https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf +# RP2040 SPI pin assignments are complicated; +# refer to GPIO function select table in https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf RP_SPI_PINSETS = [ { @@ -85,7 +86,7 @@ RP_SPI_PINSETS = [ { CONF_MISO_PIN: [8, 12, 24, 28, -1], CONF_CLK_PIN: [10, 14, 26], - CONF_MOSI_PIN: [11, 23, 27, -1], + CONF_MOSI_PIN: [11, 15, 27, -1], }, ] From 83a1fc5fdbc7620bb909b7a76cb0d92156589408 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Feb 2024 18:39:47 -1000 Subject: [PATCH 374/436] dashboard: move storage json update to a background task in edit save (#6280) * dashboard: move storage json update to a background task in edit save * dashboard: move storage json update to a background task in edit save * fix typing * docs --- esphome/dashboard/const.py | 2 ++ esphome/dashboard/core.py | 16 ++++++++++++++++ esphome/dashboard/entries.py | 10 ++++++++++ esphome/dashboard/web_server.py | 10 +++------- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py index 190d6c4a9a..db66cb5ead 100644 --- a/esphome/dashboard/const.py +++ b/esphome/dashboard/const.py @@ -8,3 +8,5 @@ MAX_EXECUTOR_WORKERS = 48 SENTINEL = object() + +DASHBOARD_COMMAND = ["esphome", "--dashboard"] diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index e22d95fba9..875ff6b91f 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -1,11 +1,13 @@ from __future__ import annotations import asyncio +import contextlib import logging import threading from dataclasses import dataclass from functools import partial from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Coroutine from ..zeroconf import DiscoveredImport from .dns import DNSCache @@ -71,6 +73,7 @@ class ESPHomeDashboard: "mdns_status", "settings", "dns_cache", + "_background_tasks", ) def __init__(self) -> None: @@ -85,6 +88,7 @@ class ESPHomeDashboard: self.mdns_status: MDNSStatus | None = None self.settings = DashboardSettings() self.dns_cache = DNSCache() + self._background_tasks: set[asyncio.Task] = set() async def async_setup(self) -> None: """Setup the dashboard.""" @@ -132,7 +136,19 @@ class ESPHomeDashboard: if settings.status_use_mqtt: status_thread_mqtt.join() self.mqtt_ping_request.set() + for task in self._background_tasks: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task await asyncio.sleep(0) + def async_create_background_task( + self, coro: Coroutine[Any, Any, Any] + ) -> asyncio.Task: + """Create a background task.""" + task = self.loop.create_task(coro) + task.add_done_callback(self._background_tasks.discard) + return task + DASHBOARD = ESPHomeDashboard() diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index ad139b830b..cd318ba8a7 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -10,12 +10,14 @@ from esphome import const, util from esphome.storage_json import StorageJSON, ext_storage_path from .const import ( + DASHBOARD_COMMAND, EVENT_ENTRY_ADDED, EVENT_ENTRY_REMOVED, EVENT_ENTRY_STATE_CHANGED, EVENT_ENTRY_UPDATED, ) from .enum import StrEnum +from .util.subprocess import async_run_system_command if TYPE_CHECKING: from .core import ESPHomeDashboard @@ -235,6 +237,14 @@ class DashboardEntries: ) return path_to_cache_key + def async_schedule_storage_json_update(self, filename: str) -> None: + """Schedule a task to update the storage JSON file.""" + self._dashboard.async_create_background_task( + async_run_system_command( + [*DASHBOARD_COMMAND, "compile", "--only-generate", filename] + ) + ) + class DashboardEntry: """Represents a single dashboard entry. diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index aca3a7a3a8..8bcc8efa0b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -9,11 +9,11 @@ import hashlib import json import logging import os -import time 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 @@ -40,6 +40,7 @@ from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_pa from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader +from .const import DASHBOARD_COMMAND from .core import DASHBOARD from .entries import EntryState, entry_state_to_bool from .util.file import write_file @@ -286,9 +287,6 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): raise NotImplementedError -DASHBOARD_COMMAND = ["esphome", "--dashboard"] - - class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): """Base class for commands that require a port.""" @@ -855,9 +853,7 @@ class EditRequestHandler(BaseHandler): loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._write_file, filename, self.request.body) # Ensure the StorageJSON is updated as well - await async_run_system_command( - [*DASHBOARD_COMMAND, "compile", "--only-generate", filename] - ) + DASHBOARD.entries.async_schedule_storage_json_update(filename) self.set_status(200) From 98552a0eaa7d9d2433e4bcfbcfcc55267a57890a Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 25 Feb 2024 10:23:01 -0800 Subject: [PATCH 375/436] make output optional for speed fan (#6274) Co-authored-by: Samuel Sieb --- esphome/components/speed/fan/__init__.py | 9 ++++++--- esphome/components/speed/fan/speed_fan.cpp | 7 ++++--- esphome/components/speed/fan/speed_fan.h | 5 +++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 3acfb005bd..221e67d6c7 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -19,7 +19,7 @@ SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_SPEED): cv.invalid( @@ -32,11 +32,14 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( async def to_code(config): - output_ = await cg.get_variable(config[CONF_OUTPUT]) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], output_, config[CONF_SPEED_COUNT]) + var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_SPEED_COUNT]) await cg.register_component(var, config) await fan.register_fan(var, config) + if CONF_OUTPUT in config: + output_ = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(output_)) + if CONF_OSCILLATION_OUTPUT in config: oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 41b222acd6..bd0af7714d 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -36,9 +36,10 @@ void SpeedFan::control(const fan::FanCall &call) { } void SpeedFan::write_state_() { - float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; - this->output_->set_level(speed); - + if (this->output_ != nullptr) { + float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; + this->output_->set_level(speed); + } if (this->oscillating_ != nullptr) this->oscillating_->set_state(this->oscillating); if (this->direction_ != nullptr) diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index ca0fe20e2a..57436d298b 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -12,9 +12,10 @@ namespace speed { class SpeedFan : public Component, public fan::Fan { public: - SpeedFan(output::FloatOutput *output, int speed_count) : output_(output), speed_count_(speed_count) {} + SpeedFan(int speed_count) : speed_count_(speed_count) {} void setup() override; void dump_config() override; + void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } @@ -24,7 +25,7 @@ class SpeedFan : public Component, public fan::Fan { void control(const fan::FanCall &call) override; void write_state_(); - output::FloatOutput *output_; + output::FloatOutput *output_{nullptr}; output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; From 77916d051e8443a431423b25bcfc6c768a9baa94 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 25 Feb 2024 10:26:35 -0800 Subject: [PATCH 376/436] fix throttle average nan handling (#6275) Co-authored-by: Samuel Sieb --- esphome/components/sensor/filter.cpp | 8 ++++++-- esphome/components/sensor/filter.h | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index b5bef4930c..6d7acad7e0 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -252,7 +252,9 @@ ThrottleAverageFilter::ThrottleAverageFilter(uint32_t time_period) : time_period optional ThrottleAverageFilter::new_value(float value) { ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::new_value(value=%f)", this, value); - if (!std::isnan(value)) { + if (std::isnan(value)) { + this->have_nan_ = true; + } else { this->sum_ += value; this->n_++; } @@ -262,12 +264,14 @@ void ThrottleAverageFilter::setup() { this->set_interval("throttle_average", this->time_period_, [this]() { ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_); if (this->n_ == 0) { - this->output(NAN); + if (this->have_nan_) + this->output(NAN); } else { this->output(this->sum_ / this->n_); this->sum_ = 0.0f; this->n_ = 0; } + this->have_nan_ = false; }); } float ThrottleAverageFilter::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index fa78f2fa46..1a08699d7b 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -245,6 +245,7 @@ class ThrottleAverageFilter : public Filter, public Component { uint32_t time_period_; float sum_{0.0f}; unsigned int n_{0}; + bool have_nan_{false}; }; using lambda_filter_t = std::function(float)>; From 4a3627c93edeb2764c3bd4134bf5b9e25923d411 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 25 Feb 2024 12:28:52 -0600 Subject: [PATCH 377/436] Fix thermostat supplemental actions (#6282) --- .../thermostat/thermostat_climate.cpp | 28 +++++++++---------- .../thermostat/thermostat_climate.h | 28 +++++++++++-------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 40a29295f1..26be6ba53a 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1,6 +1,5 @@ #include "thermostat_climate.h" #include "esphome/core/log.h" -#include namespace esphome { namespace thermostat { @@ -63,6 +62,15 @@ void ThermostatClimate::setup() { this->publish_state(); } +void ThermostatClimate::loop() { + for (auto &timer : this->timer_) { + if (timer.active && (timer.started + timer.time < millis())) { + timer.active = false; + timer.func(); + } + } +} + float ThermostatClimate::cool_deadband() { return this->cooling_deadband_; } float ThermostatClimate::cool_overrun() { return this->cooling_overrun_; } float ThermostatClimate::heat_deadband() { return this->heating_deadband_; } @@ -439,6 +447,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } + this->cooling_max_runtime_exceeded_ = false; trig = this->cool_action_trigger_; ESP_LOGVV(TAG, "Switching to COOLING action"); action_ready = true; @@ -452,6 +461,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } + this->heating_max_runtime_exceeded_ = false; trig = this->heat_action_trigger_; ESP_LOGVV(TAG, "Switching to HEATING action"); action_ready = true; @@ -752,15 +762,15 @@ bool ThermostatClimate::heating_action_ready_() { void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { if (this->timer_duration_(timer_index) > 0) { - this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), - this->timer_cbf_(timer_index)); + this->timer_[timer_index].started = millis(); this->timer_[timer_index].active = true; } } bool ThermostatClimate::cancel_timer_(ThermostatClimateTimerIndex timer_index) { + auto ret = this->timer_[timer_index].active; this->timer_[timer_index].active = false; - return this->cancel_timeout(this->timer_[timer_index].name); + return ret; } bool ThermostatClimate::timer_active_(ThermostatClimateTimerIndex timer_index) { @@ -777,7 +787,6 @@ std::function ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex void ThermostatClimate::cooling_max_run_time_timer_callback_() { ESP_LOGVV(TAG, "cooling_max_run_time timer expired"); - this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].active = false; this->cooling_max_runtime_exceeded_ = true; this->trigger_supplemental_action_(); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); @@ -785,21 +794,18 @@ void ThermostatClimate::cooling_max_run_time_timer_callback_() { void ThermostatClimate::cooling_off_timer_callback_() { ESP_LOGVV(TAG, "cooling_off timer expired"); - this->timer_[thermostat::TIMER_COOLING_OFF].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::cooling_on_timer_callback_() { ESP_LOGVV(TAG, "cooling_on timer expired"); - this->timer_[thermostat::TIMER_COOLING_ON].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::fan_mode_timer_callback_() { ESP_LOGVV(TAG, "fan_mode timer expired"); - this->timer_[thermostat::TIMER_FAN_MODE].active = false; this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)); if (this->supports_fan_only_action_uses_fan_mode_timer_) this->switch_to_action_(this->compute_action_()); @@ -807,19 +813,16 @@ void ThermostatClimate::fan_mode_timer_callback_() { void ThermostatClimate::fanning_off_timer_callback_() { ESP_LOGVV(TAG, "fanning_off timer expired"); - this->timer_[thermostat::TIMER_FANNING_OFF].active = false; this->switch_to_action_(this->compute_action_()); } void ThermostatClimate::fanning_on_timer_callback_() { ESP_LOGVV(TAG, "fanning_on timer expired"); - this->timer_[thermostat::TIMER_FANNING_ON].active = false; this->switch_to_action_(this->compute_action_()); } void ThermostatClimate::heating_max_run_time_timer_callback_() { ESP_LOGVV(TAG, "heating_max_run_time timer expired"); - this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].active = false; this->heating_max_runtime_exceeded_ = true; this->trigger_supplemental_action_(); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); @@ -827,21 +830,18 @@ void ThermostatClimate::heating_max_run_time_timer_callback_() { void ThermostatClimate::heating_off_timer_callback_() { ESP_LOGVV(TAG, "heating_off timer expired"); - this->timer_[thermostat::TIMER_HEATING_OFF].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::heating_on_timer_callback_() { ESP_LOGVV(TAG, "heating_on timer expired"); - this->timer_[thermostat::TIMER_HEATING_ON].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::idle_on_timer_callback_() { ESP_LOGVV(TAG, "idle_on timer expired"); - this->timer_[thermostat::TIMER_IDLE_ON].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 559812a94f..50510cf070 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -1,10 +1,12 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" +#include #include #include @@ -26,9 +28,9 @@ enum ThermostatClimateTimerIndex : size_t { enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; struct ThermostatClimateTimer { - const std::string name; bool active; uint32_t time; + uint32_t started; std::function func; }; @@ -59,6 +61,7 @@ class ThermostatClimate : public climate::Climate, public Component { ThermostatClimate(); void setup() override; void dump_config() override; + void loop() override; void set_default_preset(const std::string &custom_preset); void set_default_preset(climate::ClimatePreset preset); @@ -441,16 +444,17 @@ class ThermostatClimate : public climate::Climate, public Component { /// Climate action timers std::vector timer_{ - {"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, - {"cool_off", false, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, - {"cool_on", false, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, - {"fan_mode", false, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, - {"fan_off", false, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, - {"fan_on", false, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, - {"heat_run", false, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, - {"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, - {"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, - {"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}}; + {false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}, + }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) std::map preset_config_{}; From b5e633a2f3c2aa3a82258125348f2363321bfb19 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 25 Feb 2024 12:37:35 -0600 Subject: [PATCH 378/436] Fix test_build_components for macOS sed (#6278) --- script/test_build_components | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/script/test_build_components b/script/test_build_components index f951ba7545..2396353198 100755 --- a/script/test_build_components +++ b/script/test_build_components @@ -28,7 +28,12 @@ start_esphome() { component_test_file="./tests/test_build_components/build/$target_component.$test_name.$target_platform.yaml" cp $target_platform_file $component_test_file - sed -i "s!\$component_test_file!../../.$f!g" $component_test_file + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS sed is...different + sed -i '' "s!\$component_test_file!../../.$f!g" $component_test_file + else + sed -i "s!\$component_test_file!../../.$f!g" $component_test_file + fi # Start esphome process echo "> [$target_component] [$test_name] [$target_platform]" From a8ab745479f5ef8304ae22e296c76a92b6ca7adb Mon Sep 17 00:00:00 2001 From: Alexander Puzynia Date: Sun, 25 Feb 2024 14:26:08 -0800 Subject: [PATCH 379/436] Allow to specify global build directory (#6276) --- docker/docker_entrypoint.sh | 6 ++++++ esphome/core/config.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docker/docker_entrypoint.sh b/docker/docker_entrypoint.sh index 75d5e0b7b5..397b1528c5 100755 --- a/docker/docker_entrypoint.sh +++ b/docker/docker_entrypoint.sh @@ -21,4 +21,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" +# If /build is mounted, use that as the build path +# otherwise use path in /config (so that builds aren't lost on container restart) +if [[ -d /build ]]; then + export ESPHOME_BUILD_PATH=/build +fi + exec esphome "$@" diff --git a/esphome/core/config.py b/esphome/core/config.py index f3d732a8fc..7449d8850b 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -38,7 +38,7 @@ from esphome.const import ( __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority -from esphome.helpers import copy_file_if_changed, walk_files +from esphome.helpers import copy_file_if_changed, get_str_env, walk_files _LOGGER = logging.getLogger(__name__) @@ -190,7 +190,8 @@ def preload_core_config(config, result): CORE.data[KEY_CORE] = {} if CONF_BUILD_PATH not in conf: - conf[CONF_BUILD_PATH] = f"build/{CORE.name}" + build_path = get_str_env("ESPHOME_BUILD_PATH", "build") + conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name) CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH]) has_oldstyle = CONF_PLATFORM in conf From 323849c8215378b24a1947d9423abd37a07ca6cd Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sun, 25 Feb 2024 19:29:39 -0300 Subject: [PATCH 380/436] Add device class support to text sensor (#6202) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 1 + esphome/components/api/api_pb2.cpp | 9 +++ esphome/components/api/api_pb2.h | 1 + esphome/components/mqtt/mqtt_text_sensor.cpp | 4 ++ esphome/components/text_sensor/__init__.py | 25 ++++++++ esphome/components/text_sensor/text_sensor.h | 5 +- .../text_sensor/test_text_sensor.py | 58 +++++++++++++++++++ .../text_sensor/test_text_sensor.yaml | 26 +++++++++ tests/test1.yaml | 4 ++ 10 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/text_sensor/test_text_sensor.py create mode 100644 tests/component_tests/text_sensor/test_text_sensor.yaml diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 04db649aef..8d79163590 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -600,6 +600,7 @@ message ListEntitiesTextSensorResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + string device_class = 8; } message TextSensorStateResponse { option (id) = 27; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4ebfb7582e..d3bfdcebb1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -543,6 +543,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) msg.icon = text_sensor->get_icon(); msg.disabled_by_default = text_sensor->is_disabled_by_default(); msg.entity_category = static_cast(text_sensor->get_entity_category()); + msg.device_class = text_sensor->get_device_class(); return this->send_list_entities_text_sensor_response(msg); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f81bf04e99..d3aa1fa2bf 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2721,6 +2721,10 @@ bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengt this->icon = value.as_string(); return true; } + case 8: { + this->device_class = value.as_string(); + return true; + } default: return false; } @@ -2743,6 +2747,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_string(8, this->device_class); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { @@ -2776,6 +2781,10 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 02fc7b88f8..ee975c1726 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -713,6 +713,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { std::string icon{}; bool disabled_by_default{false}; enums::EntityCategory entity_category{}; + std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index d0d3174bfe..b0754bc8b3 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -1,6 +1,8 @@ #include "mqtt_text_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_TEXT_SENSOR @@ -13,6 +15,8 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + if (!this->sensor_->get_device_class().empty()) + root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 3ed6b72d8f..cf7d23aec7 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt from esphome.const import ( + CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_FILTERS, CONF_ICON, @@ -14,12 +15,21 @@ from esphome.const import ( CONF_STATE, CONF_FROM, CONF_TO, + DEVICE_CLASS_DATE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TIMESTAMP, ) 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 +DEVICE_CLASSES = [ + DEVICE_CLASS_DATE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TIMESTAMP, +] + IS_PLATFORM_COMPONENT = True @@ -112,10 +122,13 @@ async def map_filter_to_code(config, filter_id): ) +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") + TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), cv.GenerateID(): cv.declare_id(TextSensor), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_VALUE): automation.validate_automation( { @@ -140,12 +153,21 @@ def text_sensor_schema( *, icon: str = _UNDEF, entity_category: str = _UNDEF, + device_class: str = _UNDEF, ) -> cv.Schema: schema = TEXT_SENSOR_SCHEMA if class_ is not _UNDEF: schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) if icon is not _UNDEF: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + if device_class is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) if entity_category is not _UNDEF: schema = schema.extend( { @@ -164,6 +186,9 @@ async def build_filters(config): async def setup_text_sensor_core_(var, config): await setup_entity(var, config) + if CONF_DEVICE_CLASS in config: + cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 996af02f7e..bd72ea70e3 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -13,6 +13,9 @@ namespace text_sensor { #define LOG_TEXT_SENSOR(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + } \ if (!(obj)->get_icon().empty()) { \ ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ } \ @@ -28,7 +31,7 @@ namespace text_sensor { public: \ void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } -class TextSensor : public EntityBase { +class TextSensor : public EntityBase, public EntityBase_DeviceClass { public: /// Getter-syntax for .state. std::string get_state() const; diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py new file mode 100644 index 0000000000..1c4ef6633d --- /dev/null +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -0,0 +1,58 @@ +"""Tests for the text sensor component.""" + + +def test_text_sensor_is_setup(generate_main): + """ + When the text is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") + + # Then + assert "new template_::TemplateTextSensor();" in main_cpp + assert "App.register_text_sensor" in main_cpp + + +def test_text_sensor_sets_mandatory_fields(generate_main): + """ + When the mandatory fields are set in the yaml, they should be set in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") + + # Then + assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp + assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp + assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp + + +def test_text_sensor_config_value_internal_set(generate_main): + """ + Test that the "internal" config value is correctly set + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") + + # Then + assert "ts_2->set_internal(true);" in main_cpp + assert "ts_3->set_internal(false);" in main_cpp + + +def test_text_sensor_device_class_set(generate_main): + """ + When the device_class of text_sensor is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") + + # Then + assert 'ts_2->set_device_class("timestamp");' in main_cpp + assert 'ts_3->set_device_class("date");' in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.yaml b/tests/component_tests/text_sensor/test_text_sensor.yaml new file mode 100644 index 0000000000..b426cb102c --- /dev/null +++ b/tests/component_tests/text_sensor/test_text_sensor.yaml @@ -0,0 +1,26 @@ +--- +esphome: + name: test + platform: ESP8266 + board: d1_mini_lite + +text_sensor: + - platform: template + id: ts_1 + name: "Template Text Sensor 1" + lambda: |- + return {"Hello World"}; + - platform: template + id: ts_2 + name: "Template Text Sensor 2" + lambda: |- + return {"2023-06-22T18:43:52+00:00"}; + device_class: timestamp + internal: true + - platform: template + id: ts_3 + name: "Template Text Sensor 3" + lambda: |- + return {"2023-06-22T18:43:52+00:00"}; + device_class: date + internal: false diff --git a/tests/test1.yaml b/tests/test1.yaml index 57456e7d6d..5f62c7ddc5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3923,6 +3923,10 @@ text_sensor: - platform: template name: Template Text Sensor id: ${textname}_text + - platform: template + name: Template Text Sensor Timestamp + id: ${textname}_text_timestamp + device_class: timestamp - platform: wifi_info scan_results: name: Scan Results From 9b77f97d87844358432ad5cd3c65aabe5483d429 Mon Sep 17 00:00:00 2001 From: puuu Date: Tue, 27 Feb 2024 12:47:45 +0900 Subject: [PATCH 381/436] CSE7766: Fix energy calculation (#6286) Co-authored-by: DAVe3283 --- esphome/components/cse7766/cse7766.cpp | 36 ++++++++++---------------- esphome/components/cse7766/cse7766.h | 4 +-- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index f482ba26c3..cdd4220e50 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -118,7 +118,7 @@ void CSE7766Component::parse_data_() { uint32_t power_coeff = this->get_24_bit_uint_(14); uint32_t power_cycle = this->get_24_bit_uint_(17); uint8_t adj = this->raw_data_[20]; - uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; + uint16_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; bool have_power = adj & 0x10; bool have_current = adj & 0x20; @@ -132,8 +132,19 @@ void CSE7766Component::parse_data_() { } } + float energy = 0.0; + if (this->energy_sensor_ != nullptr) { + if (this->cf_pulses_last_ == 0 && !this->energy_sensor_->has_state()) { + this->cf_pulses_last_ = cf_pulses; + } + uint16_t cf_diff = cf_pulses - this->cf_pulses_last_; + this->cf_pulses_total_ += cf_diff; + this->cf_pulses_last_ = cf_pulses; + energy = this->cf_pulses_total_ * float(power_coeff) / 1000000.0f / 3600.0f; + this->energy_sensor_->publish_state(energy); + } + float power = 0.0f; - float energy = 0.0f; if (power_cycle_exceeds_range) { // Datasheet: power cycle exceeding range means active power is 0 if (this->power_sensor_ != nullptr) { @@ -144,27 +155,6 @@ void CSE7766Component::parse_data_() { if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); } - - // Add CF pulses to the total energy only if we have Power coefficient to multiply by - - if (this->cf_pulses_last_ == 0) { - this->cf_pulses_last_ = cf_pulses; - } - - uint32_t cf_diff; - if (cf_pulses < this->cf_pulses_last_) { - cf_diff = cf_pulses + (0x10000 - this->cf_pulses_last_); - } else { - cf_diff = cf_pulses - this->cf_pulses_last_; - } - this->cf_pulses_last_ = cf_pulses; - - energy = cf_diff * float(power_coeff) / 1000000.0f / 3600.0f; - this->energy_total_ += energy; - if (this->energy_sensor_ != nullptr) - this->energy_sensor_->publish_state(this->energy_total_); - } else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) { - this->energy_sensor_->publish_state(0); } float current = 0.0f; diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 3ab8d609bd..4d9ba7be74 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -30,8 +30,8 @@ class CSE7766Component : public Component, public uart::UARTDevice { sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; - float energy_total_{0.0f}; - uint32_t cf_pulses_last_{0}; + uint32_t cf_pulses_total_{0}; + uint16_t cf_pulses_last_{0}; }; } // namespace cse7766 From 5e04914a11f74df5151d9fe0e0e32563950678c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:49:33 +1300 Subject: [PATCH 382/436] Bump pytest from 8.0.1 to 8.0.2 (#6288) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index eef49e5ced..c50a688cd0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.15.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==8.0.1 +pytest==8.0.2 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-asyncio==0.23.5 From f73518dbeb3bf6b6db94e5c96adf63e8834f374c Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Tue, 27 Feb 2024 09:16:20 +0100 Subject: [PATCH 383/436] Improve dualstack and IPv6 support (#5449) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/async_tcp/__init__.py | 2 +- .../esp32_improv/esp32_improv_component.cpp | 10 +- .../ethernet/ethernet_component.cpp | 92 +++++++++++-------- .../components/ethernet/ethernet_component.h | 4 +- .../ethernet_info/ethernet_info_text_sensor.h | 21 ++++- .../components/ethernet_info/text_sensor.py | 26 ++++-- .../components/improv_base/improv_base.cpp | 9 +- .../improv_serial/improv_serial_component.cpp | 10 +- esphome/components/mqtt/mqtt_client.cpp | 20 ++-- esphome/components/network/__init__.py | 5 +- esphome/components/network/ip_address.h | 9 ++ esphome/components/network/util.cpp | 6 +- esphome/components/network/util.h | 2 +- esphome/components/socket/socket.cpp | 12 +-- .../components/wake_on_lan/wake_on_lan.cpp | 8 +- esphome/components/wifi/wifi_component.cpp | 13 ++- esphome/components/wifi/wifi_component.h | 8 +- .../wifi/wifi_component_esp32_arduino.cpp | 48 ++++++++-- .../wifi/wifi_component_esp8266.cpp | 26 +++--- .../wifi/wifi_component_esp_idf.cpp | 53 +++++++---- .../wifi/wifi_component_libretiny.cpp | 2 +- .../components/wifi/wifi_component_pico_w.cpp | 10 +- esphome/components/wifi_info/text_sensor.py | 19 +++- .../wifi_info/wifi_info_text_sensor.h | 22 ++++- esphome/const.py | 1 + platformio.ini | 2 +- 26 files changed, 300 insertions(+), 140 deletions(-) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 0a69943a25..eae8c0e2df 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): if CORE.is_esp32 or CORE.is_libretiny: # https://github.com/esphome/AsyncTCP/blob/master/library.json - cg.add_library("esphome/AsyncTCP-esphome", "2.0.1") + cg.add_library("esphome/AsyncTCP-esphome", "2.1.3") elif CORE.is_esp8266: # https://github.com/esphome/ESPAsyncTCP cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 90e69e1cfa..a6037a773c 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -141,9 +141,13 @@ void ESP32ImprovComponent::loop() { std::vector urls = {ESPHOME_MY_LINK}; #ifdef USE_WEBSERVER - auto ip = wifi::global_wifi_component->wifi_sta_ip(); - std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); - urls.push_back(webserver_url); + for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { + if (ip.is_ip4()) { + std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); + urls.push_back(webserver_url); + break; + } + } #endif std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); this->send_response_(data); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 50d5855e6a..8071e9c97a 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -119,10 +119,10 @@ void EthernetComponent::setup() { ESPHL_ERROR_CHECK(err, "ETH event handler register error"); err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "GOT IP6 event handler register error"); -#endif /* ENABLE_IPV6 */ + ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error"); +#endif /* USE_NETWORK_IPV6 */ /* start Ethernet driver state machine */ err = esp_eth_start(this->eth_handle_); @@ -165,20 +165,6 @@ void EthernetComponent::loop() { this->state_ = EthernetComponentState::CONNECTING; this->start_connect_(); } -#if ENABLE_IPV6 - else if (this->got_ipv6_) { - esp_ip6_addr_t ip6_addr; - if (esp_netif_get_ip6_global(this->eth_netif_, &ip6_addr) == 0 && - esp_netif_ip6_get_addr_type(&ip6_addr) == ESP_IP6_ADDR_IS_GLOBAL) { - ESP_LOGCONFIG(TAG, "IPv6 Addr (Global): " IPV6STR, IPV62STR(ip6_addr)); - } else { - esp_netif_get_ip6_linklocal(this->eth_netif_, &ip6_addr); - ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(ip6_addr)); - } - - this->got_ipv6_ = false; - } -#endif /* ENABLE_IPV6 */ break; } } @@ -234,10 +220,28 @@ float EthernetComponent::get_setup_priority() const { return setup_priority::WIF bool EthernetComponent::can_proceed() { return this->is_connected(); } -network::IPAddress EthernetComponent::get_ip_address() { +network::IPAddresses EthernetComponent::get_ip_addresses() { + network::IPAddresses addresses; esp_netif_ip_info_t ip; - esp_netif_get_ip_info(this->eth_netif_, &ip); - return network::IPAddress(&ip.ip); + esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + // TODO: do something smarter + // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); + } +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + addresses[i + 1] = network::IPAddress(&if_ip6s[i]); + } +#endif /* USE_NETWORK_IPV6 */ + + return addresses; } void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { @@ -269,20 +273,33 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + const esp_netif_ip_info_t *ip_info = &event->ip_info; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip)); + global_eth_component->got_ipv4_address_ = true; +#if USE_NETWORK_IPV6 + global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; +#else global_eth_component->connected_ = true; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%" PRId32 ")", event_id); +#endif /* USE_NETWORK_IPV6 */ } -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { - ESP_LOGV(TAG, "[Ethernet event] ETH Got IP6 (num=%" PRId32 ")", event_id); - global_eth_component->got_ipv6_ = true; + ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip)); global_eth_component->ipv6_count_ += 1; + global_eth_component->connected_ = + global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); } -#endif /* ENABLE_IPV6 */ +#endif /* USE_NETWORK_IPV6 */ void EthernetComponent::start_connect_() { + global_eth_component->got_ipv4_address_ = false; +#if USE_NETWORK_IPV6 + global_eth_component->ipv6_count_ = 0; +#endif /* USE_NETWORK_IPV6 */ this->connect_begin_ = millis(); this->status_set_warning(); @@ -334,12 +351,12 @@ void EthernetComponent::start_connect_() { if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); } -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 err = esp_netif_create_ip6_linklocal(this->eth_netif_); if (err != ESP_OK) { - ESPHL_ERROR_CHECK(err, "IPv6 local failed"); + ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed"); } -#endif /* ENABLE_IPV6 */ +#endif /* USE_NETWORK_IPV6 */ } this->connect_begin_ = millis(); @@ -362,18 +379,15 @@ void EthernetComponent::dump_connect_params_() { ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1).str().c_str()); ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2).str().c_str()); -#if ENABLE_IPV6 - if (this->ipv6_count_ > 0) { - esp_ip6_addr_t ip6_addr; - esp_netif_get_ip6_linklocal(this->eth_netif_, &ip6_addr); - ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(ip6_addr)); - - if (esp_netif_get_ip6_global(this->eth_netif_, &ip6_addr) == 0 && - esp_netif_ip6_get_addr_type(&ip6_addr) == ESP_IP6_ADDR_IS_GLOBAL) { - ESP_LOGCONFIG(TAG, "IPv6 Addr (Global): " IPV6STR, IPV62STR(ip6_addr)); - } +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); } -#endif /* ENABLE_IPV6 */ +#endif /* USE_NETWORK_IPV6 */ esp_err_t err; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 11f50af966..bfd0944af7 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -58,7 +58,7 @@ class EthernetComponent : public Component { void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); void set_manual_ip(const ManualIP &manual_ip); - network::IPAddress get_ip_address(); + network::IPAddresses get_ip_addresses(); std::string get_use_address() const; void set_use_address(const std::string &use_address); bool powerdown(); @@ -87,8 +87,8 @@ class EthernetComponent : public Component { bool started_{false}; bool connected_{false}; + bool got_ipv4_address_{false}; #if LWIP_IPV6 - bool got_ipv6_{false}; uint8_t ipv6_count_{0}; #endif /* LWIP_IPV6 */ EthernetComponentState state_{EthernetComponentState::STOPPED}; diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 2d46fe18eb..62c5781f66 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -12,19 +12,30 @@ namespace ethernet_info { class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { - auto ip = ethernet::global_eth_component->get_ip_address(); - if (ip != this->last_ip_) { - this->last_ip_ = ip; - this->publish_state(network::IPAddress(ip).str()); + auto ips = ethernet::global_eth_component->get_ip_addresses(); + if (ips != this->last_ips_) { + this->last_ips_ = ips; + this->publish_state(ips[0].str()); + uint8_t sensor = 0; + for (auto &ip : ips) { + if (ip.is_set()) { + if (this->ip_sensors_[sensor] != nullptr) { + this->ip_sensors_[sensor]->publish_state(ip.str()); + sensor++; + } + } + } } } float get_setup_priority() const override { return setup_priority::ETHERNET; } std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; } void dump_config() override; + void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - network::IPAddress last_ip_; + network::IPAddresses last_ips_; + std::array ip_sensors_; }; } // namespace ethernet_info diff --git a/esphome/components/ethernet_info/text_sensor.py b/esphome/components/ethernet_info/text_sensor.py index 7cb9944c92..b802c427e8 100644 --- a/esphome/components/ethernet_info/text_sensor.py +++ b/esphome/components/ethernet_info/text_sensor.py @@ -18,17 +18,25 @@ CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressEsthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")) + ) + .extend(cv.polling_component_schema("1s")) + .extend( + { + cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + for x in range(5) + } + ) } ) -async def setup_conf(config, key): - if key in config: - conf = config[key] - var = await text_sensor.new_text_sensor(conf) - await cg.register_component(var, conf) - - async def to_code(config): - await setup_conf(config, CONF_IP_ADDRESS) + if conf := config.get(CONF_IP_ADDRESS): + ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS]) + await cg.register_component(ip_info, config[CONF_IP_ADDRESS]) + for x in range(5): + if sensor_conf := conf.get(f"address_{x}"): + sens = await text_sensor.new_text_sensor(sensor_conf) + cg.add(ip_info.add_ip_sensors(x, sens)) diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp index f18a1061fb..e890187d1a 100644 --- a/esphome/components/improv_base/improv_base.cpp +++ b/esphome/components/improv_base/improv_base.cpp @@ -21,8 +21,13 @@ std::string ImprovBase::get_formatted_next_url_() { // Ip address pos = this->next_url_.find("{{ip_address}}"); if (pos != std::string::npos) { - std::string ip = network::get_ip_address().str(); - copy.replace(pos, 14, ip); + for (auto &ip : network::get_ip_addresses()) { + if (ip.is_ip4()) { + std::string ipa = ip.str(); + copy.replace(pos, 14, ipa); + break; + } + } } return copy; diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 2318fd43cb..0325bad4df 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -155,9 +155,13 @@ std::vector ImprovSerialComponent::build_rpc_settings_response_(improv: urls.push_back(this->get_formatted_next_url_()); } #ifdef USE_WEBSERVER - auto ip = wifi::global_wifi_component->wifi_sta_ip(); - std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); - urls.push_back(webserver_url); + for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { + if (ip.is_ip4()) { + std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); + urls.push_back(webserver_url); + break; + } + } #endif std::vector data = improv::build_rpc_response(command, urls, false); return data; diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 923762aea4..0b18413928 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -91,8 +91,13 @@ void MQTTClientComponent::send_device_info_() { this->publish_json( topic, [](JsonObject root) { - auto ip = network::get_ip_address(); - root["ip"] = ip.str(); + uint8_t index = 0; + for (auto &ip : network::get_ip_addresses()) { + if (ip.is_set()) { + root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str(); + index++; + } + } root["name"] = App.get_name(); #ifdef USE_API root["port"] = api::global_api_server->get_port(); @@ -159,14 +164,13 @@ void MQTTClientComponent::start_dnslookup_() { this->dns_resolve_error_ = false; this->dns_resolved_ = false; ip_addr_t addr; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if USE_NETWORK_IPV6 + err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, + MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4); +#else err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4); -#endif -#ifdef USE_ESP8266 - err_t err = dns_gethostbyname(this->credentials_.address.c_str(), &addr, - esphome::mqtt::MQTTClientComponent::dns_found_callback, this); -#endif +#endif /* USE_NETWORK_IPV6 */ switch (err) { case ERR_OK: { // Got IP immediately diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index dd1353f86f..4e87ff1c12 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -5,6 +5,7 @@ from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.const import ( CONF_ENABLE_IPV6, + CONF_MIN_IPV6_ADDR_COUNT, ) CODEOWNERS = ["@esphome/core"] @@ -16,12 +17,14 @@ IPAddress = network_ns.class_("IPAddress") CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_ENABLE_IPV6, default=False): cv.boolean, + cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, } ) async def to_code(config): - cg.add_define("ENABLE_IPV6", config[CONF_ENABLE_IPV6]) + cg.add_define("USE_NETWORK_IPV6", config[CONF_ENABLE_IPV6]) + cg.add_define("USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT]) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", config[CONF_ENABLE_IPV6]) add_idf_sdkconfig_option( diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 02e71790a7..b02c358a77 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -77,6 +77,13 @@ struct IPAddress { } #endif /* LWIP_IPV6 */ IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); } + IPAddress(esp_ip_addr_t *other_ip) { +#if LWIP_IPV6 + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); +#else + memcpy((void *) &ip_addr_, (void *) &other_ip->u_addr.ip4, sizeof(ip_addr_)); +#endif + } operator esp_ip_addr_t() const { esp_ip_addr_t tmp; #if LWIP_IPV6 @@ -128,5 +135,7 @@ struct IPAddress { ip_addr_t ip_addr_; }; +using IPAddresses = std::array; + } // namespace network } // namespace esphome diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 109c2947ce..445485b644 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -37,14 +37,14 @@ bool is_disabled() { return false; } -network::IPAddress get_ip_address() { +network::IPAddresses get_ip_addresses() { #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr) - return ethernet::global_eth_component->get_ip_address(); + return ethernet::global_eth_component->get_ip_addresses(); #endif #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->get_ip_address(); + return wifi::global_wifi_component->get_ip_addresses(); #endif return {}; } diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 0322f19215..5377d44f2f 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -12,7 +12,7 @@ bool is_connected(); bool is_disabled(); /// Get the active network hostname std::string get_use_address(); -IPAddress get_ip_address(); +IPAddresses get_ip_addresses(); } // namespace network } // namespace esphome diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index d0fce9198f..b200046d7f 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -10,15 +10,15 @@ namespace socket { Socket::~Socket() {} std::unique_ptr socket_ip(int type, int protocol) { -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 return socket(AF_INET6, type, protocol); #else return socket(AF_INET, type, protocol); -#endif +#endif /* USE_NETWORK_IPV6 */ } socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 if (addrlen < sizeof(sockaddr_in6)) { errno = EINVAL; return 0; @@ -47,11 +47,11 @@ 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 +#endif /* USE_NETWORK_IPV6 */ } socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 if (addrlen < sizeof(sockaddr_in6)) { errno = EINVAL; return 0; @@ -73,7 +73,7 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po server->sin_addr.s_addr = ESPHOME_INADDR_ANY; server->sin_port = htons(port); return sizeof(sockaddr_in); -#endif +#endif /* USE_NETWORK_IPV6 */ } } // namespace socket } // namespace esphome diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index a4dd0f3b6f..f414bf6c71 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -32,8 +32,12 @@ void WakeOnLanButton::press_action() { bool end_status = false; IPAddress broadcast = IPAddress(255, 255, 255, 255); #ifdef USE_ESP8266 - begin_status = this->udp_client_.beginPacketMulticast(broadcast, 9, - IPAddress((ip_addr_t) esphome::network::get_ip_address()), 128); + for (auto ip : esphome::network::get_ip_addresses()) { + if (ip.is_ip4()) { + begin_status = this->udp_client_.beginPacketMulticast(broadcast, 9, ip, 128); + break; + } + } #endif #ifdef USE_ESP32 begin_status = this->udp_client_.beginPacket(broadcast, 9); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 05938d87a2..15a994d8eb 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -207,14 +207,13 @@ void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } #endif - -network::IPAddress WiFiComponent::get_ip_address() { +network::IPAddresses WiFiComponent::get_ip_addresses() { if (this->has_sta()) - return this->wifi_sta_ip(); + return this->wifi_sta_ip_addresses(); #ifdef USE_WIFI_AP if (this->has_ap()) - return this->wifi_soft_ap_ip(); + return {this->wifi_soft_ap_ip()}; #endif // USE_WIFI_AP return {}; @@ -412,7 +411,11 @@ void WiFiComponent::print_connect_params_() { return; } ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); - ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_ip().str().c_str()); + for (auto &ip : wifi_sta_ip_addresses()) { + if (ip.is_set()) { + ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); + } + } ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index be5095105c..d9cb6a9ae2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -258,7 +258,7 @@ class WiFiComponent : public Component { #endif network::IPAddress get_dns_address(int num); - network::IPAddress get_ip_address(); + network::IPAddresses get_ip_addresses(); std::string get_use_address() const; void set_use_address(const std::string &use_address); @@ -293,7 +293,7 @@ class WiFiComponent : public Component { }); } - network::IPAddress wifi_sta_ip(); + network::IPAddresses wifi_sta_ip_addresses(); std::string wifi_ssid(); bssid_t wifi_bssid(); @@ -396,6 +396,10 @@ class WiFiComponent : public Component { bool rrm_{false}; #endif bool enable_on_boot_; + bool got_ipv4_address_{false}; +#if USE_NETWORK_IPV6 + uint8_t num_ipv6_addresses_{0}; +#endif /* USE_NETWORK_IPV6 */ Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 5d8aa7f749..d7241fb66c 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -131,7 +131,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { // TODO: is this needed? #if LWIP_IPV6 dns.type = IPADDR_TYPE_V4; -#endif +#endif /* LWIP_IPV6 */ if (manual_ip->dns1.is_set()) { dns = manual_ip->dns1; dns_setserver(0, &dns); @@ -144,12 +144,36 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return true; } -network::IPAddress WiFiComponent::wifi_sta_ip() { +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (!this->has_sta()) return {}; + network::IPAddresses addresses; tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); - return network::IPAddress(&ip.ip); + esp_err_t err = tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + // TODO: do something smarter + // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); + } +#if LWIP_IPV6 + ip6_addr_t ipv6; + err = tcpip_adapter_get_ip6_global(TCPIP_ADAPTER_IF_STA, &ipv6); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip6_gobal failed: %s", esp_err_to_name(err)); + } else { + addresses[1] = network::IPAddress(&ipv6); + } + err = tcpip_adapter_get_ip6_linklocal(TCPIP_ADAPTER_IF_STA, &ipv6); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip6_linklocal failed: %s", esp_err_to_name(err)); + } else { + addresses[2] = network::IPAddress(&ipv6); + } +#endif /* LWIP_IPV6 */ + + return addresses; } bool WiFiComponent::wifi_apply_hostname_() { @@ -440,9 +464,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 this->set_timeout(100, [] { WiFi.enableIpV6(); }); -#endif /* ENABLE_IPV6 */ +#endif /* USE_NETWORK_IPV6 */ break; } @@ -494,18 +518,26 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str()); + this->got_ipv4_address_ = true; +#if USE_NETWORK_IPV6 + s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT; +#else s_sta_connecting = false; +#endif /* USE_NETWORK_IPV6 */ break; } -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { auto it = info.got_ip6.ip6_info; ESP_LOGV(TAG, "Got IPv6 address=" IPV6STR, IPV62STR(it.ip)); + this->num_ipv6_addresses_++; + s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT)); break; } -#endif /* ENABLE_IPV6 */ +#endif /* USE_NETWORK_IPV6 */ case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { ESP_LOGV(TAG, "Event: Lost IP"); + this->got_ipv4_address_ = false; break; } case ESPHOME_EVENT_ID_WIFI_AP_START: { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 15b0c65641..f274e37a9f 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -17,10 +17,8 @@ extern "C" { #include "lwip/dhcp.h" #include "lwip/init.h" // LWIP_VERSION_ #include "lwip/apps/sntp.h" -#if LWIP_IPV6 #include "lwip/netif.h" // struct netif #include -#endif #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) #include "LwipDhcpServer.h" #define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease) @@ -185,12 +183,15 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return ret; } -network::IPAddress WiFiComponent::wifi_sta_ip() { +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (!this->has_sta()) return {}; - struct ip_info ip {}; - wifi_get_ip_info(STATION_IF, &ip); - return network::IPAddress(&ip.ip); + network::IPAddresses addresses; + uint8_t index = 0; + for (auto &addr : addrList) { + addresses[index++] = addr.ipFromNetifNum(); + } + return addresses; } bool WiFiComponent::wifi_apply_hostname_() { const std::string &hostname = App.get_name(); @@ -327,17 +328,20 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } -#if ENABLE_IPV6 - for (bool configured = false; !configured;) { +#if USE_NETWORK_IPV6 + bool connected = false; + while (!connected) { + uint8_t ipv6_addr_count = 0; for (auto addr : addrList) { ESP_LOGV(TAG, "Address %s", addr.toString().c_str()); - if ((configured = !addr.isLocal() && addr.isV6())) { - break; + if (addr.isV6()) { + ipv6_addr_count++; } } delay(500); // NOLINT + connected = (ipv6_addr_count >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); } -#endif +#endif /* USE_NETWORK_IPV6 */ if (ap.get_channel().has_value()) { ret = wifi_set_channel(*ap.get_channel()); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 0035733553..ebb2fb92ea 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -39,14 +39,11 @@ static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - #ifdef USE_WIFI_AP -static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#endif // USE_WIFI_AP - +static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif // USE_WIFI_AP static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -66,9 +63,9 @@ struct IDFWiFiEvent { wifi_event_ap_probe_req_rx_t ap_probe_req_rx; wifi_event_bss_rssi_low_t bss_rssi_low; ip_event_got_ip_t ip_got_ip; -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 ip_event_got_ip6_t ip_got_ip6; -#endif /* ENABLE_IPV6 */ +#endif /* USE_NETWORK_IPV6 */ ip_event_ap_staipassigned_t ip_ap_staipassigned; } data; }; @@ -92,7 +89,7 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi memcpy(&event.data.sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t)); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { memcpy(&event.data.ip_got_ip, event_data, sizeof(ip_event_got_ip_t)); -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 } else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) { memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t)); #endif @@ -411,7 +408,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // may be called from wifi_station_connect s_sta_connecting = true; s_sta_connected = false; - s_sta_got_ip = false; s_sta_connect_error = false; s_sta_connect_not_found = false; @@ -476,17 +472,29 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return true; } -network::IPAddress WiFiComponent::wifi_sta_ip() { +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (!this->has_sta()) return {}; + network::IPAddresses addresses; esp_netif_ip_info_t ip; esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); if (err != ESP_OK) { ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); // TODO: do something smarter // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); } - return network::IPAddress(&ip.ip); +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + addresses[i + 1] = network::IPAddress(&if_ip6s[i]); + } +#endif /* USE_NETWORK_IPV6 */ + return addresses; } bool WiFiComponent::wifi_apply_hostname_() { @@ -521,7 +529,7 @@ const char *get_auth_mode_str(uint8_t mode) { std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } #if LWIP_IPV6 std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } -#endif +#endif /* LWIP_IPV6 */ const char *get_disconnect_reason_str(uint8_t reason) { switch (reason) { case WIFI_REASON_AUTH_EXPIRE: @@ -658,22 +666,23 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); -#endif /* ENABLE_IPV6 */ +#endif /* USE_NETWORK_IPV6 */ ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), format_ip4_addr(it.ip_info.gw).c_str()); - s_sta_got_ip = true; + this->got_ipv4_address_ = true; -#if ENABLE_IPV6 +#if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); -#endif /* ENABLE_IPV6 */ + this->num_ipv6_addresses_++; +#endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { ESP_LOGV(TAG, "Event: Lost IP"); - s_sta_got_ip = false; + this->got_ipv4_address_ = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) { const auto &it = data->data.sta_scan_done; @@ -737,8 +746,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - if (s_sta_connected && s_sta_got_ip) { + if (s_sta_connected && this->got_ipv4_address_) { +#if USE_NETWORK_IPV6 + if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) { + return WiFiSTAConnectStatus::CONNECTED; + } +#else return WiFiSTAConnectStatus::CONNECTED; +#endif /* USE_NETWORK_IPV6 */ } if (s_sta_connect_error) { return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 29c6ce64d0..f6b0fb2699 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -81,7 +81,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return true; } -network::IPAddress WiFiComponent::wifi_sta_ip() { +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (!this->has_sta()) return {}; return {WiFi.localIP()}; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index c71203a877..2bb1af5489 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -6,6 +6,7 @@ #include "lwip/dns.h" #include "lwip/err.h" #include "lwip/netif.h" +#include #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -173,7 +174,14 @@ std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); } -network::IPAddress WiFiComponent::wifi_sta_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { + network::IPAddresses addresses; + uint8_t index = 0; + for (auto addr : addrList) { + addresses[index++] = addr.ipFromNetifNum(); + } + return addresses; +} network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 659fd08db1..75513712dd 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -37,7 +37,16 @@ CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ) + .extend(cv.polling_component_schema("1s")) + .extend( + { + cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + for x in range(5) + } + ), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("60s")), @@ -65,9 +74,15 @@ async def setup_conf(config, key): async def to_code(config): - await setup_conf(config, CONF_IP_ADDRESS) await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_MAC_ADDRESS) await setup_conf(config, CONF_SCAN_RESULTS) await setup_conf(config, CONF_DNS_ADDRESS) + if conf := config.get(CONF_IP_ADDRESS): + wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS]) + await cg.register_component(wifi_info, config[CONF_IP_ADDRESS]) + for x in range(5): + if sensor_conf := conf.get(f"address_{x}"): + sens = await text_sensor.new_text_sensor(sensor_conf) + cg.add(wifi_info.add_ip_sensors(x, sens)) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 35ce108c86..9ffbd3f418 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" +#include namespace esphome { namespace wifi_info { @@ -10,18 +11,29 @@ namespace wifi_info { class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { - auto ip = wifi::global_wifi_component->wifi_sta_ip(); - if (ip != this->last_ip_) { - this->last_ip_ = ip; - this->publish_state(ip.str()); + auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); + if (ips != this->last_ips_) { + this->last_ips_ = ips; + this->publish_state(ips[0].str()); + uint8_t sensor = 0; + for (auto &ip : ips) { + if (ip.is_set()) { + if (this->ip_sensors_[sensor] != nullptr) { + this->ip_sensors_[sensor]->publish_state(ip.str()); + sensor++; + } + } + } } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; } void dump_config() override; + void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - network::IPAddress last_ip_; + network::IPAddresses last_ips_; + std::array ip_sensors_; }; class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor { diff --git a/esphome/const.py b/esphome/const.py index 1d7f43d841..044465de7b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -449,6 +449,7 @@ CONF_MIN_FANNING_RUN_TIME = "min_fanning_run_time" CONF_MIN_HEATING_OFF_TIME = "min_heating_off_time" CONF_MIN_HEATING_RUN_TIME = "min_heating_run_time" CONF_MIN_IDLE_TIME = "min_idle_time" +CONF_MIN_IPV6_ADDR_COUNT = "min_ipv6_addr_count" CONF_MIN_LENGTH = "min_length" CONF_MIN_LEVEL = "min_level" CONF_MIN_POWER = "min_power" diff --git a/platformio.ini b/platformio.ini index 8ab2fb6f48..8ba8b8a2cf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -116,7 +116,7 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - esphome/AsyncTCP-esphome@1.2.2 ; async_tcp + esphome/AsyncTCP-esphome@2.1.3 ; async_tcp WiFiClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) From 37138d4f28456d049c747b250ce46cef25e4ea73 Mon Sep 17 00:00:00 2001 From: Darek Date: Tue, 27 Feb 2024 21:29:56 +0100 Subject: [PATCH 384/436] Waveshare e-ink 2IN9_V2 - fix full and partial update based on vendor SDK and examples (#5481) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_epaper.cpp | 213 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 30 +++ 3 files changed, 247 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index fa7c104951..7f86bf8d08 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -45,6 +45,9 @@ WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( WaveshareEPaper2P9InBV3 = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InBV3", WaveshareEPaper ) +WaveshareEPaper2P9InV2R2 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InV2R2", WaveshareEPaper +) GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper @@ -107,6 +110,7 @@ MODELS = { "2.70inv2": ("b", WaveshareEPaper2P7InV2), "2.90in-b": ("b", WaveshareEPaper2P9InB), "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), + "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 9118475c36..a4417e0d1c 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -83,6 +83,33 @@ static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B1[LUT_SIZE_TTGO_B1] = { 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +// clang-format off +// Disable formatting to preserve the same look as in Waveshare examples +static const uint8_t PARTIAL_UPD_2IN9_LUT_SIZE = 159; +static const uint8_t PARTIAL_UPD_2IN9_LUT[PARTIAL_UPD_2IN9_LUT_SIZE] = +{ + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, + 0x22, 0x17, 0x41, 0xB0, 0x32, 0x36, +}; +// clang-format on + void WaveshareEPaperBase::setup_pins_() { this->init_internal_(this->get_buffer_length_()); this->dc_pin_->setup(); // OUTPUT @@ -1118,6 +1145,192 @@ void WaveshareEPaper2P9InBV3::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 2.90in v2 rev2 +// based on SDK and examples in ZIP file from: +// https://www.waveshare.com/pico-epaper-2.9.htm +// ======================================================== + +void WaveshareEPaper2P9InV2R2::initialize() { + this->reset_(); + this->wait_until_idle_(); + + this->command(0x12); // SWRESET + this->wait_until_idle_(); + + this->command(0x01); + this->data(0x27); + this->data(0x01); + this->data(0x00); + + this->command(0x11); + this->data(0x03); + + // SetWindows(0, 0, w, h) + this->command(0x44); + this->data(0x00); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); + + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data((this->get_height_internal() - 1) & 0xFF); + this->data(((this->get_height_internal() - 1) >> 8) & 0xFF); + + this->command(0x21); + this->data(0x00); + this->data(0x80); + + // SetCursor(0, 0) + this->command(0x4E); + this->data(0x00); + this->command(0x4f); + this->data(0x00); + this->data(0x00); + + this->wait_until_idle_(); +} + +WaveshareEPaper2P9InV2R2::WaveshareEPaper2P9InV2R2() { this->reset_duration_ = 10; } + +void WaveshareEPaper2P9InV2R2::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(reset_duration_); // NOLINT + this->reset_pin_->digital_write(true); + delay(reset_duration_); // NOLINT + } +} + +void WaveshareEPaper2P9InV2R2::display() { + if (!this->wait_until_idle_()) { + this->status_set_warning(); + ESP_LOGE(TAG, "fail idle 1"); + return; + } + + if (this->full_update_every_ == 1) { + // do single full update + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplay + this->command(0x22); + this->data(0xF7); + this->command(0x20); + return; + } + + // if (this->full_update_every_ == 1 || + if (this->at_update_ == 0) { + // do base update + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + this->command(0x26); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplay + this->command(0x22); + this->data(0xF7); + this->command(0x20); + } else { + // do partial update + this->reset_(); + + this->write_lut_(PARTIAL_UPD_2IN9_LUT, PARTIAL_UPD_2IN9_LUT_SIZE); + + this->command(0x37); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x40); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + + this->command(0x3C); + this->data(0x80); + + this->command(0x22); + this->data(0xC0); + this->command(0x20); + + if (!this->wait_until_idle_()) { + ESP_LOGE(TAG, "fail idle 2"); + } + + // SetWindows(0, 0, w, h) + this->command(0x44); + this->data(0x00); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); + + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data((this->get_height_internal() - 1) & 0xFF); + this->data(((this->get_height_internal() - 1) >> 8) & 0xFF); + + // SetCursor(0, 0) + this->command(0x4E); + this->data(0x00); + this->command(0x4f); + this->data(0x00); + this->data(0x00); + + // write b/w + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplayPartial + this->command(0x22); + this->data(0x0F); + this->command(0x20); + } + + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; +} + +void WaveshareEPaper2P9InV2R2::write_lut_(const uint8_t *lut, const uint8_t size) { + // COMMAND WRITE LUT REGISTER + this->command(0x32); + for (uint8_t i = 0; i < size; i++) + this->data(lut[i]); +} + +void WaveshareEPaper2P9InV2R2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9inV2R2"); + ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void WaveshareEPaper2P9InV2R2::deep_sleep() { + this->command(0x10); + this->data(0x01); +} + +int WaveshareEPaper2P9InV2R2::get_width_internal() { return 128; } +int WaveshareEPaper2P9InV2R2::get_height_internal() { return 296; } +int WaveshareEPaper2P9InV2R2::get_width_controller() { return this->get_width_internal(); } +void WaveshareEPaper2P9InV2R2::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + // ======================================================== // Good Display 2.9in black/white/grey // Datasheet: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 47f0cb27b6..f12d5f7d54 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -337,6 +337,36 @@ class WaveshareEPaper2P9InBV3 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P9InV2R2 : public WaveshareEPaper { + public: + WaveshareEPaper2P9InV2R2(); + + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override; + + void set_full_update_every(uint32_t full_update_every); + + protected: + void write_lut_(const uint8_t *lut, uint8_t size); + + int get_width_internal() override; + + int get_height_internal() override; + + int get_width_controller() override; + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + + private: + void reset_(); +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; From c43c9ad1c5a8ab09759e639059d08fd869d3e6d2 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Tue, 27 Feb 2024 23:31:33 +0100 Subject: [PATCH 385/436] Add RTTTL volume control. (#5968) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/rtttl/__init__.py | 4 ++++ esphome/components/rtttl/rtttl.cpp | 29 ++++++++++++++++------------ esphome/components/rtttl/rtttl.h | 14 +++++++++++--- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 6163129529..10f1313408 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_PLATFORM, CONF_TRIGGER_ID, CONF_SPEAKER, + CONF_GAIN, ) _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), cv.Optional(CONF_OUTPUT): cv.use_id(FloatOutput), cv.Optional(CONF_SPEAKER): cv.use_id(Speaker), + cv.Optional(CONF_GAIN, default="0.6"): cv.percentage, cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -98,6 +100,8 @@ async def to_code(config): out = await cg.get_variable(config[CONF_SPEAKER]) cg.add(var.set_speaker(out)) + cg.add(var.set_gain(config[CONF_GAIN])) + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 199a373785..0bdf65b7bd 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -16,7 +16,7 @@ static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; -static const uint16_t I2S_SPEED = 1600; +static const uint16_t I2S_SPEED = 1000; #undef HALF_PI static const double HALF_PI = 1.5707963267948966192313216916398; @@ -136,7 +136,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 = 8192 * sin(deg2rad(rem)); + int16_t val = (49152 * this->gain_) * sin(deg2rad(rem)); sample[x].left = val; sample[x].right = val; @@ -269,7 +269,7 @@ void Rtttl::loop() { } if (this->output_freq_ != 0) { this->output_->update_frequency(this->output_freq_); - this->output_->set_level(0.5); + this->output_->set_level(this->gain_); } else { this->output_->set_level(0.0); } @@ -278,18 +278,23 @@ void Rtttl::loop() { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { this->samples_sent_ = 0; - this->samples_count_ = (this->sample_rate_ * this->note_duration_) / I2S_SPEED; - // Convert from frequency in Hz to high and low samples in fixed point + this->samples_gap_ = 0; + this->samples_per_wave_ = 0; + this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms); + if (need_note_gap) { + this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); + } if (this->output_freq_ != 0) { this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_; - } else { - this->samples_per_wave_ = 0; - } - if (need_note_gap) { - this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / I2S_SPEED; - } else { - this->samples_gap_ = 0; + + // 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; } + // Convert from frequency in Hz to high and low samples in fixed point } #endif diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index e09b0265be..bf089ce980 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -15,7 +15,7 @@ namespace esphome { namespace rtttl { #ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 256; +static const size_t SAMPLE_BUFFER_SIZE = 512; struct SpeakerSample { int16_t left{0}; @@ -31,6 +31,13 @@ class Rtttl : public Component { #ifdef USE_SPEAKER void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif + void set_gain(float gain) { + if (gain < 0.1f) + gain = 0.1f; + if (gain > 1.0f) + gain = 1.0f; + this->gain_ = gain; + } void play(std::string rtttl); void stop(); void dump_config() override; @@ -60,6 +67,7 @@ class Rtttl : public Component { uint16_t note_duration_; uint32_t output_freq_; + float gain_{0.6f}; #ifdef USE_OUTPUT output::FloatOutput *output_; @@ -68,13 +76,13 @@ class Rtttl : public Component { void play_output_(); #ifdef USE_SPEAKER - speaker::Speaker *speaker_; - void play_speaker_(); + speaker::Speaker *speaker_{nullptr}; int sample_rate_{16000}; int samples_per_wave_{0}; int samples_sent_{0}; int samples_count_{0}; int samples_gap_{0}; + #endif CallbackManager on_finished_playback_callback_; From 5393a09872fd09479c43cbfdc7c88ed17a39815a Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Wed, 28 Feb 2024 03:42:11 +0100 Subject: [PATCH 386/436] Touchscreen component and driver fixes (#5997) * Introduce calibration settings for all touchscreen drivers. this will override the common values. The x,y coordinates only calculated when the right calibrations are set. * resolve issues reported by CI * remove unneeded spaces and newlines * Forgot to remove some obsolete code * remove get_setup_priority from xpt2046 * remove media_player changes. * media_player: removed to much, * Update suggestions * referd back the `get_setup_priority` removal so it can be moved into a othe PR. * tt21100: restore init read * fix spacing * load native display dimensions instead of using internal dimensons. and load it only onse on setup * moved `update_touches()` to protexted section * adding Clydes PR#6049 * add multitouch test script * Update all Touchscreen replacing `get_*_internal` to `get_native_*` * fixed some CI recomendations * couple of fixes * make sure the display is running before touchscreen is setup * fix clang * revert back last changes * xpt2046: change log level for testing * logging information * add test file * fix polling issue with the for example the xpt2046 * fixed some CI issues * fixed some CI issues * restore mirror parameter discriptions * same for the swap_xy * same for the transform * remove the above const from const.py * and put the above const bacl const.py * Merge branch 'nvds-touchscreen-fix1' of https://github.com/nielsnl68/esphome into nvds-touchscreen-fix1 * and put the above const bacl const.py * [tt21100] making interupt pin optional * [tt21100] making interupt pin optional (now complete) * update the display part based on @clyde' s changes. * fix issue with ft6x36 touvhscreen * reverd back touch check. add comment * add some extra checks to the ft6x36 * add an other log and a typo fixed * okay an other fix. * add an extra check like others do and fix data type * [ft6336] fix update race when ts is touched. * [touchscreen] update some log's with a verbose level. * fix clang issues * fix the clang issues * fix the clang issues * fix virtual issue. * fix the clang issues * an other clang issues * remove anti-aliased fonts support. It does not belong here. * remove anti-aliased fonts support. It does not belong here. * rename test script * Moving the test files to there right location. * rename the test files * clean up the code * add a new line * clang fixings * clang fixings * remove comment * remove comment * Update esphome/components/touchscreen/__init__.py Co-authored-by: guillempages * Update esphome/components/touchscreen/__init__.py Co-authored-by: guillempages * Update esphome/components/touchscreen/__init__.py Co-authored-by: guillempages * Update esphome/components/touchscreen/touchscreen.cpp * Update esphome/components/touchscreen/touchscreen.cpp * [ft63x6] add threshold --------- Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: guillempages --- esphome/components/display/display.h | 16 +++- esphome/components/display/display_buffer.h | 3 - .../ektf2232/touchscreen/ektf2232.cpp | 33 ++++--- .../ft5x06/touchscreen/ft5x06_touchscreen.h | 10 +- esphome/components/ft63x6/ft63x6.cpp | 91 ++++++++++++++----- esphome/components/ft63x6/ft63x6.h | 18 +++- esphome/components/ft63x6/touchscreen.py | 3 +- .../gt911/touchscreen/gt911_touchscreen.cpp | 10 +- .../touchscreen/lilygo_t5_47_touchscreen.cpp | 11 ++- esphome/components/touchscreen/__init__.py | 67 +++++++++++++- .../components/touchscreen/touchscreen.cpp | 73 +++++++++------ esphome/components/touchscreen/touchscreen.h | 13 +-- .../tt21100/touchscreen/__init__.py | 7 +- .../tt21100/touchscreen/tt21100.cpp | 19 ++-- .../xpt2046/touchscreen/__init__.py | 69 +++++--------- .../xpt2046/touchscreen/xpt2046.cpp | 7 +- .../components/xpt2046/touchscreen/xpt2046.h | 2 +- tests/components/ft63x6/test.esp32.yaml | 38 ++++++++ tests/components/tt21100/test.esp32-s2.yaml | 43 +++++++++ tests/components/xpt2046/test.esp32-s2.yaml | 37 ++++++++ tests/test4.yaml | 9 +- 21 files changed, 422 insertions(+), 157 deletions(-) create mode 100644 tests/components/ft63x6/test.esp32.yaml create mode 100644 tests/components/tt21100/test.esp32-s2.yaml create mode 100644 tests/components/xpt2046/test.esp32-s2.yaml diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index daa5028d6b..8880a2a21c 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -175,10 +175,15 @@ class Display : public PollingComponent { /// Clear the entire screen by filling it with OFF pixels. void clear(); - /// Get the width of the image in pixels with rotation applied. - virtual int get_width() = 0; - /// Get the height of the image in pixels with rotation applied. - virtual int get_height() = 0; + /// Get the calculated width of the display in pixels with rotation applied. + virtual int get_width() { return this->get_width_internal(); } + /// Get the calculated height of the display in pixels with rotation applied. + virtual int get_height() { return this->get_height_internal(); } + + /// Get the native (original) width of the display in pixels. + int get_native_width() { return this->get_width_internal(); } + /// Get the native (original) height of the display in pixels. + int get_native_height() { return this->get_height_internal(); } /// Set a single pixel at the specified coordinates to default color. inline void draw_pixel_at(int x, int y) { this->draw_pixel_at(x, y, COLOR_ON); } @@ -538,6 +543,9 @@ class Display : public PollingComponent { void do_update_(); void clear_clipping_(); + virtual int get_height_internal() = 0; + virtual int get_width_internal() = 0; + /** * This method fills a triangle using only integer variables by using a * modified bresenham algorithm. diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 869d97613a..b7c4db56be 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -22,9 +22,6 @@ class DisplayBuffer : public Display { /// Set a single pixel at the specified coordinates to the given color. void draw_pixel_at(int x, int y, Color color) override; - virtual int get_height_internal() = 0; - virtual int get_width_internal() = 0; - protected: virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp index 00e00bc7e6..ef8f1c6802 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -34,24 +34,27 @@ void EKTF2232Touchscreen::setup() { // Get touch resolution uint8_t received[4]; - this->write(GET_X_RES, 4); - if (this->read(received, 4)) { - ESP_LOGE(TAG, "Failed to read X resolution!"); - this->interrupt_pin_->detach_interrupt(); - this->mark_failed(); - return; + if (this->x_raw_max_ == this->x_raw_min_) { + this->write(GET_X_RES, 4); + if (this->read(received, 4)) { + ESP_LOGE(TAG, "Failed to read X resolution!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); } - this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); - this->write(GET_Y_RES, 4); - if (this->read(received, 4)) { - ESP_LOGE(TAG, "Failed to read Y resolution!"); - this->interrupt_pin_->detach_interrupt(); - this->mark_failed(); - return; + if (this->y_raw_max_ == this->y_raw_min_) { + this->write(GET_Y_RES, 4); + if (this->read(received, 4)) { + ESP_LOGE(TAG, "Failed to read Y resolution!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); } - this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); - this->set_power_state(true); } diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h index 0b3a2c1b86..0a1e51227d 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -66,8 +66,14 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice return; } // reading the chip registers to get max x/y does not seem to work. - this->x_raw_max_ = this->display_->get_width(); - this->y_raw_max_ = this->display_->get_height(); + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->x_raw_max_ = this->display_->get_native_height(); + } + } esph_log_config(TAG, "FT5x06 Touchscreen setup complete"); } diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp index f796f0242a..fe64f76fac 100644 --- a/esphome/components/ft63x6/ft63x6.cpp +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -12,21 +12,23 @@ // Reference: https://focuslcds.com/content/FT6236.pdf namespace esphome { namespace ft63x6 { +static const uint8_t FT6X36_ADDR_DEVICE_MODE = 0x00; +static const uint8_t FT63X6_ADDR_TD_STATUS = 0x02; static const uint8_t FT63X6_ADDR_TOUCH1_STATE = 0x03; static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; +static const uint8_t FT63X6_ADDR_TOUCH1_WEIGHT = 0x07; +static const uint8_t FT63X6_ADDR_TOUCH1_MISC = 0x08; +static const uint8_t FT6X36_ADDR_THRESHHOLD = 0x80; +static const uint8_t FT6X36_ADDR_TOUCHRATE_ACTIVE = 0x88; +static const uint8_t FT63X6_ADDR_CHIP_ID = 0xA3; -static const uint8_t FT63X6_ADDR_TOUCH2_STATE = 0x09; -static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09; -static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; -static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B; - -static const char *const TAG = "FT63X6Touchscreen"; +static const char *const TAG = "FT63X6"; void FT63X6Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen..."); + ESP_LOGCONFIG(TAG, "Setting up FT63X6 Touchscreen..."); if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); @@ -35,10 +37,9 @@ void FT63X6Touchscreen::setup() { if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); + this->hard_reset_(); } - this->hard_reset_(); - // Get touch resolution if (this->x_raw_max_ == this->x_raw_min_) { this->x_raw_max_ = 320; @@ -46,6 +47,15 @@ void FT63X6Touchscreen::setup() { if (this->y_raw_max_ == this->y_raw_min_) { this->y_raw_max_ = 480; } + uint8_t chip_id = this->read_byte_(FT63X6_ADDR_CHIP_ID); + if (chip_id != 0) { + ESP_LOGI(TAG, "FT6336U touch driver started chipid: %d", chip_id); + } else { + ESP_LOGE(TAG, "FT6336U touch driver failed to start"); + } + this->write_byte(FT6X36_ADDR_DEVICE_MODE, 0x00); + this->write_byte(FT6X36_ADDR_THRESHHOLD, this->threshold_); + this->write_byte(FT6X36_ADDR_TOUCHRATE_ACTIVE, 0x0E); } void FT63X6Touchscreen::hard_reset_() { @@ -65,28 +75,61 @@ void FT63X6Touchscreen::dump_config() { } void FT63X6Touchscreen::update_touches() { - uint8_t data[15]; uint16_t touch_id, x, y; - if (!this->read_bytes(0x00, (uint8_t *) data, 15)) { - ESP_LOGE(TAG, "Failed to read touch data"); - this->skip_update_ = true; + uint8_t touches = this->read_touch_number_(); + if ((touches == 0x00) || (touches == 0xff)) { + // ESP_LOGD(TAG, "No touches detected"); return; } - if (((data[FT63X6_ADDR_TOUCH1_STATE] >> 6) & 0x01) == 0) { - touch_id = data[FT63X6_ADDR_TOUCH1_ID] >> 4; // id1 = 0 or 1 - x = encode_uint16(data[FT63X6_ADDR_TOUCH1_X] & 0x0F, data[FT63X6_ADDR_TOUCH1_X + 1]); - y = encode_uint16(data[FT63X6_ADDR_TOUCH1_Y] & 0x0F, data[FT63X6_ADDR_TOUCH1_Y + 1]); - this->add_raw_touch_position_(touch_id, x, y); - } - if (((data[FT63X6_ADDR_TOUCH2_STATE] >> 6) & 0x01) == 0) { - touch_id = data[FT63X6_ADDR_TOUCH2_ID] >> 4; // id1 = 0 or 1 - x = encode_uint16(data[FT63X6_ADDR_TOUCH2_X] & 0x0F, data[FT63X6_ADDR_TOUCH2_X + 1]); - y = encode_uint16(data[FT63X6_ADDR_TOUCH2_Y] & 0x0F, data[FT63X6_ADDR_TOUCH2_Y + 1]); - this->add_raw_touch_position_(touch_id, x, y); + ESP_LOGV(TAG, "Touches found: %d", touches); + + for (auto point = 0; point < touches; point++) { + if (((this->read_touch_event_(point)) & 0x01) == 0) { // checking event flag bit 6 if it is null + touch_id = this->read_touch_id_(point); // id1 = 0 or 1 + x = this->read_touch_x_(point); + y = this->read_touch_y_(point); + if ((x == 0) && (y == 0)) { + ESP_LOGW(TAG, "Reporting a (0,0) touch on %d", touch_id); + } + this->add_raw_touch_position_(touch_id, x, y, this->read_touch_weight_(point)); + } } } +uint8_t FT63X6Touchscreen::read_touch_number_() { return this->read_byte_(FT63X6_ADDR_TD_STATUS) & 0x0F; } +// Touch 1 functions +uint16_t FT63X6Touchscreen::read_touch_x_(uint8_t touch) { + uint8_t read_buf[2]; + read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6)); + read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + 1 + (touch * 6)); + return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; +} +uint16_t FT63X6Touchscreen::read_touch_y_(uint8_t touch) { + uint8_t read_buf[2]; + read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + (touch * 6)); + read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + 1 + (touch * 6)); + return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; +} +uint8_t FT63X6Touchscreen::read_touch_event_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6)) >> 6; +} +uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_ID + (touch * 6)) >> 4; +} +uint8_t FT63X6Touchscreen::read_touch_weight_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_WEIGHT + (touch * 6)); +} +uint8_t FT63X6Touchscreen::read_touch_misc_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_MISC + (touch * 6)) >> 4; +} + +uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { + uint8_t byte = 0; + this->read_byte(addr, &byte); + return byte; +} + } // namespace ft63x6 } // namespace esphome diff --git a/esphome/components/ft63x6/ft63x6.h b/esphome/components/ft63x6/ft63x6.h index 79b1991041..8000894294 100644 --- a/esphome/components/ft63x6/ft63x6.h +++ b/esphome/components/ft63x6/ft63x6.h @@ -16,6 +16,8 @@ namespace ft63x6 { using namespace touchscreen; +static const uint8_t FT6X36_DEFAULT_THRESHOLD = 22; + class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; @@ -23,18 +25,26 @@ class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + void set_threshold(uint8_t threshold) { this->threshold_ = threshold; } protected: void hard_reset_(); - uint8_t read_byte_(uint8_t addr); void update_touches() override; InternalGPIOPin *interrupt_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; + uint8_t threshold_{FT6X36_DEFAULT_THRESHOLD}; - uint8_t read_touch_count_(); - uint16_t read_touch_coordinate_(uint8_t coordinate); - uint8_t read_touch_id_(uint8_t id_address); + uint8_t read_touch_number_(); + + uint16_t read_touch_x_(uint8_t touch); + uint16_t read_touch_y_(uint8_t touch); + uint8_t read_touch_event_(uint8_t touch); + uint8_t read_touch_id_(uint8_t touch); + uint8_t read_touch_weight_(uint8_t touch); + uint8_t read_touch_misc_(uint8_t touch); + + uint8_t read_byte_(uint8_t addr); }; } // namespace ft63x6 diff --git a/esphome/components/ft63x6/touchscreen.py b/esphome/components/ft63x6/touchscreen.py index d77d9ca287..95fa371433 100644 --- a/esphome/components/ft63x6/touchscreen.py +++ b/esphome/components/ft63x6/touchscreen.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import i2c, touchscreen -from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN, CONF_THRESHOLD CODEOWNERS = ["@gpambrozio"] DEPENDENCIES = ["i2c"] @@ -26,6 +26,7 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( pins.internal_gpio_input_pin_schema ), cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_THRESHOLD): cv.uint8_t, } ).extend(i2c.i2c_device_schema(0x38)) ) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 68ed66a89f..99dba66c22 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -48,9 +48,13 @@ void GT911Touchscreen::setup() { if (err == i2c::ERROR_OK) { err = this->read(data, sizeof(data)); if (err == i2c::ERROR_OK) { - this->x_raw_max_ = encode_uint16(data[1], data[0]); - this->y_raw_max_ = encode_uint16(data[3], data[2]); - esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_); + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = encode_uint16(data[1], data[0]); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = encode_uint16(data[3], data[2]); + } + esph_log_d(TAG, "calibration max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_); } } } diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index 64cc7ad4d1..58f2a42812 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -38,9 +38,14 @@ void LilygoT547Touchscreen::setup() { } this->write_register(POWER_REGISTER, WAKEUP_CMD, 1); - - this->x_raw_max_ = this->get_width_(); - this->y_raw_max_ = this->get_height_(); + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->x_raw_max_ = this->display_->get_native_height(); + } + } } void LilygoT547Touchscreen::update_touches() { diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index c4945617f9..5417878b1c 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -3,14 +3,17 @@ import esphome.codegen as cg from esphome.components import display from esphome import automation + from esphome.const import ( CONF_ON_TOUCH, CONF_ON_RELEASE, + CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y, - CONF_SWAP_XY, CONF_TRANSFORM, + CONF_CALIBRATION, ) + from esphome.core import coroutine_with_priority CODEOWNERS = ["@jesserockz", "@nielsnl68"] @@ -34,6 +37,56 @@ CONF_ON_UPDATE = "on_update" CONF_TOUCH_TIMEOUT = "touch_timeout" +CONF_X_MIN = "x_min" +CONF_X_MAX = "x_max" +CONF_Y_MIN = "y_min" +CONF_Y_MAX = "y_max" + + +def validate_calibration(config): + if CONF_CALIBRATION in config: + calibration_config = config[CONF_CALIBRATION] + if ( + cv.int_([CONF_X_MIN]) != 0 + and cv.int_(calibration_config[CONF_X_MAX]) != 0 + and abs( + cv.int_(calibration_config[CONF_X_MIN]) + - cv.int_(calibration_config[CONF_X_MAX]) + ) + < 10 + ): + raise cv.Invalid("Calibration X values difference must be more than 10") + + if ( + cv.int_(calibration_config[CONF_Y_MIN]) != 0 + and cv.int_(calibration_config[CONF_Y_MAX]) != 0 + and abs( + cv.int_(calibration_config[CONF_Y_MIN]) + - cv.int_(calibration_config[CONF_Y_MAX]) + ) + < 10 + ): + raise cv.Invalid("Calibration Y values difference must be more than 10") + + return config + + +def calibration_schema(default_max_values): + return cv.Schema( + { + cv.Optional(CONF_X_MIN, default=0): cv.int_range(min=0, max=4095), + cv.Optional(CONF_X_MAX, default=default_max_values): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_Y_MIN, default=0): cv.int_range(min=0, max=4095), + cv.Optional(CONF_Y_MAX, default=default_max_values): cv.int_range( + min=0, max=4095 + ), + }, + validate_calibration, + ) + + def touchscreen_schema(default_touch_timeout): return cv.Schema( { @@ -49,6 +102,7 @@ def touchscreen_schema(default_touch_timeout): cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)), ), + cv.Optional(CONF_CALIBRATION): calibration_schema(0), cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), @@ -74,6 +128,17 @@ async def register_touchscreen(var, config): cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + if CONF_CALIBRATION in config: + calibration_config = config[CONF_CALIBRATION] + cg.add( + var.set_calibration( + calibration_config[CONF_X_MIN], + calibration_config[CONF_X_MAX], + calibration_config[CONF_Y_MIN], + calibration_config[CONF_Y_MAX], + ) + ) + if CONF_ON_TOUCH in config: await automation.build_automation( var.get_touch_trigger(), diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 18a4230197..83783a634f 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -13,6 +13,15 @@ void Touchscreen::attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::Int irq_pin->attach_interrupt(TouchscreenInterrupt::gpio_intr, &this->store_, type); this->store_.init = true; this->store_.touched = false; + ESP_LOGD(TAG, "Attach Touch Interupt"); +} + +void Touchscreen::call_setup() { + if (this->display_ != nullptr) { + this->display_width_ = this->display_->get_native_width(); + this->display_height_ = this->display_->get_native_height(); + } + PollingComponent::call_setup(); } void Touchscreen::update() { @@ -20,19 +29,22 @@ void Touchscreen::update() { this->store_.touched = true; } else { // no need to poll if we have interrupts. + ESP_LOGW(TAG, "Touch Polling Stopped. You can safely remove the 'update_interval:' variable from the YAML file."); this->stop_poller(); } } void Touchscreen::loop() { if (this->store_.touched) { + ESP_LOGVV(TAG, "<< Do Touch loop >>"); this->first_touch_ = this->touches_.empty(); this->need_update_ = false; + this->was_touched_ = this->is_touched_; this->is_touched_ = false; this->skip_update_ = false; for (auto &tp : this->touches_) { if (tp.second.state == STATE_PRESSED || tp.second.state == STATE_UPDATED) { - tp.second.state = tp.second.state | STATE_RELEASING; + tp.second.state |= STATE_RELEASING; } else { tp.second.state = STATE_RELEASED; } @@ -42,7 +54,7 @@ void Touchscreen::loop() { this->update_touches(); if (this->skip_update_) { for (auto &tp : this->touches_) { - tp.second.state = tp.second.state & -STATE_RELEASING; + tp.second.state &= ~STATE_RELEASING; } } else { this->store_.touched = false; @@ -65,21 +77,25 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r } else { tp = this->touches_[id]; tp.state = STATE_UPDATED; + tp.y_prev = tp.y; + tp.x_prev = tp.x; } tp.x_raw = x_raw; tp.y_raw = y_raw; tp.z_raw = z_raw; + if (this->x_raw_max_ != this->x_raw_min_ and this->y_raw_max_ != this->y_raw_min_) { + x = this->normalize_(x_raw, this->x_raw_min_, this->x_raw_max_, this->invert_x_); + y = this->normalize_(y_raw, this->y_raw_min_, this->y_raw_max_, this->invert_y_); - x = this->normalize_(x_raw, this->x_raw_min_, this->x_raw_max_, this->invert_x_); - y = this->normalize_(y_raw, this->y_raw_min_, this->y_raw_max_, this->invert_y_); + if (this->swap_x_y_) { + std::swap(x, y); + } - if (this->swap_x_y_) { - std::swap(x, y); + tp.x = (uint16_t) ((int) x * this->display_width_ / 0x1000); + tp.y = (uint16_t) ((int) y * this->display_height_ / 0x1000); + } else { + tp.state |= STATE_CALIBRATE; } - - tp.x = (uint16_t) ((int) x * this->get_width_() / 0x1000); - tp.y = (uint16_t) ((int) y * this->get_height_() / 0x1000); - if (tp.state == STATE_PRESSED) { tp.x_org = tp.x; tp.y_org = tp.y; @@ -94,19 +110,30 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r } void Touchscreen::send_touches_() { + TouchPoints_t touches; + for (auto tp : this->touches_) { + ESP_LOGV(TAG, "Touch status: %d/%d: raw:(%4d,%4d,%4d) calc:(%3d,%4d)", tp.second.id, tp.second.state, + tp.second.x_raw, tp.second.y_raw, tp.second.z_raw, tp.second.x, tp.second.y); + touches.push_back(tp.second); + } + if (this->need_update_ || (!this->is_touched_ && this->was_touched_)) { + this->update_trigger_.trigger(touches); + for (auto *listener : this->touch_listeners_) { + listener->update(touches); + } + } if (!this->is_touched_) { - if (this->touch_timeout_ > 0) { - this->cancel_timeout(TAG); + if (this->was_touched_) { + if (this->touch_timeout_ > 0) { + this->cancel_timeout(TAG); + } + this->release_trigger_.trigger(); + for (auto *listener : this->touch_listeners_) + listener->release(); + this->touches_.clear(); + this->was_touched_ = false; } - this->release_trigger_.trigger(); - for (auto *listener : this->touch_listeners_) - listener->release(); - this->touches_.clear(); } else { - TouchPoints_t touches; - for (auto tp : this->touches_) { - touches.push_back(tp.second); - } if (this->first_touch_) { TouchPoint tp = this->touches_.begin()->second; this->touch_trigger_.trigger(tp, touches); @@ -114,12 +141,6 @@ void Touchscreen::send_touches_() { listener->touch(tp); } } - if (this->need_update_) { - this->update_trigger_.trigger(touches); - for (auto *listener : this->touch_listeners_) { - listener->update(touches); - } - } } } diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 06aff68f07..21111f87b3 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -16,6 +16,7 @@ static const uint8_t STATE_RELEASED = 0x00; static const uint8_t STATE_PRESSED = 0x01; static const uint8_t STATE_UPDATED = 0x02; static const uint8_t STATE_RELEASING = 0x04; +static const uint8_t STATE_CALIBRATE = 0x07; struct TouchPoint { uint8_t id; @@ -68,8 +69,6 @@ class Touchscreen : public PollingComponent { void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } - virtual void update_touches() = 0; - optional get_touch() { return this->touches_.begin()->second; } TouchPoints_t get_touches() { @@ -82,6 +81,7 @@ class Touchscreen : public PollingComponent { void update() override; void loop() override; + void call_setup() override; protected: /// Call this function to send touch points to the `on_touch` listener and the binary_sensors. @@ -90,17 +90,17 @@ class Touchscreen : public PollingComponent { void add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); + virtual void update_touches() = 0; + void send_touches_(); int16_t normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted = false); - uint16_t get_width_() { return this->display_->get_width(); } - - uint16_t get_height_() { return this->display_->get_height(); } - display::Display *display_{nullptr}; int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0}; + int16_t display_width_{0}, display_height_{0}; + uint16_t touch_timeout_{0}; bool invert_x_{false}, invert_y_{false}, swap_x_y_{false}; @@ -115,6 +115,7 @@ class Touchscreen : public PollingComponent { bool first_touch_{true}; bool need_update_{false}; bool is_touched_{false}; + bool was_touched_{false}; bool skip_update_{false}; }; diff --git a/esphome/components/tt21100/touchscreen/__init__.py b/esphome/components/tt21100/touchscreen/__init__.py index 4458ad0974..510ca2df3a 100644 --- a/esphome/components/tt21100/touchscreen/__init__.py +++ b/esphome/components/tt21100/touchscreen/__init__.py @@ -20,7 +20,7 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Schema( { cv.GenerateID(): cv.declare_id(TT21100Touchscreen), - cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ).extend(i2c.i2c_device_schema(0x24)) @@ -32,8 +32,9 @@ async def to_code(config): await touchscreen.register_touchscreen(var, config) await i2c.register_i2c_device(var, config) - interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_interrupt_pin(interrupt_pin)) + if CONF_INTERRUPT_PIN in config: + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) if CONF_RESET_PIN in config: rts_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index ba4b0ee02d..2bea72a59e 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -50,10 +50,11 @@ void TT21100Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up TT21100 Touchscreen..."); // Register interrupt pin - this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - this->interrupt_pin_->setup(); - - this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } // Perform reset if necessary if (this->reset_pin_ != nullptr) { @@ -62,8 +63,14 @@ void TT21100Touchscreen::setup() { } // Update display dimensions if they were updated during display setup - this->x_raw_max_ = this->get_width_(); - this->y_raw_max_ = this->get_height_(); + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->x_raw_max_ = this->display_->get_native_height(); + } + } // Trigger initial read to activate the interrupt this->store_.touched = true; diff --git a/esphome/components/xpt2046/touchscreen/__init__.py b/esphome/components/xpt2046/touchscreen/__init__.py index 9f08f38c3f..d45f309a3b 100644 --- a/esphome/components/xpt2046/touchscreen/__init__.py +++ b/esphome/components/xpt2046/touchscreen/__init__.py @@ -15,35 +15,11 @@ XPT2046Component = XPT2046_ns.class_( spi.SPIDevice, ) - CONF_CALIBRATION_X_MIN = "calibration_x_min" CONF_CALIBRATION_X_MAX = "calibration_x_max" CONF_CALIBRATION_Y_MIN = "calibration_y_min" CONF_CALIBRATION_Y_MAX = "calibration_y_max" - -def validate_xpt2046(config): - if ( - abs( - cv.int_(config[CONF_CALIBRATION_X_MAX]) - - cv.int_(config[CONF_CALIBRATION_X_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration X values difference < 1000") - - if ( - abs( - cv.int_(config[CONF_CALIBRATION_Y_MAX]) - - cv.int_(config[CONF_CALIBRATION_Y_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration Y values difference < 1000") - - return config - - CONFIG_SCHEMA = cv.All( touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Schema( @@ -52,42 +28,41 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INTERRUPT_PIN): cv.All( pins.internal_gpio_input_pin_schema ), - cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), + cv.Optional( + touchscreen.CONF_CALIBRATION + ): touchscreen.calibration_schema(4095), + cv.Optional(CONF_CALIBRATION_X_MIN): cv.invalid( + "Deprecated: use the new 'calibration' configuration variable" + ), + cv.Optional(CONF_CALIBRATION_X_MAX): cv.invalid( + "Deprecated: use the new 'calibration' configuration variable" + ), + cv.Optional(CONF_CALIBRATION_Y_MIN): cv.invalid( + "Deprecated: use the new 'calibration' configuration variable" + ), + cv.Optional(CONF_CALIBRATION_Y_MAX): cv.invalid( + "Deprecated: use the new 'calibration' configuration variable" + ), + cv.Optional(CONF_CALIBRATION_Y_MAX): cv.invalid( + "Deprecated: use the new 'calibration' configuration variable" + ), + cv.Optional("report_interval"): cv.invalid( + "Deprecated: use the 'update_interval' configuration variable" + ), }, ) ).extend(spi.spi_device_schema()), - validate_xpt2046, ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await touchscreen.register_touchscreen(var, config) await spi.register_spi_device(var, config) + await touchscreen.register_touchscreen(var, config) cg.add(var.set_threshold(config[CONF_THRESHOLD])) - cg.add( - var.set_calibration( - config[CONF_CALIBRATION_X_MIN], - config[CONF_CALIBRATION_X_MAX], - config[CONF_CALIBRATION_Y_MIN], - config[CONF_CALIBRATION_Y_MAX], - ) - ) - if CONF_INTERRUPT_PIN in config: pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp index a268da06dd..a4e2b84656 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.cpp +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -32,9 +32,8 @@ void XPT2046Component::update_touches() { int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */); int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */); - ESP_LOGVV(TAG, "touch_pressure %d, %d", touch_pressure_1, touch_pressure_2); z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; - + ESP_LOGVV(TAG, "Touchscreen Update z = %d", z_raw); touch = (z_raw >= this->threshold_); if (touch) { read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy @@ -53,7 +52,7 @@ void XPT2046Component::update_touches() { x_raw = best_two_avg(data[1], data[3], data[5]); y_raw = best_two_avg(data[0], data[2], data[4]); - ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); + ESP_LOGD(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); this->add_raw_touch_position_(0, x_raw, y_raw, z_raw); } @@ -77,7 +76,7 @@ void XPT2046Component::dump_config() { LOG_UPDATE_INTERVAL(this); } -float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } +// float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) { int16_t delta_a, delta_b, delta_c; diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.h b/esphome/components/xpt2046/touchscreen/xpt2046.h index ff866bc86b..a635c08f82 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.h +++ b/esphome/components/xpt2046/touchscreen/xpt2046.h @@ -23,7 +23,7 @@ class XPT2046Component : public Touchscreen, void setup() override; void dump_config() override; - float get_setup_priority() const override; + // float get_setup_priority() const override; protected: static int16_t best_two_avg(int16_t value1, int16_t value2, int16_t value3); diff --git a/tests/components/ft63x6/test.esp32.yaml b/tests/components/ft63x6/test.esp32.yaml new file mode 100644 index 0000000000..32d6634dae --- /dev/null +++ b/tests/components/ft63x6/test.esp32.yaml @@ -0,0 +1,38 @@ +spi: + clk_pin: 14 + mosi_pin: 13 + +i2c: + sda: GPIO18 + scl: GPIO19 + +display: + - id: my_display + platform: ili9xxx + dimensions: 480x320 + model: ST7796 + cs_pin: 15 + dc_pin: 21 + reset_pin: 22 + transform: + swap_xy: true + mirror_x: true + mirror_y: true + auto_clear_enabled: false + +touchscreen: + - platform: ft63x6 + interrupt_pin: GPIO39 + transform: + swap_xy: true + mirror_x: false + mirror_y: true + on_touch: + - logger.log: + format: tp touched + on_update: + - logger.log: + format: to updated + on_release: + - logger.log: + format: to released diff --git a/tests/components/tt21100/test.esp32-s2.yaml b/tests/components/tt21100/test.esp32-s2.yaml new file mode 100644 index 0000000000..7ebabcb130 --- /dev/null +++ b/tests/components/tt21100/test.esp32-s2.yaml @@ -0,0 +1,43 @@ +i2c: + sda: GPIO8 + scl: GPIO18 + +spi: + clk_pin: 7 + mosi_pin: 11 + miso_pin: 9 + +display: + - platform: ili9xxx + id: my_display + model: ili9341 + cs_pin: 5 + dc_pin: 12 + reset_pin: 33 + auto_clear_enabled: false + data_rate: 40MHz + dimensions: 320x240 + update_interval: never + transform: + mirror_y: false + mirror_x: false + swap_xy: true + +touchscreen: + - platform: tt21100 + address: 0x24 + interrupt_pin: GPIO3 + on_touch: + - logger.log: "Touchscreen:: Touched" + +binary_sensor: + - platform: tt21100 + index: 0 + name: "Home" + + - platform: touchscreen + name: FanLo + x_min: 0 + x_max: 105 + y_min: 0 + y_max: 80 diff --git a/tests/components/xpt2046/test.esp32-s2.yaml b/tests/components/xpt2046/test.esp32-s2.yaml new file mode 100644 index 0000000000..6232ca957b --- /dev/null +++ b/tests/components/xpt2046/test.esp32-s2.yaml @@ -0,0 +1,37 @@ +spi: + clk_pin: 7 + mosi_pin: 11 + miso_pin: 9 + +display: + - platform: ili9xxx + id: my_display + model: ili9341 + cs_pin: 5 + dc_pin: 12 + reset_pin: 33 + auto_clear_enabled: false + data_rate: 40MHz + dimensions: 320x240 + update_interval: never + transform: + mirror_y: false + mirror_x: false + swap_xy: true + +touchscreen: + - platform: xpt2046 + display: my_display + id: my_toucher + update_interval: 50ms + cs_pin: 18 + threshold: 300 + calibration: + x_min: 210 + x_max: 3890 + y_min: 170 + y_max: 3730 + transform: + mirror_x: false + mirror_y: true + swap_xy: true diff --git a/tests/test4.yaml b/tests/test4.yaml index e46102e88a..7cda05381f 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -976,10 +976,11 @@ touchscreen: display: inkplate_display update_interval: 50ms threshold: 400 - calibration_x_min: 3860 - calibration_x_max: 280 - calibration_y_min: 340 - calibration_y_max: 3860 + calibration: + x_min: 3860 + x_max: 280 + y_min: 340 + y_max: 3860 on_touch: - logger.log: format: Touch at (%d, %d) From 3c651f409185eb3bfdd5a69b81d1b307aa9df3fa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 28 Feb 2024 21:01:56 +1300 Subject: [PATCH 387/436] Add `on_update` trigger for Project versions (#6298) --- esphome/components/touchscreen/__init__.py | 2 +- esphome/const.py | 1 + esphome/core/base_automation.h | 23 ++++++++++++++++++++++ esphome/core/config.py | 23 +++++++++++++++++++--- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 5417878b1c..400ba7d5ad 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -7,6 +7,7 @@ from esphome import automation from esphome.const import ( CONF_ON_TOUCH, CONF_ON_RELEASE, + CONF_ON_UPDATE, CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y, @@ -33,7 +34,6 @@ TouchListener = touchscreen_ns.class_("TouchListener") CONF_DISPLAY = "display" CONF_TOUCHSCREEN_ID = "touchscreen_id" CONF_REPORT_INTERVAL = "report_interval" # not used yet: -CONF_ON_UPDATE = "on_update" CONF_TOUCH_TIMEOUT = "touch_timeout" diff --git a/esphome/const.py b/esphome/const.py index 044465de7b..ed0fdf4fa9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -537,6 +537,7 @@ CONF_ON_TOUCH = "on_touch" CONF_ON_TURN_OFF = "on_turn_off" CONF_ON_TURN_ON = "on_turn_on" CONF_ON_UNLOCK = "on_unlock" +CONF_ON_UPDATE = "on_update" CONF_ON_VALUE = "on_value" CONF_ON_VALUE_RANGE = "on_value_range" CONF_ONE = "one" diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 50087f3efd..b0ae0aff84 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -2,6 +2,8 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" #include @@ -125,6 +127,27 @@ class LoopTrigger : public Trigger<>, public Component { float get_setup_priority() const override { return setup_priority::DATA; } }; +#ifdef ESPHOME_PROJECT_NAME +class ProjectUpdateTrigger : public Trigger, public Component { + public: + void setup() override { + uint32_t hash = fnv1_hash(ESPHOME_PROJECT_NAME); + ESPPreferenceObject pref = global_preferences->make_preference(hash, true); + char previous_version[30]; + char current_version[30] = ESPHOME_PROJECT_VERSION; + if (pref.load(&previous_version)) { + int cmp = strcmp(previous_version, current_version); + if (cmp < 0) { + this->trigger(previous_version); + } + } + pref.save(¤t_version); + global_preferences->sync(); + } + float get_setup_priority() const override { return setup_priority::PROCESSOR; } +}; +#endif + template class DelayAction : public Action, public Component { public: explicit DelayAction() = default; diff --git a/esphome/core/config.py b/esphome/core/config.py index 7449d8850b..792f9da6dd 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, + CONF_ON_UPDATE, CONF_PLATFORM, CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, @@ -52,6 +53,9 @@ ShutdownTrigger = cg.esphome_ns.class_( LoopTrigger = cg.esphome_ns.class_( "LoopTrigger", cg.Component, automation.Trigger.template() ) +ProjectUpdateTrigger = cg.esphome_ns.class_( + "ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string) +) VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$") @@ -151,6 +155,13 @@ CONFIG_SCHEMA = cv.All( cv.string_strict, valid_project_name ), cv.Required(CONF_VERSION): cv.string_strict, + cv.Optional(CONF_ON_UPDATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ProjectUpdateTrigger + ), + } + ), } ), cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( @@ -380,9 +391,15 @@ async def to_code(config): if config[CONF_INCLUDES]: CORE.add_job(add_includes, config[CONF_INCLUDES]) - if CONF_PROJECT in config: - cg.add_define("ESPHOME_PROJECT_NAME", config[CONF_PROJECT][CONF_NAME]) - cg.add_define("ESPHOME_PROJECT_VERSION", config[CONF_PROJECT][CONF_VERSION]) + if project_conf := config.get(CONF_PROJECT): + cg.add_define("ESPHOME_PROJECT_NAME", project_conf[CONF_NAME]) + cg.add_define("ESPHOME_PROJECT_VERSION", project_conf[CONF_VERSION]) + for conf in project_conf.get(CONF_ON_UPDATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + await cg.register_component(trigger, conf) + await automation.build_automation( + trigger, [(cg.std_string, "version")], conf + ) if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) From ad7866b80ed038fff597d7bd7bdfb052f9a9ba81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:55:30 +1300 Subject: [PATCH 388/436] Bump peter-evans/create-pull-request from 6.0.0 to 6.0.1 (#6302) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index efa1aefae5..ef3c04b595 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -37,7 +37,7 @@ jobs: python ./script/sync-device_class.py - name: Commit changes - uses: peter-evans/create-pull-request@v6.0.0 + uses: peter-evans/create-pull-request@v6.0.1 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot From f53f91e191e110c22aaa16c6f94f75d3d5ab6da7 Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Wed, 28 Feb 2024 14:12:02 -0700 Subject: [PATCH 389/436] CSE7766 Apparent Power & Power Factor calculations (#6292) --- esphome/components/cse7766/cse7766.cpp | 28 ++++++++++++++++++++++++++ esphome/components/cse7766/cse7766.h | 6 ++++++ esphome/components/cse7766/sensor.py | 24 +++++++++++++++++++++- tests/test3.yaml | 6 +++++- 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index cdd4220e50..f1420aa127 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -173,6 +173,32 @@ void CSE7766Component::parse_data_() { } } + if (have_voltage && have_current) { + const float apparent_power = voltage * current; + if (this->apparent_power_sensor_ != nullptr) { + this->apparent_power_sensor_->publish_state(apparent_power); + } + if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { + float pf = NAN; + if (apparent_power > 0) { + pf = power / apparent_power; + if (pf < 0 || pf > 1) { + ESP_LOGD(TAG, "Impossible power factor: %.4f not in interval [0, 1]", pf); + pf = NAN; + } + } else if (apparent_power == 0 && power == 0) { + // No load, report ideal power factor + pf = 1.0f; + } else if (current == 0 && calculated_current <= 0.05f) { + // Datasheet: minimum measured current is 50mA + ESP_LOGV(TAG, "Can't calculate power factor (current below minimum for CSE7766)"); + } else { + ESP_LOGW(TAG, "Can't calculate power factor from P = %.4f W, S = %.4f VA", power, apparent_power); + } + this->power_factor_sensor_->publish_state(pf); + } + } + #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { std::stringstream ss; @@ -205,6 +231,8 @@ void CSE7766Component::dump_config() { LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Energy", this->energy_sensor_); + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); this->check_uart_settings(4800); } diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 4d9ba7be74..0b724d6bbb 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -13,6 +13,10 @@ class CSE7766Component : public Component, public uart::UARTDevice { void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) { + apparent_power_sensor_ = apparent_power_sensor; + } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } void loop() override; float get_setup_priority() const override; @@ -30,6 +34,8 @@ class CSE7766Component : public Component, public uart::UARTDevice { sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; + sensor::Sensor *apparent_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; uint32_t cf_pulses_total_{0}; uint16_t cf_pulses_last_{0}; }; diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index f2750bb4f2..b64dcf7de3 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -2,19 +2,24 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import ( + CONF_APPARENT_POWER, CONF_CURRENT, CONF_ENERGY, CONF_ID, CONF_POWER, + CONF_POWER_FACTOR, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, - UNIT_VOLT, UNIT_AMPERE, + UNIT_VOLT, + UNIT_VOLT_AMPS, UNIT_WATT, UNIT_WATT_HOURS, ) @@ -51,6 +56,17 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), } ).extend(uart.UART_DEVICE_SCHEMA) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( @@ -75,3 +91,9 @@ async def to_code(config): if energy_config := config.get(CONF_ENERGY): sens = await sensor.new_sensor(energy_config) cg.add(var.set_energy_sensor(sens)) + if apparent_power_config := config.get(CONF_APPARENT_POWER): + sens = await sensor.new_sensor(apparent_power_config) + cg.add(var.set_apparent_power_sensor(sens)) + if power_factor_config := config.get(CONF_POWER_FACTOR): + sens = await sensor.new_sensor(power_factor_config) + cg.add(var.set_power_factor_sensor(sens)) diff --git a/tests/test3.yaml b/tests/test3.yaml index 68b9a544e1..ec71bdca8b 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -634,7 +634,11 @@ sensor: current: name: CSE7766 Current power: - name: CSE776 Power + name: CSE7766 Power + apparent_power: + name: CSE7766 Apparent Power + power_factor: + name: CSE7766 Power Factor - platform: fingerprint_grow fingerprint_count: From 4a9d7771fe667ae5163b2f806f6ac4f5304cb4de Mon Sep 17 00:00:00 2001 From: Jeroen van Oort Date: Fri, 1 Mar 2024 01:46:08 +0100 Subject: [PATCH 390/436] Adding W5500 support to ethernet component (#4424) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ethernet/__init__.py | 128 +++++++++++++++--- .../ethernet/ethernet_component.cpp | 114 +++++++++++++++- .../components/ethernet/ethernet_component.h | 26 +++- esphome/const.py | 1 + .../ethernet/lan8720.esp32-idf.yaml | 12 ++ tests/components/ethernet/lan8720.esp32.yaml | 12 ++ .../components/ethernet/w5500.esp32-idf.yaml | 14 ++ tests/components/ethernet/w5500.esp32.yaml | 14 ++ 8 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 tests/components/ethernet/lan8720.esp32-idf.yaml create mode 100644 tests/components/ethernet/lan8720.esp32.yaml create mode 100644 tests/components/ethernet/w5500.esp32-idf.yaml create mode 100644 tests/components/ethernet/w5500.esp32.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 6f0f3741dd..de6040339a 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,6 +1,13 @@ 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 ( + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) from esphome.const import ( CONF_DOMAIN, CONF_ID, @@ -12,9 +19,17 @@ from esphome.const import ( CONF_SUBNET, CONF_DNS1, CONF_DNS2, + CONF_CLK_PIN, + CONF_MISO_PIN, + CONF_MOSI_PIN, + CONF_CS_PIN, + CONF_INTERRUPT_PIN, + CONF_RESET_PIN, + CONF_SPI, ) 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 CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] @@ -27,6 +42,8 @@ CONF_MDIO_PIN = "mdio_pin" CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" +CONF_CLOCK_SPEED = "clock_speed" + EthernetType = ethernet_ns.enum("EthernetType") ETHERNET_TYPES = { "LAN8720": EthernetType.ETHERNET_TYPE_LAN8720, @@ -36,8 +53,11 @@ ETHERNET_TYPES = { "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, + "W5500": EthernetType.ETHERNET_TYPE_W5500, } +SPI_ETHERNET_TYPES = ["W5500"] + emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") CLK_MODES = { @@ -84,11 +104,22 @@ def _validate(config): return config -CONFIG_SCHEMA = cv.All( +BASE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(EthernetComponent), + cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, + cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, + cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional("enable_mdns"): cv.invalid( + "This option has been removed. Please use the [disabled] option under the " + "new mdns component instead." + ), + } +).extend(cv.COMPONENT_SCHEMA) + +RMII_SCHEMA = BASE_SCHEMA.extend( cv.Schema( { - cv.GenerateID(): cv.declare_id(EthernetComponent), - cv.Required(CONF_TYPE): cv.enum(ETHERNET_TYPES, upper=True), cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_CLK_MODE, default="GPIO0_IN"): cv.enum( @@ -96,19 +127,64 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, - cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, - cv.Optional(CONF_USE_ADDRESS): cv.string_strict, - cv.Optional("enable_mdns"): cv.invalid( - "This option has been removed. Please use the [disabled] option under the " - "new mdns component instead." + } + ) +) + +SPI_SCHEMA = BASE_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( + cv.frequency, cv.int_range(int(8e6), int(80e6)) ), } - ).extend(cv.COMPONENT_SCHEMA), + ), +) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "LAN8720": RMII_SCHEMA, + "RTL8201": RMII_SCHEMA, + "DP83848": RMII_SCHEMA, + "IP101": RMII_SCHEMA, + "JL1101": RMII_SCHEMA, + "W5500": SPI_SCHEMA, + }, + upper=True, + ), _validate, ) +def _final_validate(config): + if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: + return + if spi_configs := fv.full_config.get().get(CONF_SPI): + variant = get_esp32_variant() + if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3): + spi_host = "SPI2_HOST" + else: + spi_host = "SPI3_HOST" + for spi_conf in spi_configs: + if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: + interface = get_spi_interface(index) + if interface == spi_host: + raise cv.Invalid( + f"`spi` component is using interface '{interface}'. " + f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.", + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + def manual_ip(config): return cg.StructInitializer( ManualIP, @@ -125,15 +201,31 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) - cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) - cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) - cg.add(var.set_type(config[CONF_TYPE])) - cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) - cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + if config[CONF_TYPE] == "W5500": + cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) + cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) + cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) + cg.add(var.set_cs_pin(config[CONF_CS_PIN])) + if CONF_INTERRUPT_PIN in config: + cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN])) + if CONF_RESET_PIN in config: + cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) + cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) - if CONF_POWER_PIN in config: - cg.add(var.set_power_pin(config[CONF_POWER_PIN])) + cg.add_define("USE_ETHERNET_SPI") + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) + add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True) + else: + cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) + cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) + cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) + cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) + if CONF_POWER_PIN in config: + cg.add(var.set_power_pin(config[CONF_POWER_PIN])) + + cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) + cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 8071e9c97a..3c61fbe0a6 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -9,6 +9,11 @@ #include #include "esp_event.h" +#ifdef USE_ETHERNET_SPI +#include +#include +#endif + namespace esphome { namespace ethernet { @@ -33,6 +38,36 @@ void EthernetComponent::setup() { } esp_err_t err; + +#ifdef USE_ETHERNET_SPI + // Install GPIO ISR handler to be able to service SPI Eth modules interrupts + gpio_install_isr_service(0); + + spi_bus_config_t buscfg = { + .mosi_io_num = this->mosi_pin_, + .miso_io_num = this->miso_pin_, + .sclk_io_num = this->clk_pin_, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + .max_transfer_sz = 0, + .flags = 0, + .intr_flags = 0, + }; + +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + auto host = SPI2_HOST; +#else + auto host = SPI3_HOST; +#endif + + err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); + ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); +#endif + err = esp_netif_init(); ESPHL_ERROR_CHECK(err, "ETH netif init error"); err = esp_event_loop_create_default(); @@ -43,10 +78,40 @@ void EthernetComponent::setup() { // Init MAC and PHY configs to default eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + +#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module + spi_device_interface_config_t devcfg = { + .command_bits = 16, // Actually it's the address phase in W5500 SPI frame + .address_bits = 8, // Actually it's the control phase in W5500 SPI frame + .dummy_bits = 0, + .mode = 0, + .duty_cycle_pos = 0, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .clock_speed_hz = this->clock_speed_, + .input_delay_ns = 0, + .spics_io_num = this->cs_pin_, + .flags = 0, + .queue_size = 20, + .pre_cb = nullptr, + .post_cb = nullptr, + }; + + spi_device_handle_t spi_handle = nullptr; + err = spi_bus_add_device(host, &devcfg, &spi_handle); + ESPHL_ERROR_CHECK(err, "SPI bus add device error"); + + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); + w5500_config.int_gpio_num = this->interrupt_pin_; + phy_config.phy_addr = this->phy_addr_spi_; + phy_config.reset_gpio_num = this->reset_pin_; + + esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); +#else phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; - eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); #if ESP_IDF_VERSION_MAJOR >= 5 eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; @@ -62,9 +127,11 @@ void EthernetComponent::setup() { mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); +#endif #endif switch (this->type_) { +#if CONFIG_ETH_USE_ESP32_EMAC case ETHERNET_TYPE_LAN8720: { this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); break; @@ -94,6 +161,13 @@ void EthernetComponent::setup() { #endif break; } +#endif +#ifdef USE_ETHERNET_SPI + case ETHERNET_TYPE_W5500: { + this->phy_ = esp_eth_phy_new_w5500(&phy_config); + break; + } +#endif default: { this->mark_failed(); return; @@ -105,10 +179,18 @@ void EthernetComponent::setup() { err = esp_eth_driver_install(ð_config, &this->eth_handle_); ESPHL_ERROR_CHECK(err, "ETH driver install error"); +#ifndef USE_ETHERNET_SPI if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. this->ksz8081_set_clock_reference_(mac); } +#endif + + // use ESP internal eth mac + uint8_t mac_addr[6]; + esp_read_mac(mac_addr, ESP_MAC_ETH); + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); + ESPHL_ERROR_CHECK(err, "set mac address error"); /* attach Ethernet driver to TCP/IP stack */ err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); @@ -200,6 +282,10 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081RNA"; break; + case ETHERNET_TYPE_W5500: + eth_type = "W5500"; + break; + default: eth_type = "Unknown"; break; @@ -207,13 +293,23 @@ void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, "Ethernet:"); this->dump_connect_params_(); +#ifdef USE_ETHERNET_SPI + ESP_LOGCONFIG(TAG, " CLK Pin: %u", this->clk_pin_); + ESP_LOGCONFIG(TAG, " MISO Pin: %u", this->miso_pin_); + ESP_LOGCONFIG(TAG, " MOSI Pin: %u", this->mosi_pin_); + ESP_LOGCONFIG(TAG, " CS Pin: %u", this->cs_pin_); + ESP_LOGCONFIG(TAG, " IRQ Pin: %u", this->interrupt_pin_); + ESP_LOGCONFIG(TAG, " Reset Pin: %d", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Clock Speed: %d MHz", this->clock_speed_ / 1000000); +#else if (this->power_pin_ != -1) { ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); } ESP_LOGCONFIG(TAG, " MDC Pin: %u", this->mdc_pin_); ESP_LOGCONFIG(TAG, " MDIO Pin: %u", this->mdio_pin_); - ESP_LOGCONFIG(TAG, " Type: %s", eth_type); ESP_LOGCONFIG(TAG, " PHY addr: %u", this->phy_addr_); +#endif + ESP_LOGCONFIG(TAG, " Type: %s", eth_type); } float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } @@ -407,15 +503,25 @@ void EthernetComponent::dump_connect_params_() { ESP_LOGCONFIG(TAG, " Link Speed: %u", speed == ETH_SPEED_100M ? 100 : 10); } +#ifdef USE_ETHERNET_SPI +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } +void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } +void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } +void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } +void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } +void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } +#else void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) { this->clk_mode_ = clk_mode; this->clk_gpio_ = clk_gpio; } +#endif +void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } std::string EthernetComponent::get_use_address() const { @@ -442,6 +548,7 @@ bool EthernetComponent::powerdown() { return true; } +#ifndef USE_ETHERNET_SPI void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { #define KSZ80XX_PC2R_REG_ADDR (0x1F) @@ -472,6 +579,7 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { #undef KSZ80XX_PC2R_REG_ADDR } +#endif } // namespace ethernet } // namespace esphome diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index bfd0944af7..621ac87c10 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -23,6 +23,7 @@ enum EthernetType { ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, ETHERNET_TYPE_KSZ8081RNA, + ETHERNET_TYPE_W5500, }; struct ManualIP { @@ -50,12 +51,22 @@ class EthernetComponent : public Component { void on_shutdown() override { powerdown(); } bool is_connected(); +#ifdef USE_ETHERNET_SPI + void set_clk_pin(uint8_t clk_pin); + void set_miso_pin(uint8_t miso_pin); + void set_mosi_pin(uint8_t mosi_pin); + void set_cs_pin(uint8_t cs_pin); + void set_interrupt_pin(uint8_t interrupt_pin); + void set_reset_pin(uint8_t reset_pin); + void set_clock_speed(int clock_speed); +#else void set_phy_addr(uint8_t phy_addr); void set_power_pin(int power_pin); void set_mdc_pin(uint8_t mdc_pin); void set_mdio_pin(uint8_t mdio_pin); - void set_type(EthernetType type); void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); +#endif + void set_type(EthernetType type); void set_manual_ip(const ManualIP &manual_ip); network::IPAddresses get_ip_addresses(); @@ -76,13 +87,24 @@ class EthernetComponent : public Component { void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); std::string use_address_; +#ifdef USE_ETHERNET_SPI + uint8_t clk_pin_; + uint8_t miso_pin_; + uint8_t mosi_pin_; + uint8_t cs_pin_; + uint8_t interrupt_pin_; + int reset_pin_{-1}; + int phy_addr_spi_{-1}; + int clock_speed_; +#else uint8_t phy_addr_{0}; int power_pin_{-1}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; - EthernetType type_{ETHERNET_TYPE_UNKNOWN}; emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; +#endif + EthernetType type_{ETHERNET_TYPE_UNKNOWN}; optional manual_ip_{}; bool started_{false}; diff --git a/esphome/const.py b/esphome/const.py index ed0fdf4fa9..33bf2351b0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -732,6 +732,7 @@ CONF_SPEED_COUNT = "speed_count" CONF_SPEED_LEVEL_COMMAND_TOPIC = "speed_level_command_topic" CONF_SPEED_LEVEL_STATE_TOPIC = "speed_level_state_topic" CONF_SPEED_STATE_TOPIC = "speed_state_topic" +CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" diff --git a/tests/components/ethernet/lan8720.esp32-idf.yaml b/tests/components/ethernet/lan8720.esp32-idf.yaml new file mode 100644 index 0000000000..b9ed9cb036 --- /dev/null +++ b/tests/components/ethernet/lan8720.esp32-idf.yaml @@ -0,0 +1,12 @@ +ethernet: + type: LAN8720 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/lan8720.esp32.yaml b/tests/components/ethernet/lan8720.esp32.yaml new file mode 100644 index 0000000000..b9ed9cb036 --- /dev/null +++ b/tests/components/ethernet/lan8720.esp32.yaml @@ -0,0 +1,12 @@ +ethernet: + type: LAN8720 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/w5500.esp32-idf.yaml b/tests/components/ethernet/w5500.esp32-idf.yaml new file mode 100644 index 0000000000..6fdccb36e3 --- /dev/null +++ b/tests/components/ethernet/w5500.esp32-idf.yaml @@ -0,0 +1,14 @@ +ethernet: + type: W5500 + clk_pin: GPIO19 + mosi_pin: GPIO21 + miso_pin: GPIO23 + cs_pin: GPIO18 + interrupt_pin: GPIO36 + reset_pin: GPIO22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/w5500.esp32.yaml b/tests/components/ethernet/w5500.esp32.yaml new file mode 100644 index 0000000000..6fdccb36e3 --- /dev/null +++ b/tests/components/ethernet/w5500.esp32.yaml @@ -0,0 +1,14 @@ +ethernet: + type: W5500 + clk_pin: GPIO19 + mosi_pin: GPIO21 + miso_pin: GPIO23 + cs_pin: GPIO18 + interrupt_pin: GPIO36 + reset_pin: GPIO22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local From cf7cc179fb9c9e55ff2c87796031ecab84c73353 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Fri, 1 Mar 2024 02:02:33 +0100 Subject: [PATCH 391/436] Fix numbering of sensors (#6305) --- esphome/components/ethernet_info/ethernet_info_text_sensor.h | 2 +- esphome/components/wifi_info/wifi_info_text_sensor.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 62c5781f66..b5764d2519 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -21,8 +21,8 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS if (ip.is_set()) { if (this->ip_sensors_[sensor] != nullptr) { this->ip_sensors_[sensor]->publish_state(ip.str()); - sensor++; } + sensor++; } } } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 9ffbd3f418..6f189da3a3 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -20,8 +20,8 @@ class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSenso if (ip.is_set()) { if (this->ip_sensors_[sensor] != nullptr) { this->ip_sensors_[sensor]->publish_state(ip.str()); - sensor++; } + sensor++; } } } From 082e76131b64662c90f5639215f982c3f5eb4aad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:47:18 +1300 Subject: [PATCH 392/436] Bump aioesphomeapi from 22.0.0 to 23.0.0 (#6293) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2def1f4edc..c6d1fab2c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.13 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==22.0.0 +aioesphomeapi==23.0.0 zeroconf==0.131.0 python-magic==0.4.27 From 4aeb8e80816decaf91373534cff2543295eea26e Mon Sep 17 00:00:00 2001 From: mathieu-mp Date: Fri, 1 Mar 2024 04:49:26 +0100 Subject: [PATCH 393/436] Add regular polygon shapes to display component (#6108) --- esphome/components/display/display.cpp | 61 ++++++++++++++++++++++ esphome/components/display/display.h | 72 ++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index e531c5cf5c..4c764da02a 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -257,6 +257,67 @@ void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Co this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color); } } +void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, + int radius, int edges, RegularPolygonVariation variation, + float rotation_degrees) { + if (edges >= 2) { + // Given the orientation of the display component, an angle is measured clockwise from the x axis. + // For a regular polygon, the human reference would be the top of the polygon, + // hence we rotate the shape by 270° to orient the polygon up. + rotation_degrees += ROTATION_270_DEGREES; + // Convert the rotation to radians, easier to use in trigonometrical calculations + float rotation_radians = rotation_degrees * PI / 180; + // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no + // additional rotation of the shape. + // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, + // this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the + // left side of the first horizontal edge. + rotation_radians -= (variation == VARIATION_FLAT_TOP) ? PI / edges : 0.0; + + float vertex_angle = ((float) vertex_id) / edges * 2 * PI + rotation_radians; + *vertex_x = (int) round(cos(vertex_angle) * radius) + center_x; + *vertex_y = (int) round(sin(vertex_angle) * radius) + center_y; + } +} + +void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, + float rotation_degrees, Color color, RegularPolygonDrawing drawing) { + if (edges >= 2) { + int previous_vertex_x, previous_vertex_y; + for (int current_vertex_id = 0; current_vertex_id <= edges; current_vertex_id++) { + int current_vertex_x, current_vertex_y; + get_regular_polygon_vertex(current_vertex_id, ¤t_vertex_x, ¤t_vertex_y, x, y, radius, edges, + variation, rotation_degrees); + if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated + if (drawing == DRAWING_FILLED) { + this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color); + } else if (drawing == DRAWING_OUTLINE) { + this->line(previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color); + } + } + previous_vertex_x = current_vertex_x; + previous_vertex_y = current_vertex_y; + } + } +} +void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color, + RegularPolygonDrawing drawing) { + regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing); +} +void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) { + regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing); +} +void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, + float rotation_degrees, Color color) { + regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED); +} +void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, + Color color) { + regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED); +} +void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) { + regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); +} void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) { int x_start, y_start; diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 8880a2a21c..f67471a02d 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -137,6 +137,42 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; +#define PI 3.1415926535897932384626433832795 + +const int EDGES_TRIGON = 3; +const int EDGES_TRIANGLE = 3; +const int EDGES_TETRAGON = 4; +const int EDGES_QUADRILATERAL = 4; +const int EDGES_PENTAGON = 5; +const int EDGES_HEXAGON = 6; +const int EDGES_HEPTAGON = 7; +const int EDGES_OCTAGON = 8; +const int EDGES_NONAGON = 9; +const int EDGES_ENNEAGON = 9; +const int EDGES_DECAGON = 10; +const int EDGES_HENDECAGON = 11; +const int EDGES_DODECAGON = 12; +const int EDGES_TRIDECAGON = 13; +const int EDGES_TETRADECAGON = 14; +const int EDGES_PENTADECAGON = 15; +const int EDGES_HEXADECAGON = 16; + +const float ROTATION_0_DEGREES = 0.0; +const float ROTATION_45_DEGREES = 45.0; +const float ROTATION_90_DEGREES = 90.0; +const float ROTATION_180_DEGREES = 180.0; +const float ROTATION_270_DEGREES = 270.0; + +enum RegularPolygonVariation { + VARIATION_POINTY_TOP = 0, + VARIATION_FLAT_TOP = 1, +}; + +enum RegularPolygonDrawing { + DRAWING_OUTLINE = 0, + DRAWING_FILLED = 1, +}; + class Display; class DisplayPage; class DisplayOnPageChangeTrigger; @@ -247,6 +283,42 @@ class Display : public PollingComponent { /// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color. void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON); + /// Get the specified vertex (x,y) coordinates for the regular polygon inscribed in the circle centered on + /// [center_x,center_y] with the given radius. Vertex id are 0-indexed and rotate clockwise. In a pointy-topped + /// variation of a polygon with a 0° rotation, the vertex #0 is located at the top of the polygon. In a flat-topped + /// variation of a polygon with a 0° rotation, the vertex #0 is located on the left-side of the horizontal top + /// edge, and the vertex #1 is located on the right-side of the horizontal top edge. + /// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon. + /// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon. + /// Use the rotation in degrees to rotate the shape clockwise. + void get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, int radius, + int edges, RegularPolygonVariation variation = VARIATION_POINTY_TOP, + float rotation_degrees = ROTATION_0_DEGREES); + + /// Draw the outline of a regular polygon inscribed in the circle centered on [x,y] with the given + /// radius and color. + /// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon. + /// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon. + /// Use the rotation in degrees to rotate the shape clockwise. + /// Use the drawing to switch between outlining or filling the polygon. + void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation = VARIATION_POINTY_TOP, + float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON, + RegularPolygonDrawing drawing = DRAWING_OUTLINE); + void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color, + RegularPolygonDrawing drawing = DRAWING_OUTLINE); + void regular_polygon(int x, int y, int radius, int edges, Color color, + RegularPolygonDrawing drawing = DRAWING_OUTLINE); + + /// Fill a regular polygon inscribed in the circle centered on [x,y] with the given radius and color. + /// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon. + /// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon. + /// Use the rotation in degrees to rotate the shape clockwise. + void filled_regular_polygon(int x, int y, int radius, int edges, + RegularPolygonVariation variation = VARIATION_POINTY_TOP, + float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON); + void filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color); + void filled_regular_polygon(int x, int y, int radius, int edges, Color color); + /** Print `text` with the anchor point at [x,y] with `font`. * * @param x The x coordinate of the text alignment anchor point. From 11cae0376939e70150462fa6780c87d34305e931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E9=87=8ESKY?= <87404327+FlyingFeng2021@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:53:12 +0800 Subject: [PATCH 394/436] Fix return value in `core/automation.h` (#6314) --- esphome/core/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 84c754e081..8c3da5e7e8 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -218,7 +218,7 @@ template class ActionList { /// Return the number of actions in this action list that are currently running. int num_running() { if (this->actions_begin_ == nullptr) - return false; + return 0; return this->actions_begin_->num_running_total(); } From 0298adb1d836d195c8080073b81ec2b327bfdec1 Mon Sep 17 00:00:00 2001 From: CptSkippy Date: Sun, 3 Mar 2024 12:00:18 -0800 Subject: [PATCH 395/436] aht10: Added new CMD and renamed existing CMD to match datasheet (#6303) --- esphome/components/aht10/aht10.cpp | 39 ++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 4d69a67487..57102e6d27 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -21,30 +21,39 @@ namespace esphome { namespace aht10 { static const char *const TAG = "aht10"; -static const size_t SIZE_CALIBRATE_CMD = 3; -static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00}; -static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00}; +static const uint8_t AHT10_INITIALIZE_CMD[] = {0xE1, 0x08, 0x00}; +static const uint8_t AHT20_INITIALIZE_CMD[] = {0xBE, 0x08, 0x00}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; -static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement -static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms -static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms -static const uint8_t AHT10_CAL_ATTEMPTS = 10; +static const uint8_t AHT10_SOFTRESET_CMD[] = {0xBA}; + +static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for initialization and temperature measurement +static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms +static const uint8_t AHT10_SOFTRESET_DELAY = 30; // ms + +static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms +static const uint8_t AHT10_INIT_ATTEMPTS = 10; + static const uint8_t AHT10_STATUS_BUSY = 0x80; void AHT10Component::setup() { - const uint8_t *calibrate_cmd; + if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Reset AHT10 failed!"); + } + delay(AHT10_SOFTRESET_DELAY); + + const uint8_t *init_cmd; switch (this->variant_) { case AHT10Variant::AHT20: - calibrate_cmd = AHT20_CALIBRATE_CMD; + init_cmd = AHT20_INITIALIZE_CMD; ESP_LOGCONFIG(TAG, "Setting up AHT20"); break; case AHT10Variant::AHT10: default: - calibrate_cmd = AHT10_CALIBRATE_CMD; + init_cmd = AHT10_INITIALIZE_CMD; ESP_LOGCONFIG(TAG, "Setting up AHT10"); } - if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) { + if (this->write(init_cmd, sizeof(init_cmd)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->mark_failed(); return; @@ -59,19 +68,19 @@ void AHT10Component::setup() { return; } ++cal_attempts; - if (cal_attempts > AHT10_CAL_ATTEMPTS) { - ESP_LOGE(TAG, "AHT10 calibration timed out!"); + if (cal_attempts > AHT10_INIT_ATTEMPTS) { + ESP_LOGE(TAG, "AHT10 initialization timed out!"); this->mark_failed(); return; } } if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED - ESP_LOGE(TAG, "AHT10 calibration failed!"); + ESP_LOGE(TAG, "AHT10 initialization failed!"); this->mark_failed(); return; } - ESP_LOGV(TAG, "AHT10 calibrated"); + ESP_LOGV(TAG, "AHT10 initialization"); } void AHT10Component::update() { From bc74dd49805417e3f1a5d0af3e1adf3d7d61ee69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E9=87=8ESKY?= <87404327+FlyingFeng2021@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:02:40 +0800 Subject: [PATCH 396/436] handling with the negative temperature in the sensor tmp102 (#6316) --- esphome/components/tmp102/tmp102.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index f6bb9a05c0..7d92a28cf1 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -28,7 +28,7 @@ void TMP102Component::dump_config() { } void TMP102Component::update() { - uint16_t raw_temperature; + int16_t raw_temperature; if (this->write(&TMP102_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; @@ -39,7 +39,9 @@ void TMP102Component::update() { return; } raw_temperature = i2c::i2ctohs(raw_temperature); - + if (raw_temperature & 0x8000) { + raw_temperature |= 0xF000; + } raw_temperature = raw_temperature >> 4; float temperature = raw_temperature * TMP102_CONVERSION_FACTOR; ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); From 56837b09471f92188b07afb1bf4d7e2785660e15 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 3 Mar 2024 23:33:39 -0800 Subject: [PATCH 397/436] fix tmp102 negative calculation (#6320) --- esphome/components/tmp102/tmp102.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index 7d92a28cf1..be60a2d8d4 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -39,9 +39,6 @@ void TMP102Component::update() { return; } raw_temperature = i2c::i2ctohs(raw_temperature); - if (raw_temperature & 0x8000) { - raw_temperature |= 0xF000; - } raw_temperature = raw_temperature >> 4; float temperature = raw_temperature * TMP102_CONVERSION_FACTOR; ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); From f3ed091395b1b12f85c859a9e5cb9a9f6a53745e Mon Sep 17 00:00:00 2001 From: Andy Barcinski Date: Mon, 4 Mar 2024 12:18:18 -0800 Subject: [PATCH 398/436] x9c: fix off by 1 error (#6318) --- esphome/components/x9c/x9c.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp index 1b283a68e5..32a1375f02 100644 --- a/esphome/components/x9c/x9c.cpp +++ b/esphome/components/x9c/x9c.cpp @@ -49,17 +49,17 @@ void X9cOutput::setup() { if (this->initial_value_ <= 0.50) { this->trim_value(-101); // Set min value (beyond 0) - this->trim_value((int) (this->initial_value_ * 100)); + this->trim_value(static_cast(roundf(this->initial_value_ * 100))); } else { this->trim_value(101); // Set max value (beyond 100) - this->trim_value((int) (this->initial_value_ * 100) - 100); + this->trim_value(static_cast(roundf(this->initial_value_ * 100) - 100)); } this->pot_value_ = this->initial_value_; this->write_state(this->initial_value_); } void X9cOutput::write_state(float state) { - this->trim_value((int) ((state - this->pot_value_) * 100)); + this->trim_value(static_cast(roundf((state - this->pot_value_) * 100))); this->pot_value_ = state; } From d5bfcd3bcf107978e863c3a230caa177acb96fc7 Mon Sep 17 00:00:00 2001 From: Dan Jackson Date: Mon, 4 Mar 2024 13:49:57 -0800 Subject: [PATCH 399/436] Support for MS8607 PHT (Pressure Humidity Temperature) sensor (#3307) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ms8607/__init__.py | 1 + esphome/components/ms8607/ms8607.cpp | 444 ++++++++++++++++++++++++++ esphome/components/ms8607/ms8607.h | 109 +++++++ esphome/components/ms8607/sensor.py | 83 +++++ esphome/core/component.h | 2 +- tests/test5.yaml | 23 ++ 7 files changed, 662 insertions(+), 1 deletion(-) create mode 100644 esphome/components/ms8607/__init__.py create mode 100644 esphome/components/ms8607/ms8607.cpp create mode 100644 esphome/components/ms8607/ms8607.h create mode 100644 esphome/components/ms8607/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 8d0ac7983f..de1f3253d3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -225,6 +225,7 @@ esphome/components/mopeka_pro_check/* @spbrogan esphome/components/mopeka_std_check/* @Fabian-Schmidt esphome/components/mpl3115a2/* @kbickar esphome/components/mpu6886/* @fabaff +esphome/components/ms8607/* @e28eta esphome/components/network/* @esphome/core esphome/components/nextion/* @senexcrenshaw esphome/components/nextion/binary_sensor/* @senexcrenshaw diff --git a/esphome/components/ms8607/__init__.py b/esphome/components/ms8607/__init__.py new file mode 100644 index 0000000000..e1cd49ec7b --- /dev/null +++ b/esphome/components/ms8607/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@e28eta"] diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp new file mode 100644 index 0000000000..4ad6ac336d --- /dev/null +++ b/esphome/components/ms8607/ms8607.cpp @@ -0,0 +1,444 @@ +#include "ms8607.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ms8607 { + +/// TAG used for logging calls +static const char *const TAG = "ms8607"; + +/// Reset the Pressure/Temperature sensor +static const uint8_t MS8607_PT_CMD_RESET = 0x1E; + +/// Beginning of PROM register addresses. Same for both i2c addresses. Each address has 16 bits of data, and +/// PROM addresses step by two, so the LSB is always 0 +static const uint8_t MS8607_PROM_START = 0xA0; +/// Last PROM register address. +static const uint8_t MS8607_PROM_END = 0xAE; +/// Number of PROM registers. +static const uint8_t MS8607_PROM_COUNT = (MS8607_PROM_END - MS8607_PROM_START) >> 1; + +/// Reset the Humidity sensor +static const uint8_t MS8607_CMD_H_RESET = 0xFE; +/// Read relative humidity, without holding i2c master +static const uint8_t MS8607_CMD_H_MEASURE_NO_HOLD = 0xF5; +/// Temperature correction coefficient for Relative Humidity from datasheet +static const float MS8607_H_TEMP_COEFFICIENT = -0.18; + +/// Read the converted analog value, either D1 (pressure) or D2 (temperature) +static const uint8_t MS8607_CMD_ADC_READ = 0x00; + +// TODO: allow OSR to be turned down for speed and/or lower power consumption via configuration. +// ms8607 supports 6 different settings + +/// Request conversion of analog D1 (pressure) with OSR=8192 (highest oversampling ratio). Takes maximum of 17.2ms +static const uint8_t MS8607_CMD_CONV_D1_OSR_8K = 0x4A; +/// Request conversion of analog D2 (temperature) with OSR=8192 (highest oversampling ratio). Takes maximum of 17.2ms +static const uint8_t MS8607_CMD_CONV_D2_OSR_8K = 0x5A; + +enum class MS8607Component::ErrorCode { + /// Component hasn't failed (yet?) + NONE = 0, + /// Both the Pressure/Temperature address and the Humidity address failed to reset + PTH_RESET_FAILED = 1, + /// Asking the Pressure/Temperature sensor to reset failed + PT_RESET_FAILED = 2, + /// Asking the Humidity sensor to reset failed + H_RESET_FAILED = 3, + /// Reading the PROM calibration values failed + PROM_READ_FAILED = 4, + /// The PROM calibration values failed the CRC check + PROM_CRC_FAILED = 5, +}; + +enum class MS8607Component::SetupStatus { + /// This component has not successfully reset the PT & H devices + NEEDS_RESET, + /// Reset commands succeeded, need to wait >= 15ms to read PROM + NEEDS_PROM_READ, + /// Successfully read PROM and ready to update sensors + SUCCESSFUL, +}; + +static uint8_t crc4(uint16_t *buffer, size_t length); +static uint8_t hsensor_crc_check(uint16_t value); + +void MS8607Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MS8607..."); + this->error_code_ = ErrorCode::NONE; + this->setup_status_ = SetupStatus::NEEDS_RESET; + + // I do not know why the device sometimes NACKs the reset command, but + // try 3 times in case it's a transitory issue on this boot + this->set_retry( + "reset", 5, 3, + [this](const uint8_t remaining_setup_attempts) { + ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, + this->humidity_device_->get_address()); + // I believe sending the reset command to both addresses is preferable to + // skipping humidity if PT fails for some reason. + // However, only consider the reset successful if they both ACK + bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0); + bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0); + + if (!(pt_successful && h_successful)) { + ESP_LOGE(TAG, "Resetting I2C devices failed"); + if (!pt_successful && !h_successful) { + this->error_code_ = ErrorCode::PTH_RESET_FAILED; + } else if (!pt_successful) { + this->error_code_ = ErrorCode::PT_RESET_FAILED; + } else { + this->error_code_ = ErrorCode::H_RESET_FAILED; + } + + if (remaining_setup_attempts > 0) { + this->status_set_error(); + } else { + this->mark_failed(); + } + return RetryResult::RETRY; + } + + this->setup_status_ = SetupStatus::NEEDS_PROM_READ; + this->error_code_ = ErrorCode::NONE; + this->status_clear_error(); + + // 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library + this->set_timeout("prom-read", 15, [this]() { + if (this->read_calibration_values_from_prom_()) { + this->setup_status_ = SetupStatus::SUCCESSFUL; + this->status_clear_error(); + } else { + this->mark_failed(); + return; + } + }); + + return RetryResult::DONE; + }, + 5.0f); // executes at now, +5ms, +25ms +} + +void MS8607Component::update() { + if (this->setup_status_ != SetupStatus::SUCCESSFUL) { + // setup is still occurring, either because reset had to retry or due to the 15ms + // delay needed between reset & reading the PROM values + return; + } + + // Updating happens async and sequentially. + // Temperature, then pressure, then humidity + this->request_read_temperature_(); +} + +void MS8607Component::dump_config() { + ESP_LOGCONFIG(TAG, "MS8607:"); + LOG_I2C_DEVICE(this); + // LOG_I2C_DEVICE doesn't work for humidity, the `address_` is protected. Log using get_address() + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->humidity_device_->get_address()); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MS8607 failed."); + switch (this->error_code_) { + case ErrorCode::PT_RESET_FAILED: + ESP_LOGE(TAG, "Temperature/Pressure RESET failed"); + break; + case ErrorCode::H_RESET_FAILED: + ESP_LOGE(TAG, "Humidity RESET failed"); + break; + case ErrorCode::PTH_RESET_FAILED: + ESP_LOGE(TAG, "Temperature/Pressure && Humidity RESET failed"); + break; + case ErrorCode::PROM_READ_FAILED: + ESP_LOGE(TAG, "Reading PROM failed"); + break; + case ErrorCode::PROM_CRC_FAILED: + ESP_LOGE(TAG, "PROM values failed CRC"); + break; + case ErrorCode::NONE: + default: + ESP_LOGE(TAG, "Error reason unknown %u", static_cast(this->error_code_)); + break; + } + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +bool MS8607Component::read_calibration_values_from_prom_() { + ESP_LOGD(TAG, "Reading calibration values from PROM"); + + uint16_t buffer[MS8607_PROM_COUNT]; + bool successful = true; + + for (uint8_t idx = 0; idx < MS8607_PROM_COUNT; ++idx) { + uint8_t const address_to_read = MS8607_PROM_START + (idx * 2); + successful &= this->read_byte_16(address_to_read, &buffer[idx]); + } + + if (!successful) { + ESP_LOGE(TAG, "Reading calibration values from PROM failed"); + this->error_code_ = ErrorCode::PROM_READ_FAILED; + return false; + } + + ESP_LOGD(TAG, "Checking CRC of calibration values from PROM"); + uint8_t const expected_crc = (buffer[0] & 0xF000) >> 12; // first 4 bits + buffer[0] &= 0x0FFF; // strip CRC from buffer, in order to run CRC + uint8_t const actual_crc = crc4(buffer, MS8607_PROM_COUNT); + + if (expected_crc != actual_crc) { + ESP_LOGE(TAG, "Incorrect CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc, actual_crc); + this->error_code_ = ErrorCode::PROM_CRC_FAILED; + return false; + } + + this->calibration_values_.pressure_sensitivity = buffer[1]; + this->calibration_values_.pressure_offset = buffer[2]; + this->calibration_values_.pressure_sensitivity_temperature_coefficient = buffer[3]; + this->calibration_values_.pressure_offset_temperature_coefficient = buffer[4]; + this->calibration_values_.reference_temperature = buffer[5]; + this->calibration_values_.temperature_coefficient_of_temperature = buffer[6]; + ESP_LOGD(TAG, "Finished reading calibration values"); + + // Skipping reading Humidity PROM, since it doesn't have anything interesting for us + + return true; +} + +/** + CRC-4 algorithm from datasheet. It operates on a buffer of 16-bit values, one byte at a time, using a 16-bit + value to collect the CRC result into. + + The provided/expected CRC value must already be zeroed out from the buffer. + */ +static uint8_t crc4(uint16_t *buffer, size_t length) { + uint16_t crc_remainder = 0; + + // algorithm to add a byte into the crc + auto apply_crc = [&crc_remainder](uint8_t next) { + crc_remainder ^= next; + for (uint8_t bit = 8; bit > 0; --bit) { + if (crc_remainder & 0x8000) { + crc_remainder = (crc_remainder << 1) ^ 0x3000; + } else { + crc_remainder = (crc_remainder << 1); + } + } + }; + + // add all the bytes + for (size_t idx = 0; idx < length; ++idx) { + for (auto byte : decode_value(buffer[idx])) { + apply_crc(byte); + } + } + // For the MS8607 CRC, add a pair of zeros to shift the last byte from `buffer` through + apply_crc(0); + apply_crc(0); + + return (crc_remainder >> 12) & 0xF; // only the most significant 4 bits +} + +/** + * @brief Calculates CRC value for the provided humidity (+ status bits) value + * + * CRC-8 check comes from other MS8607 libraries on github. I did not find it in the datasheet, + * and it differs from the crc8 implementation that's already part of esphome. + * + * @param value two byte humidity sensor value read from i2c + * @return uint8_t computed crc value + */ +static uint8_t hsensor_crc_check(uint16_t value) { + uint32_t polynom = 0x988000; // x^8 + x^5 + x^4 + 1 + uint32_t msb = 0x800000; + uint32_t mask = 0xFF8000; + uint32_t result = (uint32_t) value << 8; // Pad with zeros as specified in spec + + while (msb != 0x80) { + // Check if msb of current value is 1 and apply XOR mask + if (result & msb) { + result = ((result ^ polynom) & mask) | (result & ~mask); + } + + // Shift by one + msb >>= 1; + mask >>= 1; + polynom >>= 1; + } + return result & 0xFF; +} + +void MS8607Component::request_read_temperature_() { + // Tell MS8607 to start ADC conversion of temperature sensor + if (!this->write_bytes(MS8607_CMD_CONV_D2_OSR_8K, nullptr, 0)) { + this->status_set_warning(); + return; + } + + auto f = std::bind(&MS8607Component::read_temperature_, this); + // datasheet says 17.2ms max conversion time at OSR 8192 + this->set_timeout("temperature", 20, f); +} + +void MS8607Component::read_temperature_() { + uint8_t bytes[3]; // 24 bits + if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) { + this->status_set_warning(); + return; + } + + const uint32_t d2_raw_temperature = encode_uint32(0, bytes[0], bytes[1], bytes[2]); + this->request_read_pressure_(d2_raw_temperature); +} + +void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) { + if (!this->write_bytes(MS8607_CMD_CONV_D1_OSR_8K, nullptr, 0)) { + this->status_set_warning(); + return; + } + + auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature); + // datasheet says 17.2ms max conversion time at OSR 8192 + this->set_timeout("pressure", 20, f); +} + +void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) { + uint8_t bytes[3]; // 24 bits + if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) { + this->status_set_warning(); + return; + } + const uint32_t d1_raw_pressure = encode_uint32(0, bytes[0], bytes[1], bytes[2]); + this->calculate_values_(d2_raw_temperature, d1_raw_pressure); +} + +void MS8607Component::request_read_humidity_(float temperature_float) { + if (!this->humidity_device_->write_bytes(MS8607_CMD_H_MEASURE_NO_HOLD, nullptr, 0)) { + ESP_LOGW(TAG, "Request to measure humidity failed"); + this->status_set_warning(); + return; + } + + auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float); + // datasheet says 15.89ms max conversion time at OSR 8192 + this->set_timeout("humidity", 20, f); +} + +void MS8607Component::read_humidity_(float temperature_float) { + uint8_t bytes[3]; + if (!this->humidity_device_->read_bytes_raw(bytes, 3)) { + ESP_LOGW(TAG, "Failed to read the measured humidity value"); + this->status_set_warning(); + return; + } + + // "the measurement is stored into 14 bits. The two remaining LSBs are used for transmitting status information. + // Bit1 of the two LSBS must be set to '1'. Bit0 is currently not assigned" + uint16_t humidity = encode_uint16(bytes[0], bytes[1]); + uint8_t const expected_crc = bytes[2]; + uint8_t const actual_crc = hsensor_crc_check(humidity); + if (expected_crc != actual_crc) { + ESP_LOGE(TAG, "Incorrect Humidity CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc, + actual_crc); + this->status_set_warning(); + return; + } + if (!(humidity & 0x2)) { + // data sheet says Bit1 should always set, but nothing about what happens if it isn't + ESP_LOGE(TAG, "Humidity status bit was not set to 1?"); + } + humidity &= ~(0b11); // strip status & unassigned bits from data + + // map 16 bit humidity value into range [-6%, 118%] + float const humidity_partial = double(humidity) / (1 << 16); + float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0); + float const compensated_humidity_percentage = + humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; + ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); + + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->publish_state(compensated_humidity_percentage); + } + this->status_clear_warning(); +} + +void MS8607Component::calculate_values_(uint32_t d2_raw_temperature, uint32_t d1_raw_pressure) { + // Perform the first order pressure/temperature calculation + + // d_t: "difference between actual and reference temperature" = D2 - [C5] * 2**8 + const int32_t d_t = int32_t(d2_raw_temperature) - (int32_t(this->calibration_values_.reference_temperature) << 8); + // actual temperature as hundredths of degree celsius in range [-4000, 8500] + // 2000 + d_t * [C6] / (2**23) + int32_t temperature = + 2000 + ((int64_t(d_t) * this->calibration_values_.temperature_coefficient_of_temperature) >> 23); + + // offset at actual temperature. [C2] * (2**17) + (d_t * [C4] / (2**6)) + int64_t pressure_offset = (int64_t(this->calibration_values_.pressure_offset) << 17) + + ((int64_t(d_t) * this->calibration_values_.pressure_offset_temperature_coefficient) >> 6); + // sensitivity at actual temperature. [C1] * (2**16) + ([C3] * d_t) / (2**7) + int64_t pressure_sensitivity = + (int64_t(this->calibration_values_.pressure_sensitivity) << 16) + + ((int64_t(d_t) * this->calibration_values_.pressure_sensitivity_temperature_coefficient) >> 7); + + // Perform the second order compensation, for non-linearity over temperature range + const int64_t d_t_squared = int64_t(d_t) * d_t; + int64_t temperature_2 = 0; + int32_t pressure_offset_2 = 0; + int32_t pressure_sensitivity_2 = 0; + if (temperature < 2000) { + // (TEMP - 2000)**2 / 2**4 + const int32_t low_temperature_adjustment = (temperature - 2000) * (temperature - 2000) >> 4; + + // T2 = 3 * (d_t**2) / 2**33 + temperature_2 = (3 * d_t_squared) >> 33; + // OFF2 = 61 * (TEMP-2000)**2 / 2**4 + pressure_offset_2 = 61 * low_temperature_adjustment; + // SENS2 = 29 * (TEMP-2000)**2 / 2**4 + pressure_sensitivity_2 = 29 * low_temperature_adjustment; + + if (temperature < -1500) { + // (TEMP+1500)**2 + const int32_t very_low_temperature_adjustment = (temperature + 1500) * (temperature + 1500); + + // OFF2 = OFF2 + 17 * (TEMP+1500)**2 + pressure_offset_2 += 17 * very_low_temperature_adjustment; + // SENS2 = SENS2 + 9 * (TEMP+1500)**2 + pressure_sensitivity_2 += 9 * very_low_temperature_adjustment; + } + } else { + // T2 = 5 * (d_t**2) / 2**38 + temperature_2 = (5 * d_t_squared) >> 38; + } + + temperature -= temperature_2; + pressure_offset -= pressure_offset_2; + pressure_sensitivity -= pressure_sensitivity_2; + + // Temperature compensated pressure. [1000, 120000] => [10.00 mbar, 1200.00 mbar] + const int32_t pressure = (((d1_raw_pressure * pressure_sensitivity) >> 21) - pressure_offset) >> 15; + + const float temperature_float = temperature / 100.0f; + const float pressure_float = pressure / 100.0f; + ESP_LOGD(TAG, "Temperature=%0.2f°C, Pressure=%0.2fhPa", temperature_float, pressure_float); + + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(temperature_float); + } + if (this->pressure_sensor_ != nullptr) { + this->pressure_sensor_->publish_state(pressure_float); // hPa aka mbar + } + this->status_clear_warning(); + + if (this->humidity_sensor_ != nullptr) { + // now that we have temperature (to compensate the humidity with), kick off that read + this->request_read_humidity_(temperature_float); + } +} + +} // namespace ms8607 +} // namespace esphome diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h new file mode 100644 index 0000000000..0bee7e97b7 --- /dev/null +++ b/esphome/components/ms8607/ms8607.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ms8607 { + +/** + Class for I2CDevice used to communicate with the Humidity sensor + on the chip. See MS8607Component instead + */ +class MS8607HumidityDevice : public i2c::I2CDevice { + public: + uint8_t get_address() { return address_; } +}; + +/** + Temperature, pressure, and humidity sensor. + + By default, the MS8607 measures sensors at the highest resolution. + A potential enhancement would be to expose the resolution as a configurable + setting. A lower resolution speeds up ADC conversion time & uses less power. + + Datasheet: + https://www.te.com/commerce/DocumentDelivery/DDEController?Action=showdoc&DocId=Data+Sheet%7FMS8607-02BA01%7FB3%7Fpdf%7FEnglish%7FENG_DS_MS8607-02BA01_B3.pdf%7FCAT-BLPS0018 + + Other implementations: + - https://github.com/TEConnectivity/MS8607_Generic_C_Driver + - https://github.com/adafruit/Adafruit_MS8607 + - https://github.com/sparkfun/SparkFun_PHT_MS8607_Arduino_Library + */ +class MS8607Component : public PollingComponent, public i2c::I2CDevice { + public: + virtual ~MS8607Component() = default; + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; } + + protected: + /** + Read and store the Pressure & Temperature calibration settings from the PROM. + Intended to be called during setup(), this will set the `failure_reason_` + */ + bool read_calibration_values_from_prom_(); + + /// Start async temperature read + void request_read_temperature_(); + /// Process async temperature read + void read_temperature_(); + /// start async pressure read + void request_read_pressure_(uint32_t raw_temperature); + /// process async pressure read + void read_pressure_(uint32_t raw_temperature); + /// start async humidity read + void request_read_humidity_(float temperature_float); + /// process async humidity read + void read_humidity_(float temperature_float); + /// use raw temperature & pressure to calculate & publish values + void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure); + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + sensor::Sensor *humidity_sensor_; + + /** I2CDevice object to communicate with secondary I2C address for the humidity sensor + * + * The MS8607 only has one set of I2C pins, despite using two different addresses. + * + * Default address for humidity is 0x40 + */ + MS8607HumidityDevice *humidity_device_; + + /// This device's pressure & temperature calibration values, read from PROM + struct CalibrationValues { + /// Pressure sensitivity | SENS-T1. [C1] + uint16_t pressure_sensitivity; + /// Temperature coefficient of pressure sensitivity | TCS. [C3] + uint16_t pressure_sensitivity_temperature_coefficient; + /// Pressure offset | OFF-T1. [C2] + uint16_t pressure_offset; + /// Temperature coefficient of pressure offset | TCO. [C4] + uint16_t pressure_offset_temperature_coefficient; + /// Reference temperature | T-REF. [C5] + uint16_t reference_temperature; + /// Temperature coefficient of the temperature | TEMPSENS. [C6] + uint16_t temperature_coefficient_of_temperature; + } calibration_values_; + + /// Possible failure reasons of this component + enum class ErrorCode; + /// Keep track of the reason why this component failed, to augment the dumped config + ErrorCode error_code_; + + /// Current progress through required component setup + enum class SetupStatus; + /// Current step in the multi-step & possibly delayed setup() process + SetupStatus setup_status_; +}; + +} // namespace ms8607 +} // namespace esphome diff --git a/esphome/components/ms8607/sensor.py b/esphome/components/ms8607/sensor.py new file mode 100644 index 0000000000..1113e14af2 --- /dev/null +++ b/esphome/components/ms8607/sensor.py @@ -0,0 +1,83 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +ms8607_ns = cg.esphome_ns.namespace("ms8607") +MS8607Component = ms8607_ns.class_( + "MS8607Component", cg.PollingComponent, i2c.I2CDevice +) + +CONF_HUMIDITY_I2C_ID = "humidity_i2c_id" +MS8607HumidityDevice = ms8607_ns.class_("MS8607HumidityDevice", i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MS8607Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, # Resolution: 0.01 + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=2, # Resolution: 0.016 + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, # Resolution: 0.04 + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_HUMIDITY_I2C_ID): cv.declare_id( + MS8607HumidityDevice + ), + } + ) + .extend(i2c.i2c_device_schema(0x40)), # default address for humidity + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x76)) # default address for temp/pressure +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + humidity_device = cg.new_Pvariable(humidity_config[CONF_HUMIDITY_I2C_ID]) + await i2c.register_i2c_device(humidity_device, humidity_config) + cg.add(var.set_humidity_device(humidity_device)) diff --git a/esphome/core/component.h b/esphome/core/component.h index 594f8b65af..deefedf3d8 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -193,7 +193,7 @@ class Component { * again in the future. * * The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is - * increased by multipling by `backoff_increase_factor` each time. If no backoff_increase_factor is + * increased by multiplying by `backoff_increase_factor` each time. If no backoff_increase_factor is * supplied (default = 1.0), the wait time will stay constant. * * The retry function f needs to accept a single argument: the number of attempts remaining. On the diff --git a/tests/test5.yaml b/tests/test5.yaml index 55efba57d1..81615b24b0 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -483,6 +483,29 @@ sensor: address: 0x77 iir_filter: 2X + - platform: ms8607 + temperature: + name: Temperature + humidity: + name: Humidity + pressure: + name: Pressure + - platform: ms8607 + id: ms8607_more_config + temperature: + name: Indoor Temperature + accuracy_decimals: 1 + pressure: + name: Indoor Pressure + internal: true + humidity: + name: Indoor Humidity + address: 0x41 + i2c_id: + i2c_id: + address: 0x77 + update_interval: 10min + - platform: sen5x id: sen54 temperature: From de2d5a65b5bf30a8f665af7c06a28596d57afc0a Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 4 Mar 2024 22:52:52 +0100 Subject: [PATCH 400/436] Separate logger implementations for each hardware platform into different files (#6167) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 14 + esphome/components/logger/logger.cpp | 312 +----------------- esphome/components/logger/logger.h | 39 +-- esphome/components/logger/logger_esp32.cpp | 201 +++++++++++ esphome/components/logger/logger_esp8266.cpp | 45 +++ esphome/components/logger/logger_host.cpp | 22 ++ .../components/logger/logger_libretiny.cpp | 62 ++++ esphome/components/logger/logger_rp2040.cpp | 39 +++ 8 files changed, 398 insertions(+), 336 deletions(-) create mode 100644 esphome/components/logger/logger_esp32.cpp create mode 100644 esphome/components/logger/logger_esp8266.cpp create mode 100644 esphome/components/logger/logger_host.cpp create mode 100644 esphome/components/logger/logger_libretiny.cpp create mode 100644 esphome/components/logger/logger_rp2040.cpp diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index fd64c65c77..fe887a392f 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -127,6 +127,10 @@ def uart_selection(value): if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS: raise cv.Invalid(f"Arduino framework does not support {value}.") variant = get_esp32_variant() + if CORE.using_esp_idf and variant == VARIANT_ESP32C3 and value == USB_CDC: + raise cv.Invalid( + f"{value} is not supported for variant {variant} when using ESP-IDF." + ) if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) if CORE.is_esp8266: @@ -274,6 +278,16 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + try: + uart_selection(USB_SERIAL_JTAG) + cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") + except cv.Invalid: + pass + try: + uart_selection(USB_CDC) + cg.add_define("USE_LOGGER_USB_CDC") + except cv.Invalid: + pass # Register at end for safe mode await cg.register_component(log, config) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index c8a3ba906c..a109368ea9 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -1,28 +1,6 @@ #include "logger.h" #include -#ifdef USE_ESP_IDF -#include - -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32H2) -#include -#include -#include -#endif - -#include "freertos/FreeRTOS.h" -#include "esp_idf_version.h" - -#include -#include -#include - -#endif // USE_ESP_IDF - -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) -#include -#endif // USE_ESP32_FRAMEWORK_ARDUINO || USE_ESP_IDF #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -106,58 +84,6 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr } #endif -#ifdef USE_ESP_IDF -void Logger::init_uart_() { - uart_config_t uart_config{}; - uart_config.baud_rate = (int) baud_rate_; - uart_config.data_bits = UART_DATA_8_BITS; - uart_config.parity = UART_PARITY_DISABLE; - uart_config.stop_bits = UART_STOP_BITS_1; - uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) - uart_config.source_clk = UART_SCLK_DEFAULT; -#endif - uart_param_config(this->uart_num_, &uart_config); - const int uart_buffer_size = tx_buffer_size_; - // Install UART driver using an event queue here - uart_driver_install(this->uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); -} - -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) -void Logger::init_usb_cdc_() {} -#endif - -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32H2) -void Logger::init_usb_serial_jtag_() { - setvbuf(stdin, NULL, _IONBF, 0); // Disable buffering on stdin - - // Minicom, screen, idf_monitor send CR when ENTER key is pressed - esp_vfs_dev_usb_serial_jtag_set_rx_line_endings(ESP_LINE_ENDINGS_CR); - // Move the caret to the beginning of the next line on '\n' - esp_vfs_dev_usb_serial_jtag_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); - - // Enable non-blocking mode on stdin and stdout - fcntl(fileno(stdout), F_SETFL, 0); - fcntl(fileno(stdin), F_SETFL, 0); - - usb_serial_jtag_driver_config_t usb_serial_jtag_config{}; - usb_serial_jtag_config.rx_buffer_size = 512; - usb_serial_jtag_config.tx_buffer_size = 512; - - esp_err_t ret = ESP_OK; - // Install USB-SERIAL-JTAG driver for interrupt-driven reads and writes - ret = usb_serial_jtag_driver_install(&usb_serial_jtag_config); - if (ret != ESP_OK) { - return; - } - - // Tell vfs to use usb-serial-jtag driver - esp_vfs_usb_serial_jtag_use_driver(); -} -#endif -#endif - int HOT Logger::level_for(const char *tag) { // Uses std::vector<> for low memory footprint, though the vector // could be sorted to minimize lookup times. This feature isn't used that @@ -169,6 +95,7 @@ int HOT Logger::level_for(const char *tag) { } return ESPHOME_LOG_LEVEL; } + void HOT Logger::log_message_(int level, const char *tag, int offset) { // remove trailing newline if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') { @@ -178,28 +105,9 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { this->set_null_terminator_(); const char *msg = this->tx_buffer_ + offset; + if (this->baud_rate_ > 0) { -#ifdef USE_ARDUINO - this->hw_serial_->println(msg); -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF - if ( -#if defined(USE_ESP32_VARIANT_ESP32S2) - this->uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) - this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_ESP32_VARIANT_ESP32S3) - this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#else - /* DISABLES CODE */ (false) // NOLINT -#endif - ) { - puts(msg); - } else { - uart_write_bytes(this->uart_num_, msg, strlen(msg)); - uart_write_bytes(this->uart_num_, "\n", 1); - } -#endif + this->write_msg_(msg); } #ifdef USE_ESP32 @@ -211,17 +119,6 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { if (xPortGetFreeHeapSize() < 2048) return; #endif -#ifdef USE_HOST - time_t rawtime; - struct tm *timeinfo; - char buffer[80]; - - time(&rawtime); - timeinfo = localtime(&rawtime); - strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo); - fputs(buffer, stdout); - puts(msg); -#endif this->log_callback_.call(level, tag, msg); } @@ -231,179 +128,6 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT } -#ifndef USE_LIBRETINY -void Logger::pre_setup() { - if (this->baud_rate_ > 0) { -#ifdef USE_ARDUINO - switch (this->uart_) { - case UART_SELECTION_UART0: -#ifdef USE_ESP8266 - case UART_SELECTION_UART0_SWAP: -#endif -#ifdef USE_RP2040 - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); -#else -#if ARDUINO_USB_CDC_ON_BOOT - this->hw_serial_ = &Serial0; - Serial0.begin(this->baud_rate_); -#else - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); -#endif -#endif -#ifdef USE_ESP8266 - if (this->uart_ == UART_SELECTION_UART0_SWAP) { - Serial.swap(); - } - Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif - break; - case UART_SELECTION_UART1: -#ifdef USE_RP2040 - this->hw_serial_ = &Serial2; - Serial2.begin(this->baud_rate_); -#else - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); -#endif -#ifdef USE_ESP8266 - Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif - break; -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ - !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) - case UART_SELECTION_UART2: - this->hw_serial_ = &Serial2; - Serial2.begin(this->baud_rate_); - break; -#endif -#if defined(USE_ESP32) && \ - (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3)) -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) - case UART_SELECTION_USB_CDC: -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3 -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) - case UART_SELECTION_USB_SERIAL_JTAG: -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) -#if ARDUINO_USB_CDC_ON_BOOT - this->hw_serial_ = &Serial; - Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection - Serial.begin(this->baud_rate_); -#else - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); -#endif // ARDUINO_USB_CDC_ON_BOOT -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3 - break; -#endif // USE_ESP32 && (USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3) -#ifdef USE_RP2040 - case UART_SELECTION_USB_CDC: - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); - break; -#endif // USE_RP2040 - } -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF - this->uart_num_ = UART_NUM_0; - switch (this->uart_) { - case UART_SELECTION_UART0: - this->uart_num_ = UART_NUM_0; - break; - case UART_SELECTION_UART1: - this->uart_num_ = UART_NUM_1; - break; -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ - !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) && !defined(USE_ESP32_VARIANT_ESP32H2) - case UART_SELECTION_UART2: - this->uart_num_ = UART_NUM_2; - break; -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 && - // !USE_ESP32_VARIANT_ESP32H2 -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - case UART_SELECTION_USB_CDC: - this->uart_num_ = -1; - this->init_usb_cdc_(); - break; -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32H2) - case UART_SELECTION_USB_SERIAL_JTAG: - this->uart_num_ = -1; - this->init_usb_serial_jtag_(); - break; -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || - // USE_ESP32_VARIANT_ESP32H2 - } - if (this->uart_num_ >= 0) { - this->init_uart_(); - } -#endif // USE_ESP_IDF - } -#ifdef USE_ESP8266 - else { - uart_set_debug(UART_NO); - } -#endif // USE_ESP8266 - - global_logger = this; -#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) - esp_log_set_vprintf(esp_idf_log_vprintf_); - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { - esp_log_level_set("*", ESP_LOG_VERBOSE); - } -#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO - - ESP_LOGI(TAG, "Log initialized"); -} -#else // USE_LIBRETINY -void Logger::pre_setup() { - if (this->baud_rate_ > 0) { - switch (this->uart_) { -#if LT_HW_UART0 - case UART_SELECTION_UART0: - this->hw_serial_ = &Serial0; - Serial0.begin(this->baud_rate_); - break; -#endif -#if LT_HW_UART1 - case UART_SELECTION_UART1: - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); - break; -#endif -#if LT_HW_UART2 - case UART_SELECTION_UART2: - this->hw_serial_ = &Serial2; - Serial2.begin(this->baud_rate_); - break; -#endif - default: - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); - if (this->uart_ != UART_SELECTION_DEFAULT) { - ESP_LOGW(TAG, " The chosen logger UART port is not available on this board." - "The default port was used instead."); - } - break; - } - - // change lt_log() port to match default Serial - if (this->uart_ == UART_SELECTION_DEFAULT) { - this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1); - lt_log_set_port(LT_UART_DEFAULT_SERIAL); - } else { - lt_log_set_port(this->uart_ - 1); - } - } - - global_logger = this; - ESP_LOGI(TAG, "Log initialized"); -} -#endif // USE_LIBRETINY - void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_.push_back(LogLevelOverride{tag, log_level}); @@ -418,38 +142,12 @@ void Logger::add_on_log_callback(std::functionbaud_rate_); -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) - ESP_LOGCONFIG(TAG, " Hardware UART: %s", UART_SELECTIONS[this->uart_]); -#endif + ESP_LOGCONFIG(TAG, " Hardware UART: %s", get_uart_selection_()); for (auto &it : this->log_levels_) { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c7f0fe4139..d70e895985 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -34,34 +34,22 @@ enum UARTSelection { #ifdef USE_LIBRETINY UART_SELECTION_DEFAULT = 0, UART_SELECTION_UART0, - UART_SELECTION_UART1, - UART_SELECTION_UART2, #else UART_SELECTION_UART0 = 0, +#endif UART_SELECTION_UART1, -#if defined(USE_ESP32) -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ - !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) && !defined(USE_ESP32_VARIANT_ESP32H2) +#if defined(USE_LIBRETINY) || defined(USE_ESP32_VARIANT_ESP32) UART_SELECTION_UART2, -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && - // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - (defined(USE_ESP32_VARIANT_ESP32C3) && defined(USE_ARDUINO)) +#endif +#ifdef USE_LOGGER_USB_CDC UART_SELECTION_USB_CDC, -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3 -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32H2) +#endif +#ifdef USE_LOGGER_USB_SERIAL_JTAG UART_SELECTION_USB_SERIAL_JTAG, -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || - // USE_ESP32_VARIANT_ESP32H2 -#endif // USE_ESP32 +#endif #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, #endif // USE_ESP8266 -#ifdef USE_RP2040 - UART_SELECTION_USB_CDC, -#endif // USE_RP2040 -#endif // USE_LIBRETINY }; #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY @@ -106,19 +94,10 @@ class Logger : public Component { #endif protected: -#ifdef USE_ESP_IDF - void init_uart_(); -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - void init_usb_cdc_(); -#endif -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32H2) - void init_usb_serial_jtag_(); -#endif -#endif void write_header_(int level, const char *tag, int line); void write_footer_(); void log_message_(int level, const char *tag, int offset = 0); + void write_msg_(const char *msg); inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; } inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; } @@ -158,6 +137,8 @@ class Logger : public Component { va_end(arg); } + const char *get_uart_selection_(); + uint32_t baud_rate_; char *tx_buffer_{nullptr}; int tx_buffer_at_{0}; diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp new file mode 100644 index 0000000000..7c4d6781c9 --- /dev/null +++ b/esphome/components/logger/logger_esp32.cpp @@ -0,0 +1,201 @@ +#ifdef USE_ESP32 +#include "logger.h" + +#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#include +#endif // USE_ESP32_FRAMEWORK_ARDUINO || USE_ESP_IDF + +#ifdef USE_ESP_IDF +#include + +#ifdef USE_LOGGER_USB_SERIAL_JTAG +#include +#include +#include +#endif + +#include "freertos/FreeRTOS.h" +#include "esp_idf_version.h" + +#include +#include +#include + +#endif // USE_ESP_IDF + +#include "esphome/core/log.h" + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +#ifdef USE_ESP_IDF + +#ifdef USE_LOGGER_USB_SERIAL_JTAG +static void init_usb_serial_jtag_() { + setvbuf(stdin, NULL, _IONBF, 0); // Disable buffering on stdin + + // Minicom, screen, idf_monitor send CR when ENTER key is pressed + esp_vfs_dev_usb_serial_jtag_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + // Move the caret to the beginning of the next line on '\n' + esp_vfs_dev_usb_serial_jtag_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); + + // Enable non-blocking mode on stdin and stdout + fcntl(fileno(stdout), F_SETFL, 0); + fcntl(fileno(stdin), F_SETFL, 0); + + usb_serial_jtag_driver_config_t usb_serial_jtag_config{}; + usb_serial_jtag_config.rx_buffer_size = 512; + usb_serial_jtag_config.tx_buffer_size = 512; + + esp_err_t ret = ESP_OK; + // Install USB-SERIAL-JTAG driver for interrupt-driven reads and writes + ret = usb_serial_jtag_driver_install(&usb_serial_jtag_config); + if (ret != ESP_OK) { + return; + } + + // Tell vfs to use usb-serial-jtag driver + esp_vfs_usb_serial_jtag_use_driver(); +} +#endif + +void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { + uart_config_t uart_config{}; + uart_config.baud_rate = (int) baud_rate; + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_DISABLE; + uart_config.stop_bits = UART_STOP_BITS_1; + uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + uart_config.source_clk = UART_SCLK_DEFAULT; +#endif + uart_param_config(uart_num, &uart_config); + const int uart_buffer_size = tx_buffer_size; + // Install UART driver using an event queue here + uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); +} + +#endif // USE_ESP_IDF + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { +#ifdef USE_ARDUINO + switch (this->uart_) { + case UART_SELECTION_UART0: +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial0; + Serial0.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif + break; + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; +#ifdef USE_ESP32_VARIANT_ESP32 + case UART_SELECTION_UART2: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; +#endif + +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + this->hw_serial_ = &Serial; +#if ARDUINO_USB_CDC_ON_BOOT + Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection +#endif + Serial.begin(this->baud_rate_); + break; +#endif + } +#endif // USE_ARDUINO + +#ifdef USE_ESP_IDF + this->uart_num_ = UART_NUM_0; + switch (this->uart_) { + case UART_SELECTION_UART0: + this->uart_num_ = UART_NUM_0; + break; + case UART_SELECTION_UART1: + this->uart_num_ = UART_NUM_1; + break; +#ifdef USE_ESP32_VARIANT_ESP32 + case UART_SELECTION_UART2: + this->uart_num_ = UART_NUM_2; + break; +#endif +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_CDC: + this->uart_num_ = -1; + break; +#endif +#ifdef USE_LOGGER_USB_SERIAL_JTAG + case UART_SELECTION_USB_SERIAL_JTAG: + this->uart_num_ = -1; + init_usb_serial_jtag_(); + break; +#endif + } + if (this->uart_num_ >= 0) { + init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + } +#endif // USE_ESP_IDF + } + + global_logger = this; +#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) + esp_log_set_vprintf(esp_idf_log_vprintf_); + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { + esp_log_level_set("*", ESP_LOG_VERBOSE); + } +#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO + + ESP_LOGI(TAG, "Log initialized"); +} + +#ifdef USE_ESP_IDF +void HOT Logger::write_msg_(const char *msg) { + if ( +#if defined(USE_ESP32_VARIANT_ESP32S2) + this->uart_ == UART_SELECTION_USB_CDC +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) + this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +#elif defined(USE_ESP32_VARIANT_ESP32S3) + this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +#else + /* DISABLES CODE */ (false) // NOLINT +#endif + ) { + puts(msg); + } else { + uart_write_bytes(this->uart_num_, msg, strlen(msg)); + uart_write_bytes(this->uart_num_, "\n", 1); + } +} +#else +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +#endif + +const char *const UART_SELECTIONS[] = { + "UART0", "UART1", +#ifdef USE_ESP32_VARIANT_ESP32 + "UART2", +#endif +#ifdef USE_LOGGER_USB_CDC + "USB_CDC", +#endif +#ifdef USE_LOGGER_USB_SERIAL_JTAG + "USB_SERIAL_JTAG", +#endif +}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome +#endif diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp new file mode 100644 index 0000000000..5bfeb21007 --- /dev/null +++ b/esphome/components/logger/logger_esp8266.cpp @@ -0,0 +1,45 @@ +#ifdef USE_ESP8266 +#include "logger.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { + case UART_SELECTION_UART0: + case UART_SELECTION_UART0_SWAP: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + Serial.swap(); + } + Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); + break; + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); + break; + } + } else { + uart_set_debug(UART_NO); + } + + global_logger = this; + + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome +#endif diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp new file mode 100644 index 0000000000..88c13bba7b --- /dev/null +++ b/esphome/components/logger/logger_host.cpp @@ -0,0 +1,22 @@ +#if defined(USE_HOST) +#include "logger.h" + +namespace esphome { +namespace logger { + +void HOT Logger::write_msg_(const char *msg) { + time_t rawtime; + struct tm *timeinfo; + char buffer[80]; + + time(&rawtime); + timeinfo = localtime(&rawtime); + strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo); + fputs(buffer, stdout); + puts(msg); +} + +} // namespace logger +} // namespace esphome + +#endif diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp new file mode 100644 index 0000000000..12e55b7cef --- /dev/null +++ b/esphome/components/logger/logger_libretiny.cpp @@ -0,0 +1,62 @@ +#ifdef USE_LIBRETINY +#include "logger.h" + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { +#if LT_HW_UART0 + case UART_SELECTION_UART0: + this->hw_serial_ = &Serial0; + Serial0.begin(this->baud_rate_); + break; +#endif +#if LT_HW_UART1 + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; +#endif +#if LT_HW_UART2 + case UART_SELECTION_UART2: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; +#endif + default: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + if (this->uart_ != UART_SELECTION_DEFAULT) { + ESP_LOGW(TAG, " The chosen logger UART port is not available on this board." + "The default port was used instead."); + } + break; + } + + // change lt_log() port to match default Serial + if (this->uart_ == UART_SELECTION_DEFAULT) { + this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1); + lt_log_set_port(LT_UART_DEFAULT_SERIAL); + } else { + lt_log_set_port(this->uart_ - 1); + } + } + + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp new file mode 100644 index 0000000000..2783d77ac1 --- /dev/null +++ b/esphome/components/logger/logger_rp2040.cpp @@ -0,0 +1,39 @@ +#ifdef USE_RP2040 +#include "logger.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { + case UART_SELECTION_UART0: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; + case UART_SELECTION_USB_CDC: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + break; + } + } + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome +#endif // USE_RP2040 From 81b8451b8a508f4fa31edce24783a57d669af9f6 Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Mon, 4 Mar 2024 22:54:01 +0100 Subject: [PATCH 401/436] Additional sensors and binary sensors support for Haier Climate (#6257) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Pavlo Dudnytskyi --- .../haier/binary_sensor/__init__.py | 70 ++++++++ esphome/components/haier/climate.py | 20 +-- esphome/components/haier/hon_climate.cpp | 136 +++++++++++++--- esphome/components/haier/hon_climate.h | 51 +++++- esphome/components/haier/hon_packet.h | 74 ++++++--- esphome/components/haier/sensor/__init__.py | 151 ++++++++++++++++++ tests/components/haier/test.esp32-c3-idf.yaml | 95 +++++++++++ tests/components/haier/test.esp32-c3.yaml | 95 +++++++++++ tests/components/haier/test.esp32-idf.yaml | 95 +++++++++++ tests/components/haier/test.esp32.yaml | 95 +++++++++++ tests/components/haier/test.esp8266.yaml | 95 +++++++++++ tests/components/haier/test.rp2040.yaml | 95 +++++++++++ tests/test3.yaml | 43 ++++- 13 files changed, 1054 insertions(+), 61 deletions(-) create mode 100644 esphome/components/haier/binary_sensor/__init__.py create mode 100644 esphome/components/haier/sensor/__init__.py create mode 100644 tests/components/haier/test.esp32-c3-idf.yaml create mode 100644 tests/components/haier/test.esp32-c3.yaml create mode 100644 tests/components/haier/test.esp32-idf.yaml create mode 100644 tests/components/haier/test.esp32.yaml create mode 100644 tests/components/haier/test.esp8266.yaml create mode 100644 tests/components/haier/test.rp2040.yaml diff --git a/esphome/components/haier/binary_sensor/__init__.py b/esphome/components/haier/binary_sensor/__init__.py new file mode 100644 index 0000000000..4f72560a7b --- /dev/null +++ b/esphome/components/haier/binary_sensor/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_FAN, + ICON_RADIATOR, +) +from ..climate import ( + CONF_HAIER_ID, + HonClimate, +) + +BinarySensorTypeEnum = HonClimate.enum("SubBinarySensorType", True) + +# Haier sensors +CONF_OUTDOOR_FAN_STATUS = "outdoor_fan_status" +CONF_DEFROST_STATUS = "defrost_status" +CONF_COMPRESSOR_STATUS = "compressor_status" +CONF_INDOOR_FAN_STATUS = "indoor_fan_status" +CONF_FOUR_WAY_VALVE_STATUS = "four_way_valve_status" +CONF_INDOOR_ELECTRIC_HEATING_STATUS = "indoor_electric_heating_status" + +# Additional icons +ICON_SNOWFLAKE_THERMOMETER = "mdi:snowflake-thermometer" +ICON_HVAC = "mdi:hvac" +ICON_VALVE = "mdi:valve" + +SENSOR_TYPES = { + CONF_OUTDOOR_FAN_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_FAN, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_DEFROST_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_SNOWFLAKE_THERMOMETER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_COMPRESSOR_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_HVAC, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_INDOOR_FAN_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_FAN, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_FOUR_WAY_VALVE_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_VALVE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_INDOOR_ELECTRIC_HEATING_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_RADIATOR, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate), + } +).extend({cv.Optional(type): schema for type, schema in SENSOR_TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_HAIER_ID]) + + for type, _ in SENSOR_TYPES.items(): + if conf := config.get(type): + sens = await binary_sensor.new_binary_sensor(conf) + binary_sensor_type = getattr(BinarySensorTypeEnum, type.upper()) + cg.add(paren.set_sub_binary_sensor(binary_sensor_type, sens)) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 1cb8773495..a700be8be2 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv import esphome.final_validate as fv -from esphome.components import uart, sensor, climate, logger +from esphome.components import uart, climate, logger from esphome import automation from esphome.const import ( CONF_BEEPER, @@ -21,10 +21,6 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VISUAL, CONF_WIFI, - DEVICE_CLASS_TEMPERATURE, - ICON_THERMOMETER, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, ) from esphome.components.climate import ( ClimateMode, @@ -42,7 +38,6 @@ PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 PROTOCOL_CONTROL_PACKET_SIZE = 10 CODEOWNERS = ["@paveldn"] -AUTO_LOAD = ["sensor"] DEPENDENCIES = ["climate", "uart"] CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control" CONF_ANSWER_TIMEOUT = "answer_timeout" @@ -58,7 +53,6 @@ CONF_WIFI_SIGNAL = "wifi_signal" PROTOCOL_HON = "HON" PROTOCOL_SMARTAIR2 = "SMARTAIR2" -PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2] haier_ns = cg.esphome_ns.namespace("haier") HaierClimateBase = haier_ns.class_( @@ -67,6 +61,7 @@ HaierClimateBase = haier_ns.class_( HonClimate = haier_ns.class_("HonClimate", HaierClimateBase) Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase) +CONF_HAIER_ID = "haier_id" AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection", True) AIRFLOW_VERTICAL_DIRECTION_OPTIONS = { @@ -239,12 +234,8 @@ CONFIG_SCHEMA = cv.All( ): cv.ensure_list( cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) ), - cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - icon=ICON_THERMOMETER, - accuracy_decimals=0, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + cv.Optional(CONF_OUTDOOR_TEMPERATURE): cv.invalid( + f"The {CONF_OUTDOOR_TEMPERATURE} option is deprecated, use a sensor for a haier platform instead" ), cv.Optional(CONF_ON_ALARM_START): automation.validate_automation( { @@ -463,9 +454,6 @@ async def to_code(config): cg.add(var.set_beeper_state(config[CONF_BEEPER])) if CONF_DISPLAY in config: cg.add(var.set_display_state(config[CONF_DISPLAY])) - if CONF_OUTDOOR_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) - cg.add(var.set_outdoor_temperature_sensor(sens)) if CONF_SUPPORTED_MODES in config: cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) if CONF_SUPPORTED_SWING_MODES in config: diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index e5aa88e2c9..9933cb4c8f 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -2,6 +2,7 @@ #include #include "esphome/components/climate/climate.h" #include "esphome/components/uart/uart.h" +#include "esphome/core/helpers.h" #include "hon_climate.h" #include "hon_packet.h" @@ -51,10 +52,9 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir } HonClimate::HonClimate() - : cleaning_status_(CleaningState::NO_CLEANING), - got_valid_outdoor_temp_(false), - active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - outdoor_sensor_(nullptr) { + : cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00} { last_status_message_ = std::unique_ptr(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]); this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; @@ -66,8 +66,6 @@ void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; } bool HonClimate::get_beeper_state() const { return this->beeper_status_; } -void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } - AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; }; void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { @@ -368,7 +366,14 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage STATUS_REQUEST( haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); - this->send_message_(STATUS_REQUEST, this->use_crc_); + static const haier_protocol::HaierMessage BIG_DATA_REQUEST( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_BIG_DATA); + if ((this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) || + (!this->should_get_big_data_())) { + this->send_message_(STATUS_REQUEST, this->use_crc_); + } else { + this->send_message_(BIG_DATA_REQUEST, this->use_crc_); + } this->last_status_request_ = now; } break; @@ -685,9 +690,87 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo } } +#ifdef USE_SENSOR +void HonClimate::set_sub_sensor(SubSensorType type, sensor::Sensor *sens) { + if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { + if (type >= SubSensorType::BIG_DATA_FRAME_SUB_SENSORS) { + if ((this->sub_sensors_[(size_t) type] != nullptr) && (sens == nullptr)) { + this->big_data_sensors_--; + } else if ((this->sub_sensors_[(size_t) type] == nullptr) && (sens != nullptr)) { + this->big_data_sensors_++; + } + } + this->sub_sensors_[(size_t) type] = sens; + } +} + +void HonClimate::update_sub_sensor_(SubSensorType type, float value) { + if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { + size_t index = (size_t) type; + if ((this->sub_sensors_[index] != nullptr) && + ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value))) + this->sub_sensors_[index]->publish_state(value); + } +} +#endif // USE_SENSOR + +#ifdef USE_BINARY_SENSOR +void HonClimate::set_sub_binary_sensor(SubBinarySensorType type, binary_sensor::BinarySensor *sens) { + if (type < SubBinarySensorType::SUB_BINARY_SENSOR_TYPE_COUNT) { + if ((this->sub_binary_sensors_[(size_t) type] != nullptr) && (sens == nullptr)) { + this->big_data_sensors_--; + } else if ((this->sub_binary_sensors_[(size_t) type] == nullptr) && (sens != nullptr)) { + this->big_data_sensors_++; + } + this->sub_binary_sensors_[(size_t) type] = sens; + } +} + +void HonClimate::update_sub_binary_sensor_(SubBinarySensorType type, uint8_t value) { + if (value < 2) { + bool converted_value = value == 1; + size_t index = (size_t) type; + if ((this->sub_binary_sensors_[index] != nullptr) && ((!this->sub_binary_sensors_[index]->has_state()) || + (this->sub_binary_sensors_[index]->state != converted_value))) + this->sub_binary_sensors_[index]->publish_state(converted_value); + } +} +#endif // USE_BINARY_SENSOR + haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { - if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_) + size_t expected_size = 2 + sizeof(hon_protocol::HaierPacketControl) + sizeof(hon_protocol::HaierPacketSensors) + + this->extra_control_packet_bytes_; + if (size < expected_size) return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; + if ((subtype == 0x7D01) && (size >= expected_size + 4 + sizeof(hon_protocol::HaierPacketBigData))) { + // Got BigData packet + const hon_protocol::HaierPacketBigData *bd_packet = + (const hon_protocol::HaierPacketBigData *) (&packet_buffer[expected_size + 4]); +#ifdef USE_SENSOR + this->update_sub_sensor_(SubSensorType::INDOOR_COIL_TEMPERATURE, bd_packet->indoor_coil_temperature / 2.0 - 20); + this->update_sub_sensor_(SubSensorType::OUTDOOR_COIL_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64); + this->update_sub_sensor_(SubSensorType::OUTDOOR_DEFROST_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64); + this->update_sub_sensor_(SubSensorType::OUTDOOR_IN_AIR_TEMPERATURE, bd_packet->outdoor_in_air_temperature - 64); + this->update_sub_sensor_(SubSensorType::OUTDOOR_OUT_AIR_TEMPERATURE, bd_packet->outdoor_out_air_temperature - 64); + this->update_sub_sensor_(SubSensorType::POWER, encode_uint16(bd_packet->power[0], bd_packet->power[1])); + this->update_sub_sensor_(SubSensorType::COMPRESSOR_FREQUENCY, bd_packet->compressor_frequency); + this->update_sub_sensor_(SubSensorType::COMPRESSOR_CURRENT, + encode_uint16(bd_packet->compressor_current[0], bd_packet->compressor_current[1]) / 10.0); + this->update_sub_sensor_( + SubSensorType::EXPANSION_VALVE_OPEN_DEGREE, + encode_uint16(bd_packet->expansion_valve_open_degree[0], bd_packet->expansion_valve_open_degree[1]) / 4095.0); +#endif // USE_SENSOR +#ifdef USE_BINARY_SENSOR + this->update_sub_binary_sensor_(SubBinarySensorType::OUTDOOR_FAN_STATUS, bd_packet->outdoor_fan_status); + this->update_sub_binary_sensor_(SubBinarySensorType::DEFROST_STATUS, bd_packet->defrost_status); + this->update_sub_binary_sensor_(SubBinarySensorType::COMPRESSOR_STATUS, bd_packet->compressor_status); + this->update_sub_binary_sensor_(SubBinarySensorType::INDOOR_FAN_STATUS, bd_packet->indoor_fan_status); + this->update_sub_binary_sensor_(SubBinarySensorType::FOUR_WAY_VALVE_STATUS, bd_packet->four_way_valve_status); + this->update_sub_binary_sensor_(SubBinarySensorType::INDOOR_ELECTRIC_HEATING_STATUS, + bd_packet->indoor_electric_heating_status); +#endif // USE_BINARY_SENSOR + } struct { hon_protocol::HaierPacketControl control; hon_protocol::HaierPacketSensors sensors; @@ -699,13 +782,17 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * if (packet.sensors.error_status != 0) { ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); } - if ((this->outdoor_sensor_ != nullptr) && +#ifdef USE_SENSOR + if ((this->sub_sensors_[(size_t) SubSensorType::OUTDOOR_TEMPERATURE] != nullptr) && (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { this->got_valid_outdoor_temp_ = true; - float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); - if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) - this->outdoor_sensor_->publish_state(otemp); + this->update_sub_sensor_(SubSensorType::OUTDOOR_TEMPERATURE, + (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET)); } + if ((this->sub_sensors_[(size_t) SubSensorType::HUMIDITY] != nullptr) && (packet.sensors.room_humidity <= 100)) { + this->update_sub_sensor_(SubSensorType::HUMIDITY, (float) packet.sensors.room_humidity); + } +#endif // USE_SENSOR bool should_publish = false; { // Extra modes/presets @@ -1009,21 +1096,22 @@ void HonClimate::fill_control_messages_queue_() { break; } } - if (quiet_mode_buf[1] != 0xFF) { + auto presets = this->traits_.get_supported_presets(); + if ((quiet_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_ECO) != presets.end()))) { this->control_messages_queue_.push( haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2)); } - if (fast_mode_buf[1] != 0xFF) { + if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { this->control_messages_queue_.push( haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2)); } - if (away_mode_buf[1] != 0xFF) { + if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { this->control_messages_queue_.push( haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + @@ -1032,7 +1120,7 @@ void HonClimate::fill_control_messages_queue_() { } } // Target temperature - if (climate_control.target_temperature.has_value()) { + if (climate_control.target_temperature.has_value() && (this->mode != ClimateMode::CLIMATE_MODE_FAN_ONLY)) { uint8_t buffer[2] = {0x00, 0x00}; buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16; this->control_messages_queue_.push( @@ -1119,12 +1207,24 @@ bool HonClimate::prepare_pending_action() { void HonClimate::process_protocol_reset() { HaierClimateBase::process_protocol_reset(); - if (this->outdoor_sensor_ != nullptr) { - this->outdoor_sensor_->publish_state(NAN); +#ifdef USE_SENSOR + for (auto &sub_sensor : this->sub_sensors_) { + if ((sub_sensor != nullptr) && sub_sensor->has_state()) + sub_sensor->publish_state(NAN); } +#endif // USE_SENSOR this->got_valid_outdoor_temp_ = false; this->hvac_hardware_info_.reset(); } +bool HonClimate::should_get_big_data_() { + if (this->big_data_sensors_ > 0) { + static uint8_t counter = 0; + counter = (counter + 1) % 3; + return counter == 1; + } + return false; +} + } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 9c05e59b87..c4fae20a98 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -1,7 +1,12 @@ #pragma once #include +#ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif #include "esphome/core/automation.h" #include "haier_base.h" @@ -34,6 +39,48 @@ enum class CleaningState : uint8_t { enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; class HonClimate : public HaierClimateBase { +#ifdef USE_SENSOR + public: + enum class SubSensorType { + // Used data based sensors + OUTDOOR_TEMPERATURE = 0, + HUMIDITY, + // Big data based sensors + INDOOR_COIL_TEMPERATURE, + OUTDOOR_COIL_TEMPERATURE, + OUTDOOR_DEFROST_TEMPERATURE, + OUTDOOR_IN_AIR_TEMPERATURE, + OUTDOOR_OUT_AIR_TEMPERATURE, + POWER, + COMPRESSOR_FREQUENCY, + COMPRESSOR_CURRENT, + EXPANSION_VALVE_OPEN_DEGREE, + SUB_SENSOR_TYPE_COUNT, + BIG_DATA_FRAME_SUB_SENSORS = INDOOR_COIL_TEMPERATURE, + }; + void set_sub_sensor(SubSensorType type, sensor::Sensor *sens); + + protected: + void update_sub_sensor_(SubSensorType type, float value); + sensor::Sensor *sub_sensors_[(size_t) SubSensorType::SUB_SENSOR_TYPE_COUNT]{nullptr}; +#endif +#ifdef USE_BINARY_SENSOR + public: + enum class SubBinarySensorType { + OUTDOOR_FAN_STATUS = 0, + DEFROST_STATUS, + COMPRESSOR_STATUS, + INDOOR_FAN_STATUS, + FOUR_WAY_VALVE_STATUS, + INDOOR_ELECTRIC_HEATING_STATUS, + SUB_BINARY_SENSOR_TYPE_COUNT, + }; + void set_sub_binary_sensor(SubBinarySensorType type, binary_sensor::BinarySensor *sens); + + protected: + void update_sub_binary_sensor_(SubBinarySensorType type, uint8_t value); + binary_sensor::BinarySensor *sub_binary_sensors_[(size_t) SubBinarySensorType::SUB_BINARY_SENSOR_TYPE_COUNT]{nullptr}; +#endif public: HonClimate(); HonClimate(const HonClimate &) = delete; @@ -42,7 +89,6 @@ class HonClimate : public HaierClimateBase { void dump_config() override; void set_beeper_state(bool state); bool get_beeper_state() const; - void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor); AirflowVerticalDirection get_vertical_airflow() const; void set_vertical_airflow(AirflowVerticalDirection direction); AirflowHorizontalDirection get_horizontal_airflow() const; @@ -64,6 +110,7 @@ class HonClimate : public HaierClimateBase { haier_protocol::HaierMessage get_power_message(bool state) override; bool prepare_pending_action() override; void process_protocol_reset() override; + bool should_get_big_data_(); // Answers handlers haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, @@ -106,12 +153,12 @@ class HonClimate : public HaierClimateBase { uint8_t active_alarms_[8]; int extra_control_packet_bytes_; HonControlMethod control_method_; - esphome::sensor::Sensor *outdoor_sensor_; std::queue control_messages_queue_; CallbackManager alarm_start_callback_{}; CallbackManager alarm_end_callback_{}; float active_alarm_count_{NAN}; std::chrono::steady_clock::time_point last_alarm_request_; + int big_data_sensors_{0}; }; class HaierAlarmStartTrigger : public Trigger { diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h index be1b0ae51c..bbca7bb653 100644 --- a/esphome/components/haier/hon_packet.h +++ b/esphome/components/haier/hon_packet.h @@ -55,18 +55,18 @@ enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, struct HaierPacketControl { // Control bytes starts here - // 10 + // 1 uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C) - // 11 + // 2 uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode uint8_t : 0; - // 12 + // 3 uint8_t fan_mode : 3; // See enum FanMode uint8_t special_mode : 2; // See enum SpecialMode uint8_t ac_mode : 3; // See enum ConditioningMode - // 13 + // 4 uint8_t : 8; - // 14 + // 5 uint8_t ten_degree : 1; // 10 degree status uint8_t display_status : 1; // If 0 disables AC's display uint8_t half_degree : 1; // Use half degree @@ -75,7 +75,7 @@ struct HaierPacketControl { uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius uint8_t : 1; uint8_t steri_clean : 1; - // 15 + // 6 uint8_t ac_power : 1; // Is ac on or off uint8_t health_mode : 1; // Health mode (negative ions) on or off uint8_t electric_heating_status : 1; // Electric heating status @@ -84,16 +84,16 @@ struct HaierPacketControl { uint8_t sleep_mode : 1; // Sleep mode uint8_t lock_remote : 1; // Disable remote uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command) - // 16 + // 7 uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%) - // 17 + // 8 uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode uint8_t : 3; uint8_t human_sensing_status : 2; // Human sensing status - // 18 + // 9 uint8_t change_filter : 1; // Filter need replacement uint8_t : 0; - // 19 + // 10 uint8_t fresh_air_status : 1; // Fresh air status uint8_t humidification_status : 1; // Humidification status uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status @@ -105,40 +105,68 @@ struct HaierPacketControl { }; struct HaierPacketSensors { - // 20 + // 11 uint8_t room_temperature; // 0.5°C step - // 21 + // 12 uint8_t room_humidity; // 0%-100% with 1% step - // 22 + // 13 uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C) - // 23 + // 14 uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad) uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad) uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple) uint8_t : 1; uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only) - // 24 + // 15 uint8_t error_status; // See enum ErrorStatus - // 25 + // 16 uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP) uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan) uint8_t : 3; uint8_t err_confirmation : 1; // If 1 clear error status - // 26 + // 17 uint16_t total_cleaning_time; // Cleaning cumulative time (1h step) - // 28 + // 19 uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) - // 30 + // 21 uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) - // 32 + // 23 uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step) - // 34 + // 25 uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step) - // 36 + // 27 uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) }; -constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors); +struct HaierPacketBigData { + // 29 + uint8_t power[2]; // AC power consumption (0W - 65535W, 1W step) + // 31 + uint8_t indoor_coil_temperature; // 0.5°C step, -20°C offset (0=-20°C) + // 32 + uint8_t outdoor_out_air_temperature; // 1°C step, -64°C offset (0=-64°C) + // 33 + uint8_t outdoor_coil_temperature; // 1°C step, -64°C offset (0=-64°C) + // 34 + uint8_t outdoor_in_air_temperature; // 1°C step, -64°C offset (0=-64°C) + // 35 + uint8_t outdoor_defrost_temperature; // 1°C step, -64°C offset (0=-64°C) + // 36 + uint8_t compressor_frequency; // 1Hz step, 0Hz - 127Hz + // 37 + uint8_t compressor_current[2]; // 0.1A step, 0.0A - 51.1A (0x0000 - 0x01FF) + // 39 + uint8_t outdoor_fan_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t defrost_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t : 0; + // 40 + uint8_t compressor_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t indoor_fan_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t four_way_valve_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t indoor_electric_heating_status : 2; // 0 - off, 1 - on, 2 - information not available + // 41 + uint8_t expansion_valve_open_degree[2]; // 0 - 4095 +}; struct DeviceVersionAnswer { char protocol_version[8]; diff --git a/esphome/components/haier/sensor/__init__.py b/esphome/components/haier/sensor/__init__.py new file mode 100644 index 0000000000..9a4965493d --- /dev/null +++ b/esphome/components/haier/sensor/__init__.py @@ -0,0 +1,151 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_POWER, + CONF_HUMIDITY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_FREQUENCY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_CURRENT_AC, + ICON_FLASH, + ICON_GAUGE, + ICON_HEATING_COIL, + ICON_PULSE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + ICON_WEATHER_WINDY, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_PERCENT, + UNIT_WATT, +) +from ..climate import ( + CONF_HAIER_ID, + HonClimate, +) + +SensorTypeEnum = HonClimate.enum("SubSensorType", True) + +# Haier sensors +CONF_COMPRESSOR_CURRENT = "compressor_current" +CONF_COMPRESSOR_FREQUENCY = "compressor_frequency" +CONF_EXPANSION_VALVE_OPEN_DEGREE = "expansion_valve_open_degree" +CONF_INDOOR_COIL_TEMPERATURE = "indoor_coil_temperature" +CONF_OUTDOOR_COIL_TEMPERATURE = "outdoor_coil_temperature" +CONF_OUTDOOR_DEFROST_TEMPERATURE = "outdoor_defrost_temperature" +CONF_OUTDOOR_IN_AIR_TEMPERATURE = "outdoor_in_air_temperature" +CONF_OUTDOOR_OUT_AIR_TEMPERATURE = "outdoor_out_air_temperature" +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" + +# Additional icons +ICON_SNOWFLAKE_THERMOMETER = "mdi:snowflake-thermometer" + +SENSOR_TYPES = { + CONF_COMPRESSOR_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_COMPRESSOR_FREQUENCY: sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_PULSE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_EXPANSION_VALVE_OPEN_DEGREE: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_GAUGE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_HUMIDITY: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_INDOOR_COIL_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_HEATING_COIL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_COIL_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_HEATING_COIL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_DEFROST_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_SNOWFLAKE_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_IN_AIR_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_WEATHER_WINDY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_OUT_AIR_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_WEATHER_WINDY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_FLASH, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate), + } +).extend({cv.Optional(type): schema for type, schema in SENSOR_TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_HAIER_ID]) + + for type, _ in SENSOR_TYPES.items(): + if conf := config.get(type): + sens = await sensor.new_sensor(conf) + sensor_type = getattr(SensorTypeEnum, type.upper()) + cg.add(paren.set_sub_sensor(sensor_type, sens)) diff --git a/tests/components/haier/test.esp32-c3-idf.yaml b/tests/components/haier/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..72cfb781a7 --- /dev/null +++ b/tests/components/haier/test.esp32-c3-idf.yaml @@ -0,0 +1,95 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_haier + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +climate: + - platform: haier + id: haier_ac + protocol: hOn + name: Haier AC + wifi_signal: true + answer_timeout: 200ms + beeper: true + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 + supported_modes: + - 'OFF' + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH + supported_presets: + - AWAY + - BOOST + - ECO + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [code, message] + +sensor: + - platform: haier + haier_id: haier_ac + outdoor_temperature: + name: Haier outdoor temperature + humidity: + name: Haier Indoor Humidity + compressor_current: + name: Haier Compressor Current + compressor_frequency: + name: Haier Compressor Frequency + expansion_valve_open_degree: + name: Haier Expansion Valve Open Degree + indoor_coil_temperature: + name: Haier Indoor Coil Temperature + outdoor_coil_temperature: + name: Haier Outdoor Coil Temperature + outdoor_defrost_temperature: + name: Haier Outdoor Defrost Temperature + outdoor_in_air_temperature: + name: Haier Outdoor In Air Temperature + outdoor_out_air_temperature: + name: Haier Outdoor Out Air Temperature + power: + name: Haier Power + +binary_sensor: + - platform: haier + haier_id: haier_ac + compressor_status: + name: Haier Outdoor Compressor Status + defrost_status: + name: Haier Defrost Status + four_way_valve_status: + name: Haier Four Way Valve Status + indoor_electric_heating_status: + name: Haier Indoor Electric Heating Status + indoor_fan_status: + name: Haier Indoor Fan Status + outdoor_fan_status: + name: Haier Outdoor Fan Status diff --git a/tests/components/haier/test.esp32-c3.yaml b/tests/components/haier/test.esp32-c3.yaml new file mode 100644 index 0000000000..72cfb781a7 --- /dev/null +++ b/tests/components/haier/test.esp32-c3.yaml @@ -0,0 +1,95 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_haier + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +climate: + - platform: haier + id: haier_ac + protocol: hOn + name: Haier AC + wifi_signal: true + answer_timeout: 200ms + beeper: true + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 + supported_modes: + - 'OFF' + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH + supported_presets: + - AWAY + - BOOST + - ECO + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [code, message] + +sensor: + - platform: haier + haier_id: haier_ac + outdoor_temperature: + name: Haier outdoor temperature + humidity: + name: Haier Indoor Humidity + compressor_current: + name: Haier Compressor Current + compressor_frequency: + name: Haier Compressor Frequency + expansion_valve_open_degree: + name: Haier Expansion Valve Open Degree + indoor_coil_temperature: + name: Haier Indoor Coil Temperature + outdoor_coil_temperature: + name: Haier Outdoor Coil Temperature + outdoor_defrost_temperature: + name: Haier Outdoor Defrost Temperature + outdoor_in_air_temperature: + name: Haier Outdoor In Air Temperature + outdoor_out_air_temperature: + name: Haier Outdoor Out Air Temperature + power: + name: Haier Power + +binary_sensor: + - platform: haier + haier_id: haier_ac + compressor_status: + name: Haier Outdoor Compressor Status + defrost_status: + name: Haier Defrost Status + four_way_valve_status: + name: Haier Four Way Valve Status + indoor_electric_heating_status: + name: Haier Indoor Electric Heating Status + indoor_fan_status: + name: Haier Indoor Fan Status + outdoor_fan_status: + name: Haier Outdoor Fan Status diff --git a/tests/components/haier/test.esp32-idf.yaml b/tests/components/haier/test.esp32-idf.yaml new file mode 100644 index 0000000000..d3eeb04d65 --- /dev/null +++ b/tests/components/haier/test.esp32-idf.yaml @@ -0,0 +1,95 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_haier + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +climate: + - platform: haier + id: haier_ac + protocol: hOn + name: Haier AC + wifi_signal: true + answer_timeout: 200ms + beeper: true + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 + supported_modes: + - 'OFF' + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH + supported_presets: + - AWAY + - BOOST + - ECO + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [code, message] + +sensor: + - platform: haier + haier_id: haier_ac + outdoor_temperature: + name: Haier outdoor temperature + humidity: + name: Haier Indoor Humidity + compressor_current: + name: Haier Compressor Current + compressor_frequency: + name: Haier Compressor Frequency + expansion_valve_open_degree: + name: Haier Expansion Valve Open Degree + indoor_coil_temperature: + name: Haier Indoor Coil Temperature + outdoor_coil_temperature: + name: Haier Outdoor Coil Temperature + outdoor_defrost_temperature: + name: Haier Outdoor Defrost Temperature + outdoor_in_air_temperature: + name: Haier Outdoor In Air Temperature + outdoor_out_air_temperature: + name: Haier Outdoor Out Air Temperature + power: + name: Haier Power + +binary_sensor: + - platform: haier + haier_id: haier_ac + compressor_status: + name: Haier Outdoor Compressor Status + defrost_status: + name: Haier Defrost Status + four_way_valve_status: + name: Haier Four Way Valve Status + indoor_electric_heating_status: + name: Haier Indoor Electric Heating Status + indoor_fan_status: + name: Haier Indoor Fan Status + outdoor_fan_status: + name: Haier Outdoor Fan Status diff --git a/tests/components/haier/test.esp32.yaml b/tests/components/haier/test.esp32.yaml new file mode 100644 index 0000000000..d3eeb04d65 --- /dev/null +++ b/tests/components/haier/test.esp32.yaml @@ -0,0 +1,95 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_haier + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +climate: + - platform: haier + id: haier_ac + protocol: hOn + name: Haier AC + wifi_signal: true + answer_timeout: 200ms + beeper: true + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 + supported_modes: + - 'OFF' + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH + supported_presets: + - AWAY + - BOOST + - ECO + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [code, message] + +sensor: + - platform: haier + haier_id: haier_ac + outdoor_temperature: + name: Haier outdoor temperature + humidity: + name: Haier Indoor Humidity + compressor_current: + name: Haier Compressor Current + compressor_frequency: + name: Haier Compressor Frequency + expansion_valve_open_degree: + name: Haier Expansion Valve Open Degree + indoor_coil_temperature: + name: Haier Indoor Coil Temperature + outdoor_coil_temperature: + name: Haier Outdoor Coil Temperature + outdoor_defrost_temperature: + name: Haier Outdoor Defrost Temperature + outdoor_in_air_temperature: + name: Haier Outdoor In Air Temperature + outdoor_out_air_temperature: + name: Haier Outdoor Out Air Temperature + power: + name: Haier Power + +binary_sensor: + - platform: haier + haier_id: haier_ac + compressor_status: + name: Haier Outdoor Compressor Status + defrost_status: + name: Haier Defrost Status + four_way_valve_status: + name: Haier Four Way Valve Status + indoor_electric_heating_status: + name: Haier Indoor Electric Heating Status + indoor_fan_status: + name: Haier Indoor Fan Status + outdoor_fan_status: + name: Haier Outdoor Fan Status diff --git a/tests/components/haier/test.esp8266.yaml b/tests/components/haier/test.esp8266.yaml new file mode 100644 index 0000000000..72cfb781a7 --- /dev/null +++ b/tests/components/haier/test.esp8266.yaml @@ -0,0 +1,95 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_haier + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +climate: + - platform: haier + id: haier_ac + protocol: hOn + name: Haier AC + wifi_signal: true + answer_timeout: 200ms + beeper: true + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 + supported_modes: + - 'OFF' + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH + supported_presets: + - AWAY + - BOOST + - ECO + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [code, message] + +sensor: + - platform: haier + haier_id: haier_ac + outdoor_temperature: + name: Haier outdoor temperature + humidity: + name: Haier Indoor Humidity + compressor_current: + name: Haier Compressor Current + compressor_frequency: + name: Haier Compressor Frequency + expansion_valve_open_degree: + name: Haier Expansion Valve Open Degree + indoor_coil_temperature: + name: Haier Indoor Coil Temperature + outdoor_coil_temperature: + name: Haier Outdoor Coil Temperature + outdoor_defrost_temperature: + name: Haier Outdoor Defrost Temperature + outdoor_in_air_temperature: + name: Haier Outdoor In Air Temperature + outdoor_out_air_temperature: + name: Haier Outdoor Out Air Temperature + power: + name: Haier Power + +binary_sensor: + - platform: haier + haier_id: haier_ac + compressor_status: + name: Haier Outdoor Compressor Status + defrost_status: + name: Haier Defrost Status + four_way_valve_status: + name: Haier Four Way Valve Status + indoor_electric_heating_status: + name: Haier Indoor Electric Heating Status + indoor_fan_status: + name: Haier Indoor Fan Status + outdoor_fan_status: + name: Haier Outdoor Fan Status diff --git a/tests/components/haier/test.rp2040.yaml b/tests/components/haier/test.rp2040.yaml new file mode 100644 index 0000000000..72cfb781a7 --- /dev/null +++ b/tests/components/haier/test.rp2040.yaml @@ -0,0 +1,95 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_haier + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +climate: + - platform: haier + id: haier_ac + protocol: hOn + name: Haier AC + wifi_signal: true + answer_timeout: 200ms + beeper: true + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 + supported_modes: + - 'OFF' + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH + supported_presets: + - AWAY + - BOOST + - ECO + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [code, message] + +sensor: + - platform: haier + haier_id: haier_ac + outdoor_temperature: + name: Haier outdoor temperature + humidity: + name: Haier Indoor Humidity + compressor_current: + name: Haier Compressor Current + compressor_frequency: + name: Haier Compressor Frequency + expansion_valve_open_degree: + name: Haier Expansion Valve Open Degree + indoor_coil_temperature: + name: Haier Indoor Coil Temperature + outdoor_coil_temperature: + name: Haier Outdoor Coil Temperature + outdoor_defrost_temperature: + name: Haier Outdoor Defrost Temperature + outdoor_in_air_temperature: + name: Haier Outdoor In Air Temperature + outdoor_out_air_temperature: + name: Haier Outdoor Out Air Temperature + power: + name: Haier Power + +binary_sensor: + - platform: haier + haier_id: haier_ac + compressor_status: + name: Haier Outdoor Compressor Status + defrost_status: + name: Haier Defrost Status + four_way_valve_status: + name: Haier Four Way Valve Status + indoor_electric_heating_status: + name: Haier Indoor Electric Heating Status + indoor_fan_status: + name: Haier Indoor Fan Status + outdoor_fan_status: + name: Haier Outdoor Fan Status diff --git a/tests/test3.yaml b/tests/test3.yaml index ec71bdca8b..1286315a7a 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -747,6 +747,31 @@ sensor: temperature: name: Kuntze temperature + - platform: haier + haier_id: haier_climate + compressor_current: + name: Haier AC compressor current + compressor_frequency: + name: Haier AC compressor frequency + expansion_valve_open_degree: + name: Haier AC expansion valve open degree + humidity: + name: Haier AC indoor humidity + indoor_coil_temperature: + name: Haier AC indoor coil temperature + outdoor_coil_temperature: + name: Haier AC outdoor coil temperature + outdoor_defrost_temperature: + name: Haier AC outdoor defrost temperature + outdoor_in_air_temperature: + name: Haier AC outdoor in air temperature + outdoor_out_air_temperature: + name: Haier AC outdoor out air temperature + outdoor_temperature: + name: Haier AC outdoor temperature + power: + name: Haier AC power + time: - platform: homeassistant @@ -813,6 +838,21 @@ binary_sensor: allow_other_uses: true number: 3 + - platform: haier + haier_id: haier_climate + compressor_status: + name: Haier AC compressor status + defrost_status: + name: Haier AC defrost status + four_way_valve_status: + name: Haier AC four-way valve status + indoor_electric_heating_status: + name: Haier AC indoor electric heating status + indoor_fan_status: + name: Haier AC indoor fan status + outdoor_fan_status: + name: Haier AC outdoor fan status + globals: - id: my_global_string type: std::string @@ -1024,14 +1064,13 @@ climate: kd_multiplier: 0.0 deadband_output_averaging_samples: 1 - platform: haier + id: haier_climate protocol: hOn name: Haier AC uart_id: uart_12 wifi_signal: true answer_timeout: 200ms beeper: true - outdoor_temperature: - name: Haier AC outdoor temperature visual: min_temperature: 16 °C max_temperature: 30 °C From 626221c5a88ee6acb0dda6dc55e19e3d3d912b88 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Mon, 4 Mar 2024 16:55:10 -0500 Subject: [PATCH 402/436] Add toggle command to cover web_server endpoint (#6319) --- esphome/components/web_server/web_server.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f87e920f13..b5f1a651e6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -748,6 +748,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa call.set_command_close(); } else if (match.method == "stop") { call.set_command_stop(); + } else if (match.method == "toggle") { + call.set_command_toggle(); } else if (match.method != "set") { request->send(404); return; From 357ac3b85f6cff3c93ec8a3c29daca1e945a4fda Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:02:05 +1300 Subject: [PATCH 403/436] Improv: support connecting to hidden networks (#6322) --- .../esp32_improv/esp32_improv_component.cpp | 2 +- .../improv_serial/improv_serial_component.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index a6037a773c..d90eaac3b6 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -293,7 +293,7 @@ void ESP32ImprovComponent::process_incoming_data_() { this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_scanning(); + wifi::global_wifi_component->start_connecting(sta, false); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 0325bad4df..40297bee68 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -196,7 +196,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_scanning(); + wifi::global_wifi_component->start_connecting(sta, false); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 15a994d8eb..8f46bf29a0 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -661,12 +661,19 @@ void WiFiComponent::retry_connect() { delay(10); if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && - (this->num_retried_ > 5 || this->error_from_callback_)) { - // If retry failed for more than 5 times, let's restart STA - ESP_LOGW(TAG, "Restarting WiFi adapter..."); - this->wifi_mode_(false, {}); - delay(100); // NOLINT - this->num_retried_ = 0; + (this->num_retried_ > 3 || this->error_from_callback_)) { + if (this->num_retried_ > 5) { + // If retry failed for more than 5 times, let's restart STA + ESP_LOGW(TAG, "Restarting WiFi adapter..."); + this->wifi_mode_(false, {}); + delay(100); // NOLINT + this->num_retried_ = 0; + } else { + // Try hidden networks after 3 failed retries + ESP_LOGD(TAG, "Retrying with hidden networks..."); + this->fast_connect_ = true; + this->num_retried_++; + } } else { this->num_retried_++; } From 96446446b2be7dd5e9072f9d7737e57eb00fe871 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 5 Mar 2024 10:40:27 -0800 Subject: [PATCH 404/436] auto load output for now (#6309) Co-authored-by: Samuel Sieb --- esphome/components/speed/fan/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 221e67d6c7..4527e1eed1 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( from .. import speed_ns +AUTO_LOAD = ["output"] + SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( From 01fc0578bdfb37d1dd582e538fff421fb848bea1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 Mar 2024 07:41:18 +1300 Subject: [PATCH 405/436] Add wake word phrase to voice assistant start command (#6290) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 9 +++++++++ esphome/components/api/api_pb2.h | 1 + .../components/micro_wake_word/micro_wake_word.cpp | 2 +- esphome/components/voice_assistant/__init__.py | 6 ++++++ .../components/voice_assistant/voice_assistant.cpp | 2 ++ esphome/components/voice_assistant/voice_assistant.h | 11 ++++++++++- 7 files changed, 30 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 8d79163590..6237ee4a52 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1450,6 +1450,7 @@ message VoiceAssistantRequest { string conversation_id = 2; uint32 flags = 3; VoiceAssistantAudioSettings audio_settings = 4; + string wake_word_phrase = 5; } message VoiceAssistantResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d3aa1fa2bf..2c5e283e3e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6603,6 +6603,10 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite this->audio_settings = value.as_message(); return true; } + case 5: { + this->wake_word_phrase = value.as_string(); + return true; + } default: return false; } @@ -6612,6 +6616,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); buffer.encode_message(4, this->audio_settings); + buffer.encode_string(5, this->wake_word_phrase); } #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantRequest::dump_to(std::string &out) const { @@ -6633,6 +6638,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append(" audio_settings: "); this->audio_settings.dump_to(out); out.append("\n"); + + out.append(" wake_word_phrase: "); + out.append("'").append(this->wake_word_phrase).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ee975c1726..161443e86d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1702,6 +1702,7 @@ class VoiceAssistantRequest : public ProtoMessage { std::string conversation_id{}; uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; + std::string wake_word_phrase{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index f0b3d55a9d..7321e5b05b 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -134,7 +134,7 @@ void MicroWakeWord::loop() { this->set_state_(State::IDLE); if (this->detected_) { this->detected_ = false; - this->wake_word_detected_trigger_->trigger(""); + this->wake_word_detected_trigger_->trigger(this->wake_word_); } } break; diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index b21a5b27da..17bdffd9da 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -42,6 +42,8 @@ CONF_AUTO_GAIN = "auto_gain" CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_VOLUME_MULTIPLIER = "volume_multiplier" +CONF_WAKE_WORD = "wake_word" + voice_assistant_ns = cg.esphome_ns.namespace("voice_assistant") VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component) @@ -285,6 +287,7 @@ VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssis VOICE_ASSISTANT_ACTION_SCHEMA.extend( { cv.Optional(CONF_SILENCE_DETECTION, default=True): cv.boolean, + cv.Optional(CONF_WAKE_WORD): cv.templatable(cv.string), } ), ) @@ -293,6 +296,9 @@ async def voice_assistant_listen_to_code(config, action_id, template_arg, args): await cg.register_parented(var, config[CONF_ID]) if CONF_SILENCE_DETECTION in config: cg.add(var.set_silence_detection(config[CONF_SILENCE_DETECTION])) + if wake_word := config.get(CONF_WAKE_WORD): + templ = await cg.templatable(wake_word, args, cg.std_string) + cg.add(var.set_wake_word(templ)) return var diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 260605c0b4..49b8fdc959 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -215,6 +215,8 @@ void VoiceAssistant::loop() { msg.conversation_id = this->conversation_id_; msg.flags = flags; msg.audio_settings = audio_settings; + msg.wake_word_phrase = this->wake_word_; + this->wake_word_ = ""; if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { ESP_LOGW(TAG, "Could not request start"); diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index f0ee793f53..14352bf3ae 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -124,6 +124,8 @@ class VoiceAssistant : public Component { void client_subscription(api::APIConnection *client, bool subscribe); api::APIConnection *get_api_connection() const { return this->api_client_; } + void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; } + protected: int read_microphone_(); void set_state_(State state); @@ -175,6 +177,8 @@ class VoiceAssistant : public Component { std::string conversation_id_{""}; + std::string wake_word_{""}; + HighFrequencyLoopRequester high_freq_; #ifdef USE_ESP_ADF @@ -200,8 +204,13 @@ class VoiceAssistant : public Component { }; template class StartAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, wake_word); + public: - void play(Ts... x) override { this->parent_->request_start(false, this->silence_detection_); } + void play(Ts... x) override { + this->parent_->set_wake_word(this->wake_word_.value(x...)); + this->parent_->request_start(false, this->silence_detection_); + } void set_silence_detection(bool silence_detection) { this->silence_detection_ = silence_detection; } From b3ff23ec76b2c4ce5f4072f1977da86741ec0a53 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:09:45 +1300 Subject: [PATCH 406/436] Merge pull request from GHSA-9p43-hj5j-96h5 --- esphome/dashboard/web_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 8bcc8efa0b..de3fe6e8ae 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -820,6 +820,7 @@ class EditRequestHandler(BaseHandler): None, self._read_file, filename, configuration ) if content is not None: + self.set_header("Content-Type", "application/yaml") self.write(content) def _read_file(self, filename: str, configuration: str) -> bytes | None: From 13736b5c5777d37470da8696163a3a7dd69c421b Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Wed, 6 Mar 2024 23:26:39 +0100 Subject: [PATCH 407/436] Update mDNS for IDF >= 5.0 (#6328) --- esphome/components/mdns/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 9efefd58cc..82cf087fdc 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -88,7 +88,7 @@ async def to_code(config): add_idf_component( name="mdns", repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.2.2", + ref="mdns-v1.2.5", path="components/mdns", ) From 90f416bd0d8cd2226aa17df8cc431e72a9392e79 Mon Sep 17 00:00:00 2001 From: sandronidi Date: Sat, 9 Mar 2024 03:16:21 +0100 Subject: [PATCH 408/436] DFPlayer: refix Bug created with PR 4758 (#5861) --- esphome/components/dfplayer/dfplayer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 39a30d035e..aa2dc260e0 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -7,10 +7,10 @@ namespace dfplayer { static const char *const TAG = "dfplayer"; void DFPlayer::play_folder(uint16_t folder, uint16_t file) { - if (folder <= 10 && file <= 1000) { + if (folder < 100 && file < 256) { this->ack_set_is_playing_ = true; this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); - } else if (folder < 100 && file < 256) { + } else if (folder <= 15 && file <= 3000) { this->ack_set_is_playing_ = true; this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); } else { From 0bc645ded7f73715dfc85b325c79310015d493dd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 10 Mar 2024 14:08:58 +1100 Subject: [PATCH 409/436] Fix build failures on host platform caused by #6167 (#6338) * Fix build failures for logger component on host platform * Add climits header * Restore logger functionality on host * Install libsodium in ci --- .github/workflows/ci.yml | 3 +++ esphome/components/debug/debug_component.cpp | 3 +++ esphome/components/logger/__init__.py | 2 ++ esphome/components/logger/logger.cpp | 2 ++ esphome/components/logger/logger.h | 2 ++ esphome/components/logger/logger_host.cpp | 2 ++ tests/components/debug/test.host.yaml | 1 + .../build_components_base.host.yaml | 18 ++++++++++++++++++ 8 files changed, 33 insertions(+) create mode 100644 tests/components/debug/test.host.yaml create mode 100644 tests/test_build_components/build_components_base.host.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b4aafb1dc..1794aeee89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -433,6 +433,9 @@ jobs: matrix: file: ${{ fromJson(needs.list-components.outputs.matrix) }} steps: + - name: Install libsodium + run: sudo apt-get install libsodium-dev + - name: Check out code from GitHub uses: actions/checkout@v4.1.1 - name: Restore Python diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index fe66220ead..f22a8a2e5d 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -6,6 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/version.h" #include +#include #ifdef USE_ESP32 @@ -49,6 +50,8 @@ static uint32_t get_free_heap() { return rp2040.getFreeHeap(); #elif defined(USE_LIBRETINY) return lt_heap_get_free(); +#elif defined(USE_HOST) + return INT_MAX; #endif } diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index fe887a392f..c05f3d54aa 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -144,6 +144,8 @@ def uart_selection(value): component = get_libretiny_component() if component in UART_SELECTION_LIBRETINY: return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) + if CORE.is_host: + raise cv.Invalid("Uart selection not valid for host platform") raise NotImplementedError diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index a109368ea9..740b12adc1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -146,8 +146,10 @@ const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DE void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:"); ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); +#ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_); ESP_LOGCONFIG(TAG, " Hardware UART: %s", get_uart_selection_()); +#endif for (auto &it : this->log_levels_) { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index d70e895985..9aabe88963 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -137,7 +137,9 @@ class Logger : public Component { va_end(arg); } +#ifndef USE_HOST const char *get_uart_selection_(); +#endif uint32_t baud_rate_; char *tx_buffer_{nullptr}; diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 88c13bba7b..edffb1a8c3 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -16,6 +16,8 @@ void HOT Logger::write_msg_(const char *msg) { puts(msg); } +void Logger::pre_setup() { global_logger = this; } + } // namespace logger } // namespace esphome diff --git a/tests/components/debug/test.host.yaml b/tests/components/debug/test.host.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/test.host.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/test_build_components/build_components_base.host.yaml b/tests/test_build_components/build_components_base.host.yaml new file mode 100644 index 0000000000..00b252da2d --- /dev/null +++ b/tests/test_build_components/build_components_base.host.yaml @@ -0,0 +1,18 @@ +esphome: + name: componenttesthost + friendly_name: $component_name + +host: + mac_address: "62:23:45:AF:B3:DD" + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file From 4ec2b37cc6f5304320c4991b9035083f3d78aac6 Mon Sep 17 00:00:00 2001 From: rafalw <59505711+rafalw1277@users.noreply.github.com> Date: Sun, 10 Mar 2024 04:14:57 +0100 Subject: [PATCH 410/436] Update bang_bang to log two decimal places in config dump (#6304) change of precision to two decimal places --- esphome/components/bang_bang/bang_bang_climate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 13b0cd2a09..b34764907f 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -194,8 +194,8 @@ void BangBangClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.2f°C", this->normal_config_.default_temperature_low); + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.2f°C", this->normal_config_.default_temperature_high); } BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default; From 1e96a19d09e5713505861db2a750101e949095f5 Mon Sep 17 00:00:00 2001 From: RFDarter Date: Sun, 10 Mar 2024 19:52:22 +0100 Subject: [PATCH 411/436] Add datetime date entities (#6191) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/codegen.py | 1 + esphome/components/api/api.proto | 43 ++++ esphome/components/api/api_connection.cpp | 37 +++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 219 ++++++++++++++++++ esphome/components/api/api_pb2.h | 50 ++++ esphome/components/api/api_pb2_service.cpp | 42 ++++ esphome/components/api/api_pb2_service.h | 15 ++ esphome/components/api/api_server.cpp | 9 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 10 +- esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.cpp | 3 + esphome/components/api/subscribe_state.h | 3 + esphome/components/datetime/__init__.py | 146 ++++++++++++ esphome/components/datetime/date_entity.cpp | 117 ++++++++++ esphome/components/datetime/date_entity.h | 117 ++++++++++ esphome/components/datetime/datetime_base.h | 34 +++ esphome/components/mqtt/__init__.py | 1 + esphome/components/mqtt/mqtt_date.cpp | 68 ++++++ esphome/components/mqtt/mqtt_date.h | 45 ++++ .../components/template/datetime/__init__.py | 94 ++++++++ .../template/datetime/template_date.cpp | 111 +++++++++ .../template/datetime/template_date.h | 46 ++++ esphome/components/time/__init__.py | 1 - .../components/web_server/list_entities.cpp | 7 + esphome/components/web_server/list_entities.h | 3 + esphome/components/web_server/web_server.cpp | 60 +++++ esphome/components/web_server/web_server.h | 9 + esphome/config_validation.py | 122 ++++++++-- esphome/const.py | 4 + esphome/core/application.h | 19 ++ esphome/core/component_iterator.cpp | 15 ++ esphome/core/component_iterator.h | 6 + esphome/core/controller.cpp | 8 +- esphome/core/controller.h | 6 + esphome/core/defines.h | 2 + esphome/core/time.cpp | 45 +++- esphome/core/time.h | 11 + esphome/cpp_types.py | 1 + script/ci-custom.py | 1 + tests/components/datetime/date.all.yaml | 1 + tests/components/template/test.all.yaml | 30 ++- 44 files changed, 1553 insertions(+), 22 deletions(-) create mode 100644 esphome/components/datetime/__init__.py create mode 100644 esphome/components/datetime/date_entity.cpp create mode 100644 esphome/components/datetime/date_entity.h create mode 100644 esphome/components/datetime/datetime_base.h create mode 100644 esphome/components/mqtt/mqtt_date.cpp create mode 100644 esphome/components/mqtt/mqtt_date.h create mode 100644 esphome/components/template/datetime/__init__.py create mode 100644 esphome/components/template/datetime/template_date.cpp create mode 100644 esphome/components/template/datetime/template_date.h create mode 100644 tests/components/datetime/date.all.yaml diff --git a/CODEOWNERS b/CODEOWNERS index de1f3253d3..43ce6e4a77 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -84,6 +84,7 @@ esphome/components/dac7678/* @NickB1 esphome/components/daikin_brc/* @hagak esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core +esphome/components/datetime/* @rfdarter esphome/components/debug/* @OttoWinter esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet @@ -338,6 +339,7 @@ esphome/components/tcl112/* @glmnet esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar +esphome/components/template/datetime/* @rfdarter esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter diff --git a/esphome/codegen.py b/esphome/codegen.py index 43b44256e2..dc17f28a03 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -87,4 +87,5 @@ from esphome.cpp_types import ( # noqa gpio_Flags, EntityCategory, Parented, + ESPTime, ) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 6237ee4a52..7efc7aef64 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -44,6 +44,7 @@ service APIConnection { rpc button_command (ButtonCommandRequest) returns (void) {} rpc lock_command (LockCommandRequest) returns (void) {} rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} + rpc date_command (DateCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} @@ -1598,3 +1599,45 @@ message TextCommandRequest { fixed32 key = 1; string state = 2; } + + +// ==================== DATETIME DATE ==================== +message ListEntitiesDateResponse { + option (id) = 100; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; +} +message DateStateResponse { + option (id) = 101; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATE"; + option (no_delay) = true; + + fixed32 key = 1; + // If the date does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + uint32 year = 3; + uint32 month = 4; + uint32 day = 5; +} +message DateCommandRequest { + option (id) = 102; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_DATE"; + option (no_delay) = true; + + fixed32 key = 1; + uint32 year = 2; + uint32 month = 3; + uint32 day = 4; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d3bfdcebb1..bd4790df95 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -698,6 +698,43 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { } #endif +#ifdef USE_DATETIME_DATE +bool APIConnection::send_date_state(datetime::DateEntity *date) { + if (!this->state_subscription_) + return false; + + DateStateResponse resp{}; + resp.key = date->get_object_id_hash(); + resp.missing_state = !date->has_state(); + resp.year = date->year; + resp.month = date->month; + resp.day = date->day; + return this->send_date_state_response(resp); +} +bool APIConnection::send_date_info(datetime::DateEntity *date) { + ListEntitiesDateResponse msg; + msg.key = date->get_object_id_hash(); + msg.object_id = date->get_object_id(); + if (date->has_own_name()) + msg.name = date->get_name(); + msg.unique_id = get_default_unique_id("date", date); + msg.icon = date->get_icon(); + msg.disabled_by_default = date->is_disabled_by_default(); + msg.entity_category = static_cast(date->get_entity_category()); + + return this->send_list_entities_date_response(msg); +} +void APIConnection::date_command(const DateCommandRequest &msg) { + datetime::DateEntity *date = App.get_date_by_key(msg.key); + if (date == nullptr) + return; + + auto call = date->make_call(); + call.set_date(msg.year, msg.month, msg.day); + call.perform(); +} +#endif + #ifdef USE_TEXT bool APIConnection::send_text_state(text::Text *text, std::string state) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 9d01468807..6fe6e0d509 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -72,6 +72,11 @@ class APIConnection : public APIServerConnection { bool send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; #endif +#ifdef USE_DATETIME_DATE + bool send_date_state(datetime::DateEntity *date); + bool send_date_info(datetime::DateEntity *date); + void date_command(const DateCommandRequest &msg) override; +#endif #ifdef USE_TEXT bool send_text_state(text::Text *text, std::string state); bool send_text_info(text::Text *text); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 2c5e283e3e..32654f3148 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -7184,6 +7184,225 @@ void TextCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesDateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesDateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesDateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesDateResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + out.append("}"); +} +#endif +bool DateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->missing_state = value.as_bool(); + return true; + } + case 3: { + this->year = value.as_uint32(); + return true; + } + case 4: { + this->month = value.as_uint32(); + return true; + } + case 5: { + this->day = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool DateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->missing_state); + buffer.encode_uint32(3, this->year); + buffer.encode_uint32(4, this->month); + buffer.encode_uint32(5, this->day); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" year: "); + sprintf(buffer, "%" PRIu32, this->year); + out.append(buffer); + out.append("\n"); + + out.append(" month: "); + sprintf(buffer, "%" PRIu32, this->month); + out.append(buffer); + out.append("\n"); + + out.append(" day: "); + sprintf(buffer, "%" PRIu32, this->day); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->year = value.as_uint32(); + return true; + } + case 3: { + this->month = value.as_uint32(); + return true; + } + case 4: { + this->day = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_uint32(2, this->year); + buffer.encode_uint32(3, this->month); + buffer.encode_uint32(4, this->day); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" year: "); + sprintf(buffer, "%" PRIu32, this->year); + out.append(buffer); + out.append("\n"); + + out.append(" month: "); + sprintf(buffer, "%" PRIu32, this->month); + out.append(buffer); + out.append("\n"); + + out.append(" day: "); + sprintf(buffer, "%" PRIu32, this->day); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 161443e86d..f9847a6a19 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1850,6 +1850,56 @@ class TextCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +class ListEntitiesDateResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DateStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool missing_state{false}; + uint32_t year{0}; + uint32_t month{0}; + uint32_t day{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DateCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + uint32_t year{0}; + uint32_t month{0}; + uint32_t day{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4c49c09c8e..4e61893bae 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -513,6 +513,24 @@ bool APIServerConnectionBase::send_text_state_response(const TextStateResponse & #endif #ifdef USE_TEXT #endif +#ifdef USE_DATETIME_DATE +bool APIServerConnectionBase::send_list_entities_date_response(const ListEntitiesDateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_date_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 100); +} +#endif +#ifdef USE_DATETIME_DATE +bool APIServerConnectionBase::send_date_state_response(const DateStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_date_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 101); +} +#endif +#ifdef USE_DATETIME_DATE +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -942,6 +960,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str()); #endif this->on_text_command_request(msg); +#endif + break; + } + case 102: { +#ifdef USE_DATETIME_DATE + DateCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str()); +#endif + this->on_date_command_request(msg); #endif break; } @@ -1218,6 +1247,19 @@ void APIServerConnection::on_media_player_command_request(const MediaPlayerComma this->media_player_command(msg); } #endif +#ifdef USE_DATETIME_DATE +void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->date_command(msg); +} +#endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 20639fc139..a3c53a7534 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -257,6 +257,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_TEXT virtual void on_text_command_request(const TextCommandRequest &value){}; +#endif +#ifdef USE_DATETIME_DATE + bool send_list_entities_date_response(const ListEntitiesDateResponse &msg); +#endif +#ifdef USE_DATETIME_DATE + bool send_date_state_response(const DateStateResponse &msg); +#endif +#ifdef USE_DATETIME_DATE + virtual void on_date_command_request(const DateCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -312,6 +321,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_MEDIA_PLAYER virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; #endif +#ifdef USE_DATETIME_DATE + virtual void date_command(const DateCommandRequest &msg) = 0; +#endif #ifdef USE_BLUETOOTH_PROXY virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; #endif @@ -398,6 +410,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_MEDIA_PLAYER void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; #endif +#ifdef USE_DATETIME_DATE + void on_date_command_request(const DateCommandRequest &msg) override; +#endif #ifdef USE_BLUETOOTH_PROXY void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 8df860bb09..b17555bb49 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -255,6 +255,15 @@ void APIServer::on_number_update(number::Number *obj, float state) { } #endif +#ifdef USE_DATETIME_DATE +void APIServer::on_date_update(datetime::DateEntity *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_date_state(obj); +} +#endif + #ifdef USE_TEXT void APIServer::on_text_update(text::Text *obj, const std::string &state) { if (obj->is_internal()) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 9605a196b3..c12355cc8b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -66,6 +66,9 @@ class APIServer : public Component, public Controller { #ifdef USE_NUMBER void on_number_update(number::Number *obj, float state) override; #endif +#ifdef USE_DATETIME_DATE + void on_date_update(datetime::DateEntity *obj) override; +#endif #ifdef USE_TEXT void on_text_update(text::Text *obj, const std::string &state) override; #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fe359143b1..cd1841de5e 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -1,8 +1,8 @@ #include "list_entities.h" -#include "esphome/core/util.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" #include "api_connection.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" namespace esphome { namespace api { @@ -60,6 +60,10 @@ bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this-> bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } #endif +#ifdef USE_DATETIME_DATE +bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_info(date); } +#endif + #ifdef USE_TEXT bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); } #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index e19c0d99f0..b49867048d 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -46,6 +46,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_NUMBER bool on_number(number::Number *number) override; #endif +#ifdef USE_DATETIME_DATE + bool on_date(datetime::DateEntity *date) override; +#endif #ifdef USE_TEXT bool on_text(text::Text *text) override; #endif diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index aff156cafd..4e7216ddef 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -42,6 +42,9 @@ bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number, number->state); } #endif +#ifdef USE_DATETIME_DATE +bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } +#endif #ifdef USE_TEXT bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text, text->state); } #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 1f27387cf2..4a96659b76 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -43,6 +43,9 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_NUMBER bool on_number(number::Number *number) override; #endif +#ifdef USE_DATETIME_DATE + bool on_date(datetime::DateEntity *date) override; +#endif #ifdef USE_TEXT bool on_text(text::Text *text) override; #endif diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py new file mode 100644 index 0000000000..3ae99cfff6 --- /dev/null +++ b/esphome/components/datetime/__init__.py @@ -0,0 +1,146 @@ +import esphome.codegen as cg + +# import cpp_generator as cpp +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_ID, + CONF_ON_VALUE, + CONF_TRIGGER_ID, + CONF_TYPE, + CONF_MQTT_ID, + CONF_DATE, + CONF_YEAR, + CONF_MONTH, + CONF_DAY, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity + + +CODEOWNERS = ["@rfdarter"] + +IS_PLATFORM_COMPONENT = True + +datetime_ns = cg.esphome_ns.namespace("datetime") +DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase) +DateEntity = datetime_ns.class_("DateEntity", DateTimeBase) + +# Actions +DateSetAction = datetime_ns.class_("DateSetAction", automation.Action) + +DateTimeStateTrigger = datetime_ns.class_( + "DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime) +) + +DATETIME_MODES = [ + "DATE", + "TIME", + "DATETIME", +] + + +_DATETIME_SCHEMA = cv.Schema( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDatetimeComponent), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger), + } + ), + } +).extend(cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)) + + +def date_schema(class_: MockObjClass) -> cv.Schema: + schema = { + cv.GenerateID(): cv.declare_id(class_), + cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True), + } + return _DATETIME_SCHEMA.extend(schema) + + +def time_schema(class_: MockObjClass) -> cv.Schema: + schema = { + cv.GenerateID(): cv.declare_id(class_), + cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True), + } + return _DATETIME_SCHEMA.extend(schema) + + +def datetime_schema(class_: MockObjClass) -> cv.Schema: + schema = { + cv.GenerateID(): cv.declare_id(class_), + cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of("DATETIME", upper=True), + } + return _DATETIME_SCHEMA.extend(schema) + + +async def setup_datetime_core_(var, config): + await setup_entity(var, config) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf) + + +async def register_datetime(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(getattr(cg.App, f"register_{config[CONF_TYPE].lower()}")(var)) + await setup_datetime_core_(var, config) + cg.add_define(f"USE_DATETIME_{config[CONF_TYPE]}") + + +async def new_datetime(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_datetime(var, config) + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_DATETIME") + cg.add_global(datetime_ns.using) + + +OPERATION_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(DateEntity), + } +) + + +@automation.register_action( + "datetime.date.set", + DateSetAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_DATE): cv.Any( + cv.returning_lambda, cv.date_time(allowed_time=False) + ), + } + ), +) +async def datetime_date_set_to_code(config, action_id, template_arg, args): + action_var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(action_var, config[CONF_ID]) + + date = config[CONF_DATE] + if cg.is_template(date): + template_ = await cg.templatable(config[CONF_DATE], [], cg.ESPTime) + cg.add(action_var.set_date(template_)) + else: + date_struct = cg.StructInitializer( + cg.ESPTime, + ("day_of_month", date[CONF_DAY]), + ("month", date[CONF_MONTH]), + ("year", date[CONF_YEAR]), + ) + cg.add(action_var.set_date(date_struct)) + return action_var diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp new file mode 100644 index 0000000000..8b58a8faf7 --- /dev/null +++ b/esphome/components/datetime/date_entity.cpp @@ -0,0 +1,117 @@ +#include "date_entity.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/core/log.h" + +namespace esphome { +namespace datetime { + +static const char *const TAG = "datetime.date_entity"; + +void DateEntity::publish_state() { + if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { + this->has_state_ = false; + return; + } + if (this->year_ < 1970 || this->year_ > 3000) { + this->has_state_ = false; + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + return; + } + if (this->month_ < 1 || this->month_ > 12) { + this->has_state_ = false; + ESP_LOGE(TAG, "Month must be between 1 and 12"); + return; + } + if (this->day_ > days_in_month(this->month_, this->year_)) { + this->has_state_ = false; + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); + return; + } + this->has_state_ = true; + ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + this->state_callback_.call(); +} + +DateCall DateEntity::make_call() { return DateCall(this); } + +void DateCall::validate_() { + if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + this->year_.reset(); + } + if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) { + ESP_LOGE(TAG, "Month must be between 1 and 12"); + this->month_.reset(); + } + if (this->day_.has_value()) { + uint16_t year = 0; + uint8_t month = 0; + if (this->month_.has_value()) { + month = *this->month_; + } else { + if (this->parent_->month != 0) { + month = this->parent_->month; + } else { + ESP_LOGE(TAG, "Month must be set to validate day"); + this->day_.reset(); + } + } + if (this->year_.has_value()) { + year = *this->year_; + } else { + if (this->parent_->year != 0) { + year = this->parent_->year; + } else { + ESP_LOGE(TAG, "Year must be set to validate day"); + this->day_.reset(); + } + } + if (this->day_.has_value() && *this->day_ > days_in_month(month, year)) { + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(month, year), month); + this->day_.reset(); + } + } +} + +void DateCall::perform() { + this->validate_(); + this->parent_->control(*this); +} + +DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) { + this->year_ = year; + this->month_ = month; + this->day_ = day; + return *this; +}; + +DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); }; + +DateCall &DateCall::set_date(const std::string &date) { + ESPTime val{}; + if (!ESPTime::strptime(date, val)) { + ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object"); + return *this; + } + return this->set_date(val); +} + +DateCall DateEntityRestoreState::to_call(DateEntity *date) { + DateCall call = date->make_call(); + call.set_date(this->year, this->month, this->day); + return call; +} + +void DateEntityRestoreState::apply(DateEntity *date) { + date->year_ = this->year; + date->month_ = this->month; + date->day_ = this->day; + date->publish_state(); +} + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h new file mode 100644 index 0000000000..ce43c5639d --- /dev/null +++ b/esphome/components/datetime/date_entity.h @@ -0,0 +1,117 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/core/time.h" + +#include "datetime_base.h" + +namespace esphome { +namespace datetime { + +#define LOG_DATETIME_DATE(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +class DateCall; +class DateEntity; + +struct DateEntityRestoreState { + uint16_t year; + uint8_t month; + uint8_t day; + + DateCall to_call(DateEntity *date); + void apply(DateEntity *date); +} __attribute__((packed)); + +class DateEntity : public DateTimeBase { + protected: + uint16_t year_; + uint8_t month_; + uint8_t day_; + + public: + void publish_state(); + DateCall make_call(); + + ESPTime state_as_esptime() const override { + ESPTime obj; + obj.year = this->year_; + obj.month = this->month_; + obj.day_of_month = this->day_; + return obj; + } + + const uint16_t &year = year_; + const uint8_t &month = month_; + const uint8_t &day = day_; + + protected: + friend class DateCall; + friend struct DateEntityRestoreState; + + virtual void control(const DateCall &call) = 0; +}; + +class DateCall { + public: + explicit DateCall(DateEntity *parent) : parent_(parent) {} + void perform(); + DateCall &set_date(uint16_t year, uint8_t month, uint8_t day); + DateCall &set_date(ESPTime time); + DateCall &set_date(const std::string &date); + + DateCall &set_year(uint16_t year) { + this->year_ = year; + return *this; + } + DateCall &set_month(uint8_t month) { + this->month_ = month; + return *this; + } + DateCall &set_day(uint8_t day) { + this->day_ = day; + return *this; + } + + optional get_year() const { return this->year_; } + optional get_month() const { return this->month_; } + optional get_day() const { return this->day_; } + + protected: + void validate_(); + + DateEntity *parent_; + + optional year_; + optional month_; + optional day_; +}; + +template class DateSetAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(ESPTime, date) + + void play(Ts... x) override { + auto call = this->parent_->make_call(); + + if (this->date_.has_value()) { + call.set_date(this->date_.value(x...)); + } + call.perform(); + } +}; + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h new file mode 100644 index 0000000000..2f2d27e102 --- /dev/null +++ b/esphome/components/datetime/datetime_base.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/time.h" + +namespace esphome { +namespace datetime { + +class DateTimeBase : public EntityBase { + public: + /// Return whether this Datetime has gotten a full state yet. + bool has_state() const { return this->has_state_; } + + virtual ESPTime state_as_esptime() const = 0; + + void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + + protected: + CallbackManager state_callback_; + + bool has_state_{false}; +}; + +class DateTimeStateTrigger : public Trigger { + public: + explicit DateTimeStateTrigger(DateTimeBase *parent) { + parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); + } +}; + +} // namespace datetime +} // namespace esphome diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 02184c8a39..f804aee31a 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -113,6 +113,7 @@ MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent) MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) +MQTTDatetimeComponent = mqtt_ns.class_("MQTTDatetimeComponent", MQTTComponent) MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent) MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp new file mode 100644 index 0000000000..088a4788ed --- /dev/null +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -0,0 +1,68 @@ +#include "mqtt_date.h" + +#include +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_DATE + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.datetime"; + +using namespace esphome::datetime; + +MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {} + +void MQTTDateComponent::setup() { + this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { + auto call = this->date_->make_call(); + if (root.containsKey("year")) { + call.set_year(root["year"]); + } + if (root.containsKey("month")) { + call.set_month(root["month"]); + } + if (root.containsKey("day")) { + call.set_day(root["day"]); + } + call.perform(); + }); + this->date_->add_on_state_callback( + [this]() { this->publish_state(this->date_->year, this->date_->month, this->date_->day); }); +} + +void MQTTDateComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) +} + +std::string MQTTDateComponent::component_type() const { return "date"; } +const EntityBase *MQTTDateComponent::get_entity() const { return this->date_; } + +void MQTTDateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // Nothing extra to add here +} +bool MQTTDateComponent::send_initial_state() { + if (this->date_->has_state()) { + return this->publish_state(this->date_->year, this->date_->month, this->date_->day); + } else { + return true; + } +} +bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { + return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { + root["year"] = year; + root["month"] = month; + root["day"] = day; + }); +} + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATE +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_date.h b/esphome/components/mqtt/mqtt_date.h new file mode 100644 index 0000000000..2776893d32 --- /dev/null +++ b/esphome/components/mqtt/mqtt_date.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_DATE + +#include "esphome/components/datetime/date_entity.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTDateComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTDatetimeComponent instance with the provided friendly_name and datetime + * + * @param datetime The datetime component. + */ + explicit MQTTDateComponent(datetime::DateEntity *date); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(uint16_t year, uint8_t month, uint8_t day); + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + datetime::DateEntity *date_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATE +#endif // USE_MQTT diff --git a/esphome/components/template/datetime/__init__.py b/esphome/components/template/datetime/__init__.py new file mode 100644 index 0000000000..034be9b3b8 --- /dev/null +++ b/esphome/components/template/datetime/__init__.py @@ -0,0 +1,94 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import datetime +from esphome.const import ( + CONF_INITIAL_VALUE, + CONF_LAMBDA, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, + CONF_SET_ACTION, +) + +from esphome.core import coroutine_with_priority +from .. import template_ns + +CODEOWNERS = ["@rfdarter"] + + +TemplateDate = template_ns.class_( + "TemplateDate", datetime.DateEntity, cg.PollingComponent +) + + +def validate(config): + config = config.copy() + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_VALUE in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + else: + if CONF_RESTORE_VALUE not in config: + config[CONF_RESTORE_VALUE] = False + + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the date and time being set." + ) + return config + + +_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } +).extend(cv.polling_component_schema("60s")) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "DATE": datetime.date_schema(TemplateDate) + .extend(_BASE_SCHEMA) + .extend( + { + cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_time=False), + } + ), + }, + upper=True, + ), + validate, +) + + +@coroutine_with_priority(-100.0) +async def to_code(config): + var = await datetime.new_datetime(config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.ESPTime) + ) + cg.add(var.set_template(template_)) + + else: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + if initial_value := config.get(CONF_INITIAL_VALUE): + cg.add(var.set_initial_value(initial_value)) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), + [(cg.ESPTime, "x")], + config[CONF_SET_ACTION], + ) + + await cg.register_component(var, config) diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp new file mode 100644 index 0000000000..01e15e532e --- /dev/null +++ b/esphome/components/template/datetime/template_date.cpp @@ -0,0 +1,111 @@ +#include "template_date.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.date"; + +void TemplateDate::setup() { + if (this->f_.has_value()) + return; + + ESPTime state{}; + + if (!this->restore_value_) { + state = this->initial_value_; + } else { + datetime::DateEntityRestoreState temp; + this->pref_ = + global_preferences->make_preference(194434030U ^ this->get_object_id_hash()); + if (this->pref_.load(&temp)) { + temp.apply(this); + return; + } else { + // set to inital value if loading from pref failed + state = this->initial_value_; + } + } + + this->year_ = state.year; + this->month_ = state.month; + this->day_ = state.day_of_month; + this->publish_state(); +} + +void TemplateDate::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->publish_state(); +} + +void TemplateDate::control(const datetime::DateCall &call) { + bool has_year = call.get_year().has_value(); + bool has_month = call.get_month().has_value(); + bool has_day = call.get_day().has_value(); + + ESPTime value = {}; + if (has_year) + value.year = *call.get_year(); + + if (has_month) + value.month = *call.get_month(); + + if (has_day) + value.day_of_month = *call.get_day(); + + this->set_trigger_->trigger(value); + + if (this->optimistic_) { + if (has_year) + this->year_ = *call.get_year(); + if (has_month) + this->month_ = *call.get_month(); + if (has_day) + this->day_ = *call.get_day(); + this->publish_state(); + } + + if (this->restore_value_) { + datetime::DateEntityRestoreState temp = {}; + if (has_year) { + temp.year = *call.get_year(); + } else { + temp.year = this->year_; + } + if (has_month) { + temp.month = *call.get_month(); + } else { + temp.month = this->month_; + } + if (has_day) { + temp.day = *call.get_day(); + } else { + temp.day = this->day_; + } + + this->pref_.save(&temp); + } +} + +void TemplateDate::dump_config() { + LOG_DATETIME_DATE("", "Template Date", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h new file mode 100644 index 0000000000..185c7ed49d --- /dev/null +++ b/esphome/components/template/datetime/template_date.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/components/datetime/date_entity.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/core/time.h" + +namespace esphome { +namespace template_ { + +class TemplateDate : public datetime::DateEntity, public PollingComponent { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + + void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const datetime::DateCall &call) override; + + bool optimistic_{false}; + ESPTime initial_value_{}; + bool restore_value_{false}; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index b2be11611d..2bb3a0cd63 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -37,7 +37,6 @@ time_ns = cg.esphome_ns.namespace("time") RealTimeClock = time_ns.class_("RealTimeClock", cg.PollingComponent) CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Component) SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component) -ESPTime = time_ns.struct("ESPTime") TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 5c9009c5da..197af1eb14 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -82,6 +82,13 @@ bool ListEntitiesIterator::on_number(number::Number *number) { } #endif +#ifdef USE_DATETIME_DATE +bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { + this->web_server_->events_.send(this->web_server_->date_json(date, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + #ifdef USE_TEXT bool ListEntitiesIterator::on_text(text::Text *text) { this->web_server_->events_.send(this->web_server_->text_json(text, text->state, DETAIL_ALL).c_str(), "state"); diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 7da5b3fe2c..cd7c9099d6 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -41,6 +41,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_NUMBER bool on_number(number::Number *number) override; #endif +#ifdef USE_DATETIME_DATE + bool on_date(datetime::DateEntity *date) override; +#endif #ifdef USE_TEXT bool on_text(text::Text *text) override; #endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b5f1a651e6..ba787239da 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -4,6 +4,7 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -853,6 +854,53 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail } #endif +#ifdef USE_DATETIME_DATE +void WebServer::on_date_update(datetime::DateEntity *obj) { + this->events_.send(this->date_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_dates()) { + if (obj->get_object_id() != match.id) + continue; + if (request->method() == HTTP_GET) { + std::string data = this->date_json(obj, DETAIL_STATE); + request->send(200, "application/json", data.c_str()); + return; + } + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (!request->hasParam("value")) { + request->send(409); + return; + } + + if (request->hasParam("value")) { + std::string value = request->getParam("value")->value().c_str(); + call.set_date(value); + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} + +std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { + return json::build_json([obj, start_config](JsonObject root) { + set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); + std::string value = str_sprintf("%d-%d-%d", obj->year, obj->month, obj->day); + root["value"] = value; + root["state"] = value; + }); +} +#endif // USE_DATETIME_DATE + #ifdef USE_TEXT void WebServer::on_text_update(text::Text *obj, const std::string &state) { this->events_.send(this->text_json(obj, state, DETAIL_STATE).c_str(), "state"); @@ -1237,6 +1285,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_DATETIME_DATE + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "date") + return true; +#endif + #ifdef USE_TEXT if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text") return true; @@ -1355,6 +1408,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif +#ifdef USE_DATETIME_DATE + if (match.domain == "date") { + this->handle_date_request(request, match); + return; + } +#endif + #ifdef USE_TEXT if (match.domain == "text") { this->handle_text_request(request, match); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 465e231984..06c59ecaca 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -221,6 +221,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string number_json(number::Number *obj, float value, JsonDetail start_config); #endif +#ifdef USE_DATETIME_DATE + void on_date_update(datetime::DateEntity *obj) override; + /// Handle a date request under '/date/'. + void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the date state with its value as a JSON string. + std::string date_json(datetime::DateEntity *obj, JsonDetail start_config); +#endif + #ifdef USE_TEXT void on_text_update(text::Text *obj, const std::string &state) override; /// Handle a text input request under '/text/'. diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 9f577773d4..848c5e4611 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -32,6 +32,9 @@ from esphome.const import ( CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, + CONF_YEAR, + CONF_MONTH, + CONF_DAY, CONF_HOUR, CONF_MINUTE, CONF_SECOND, @@ -817,21 +820,112 @@ positive_not_null_time_period = All( def time_of_day(value): - value = string(value) - try: - date = datetime.strptime(value, "%H:%M:%S") - except ValueError as err: - try: - date = datetime.strptime(value, "%H:%M:%S %p") - except ValueError: - # pylint: disable=raise-missing-from - raise Invalid(f"Invalid time of day: {err}") + return date_time(allowed_date=False, allowed_time=True)(value) - return { - CONF_HOUR: date.hour, - CONF_MINUTE: date.minute, - CONF_SECOND: date.second, - } + +def date_time(allowed_date: bool = True, allowed_time: bool = True): + + pattern_str = r"^" # Start of string + if allowed_date: + pattern_str += ( + r"(" # 1. Optional Date group + r"\d{4}-\d{1,2}-\d{1,2}" # Date + r"(?:\s(?=.+))?" # Space after date only if time is following + r")?" # End optional Date group + ) + if allowed_time: + pattern_str += ( + r"(" # 2. Optional Time group + r"(\d{1,2}:\d{2})" # 3. Hour/Minute + r"(:\d{2})?" # 4. Seconds + r"(" # 5. Optional AM/PM group + r"(\s)?" # 6. Optional Space + r"(?:AM|PM|am|pm)" # AM/PM string matching + r")?" # End optional AM/PM group + r")?" # End optional Time group + ) + pattern_str += r"$" # End of string + + pattern = re.compile(pattern_str) + + exc_message = "" + if allowed_date: + exc_message += "date" + if allowed_time: + exc_message += "/" + if allowed_time: + exc_message += "time" + + schema = Schema({}) + if allowed_date: + schema = schema.extend( + { + Optional(CONF_YEAR): int_range(min=1970, max=3000), + Optional(CONF_MONTH): int_range(min=1, max=12), + Optional(CONF_DAY): int_range(min=1, max=31), + } + ) + if allowed_time: + schema = schema.extend( + { + Optional(CONF_HOUR): int_range(min=0, max=23), + Optional(CONF_MINUTE): int_range(min=0, max=59), + Optional(CONF_SECOND): int_range(min=0, max=59), + } + ) + + def validator(value): + if isinstance(value, dict): + return schema(value) + value = string(value) + + match = pattern.match(value) + if match is None: + # pylint: disable=raise-missing-from + raise Invalid(f"Invalid {exc_message}: {value}") + + if allowed_date: + has_date = match[1] is not None + if allowed_time: + has_time = match[2] is not None + has_seconds = match[3] is not None + has_ampm = match[4] is not None + has_ampm_space = match[5] is not None + + format = "" + if allowed_date and has_date: + format += "%Y-%m-%d" + if allowed_time and has_time: + format += " " + if allowed_time and has_time: + format += "%H:%M" + if has_seconds: + format += ":%S" + if has_ampm_space: + format += " " + if has_ampm: + format += "%p" + + try: + date_obj = datetime.strptime(value, format) + except ValueError as err: + # pylint: disable=raise-missing-from + raise Invalid(f"Invalid {exc_message}: {err}") + + return_value = {} + if allowed_date and has_date: + return_value[CONF_YEAR] = date_obj.year + return_value[CONF_MONTH] = date_obj.month + return_value[CONF_DAY] = date_obj.day + + if allowed_time and has_time: + return_value[CONF_HOUR] = date_obj.hour + return_value[CONF_MINUTE] = date_obj.minute + return_value[CONF_SECOND] = date_obj.second if has_seconds else 0 + + return schema(return_value) + + return validator def mac_address(value): diff --git a/esphome/const.py b/esphome/const.py index 33bf2351b0..35f9b46194 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -176,6 +176,8 @@ CONF_DATA_PIN = "data_pin" CONF_DATA_PINS = "data_pins" CONF_DATA_RATE = "data_rate" CONF_DATA_TEMPLATE = "data_template" +CONF_DATE = "date" +CONF_DAY = "day" CONF_DAYS_OF_MONTH = "days_of_month" CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" @@ -468,6 +470,7 @@ CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_STATE_TOPIC = "mode_state_topic" CONF_MODEL = "model" CONF_MOISTURE = "moisture" +CONF_MONTH = "month" CONF_MONTHS = "months" CONF_MOSI_PIN = "mosi_pin" CONF_MOTION = "motion" @@ -871,6 +874,7 @@ CONF_WINDOW_SIZE = "window_size" CONF_WRITE_PIN = "write_pin" CONF_X_GRID = "x_grid" CONF_Y_GRID = "y_grid" +CONF_YEAR = "year" CONF_ZERO = "zero" TYPE_GIT = "git" diff --git a/esphome/core/application.h b/esphome/core/application.h index 059e393912..26125dd935 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -39,6 +39,9 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_DATETIME_DATE +#include "esphome/components/datetime/date_entity.h" +#endif #ifdef USE_TEXT #include "esphome/components/text/text.h" #endif @@ -121,6 +124,10 @@ class Application { void register_number(number::Number *number) { this->numbers_.push_back(number); } #endif +#ifdef USE_DATETIME_DATE + void register_date(datetime::DateEntity *date) { this->dates_.push_back(date); } +#endif + #ifdef USE_TEXT void register_text(text::Text *text) { this->texts_.push_back(text); } #endif @@ -289,6 +296,15 @@ class Application { 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_) + 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) { @@ -382,6 +398,9 @@ class Application { #ifdef USE_NUMBER std::vector numbers_{}; #endif +#ifdef USE_DATETIME_DATE + std::vector dates_{}; +#endif #ifdef USE_SELECT std::vector selects_{}; #endif diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 0bf8fb6f83..1e06221af6 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -202,6 +202,21 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_DATETIME_DATE + case IteratorState::DATETIME_DATE: + if (this->at_ >= App.get_dates().size()) { + advance_platform = true; + } else { + auto *date = App.get_dates()[this->at_]; + if (date->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_date(date); + } + } + break; +#endif #ifdef USE_TEXT case IteratorState::TEXT: if (this->at_ >= App.get_texts().size()) { diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 646c39705f..02c6dddacb 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -57,6 +57,9 @@ class ComponentIterator { #ifdef USE_NUMBER virtual bool on_number(number::Number *number) = 0; #endif +#ifdef USE_DATETIME_DATE + virtual bool on_date(datetime::DateEntity *date) = 0; +#endif #ifdef USE_TEXT virtual bool on_text(text::Text *text) = 0; #endif @@ -114,6 +117,9 @@ class ComponentIterator { #ifdef USE_NUMBER NUMBER, #endif +#ifdef USE_DATETIME_DATE + DATETIME_DATE, +#endif #ifdef USE_TEXT TEXT, #endif diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 95f63b6224..43b8fea50c 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -1,6 +1,6 @@ #include "controller.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { @@ -59,6 +59,12 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); } #endif +#ifdef USE_DATETIME_DATE + for (auto *obj : App.get_dates()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); }); + } +#endif #ifdef USE_TEXT for (auto *obj : App.get_texts()) { if (include_internal || !obj->is_internal()) diff --git a/esphome/core/controller.h b/esphome/core/controller.h index f977d8a36a..c31cd22d07 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -31,6 +31,9 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_DATETIME_DATE +#include "esphome/components/datetime/date_entity.h" +#endif #ifdef USE_TEXT #include "esphome/components/text/text.h" #endif @@ -79,6 +82,9 @@ class Controller { #ifdef USE_NUMBER virtual void on_number_update(number::Number *obj, float state){}; #endif +#ifdef USE_DATETIME_DATE + virtual void on_date_update(datetime::DateEntity *obj){}; +#endif #ifdef USE_TEXT virtual void on_text_update(text::Text *obj, const std::string &state){}; #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 75ed24ddfe..86f89e7bf6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -34,6 +34,8 @@ #define USE_MEDIA_PLAYER #define USE_MQTT #define USE_NUMBER +#define USE_DATETIME +#define USE_DATETIME_DATE #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 751b2a2703..2e46a611e6 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -1,10 +1,13 @@ +#include + +#include "helpers.h" #include "time.h" // NOLINT namespace esphome { -static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } +bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } -static uint8_t days_in_month(uint8_t month, uint16_t year) { +uint8_t days_in_month(uint8_t month, uint16_t year) { static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; uint8_t days = DAYS_IN_MONTH[month]; if (month == 2 && is_leap_year(year)) @@ -61,6 +64,44 @@ std::string ESPTime::strftime(const std::string &format) { return timestr; } +bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { + // clang-format off + std::regex dt_regex(R"(^ + ( + (\d{4})-(\d{1,2})-(\d{1,2}) + (?:\s(?=.+)) + )? + ( + (\d{1,2}):(\d{2}) + (?::(\d{2}))? + )? + $)"); + // clang-format on + + std::smatch match; + if (std::regex_match(time_to_parse, match, dt_regex) == 0) + return false; + + if (match[1].matched) { // Has date parts + + esp_time.year = parse_number(match[2].str()).value_or(0); + esp_time.month = parse_number(match[3].str()).value_or(0); + esp_time.day_of_month = parse_number(match[4].str()).value_or(0); + } + if (match[5].matched) { // Has time parts + + esp_time.hour = parse_number(match[6].str()).value_or(0); + esp_time.minute = parse_number(match[7].str()).value_or(0); + if (match[8].matched) { + esp_time.second = parse_number(match[8].str()).value_or(0); + } else { + esp_time.second = 0; + } + } + + return true; +} + void ESPTime::increment_second() { this->timestamp++; if (!increment_time_value(this->second, 0, 60)) diff --git a/esphome/core/time.h b/esphome/core/time.h index 670bf0ee73..4300cf26b7 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -9,6 +9,10 @@ namespace esphome { template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); +bool is_leap_year(uint32_t year); + +uint8_t days_in_month(uint8_t month, uint16_t year); + /// A more user-friendly version of struct tm from time.h struct ESPTime { /** seconds after the minute [0-60] @@ -63,6 +67,13 @@ struct ESPTime { this->day_of_year < 367 && this->month > 0 && this->month < 13; } + /** Convert a string to ESPTime struct as specified by the format argument. + * @param time_to_parse null-terminated c string formatet like this: 2020-08-25 05:30:00. + * @param esp_time an instance of a ESPTime struct + * @return the success sate of the parsing + */ + static bool strptime(const std::string &time_to_parse, ESPTime &esp_time); + /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 7d0e386b66..0f1b7f236b 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -38,3 +38,4 @@ gpio_ns = esphome_ns.namespace("gpio") gpio_Flags = gpio_ns.enum("Flags", is_class=True) EntityCategory = esphome_ns.enum("EntityCategory") Parented = esphome_ns.class_("Parented") +ESPTime = esphome_ns.struct("ESPTime") diff --git a/script/ci-custom.py b/script/ci-custom.py index 41ce030d48..422fdf52f0 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -609,6 +609,7 @@ def lint_trailing_whitespace(fname, match): "esphome/components/button/button.h", "esphome/components/climate/climate.h", "esphome/components/cover/cover.h", + "esphome/components/datetime/date_entity.h", "esphome/components/display/display.h", "esphome/components/fan/fan.h", "esphome/components/i2c/i2c.h", diff --git a/tests/components/datetime/date.all.yaml b/tests/components/datetime/date.all.yaml new file mode 100644 index 0000000000..3f5996bb8b --- /dev/null +++ b/tests/components/datetime/date.all.yaml @@ -0,0 +1 @@ +datetime: diff --git a/tests/components/template/test.all.yaml b/tests/components/template/test.all.yaml index ad67b4e6ae..e50ffd7f67 100644 --- a/tests/components/template/test.all.yaml +++ b/tests/components/template/test.all.yaml @@ -19,7 +19,20 @@ esphome: # Templated - sensor.template.publish: id: template_sens - state: !lambda 'return 42.0;' + state: !lambda "return 42.0;" + + - datetime.date.set: + id: test_date + date: + year: 2021 + month: 1 + day: 1 + - datetime.date.set: + id: test_date + date: !lambda "return {.day_of_month = 1, .month = 1, .year = 2021};" + - datetime.date.set: + id: test_date + date: "2021-01-01" binary_sensor: - platform: template @@ -125,3 +138,18 @@ alarm_control_panel: name: Alarm Panel codes: - "1234" + +datetime: + - platform: template + name: Date + id: test_date + type: date + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "Date: %04d-%02d-%02d" + args: + - x.year + - x.month + - x.day_of_month From fc0d5abc54ca4e67711c35682832b1e56e954585 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 11 Mar 2024 00:19:09 +0300 Subject: [PATCH 412/436] Add AGS10 Sensor (#6070) --- CODEOWNERS | 1 + esphome/components/ags10/__init__.py | 1 + esphome/components/ags10/ags10.cpp | 212 ++++++++++++++++++ esphome/components/ags10/ags10.h | 152 +++++++++++++ esphome/components/ags10/sensor.py | 132 +++++++++++ tests/components/ags10/test.esp32-c3-idf.yaml | 12 + tests/components/ags10/test.esp32-c3.yaml | 12 + tests/components/ags10/test.esp32-idf.yaml | 12 + tests/components/ags10/test.esp32.yaml | 12 + tests/components/ags10/test.esp8266.yaml | 12 + 10 files changed, 558 insertions(+) create mode 100644 esphome/components/ags10/__init__.py create mode 100644 esphome/components/ags10/ags10.cpp create mode 100644 esphome/components/ags10/ags10.h create mode 100644 esphome/components/ags10/sensor.py create mode 100644 tests/components/ags10/test.esp32-c3-idf.yaml create mode 100644 tests/components/ags10/test.esp32-c3.yaml create mode 100644 tests/components/ags10/test.esp32-idf.yaml create mode 100644 tests/components/ags10/test.esp32.yaml create mode 100644 tests/components/ags10/test.esp8266.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 43ce6e4a77..c3efe54f0f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -22,6 +22,7 @@ esphome/components/ade7880/* @kpfleming esphome/components/ade7953/* @angelnu esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_spi/* @angelnu +esphome/components/ags10/* @mak-42 esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_mini/* @ncareau diff --git a/esphome/components/ags10/__init__.py b/esphome/components/ags10/__init__.py new file mode 100644 index 0000000000..37f34d06df --- /dev/null +++ b/esphome/components/ags10/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@mak-42"] diff --git a/esphome/components/ags10/ags10.cpp b/esphome/components/ags10/ags10.cpp new file mode 100644 index 0000000000..dfaa00e2e9 --- /dev/null +++ b/esphome/components/ags10/ags10.cpp @@ -0,0 +1,212 @@ +#include "ags10.h" + +namespace esphome { +namespace ags10 { +static const char *const TAG = "ags10"; + +// Data acquisition. +static const uint8_t REG_TVOC = 0x00; +// Zero-point calibration. +static const uint8_t REG_CALIBRATION = 0x01; +// Read version. +static const uint8_t REG_VERSION = 0x11; +// Read current resistance. +static const uint8_t REG_RESISTANCE = 0x20; +// Modify target address. +static const uint8_t REG_ADDRESS = 0x21; + +// Zero-point calibration with current resistance. +static const uint16_t ZP_CURRENT = 0x0000; +// Zero-point reset. +static const uint16_t ZP_DEFAULT = 0xFFFF; + +void AGS10Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ags10..."); + + auto version = this->read_version_(); + if (version) { + ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version); + if (this->version_ != nullptr) { + this->version_->publish_state(*version); + } + } else { + ESP_LOGE(TAG, "AGS10 Sensor Version: unknown"); + } + + auto resistance = this->read_resistance_(); + if (resistance) { + ESP_LOGD(TAG, "AGS10 Sensor Resistance: 0x%08X", *resistance); + if (this->resistance_ != nullptr) { + this->resistance_->publish_state(*resistance); + } + } else { + ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown"); + } + + ESP_LOGD(TAG, "Sensor initialized"); +} + +void AGS10Component::update() { + auto tvoc = this->read_tvoc_(); + if (tvoc) { + this->tvoc_->publish_state(*tvoc); + this->status_clear_warning(); + } else { + this->status_set_warning(); + } +} + +void AGS10Component::dump_config() { + ESP_LOGCONFIG(TAG, "AGS10:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case NONE: + break; + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with AGS10 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case ILLEGAL_STATUS: + ESP_LOGE(TAG, "AGS10 is not ready to return TVOC data or sensor in pre-heat stage."); + break; + case UNSUPPORTED_UNITS: + ESP_LOGE(TAG, "AGS10 returns TVOC data in unsupported units."); + break; + default: + ESP_LOGE(TAG, "Unknown error: %d", this->error_code_); + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_); + LOG_SENSOR(" ", "Firmware Version Sensor", this->version_); + LOG_SENSOR(" ", "Resistance Sensor", this->resistance_); +} + +/** + * Sets new I2C address of AGS10. + */ +bool AGS10Component::new_i2c_address(uint8_t newaddress) { + uint8_t rev_newaddress = ~newaddress; + std::array data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0}; + data[4] = calc_crc8_(data, 4); + if (!this->write_bytes(REG_ADDRESS, data)) { + this->error_code_ = COMMUNICATION_FAILED; + this->status_set_warning(); + ESP_LOGE(TAG, "couldn't write the new I2C address 0x%02X", newaddress); + return false; + } + this->set_i2c_address(newaddress); + ESP_LOGW(TAG, "changed I2C address to 0x%02X", newaddress); + this->error_code_ = NONE; + this->status_clear_warning(); + return true; +} + +bool AGS10Component::set_zero_point_with_factory_defaults() { return this->set_zero_point_with(ZP_DEFAULT); } + +bool AGS10Component::set_zero_point_with_current_resistance() { return this->set_zero_point_with(ZP_CURRENT); } + +bool AGS10Component::set_zero_point_with(uint16_t value) { + std::array data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0}; + data[4] = calc_crc8_(data, 4); + if (!this->write_bytes(REG_CALIBRATION, data)) { + this->error_code_ = COMMUNICATION_FAILED; + this->status_set_warning(); + ESP_LOGE(TAG, "unable to set zero-point calibration with 0x%02X", value); + return false; + } + if (value == ZP_CURRENT) { + ESP_LOGI(TAG, "zero-point calibration has been set with current resistance"); + } else if (value == ZP_DEFAULT) { + ESP_LOGI(TAG, "zero-point calibration has been reset to the factory defaults"); + } else { + ESP_LOGI(TAG, "zero-point calibration has been set with 0x%02X", value); + } + this->error_code_ = NONE; + this->status_clear_warning(); + return true; +} + +optional AGS10Component::read_tvoc_() { + auto data = this->read_and_check_<5>(REG_TVOC); + if (!data) { + return nullopt; + } + + auto res = *data; + auto status_byte = res[0]; + + int units = status_byte & 0x0e; + int status_bit = status_byte & 0x01; + + if (status_bit != 0) { + this->error_code_ = ILLEGAL_STATUS; + ESP_LOGW(TAG, "Reading AGS10 data failed: illegal status (not ready or sensor in pre-heat stage)!"); + return nullopt; + } + + if (units != 0) { + this->error_code_ = UNSUPPORTED_UNITS; + ESP_LOGE(TAG, "Reading AGS10 data failed: unsupported units (%d)!", units); + return nullopt; + } + + return encode_uint24(res[1], res[2], res[3]); +} + +optional AGS10Component::read_version_() { + auto data = this->read_and_check_<5>(REG_VERSION); + if (data) { + auto res = *data; + return res[3]; + } + return nullopt; +} + +optional AGS10Component::read_resistance_() { + auto data = this->read_and_check_<5>(REG_RESISTANCE); + if (data) { + auto res = *data; + return encode_uint32(res[0], res[1], res[2], res[3]); + } + return nullopt; +} + +template optional> AGS10Component::read_and_check_(uint8_t a_register) { + auto data = this->read_bytes(a_register); + if (!data.has_value()) { + this->error_code_ = COMMUNICATION_FAILED; + ESP_LOGE(TAG, "Reading AGS10 version failed!"); + return optional>(); + } + auto len = N - 1; + auto res = *data; + auto crc_byte = res[len]; + + if (crc_byte != calc_crc8_(res, len)) { + this->error_code_ = CRC_CHECK_FAILED; + ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); + return optional>(); + } + + return data; +} + +template uint8_t AGS10Component::calc_crc8_(std::array dat, uint8_t num) { + uint8_t i, byte1, crc = 0xFF; + for (byte1 = 0; byte1 < num; byte1++) { + crc ^= (dat[byte1]); + for (i = 0; i < 8; i++) { + if (crc & 0x80) { + crc = (crc << 1) ^ 0x31; + } else { + crc = (crc << 1); + } + } + } + return crc; +} +} // namespace ags10 +} // namespace esphome diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h new file mode 100644 index 0000000000..f2201fe70c --- /dev/null +++ b/esphome/components/ags10/ags10.h @@ -0,0 +1,152 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ags10 { + +class AGS10Component : public PollingComponent, public i2c::I2CDevice { + public: + /** + * Sets TVOC sensor. + */ + void set_tvoc(sensor::Sensor *tvoc) { this->tvoc_ = tvoc; } + + /** + * Sets version info sensor. + */ + void set_version(sensor::Sensor *version) { this->version_ = version; } + + /** + * Sets resistance info sensor. + */ + void set_resistance(sensor::Sensor *resistance) { this->resistance_ = resistance; } + + void setup() override; + + void update() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + /** + * Modifies target address of AGS10. + * + * New address is saved and takes effect immediately even after power-off. + */ + bool new_i2c_address(uint8_t newaddress); + + /** + * Sets zero-point with factory defaults. + */ + bool set_zero_point_with_factory_defaults(); + + /** + * Sets zero-point with current sensor resistance. + */ + bool set_zero_point_with_current_resistance(); + + /** + * Sets zero-point with the value. + */ + bool set_zero_point_with(uint16_t value); + + protected: + /** + * TVOC. + */ + sensor::Sensor *tvoc_{nullptr}; + + /** + * Firmvare version. + */ + sensor::Sensor *version_{nullptr}; + + /** + * Resistance. + */ + sensor::Sensor *resistance_{nullptr}; + + /** + * Last operation error code. + */ + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + CRC_CHECK_FAILED, + ILLEGAL_STATUS, + UNSUPPORTED_UNITS, + } error_code_{NONE}; + + /** + * Reads and returns value of TVOC. + */ + optional read_tvoc_(); + + /** + * Reads and returns a firmware version of AGS10. + */ + optional read_version_(); + + /** + * Reads and returns the resistance of AGS10. + */ + optional read_resistance_(); + + /** + * Read, checks and returns data from the sensor. + */ + template optional> read_and_check_(uint8_t a_register); + + /** + * Calculates CRC8 value. + * + * CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1) + * + * @param[in] dat the data buffer + * @param num number of bytes in the buffer + */ + template uint8_t calc_crc8_(std::array dat, uint8_t num); +}; + +template class AGS10NewI2cAddressAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, new_address) + + void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } +}; + +enum AGS10SetZeroPointActionMode { + // Zero-point reset. + FACTORY_DEFAULT, + // Zero-point calibration with current resistance. + CURRENT_VALUE, + // Zero-point calibration with custom resistance. + CUSTOM_VALUE, +}; + +template class AGS10SetZeroPointAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, value) + TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) + + void play(Ts... x) override { + switch (this->mode_.value(x...)) { + case FACTORY_DEFAULT: + this->parent_->set_zero_point_with_factory_defaults(); + break; + case CURRENT_VALUE: + this->parent_->set_zero_point_with_current_resistance(); + break; + case CUSTOM_VALUE: + this->parent_->set_zero_point_with(this->value_.value(x...)); + break; + } + } +}; +} // namespace ags10 +} // namespace esphome diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py new file mode 100644 index 0000000000..59aebd636b --- /dev/null +++ b/esphome/components/ags10/sensor.py @@ -0,0 +1,132 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + ICON_RADIATOR, + ICON_RESTART, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_OHM, + UNIT_PARTS_PER_BILLION, + CONF_ADDRESS, + CONF_TVOC, + CONF_VERSION, + CONF_MODE, + CONF_VALUE, +) + +CONF_RESISTANCE = "resistance" + +DEPENDENCIES = ["i2c"] + +ags10_ns = cg.esphome_ns.namespace("ags10") +AGS10Component = ags10_ns.class_("AGS10Component", cg.PollingComponent, i2c.I2CDevice) + +# Actions +AGS10NewI2cAddressAction = ags10_ns.class_( + "AGS10NewI2cAddressAction", automation.Action +) +AGS10SetZeroPointAction = ags10_ns.class_("AGS10SetZeroPointAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AGS10Component), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + icon=ICON_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_RESISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_RESTART, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x1A)) +) + +FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency="15khz") + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) + + if version_config := config.get(CONF_VERSION): + sens = await sensor.new_sensor(version_config) + cg.add(var.set_version(sens)) + + if resistance_config := config.get(CONF_RESISTANCE): + sens = await sensor.new_sensor(resistance_config) + cg.add(var.set_resistance(sens)) + + +AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(AGS10Component), + cv.Required(CONF_ADDRESS): cv.templatable(cv.i2c_address), + }, + key=CONF_ADDRESS, +) + + +@automation.register_action( + "ags10.new_i2c_address", + AGS10NewI2cAddressAction, + AGS10_NEW_I2C_ADDRESS_SCHEMA, +) +async def ags10newi2caddress_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + address = await cg.templatable(config[CONF_ADDRESS], args, int) + cg.add(var.set_new_address(address)) + return var + + +AGS10SetZeroPointActionMode = ags10_ns.enum("AGS10SetZeroPointActionMode") +AGS10_SET_ZERO_POINT_ACTION_MODE = { + "FACTORY_DEFAULT": AGS10SetZeroPointActionMode.FACTORY_DEFAULT, + "CURRENT_VALUE": AGS10SetZeroPointActionMode.CURRENT_VALUE, + "CUSTOM_VALUE": AGS10SetZeroPointActionMode.CUSTOM_VALUE, +} + +AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(AGS10Component), + cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True), + cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t), + }, +) + + +@automation.register_action( + "ags10.set_zero_point", + AGS10SetZeroPointAction, + AGS10_SET_ZERO_POINT_SCHEMA, +) +async def ags10setzeropoint_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + mode = await cg.templatable(config.get(CONF_MODE), args, enumerate) + cg.add(var.set_mode(mode)) + value = await cg.templatable(config[CONF_VALUE], args, int) + cg.add(var.set_value(value)) + return var diff --git a/tests/components/ags10/test.esp32-c3-idf.yaml b/tests/components/ags10/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e338fc78e0 --- /dev/null +++ b/tests/components/ags10/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 5 + sda: 4 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp32-c3.yaml b/tests/components/ags10/test.esp32-c3.yaml new file mode 100644 index 0000000000..e338fc78e0 --- /dev/null +++ b/tests/components/ags10/test.esp32-c3.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 5 + sda: 4 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp32-idf.yaml b/tests/components/ags10/test.esp32-idf.yaml new file mode 100644 index 0000000000..b3b53c0d31 --- /dev/null +++ b/tests/components/ags10/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 16 + sda: 17 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp32.yaml b/tests/components/ags10/test.esp32.yaml new file mode 100644 index 0000000000..b3b53c0d31 --- /dev/null +++ b/tests/components/ags10/test.esp32.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 16 + sda: 17 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp8266.yaml b/tests/components/ags10/test.esp8266.yaml new file mode 100644 index 0000000000..e338fc78e0 --- /dev/null +++ b/tests/components/ags10/test.esp8266.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 5 + sda: 4 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s From 50154362954046b39bea062e277de8595752f76f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:20:53 +1300 Subject: [PATCH 413/436] Bump aioesphomeapi from 23.0.0 to 23.1.0 (#6332) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c6d1fab2c1..cbf925c1b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.13 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==23.0.0 +aioesphomeapi==23.1.0 zeroconf==0.131.0 python-magic==0.4.27 From 732fcc16f3fd3df01525f01bb0ee8a98c00b8ef7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:21:39 +1300 Subject: [PATCH 414/436] Bump pytest-asyncio from 0.23.5 to 0.23.5.post1 (#6334) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c50a688cd0..b370214380 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==8.0.2 pytest-cov==4.1.0 pytest-mock==3.12.0 -pytest-asyncio==0.23.5 +pytest-asyncio==0.23.5.post1 asyncmock==0.4.2 hypothesis==6.92.1 From 1253583c2de19506c6f1faf66576278e8dbb18a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:22:31 +1300 Subject: [PATCH 415/436] Bump docker/setup-buildx-action from 3.0.0 to 3.1.0 (#6295) 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 8fe8bbdc52..1637f69954 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.0.0 + uses: docker/setup-buildx-action@v3.1.0 - name: Set up QEMU uses: docker/setup-qemu-action@v3.0.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 625a8c8ecb..61f5ba4b23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.1.0 - name: Set up QEMU if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v3.0.0 @@ -163,7 +163,7 @@ jobs: name: digests-${{ matrix.image.target }}-${{ matrix.registry }} path: /tmp/digests - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.1.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' From 89b3bc7d709e56033bd80c182ff42441f13ae8fe Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:36:44 +1300 Subject: [PATCH 416/436] Set dependabot to look at composite actions versions (#6343) --- .github/dependabot.yml | 10 ++++++++++ .github/workflows/ci.yml | 1 + 2 files changed, 11 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 666532f360..3b6495653b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,3 +13,13 @@ updates: schedule: interval: daily open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: "/.github/actions/build-image" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: "/.github/actions/restore-python" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1794aeee89..05503ee18a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ on: - "!.github/workflows/*.yml" - ".github/workflows/ci.yml" - "!.yamllint" + - "!.github/dependabot.yml" merge_group: permissions: From c52052563f450fb04a9974f2205abbe04dd967dd Mon Sep 17 00:00:00 2001 From: Solomon Date: Sun, 10 Mar 2024 18:09:05 -0400 Subject: [PATCH 417/436] ads1118 component (#5711) Co-authored-by: Solomon Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ads1118/__init__.py | 25 ++++ esphome/components/ads1118/ads1118.cpp | 126 ++++++++++++++++++ esphome/components/ads1118/ads1118.h | 46 +++++++ esphome/components/ads1118/sensor/__init__.py | 97 ++++++++++++++ .../ads1118/sensor/ads1118_sensor.cpp | 29 ++++ .../ads1118/sensor/ads1118_sensor.h | 36 +++++ tests/test1.yaml | 14 ++ 8 files changed, 374 insertions(+) create mode 100644 esphome/components/ads1118/__init__.py create mode 100644 esphome/components/ads1118/ads1118.cpp create mode 100644 esphome/components/ads1118/ads1118.h create mode 100644 esphome/components/ads1118/sensor/__init__.py create mode 100644 esphome/components/ads1118/sensor/ads1118_sensor.cpp create mode 100644 esphome/components/ads1118/sensor/ads1118_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index c3efe54f0f..e1bd38adde 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -22,6 +22,7 @@ esphome/components/ade7880/* @kpfleming esphome/components/ade7953/* @angelnu esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_spi/* @angelnu +esphome/components/ads1118/* @solomondg1 esphome/components/ags10/* @mak-42 esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau diff --git a/esphome/components/ads1118/__init__.py b/esphome/components/ads1118/__init__.py new file mode 100644 index 0000000000..f8d51101a6 --- /dev/null +++ b/esphome/components/ads1118/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +CODEOWNERS = ["@solomondg1"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +CONF_ADS1118_ID = "ads1118_id" + +ads1118_ns = cg.esphome_ns.namespace("ads1118") +ADS1118 = ads1118_ns.class_("ADS1118", cg.Component, spi.SPIDevice) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS1118), + } +).extend(spi.spi_device_schema(cs_pin_required=True)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/ads1118/ads1118.cpp b/esphome/components/ads1118/ads1118.cpp new file mode 100644 index 0000000000..7b9d0cc36b --- /dev/null +++ b/esphome/components/ads1118/ads1118.cpp @@ -0,0 +1,126 @@ +#include "ads1118.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1118 { + +static const char *const TAG = "ads1118"; +static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111; + +void ADS1118::setup() { + ESP_LOGCONFIG(TAG, "Setting up ads1118"); + this->spi_setup(); + + this->config_ = 0; + // Setup multiplexer + // 0bx000xxxxxxxxxxxx + this->config_ |= ADS1118_MULTIPLEXER_P0_NG << 12; + + // Setup Gain + // 0bxxxx000xxxxxxxxx + this->config_ |= ADS1118_GAIN_6P144 << 9; + + // Set singleshot mode + // 0bxxxxxxx1xxxxxxxx + this->config_ |= 0b0000000100000000; + + // Set data rate - 860 samples per second (we're in singleshot mode) + // 0bxxxxxxxx100xxxxx + this->config_ |= ADS1118_DATA_RATE_860_SPS << 5; + + // Set temperature sensor mode - ADC + // 0bxxxxxxxxxxx0xxxx + this->config_ |= 0b0000000000000000; + + // Set DOUT pull up - enable + // 0bxxxxxxxxxxxx0xxx + this->config_ |= 0b0000000000001000; + + // NOP - must be 01 + // 0bxxxxxxxxxxxxx01x + this->config_ |= 0b0000000000000010; + + // Not used - can be 0 or 1, lets be positive + // 0bxxxxxxxxxxxxxxx1 + this->config_ |= 0b0000000000000001; +} + +void ADS1118::dump_config() { + ESP_LOGCONFIG(TAG, "ADS1118:"); + LOG_PIN(" CS Pin:", this->cs_); +} + +float ADS1118::request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode) { + uint16_t temp_config = this->config_; + // Multiplexer + // 0bxBBBxxxxxxxxxxxx + temp_config &= 0b1000111111111111; + temp_config |= (multiplexer & 0b111) << 12; + + // Gain + // 0bxxxxBBBxxxxxxxxx + temp_config &= 0b1111000111111111; + temp_config |= (gain & 0b111) << 9; + + if (temperature_mode) { + // Set temperature sensor mode + // 0bxxxxxxxxxxx1xxxx + temp_config |= 0b0000000000010000; + } else { + // Set ADC mode + // 0bxxxxxxxxxxx0xxxx + temp_config &= 0b1111111111101111; + } + + // Start conversion + temp_config |= 0b1000000000000000; + + this->enable(); + this->write_byte16(temp_config); + this->disable(); + + // about 1.2 ms with 860 samples per second + delay(2); + + this->enable(); + uint8_t adc_first_byte = this->read_byte(); + uint8_t adc_second_byte = this->read_byte(); + this->disable(); + uint16_t raw_conversion = encode_uint16(adc_first_byte, adc_second_byte); + + auto signed_conversion = static_cast(raw_conversion); + + if (temperature_mode) { + return (signed_conversion >> 2) * 0.03125f; + } else { + float millivolts; + float divider = 32768.0f; + switch (gain) { + case ADS1118_GAIN_6P144: + millivolts = (signed_conversion * 6144) / divider; + break; + case ADS1118_GAIN_4P096: + millivolts = (signed_conversion * 4096) / divider; + break; + case ADS1118_GAIN_2P048: + millivolts = (signed_conversion * 2048) / divider; + break; + case ADS1118_GAIN_1P024: + millivolts = (signed_conversion * 1024) / divider; + break; + case ADS1118_GAIN_0P512: + millivolts = (signed_conversion * 512) / divider; + break; + case ADS1118_GAIN_0P256: + millivolts = (signed_conversion * 256) / divider; + break; + default: + millivolts = NAN; + } + + return millivolts / 1e3f; + } +} + +} // namespace ads1118 +} // namespace esphome diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h new file mode 100644 index 0000000000..8b9aa15cd2 --- /dev/null +++ b/esphome/components/ads1118/ads1118.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ads1118 { + +enum ADS1118Multiplexer { + ADS1118_MULTIPLEXER_P0_N1 = 0b000, + ADS1118_MULTIPLEXER_P0_N3 = 0b001, + ADS1118_MULTIPLEXER_P1_N3 = 0b010, + ADS1118_MULTIPLEXER_P2_N3 = 0b011, + ADS1118_MULTIPLEXER_P0_NG = 0b100, + ADS1118_MULTIPLEXER_P1_NG = 0b101, + ADS1118_MULTIPLEXER_P2_NG = 0b110, + ADS1118_MULTIPLEXER_P3_NG = 0b111, +}; + +enum ADS1118Gain { + ADS1118_GAIN_6P144 = 0b000, + ADS1118_GAIN_4P096 = 0b001, + ADS1118_GAIN_2P048 = 0b010, + ADS1118_GAIN_1P024 = 0b011, + ADS1118_GAIN_0P512 = 0b100, + ADS1118_GAIN_0P256 = 0b101, +}; + +class ADS1118 : public Component, + public spi::SPIDevice { + public: + ADS1118() = default; + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + /// Helper method to request a measurement from a sensor. + float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode); + + protected: + uint16_t config_{0}; +}; + +} // namespace ads1118 +} // namespace esphome diff --git a/esphome/components/ads1118/sensor/__init__.py b/esphome/components/ads1118/sensor/__init__.py new file mode 100644 index 0000000000..4e89115447 --- /dev/null +++ b/esphome/components/ads1118/sensor/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_GAIN, + CONF_MULTIPLEXER, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_VOLT, + CONF_TYPE, +) +from .. import ads1118_ns, ADS1118, CONF_ADS1118_ID + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["ads1118"] + +ADS1118Multiplexer = ads1118_ns.enum("ADS1118Multiplexer") +MUX = { + "A0_A1": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_N1, + "A0_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_N3, + "A1_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P1_N3, + "A2_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P2_N3, + "A0_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_NG, + "A1_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P1_NG, + "A2_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P2_NG, + "A3_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P3_NG, +} + +ADS1118Gain = ads1118_ns.enum("ADS1118Gain") +GAIN = { + "6.144": ADS1118Gain.ADS1118_GAIN_6P144, + "4.096": ADS1118Gain.ADS1118_GAIN_4P096, + "2.048": ADS1118Gain.ADS1118_GAIN_2P048, + "1.024": ADS1118Gain.ADS1118_GAIN_1P024, + "0.512": ADS1118Gain.ADS1118_GAIN_0P512, + "0.256": ADS1118Gain.ADS1118_GAIN_0P256, +} + + +ADS1118Sensor = ads1118_ns.class_( + "ADS1118Sensor", + cg.PollingComponent, + sensor.Sensor, + voltage_sampler.VoltageSampler, + cg.Parented.template(ADS1118), +) + +TYPE_ADC = "adc" +TYPE_TEMPERATURE = "temperature" + +CONFIG_SCHEMA = cv.typed_schema( + { + TYPE_ADC: sensor.sensor_schema( + ADS1118Sensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_ADS1118_ID): cv.use_id(ADS1118), + cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), + cv.Required(CONF_GAIN): cv.enum(GAIN, string=True), + } + ) + .extend(cv.polling_component_schema("60s")), + TYPE_TEMPERATURE: sensor.sensor_schema( + ADS1118Sensor, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_ADS1118_ID): cv.use_id(ADS1118), + } + ) + .extend(cv.polling_component_schema("60s")), + }, + default_type=TYPE_ADC, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_ADS1118_ID]) + + if config[CONF_TYPE] == TYPE_ADC: + cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) + cg.add(var.set_gain(config[CONF_GAIN])) + if config[CONF_TYPE] == TYPE_TEMPERATURE: + cg.add(var.set_temperature_mode(True)) diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.cpp b/esphome/components/ads1118/sensor/ads1118_sensor.cpp new file mode 100644 index 0000000000..c3ce3bdc9c --- /dev/null +++ b/esphome/components/ads1118/sensor/ads1118_sensor.cpp @@ -0,0 +1,29 @@ +#include "ads1118_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1118 { + +static const char *const TAG = "ads1118.sensor"; + +void ADS1118Sensor::dump_config() { + LOG_SENSOR(" ", "ADS1118 Sensor", this); + ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); + ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); +} + +float ADS1118Sensor::sample() { + return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->temperature_mode_); +} + +void ADS1118Sensor::update() { + float v = this->sample(); + if (!std::isnan(v)) { + ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); + this->publish_state(v); + } +} + +} // namespace ads1118 +} // namespace esphome diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.h b/esphome/components/ads1118/sensor/ads1118_sensor.h new file mode 100644 index 0000000000..d2d7a03f59 --- /dev/null +++ b/esphome/components/ads1118/sensor/ads1118_sensor.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include "../ads1118.h" + +namespace esphome { +namespace ads1118 { + +class ADS1118Sensor : public PollingComponent, + public sensor::Sensor, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void update() override; + + void set_multiplexer(ADS1118Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } + void set_gain(ADS1118Gain gain) { this->gain_ = gain; } + void set_temperature_mode(bool temp) { this->temperature_mode_ = temp; } + + float sample() override; + + void dump_config() override; + + protected: + ADS1118Multiplexer multiplexer_{ADS1118_MULTIPLEXER_P0_NG}; + ADS1118Gain gain_{ADS1118_GAIN_6P144}; + bool temperature_mode_; +}; + +} // namespace ads1118 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 5f62c7ddc5..064924b864 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -322,6 +322,12 @@ ads1115: address: 0x48 i2c_id: i2c_bus +ads1118: + spi_id: spi_bus + cs_pin: + allow_other_uses: true + number: GPIO12 + as5600: i2c_id: i2c_bus dir_pin: @@ -571,6 +577,14 @@ sensor: state_topic: hi/me retain: false availability: + - platform: ads1118 + name: ads1118 adc + multiplexer: A0_A1 + gain: 1.024 + type: adc + - platform: ads1118 + name: ads1118 temperature + type: temperature - platform: as5600 name: AS5600 Position raw_position: From db813bbf04381d216624c95416bce9ee3be6c318 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:09:40 +1300 Subject: [PATCH 418/436] Bump actions/cache from 4.0.0 to 4.0.1 (#6306) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- .github/workflows/ci.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 3c1a5e2b04..756e279635 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.3.2 + uses: actions/cache/restore@v4.0.1 with: path: venv # yamllint disable-line rule:line-length diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05503ee18a..c3de879176 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.1 with: path: venv # yamllint disable-line rule:line-length @@ -367,7 +367,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.1 with: path: ~/.platformio # yamllint disable-line rule:line-length From 3e2ce363a24418090c4cf05dae815f43796ddf50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:11:42 +1300 Subject: [PATCH 419/436] Bump docker/build-push-action from 5.0.0 to 5.2.0 in /.github/actions/build-image (#6347) --- .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 4c98a47ecd..f93cf61abb 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -36,7 +36,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v5.0.0 + uses: docker/build-push-action@v5.2.0 with: context: . file: ./docker/Dockerfile @@ -67,7 +67,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v5.0.0 + uses: docker/build-push-action@v5.2.0 with: context: . file: ./docker/Dockerfile From 0cdd0b295e0689c61fdcad7223402aec4dc38015 Mon Sep 17 00:00:00 2001 From: NewoPL <27411874+NewoPL@users.noreply.github.com> Date: Sun, 10 Mar 2024 23:15:32 +0100 Subject: [PATCH 420/436] fix: modbus_textsensor response is too long in some cases (#6333) --- .../modbus_controller/text_sensor/modbus_textsensor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp index c90890c88f..359c6e2f50 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -13,10 +13,10 @@ void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Te void ModbusTextSensor::parse_and_publish(const std::vector &data) { std::ostringstream output; - uint8_t max_items = this->response_bytes; + uint8_t items_left = this->response_bytes; uint8_t index = this->offset; char buffer[4]; - while ((max_items != 0) && index < data.size()) { + while ((items_left > 0) && index < data.size()) { uint8_t b = data[index]; switch (this->encode_) { case RawEncoding::HEXBYTES: @@ -33,7 +33,7 @@ void ModbusTextSensor::parse_and_publish(const std::vector &data) { output << (char) b; break; } - + items_left--; index++; } From 6a46548a8b5de4619bf842c335e9cbb805ba6eab Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 10 Mar 2024 15:42:02 -0700 Subject: [PATCH 421/436] add template fan (#6310) --- CODEOWNERS | 1 + esphome/components/fan/__init__.py | 46 ++++++++++++++-- esphome/components/fan/automation.h | 53 +++++++++++++++++-- esphome/components/speed/fan/__init__.py | 9 ++-- esphome/components/speed/fan/speed_fan.cpp | 6 +-- esphome/components/speed/fan/speed_fan.h | 2 +- esphome/components/template/fan/__init__.py | 43 +++++++++++++++ .../components/template/fan/template_fan.cpp | 38 +++++++++++++ .../components/template/fan/template_fan.h | 33 ++++++++++++ esphome/const.py | 2 + 10 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 esphome/components/template/fan/__init__.py create mode 100644 esphome/components/template/fan/template_fan.cpp create mode 100644 esphome/components/template/fan/template_fan.h diff --git a/CODEOWNERS b/CODEOWNERS index e1bd38adde..fd8afa27d2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -342,6 +342,7 @@ esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/template/datetime/* @rfdarter +esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index fd0f2f66cb..05e276d987 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -15,7 +15,10 @@ from esphome.const import ( CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_OFF_SPEED_CYCLE, + CONF_ON_DIRECTION_SET, + CONF_ON_OSCILLATING_SET, CONF_ON_SPEED_SET, + CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_ON_PRESET_SET, @@ -55,11 +58,22 @@ TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) ToggleAction = fan_ns.class_("ToggleAction", automation.Action) CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) +FanStateTrigger = fan_ns.class_( + "FanStateTrigger", automation.Trigger.template(Fan.operator("ptr")) +) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) -FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) +FanDirectionSetTrigger = fan_ns.class_( + "FanDirectionSetTrigger", automation.Trigger.template(FanDirection) +) +FanOscillatingSetTrigger = fan_ns.class_( + "FanOscillatingSetTrigger", automation.Trigger.template(cg.bool_) +) +FanSpeedSetTrigger = fan_ns.class_( + "FanSpeedSetTrigger", automation.Trigger.template(cg.int_) +) FanPresetSetTrigger = fan_ns.class_( - "FanPresetSetTrigger", automation.Trigger.template() + "FanPresetSetTrigger", automation.Trigger.template(cg.std_string) ) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) @@ -90,6 +104,11 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanStateTrigger), + } + ), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOnTrigger), @@ -100,6 +119,16 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), } ), + cv.Optional(CONF_ON_DIRECTION_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanDirectionSetTrigger), + } + ), + cv.Optional(CONF_ON_OSCILLATING_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanOscillatingSetTrigger), + } + ), cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), @@ -186,18 +215,27 @@ async def setup_fan_core_(var, config): mqtt_.set_custom_speed_command_topic(config[CONF_SPEED_COMMAND_TOPIC]) ) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(Fan.operator("ptr"), "x")], conf) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DIRECTION_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(FanDirection, "x")], conf) + for conf in config.get(CONF_ON_OSCILLATING_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.bool_, "x")], conf) for conf in config.get(CONF_ON_SPEED_SET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [(cg.int_, "x")], conf) for conf in config.get(CONF_ON_PRESET_SET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) async def register_fan(var, config): diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index b5bdeb8a29..d480a2ef44 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -111,6 +111,13 @@ template class FanIsOffCondition : public Condition { Fan *state_; }; +class FanStateTrigger : public Trigger { + public: + FanStateTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { this->trigger(state); }); + } +}; + class FanTurnOnTrigger : public Trigger<> { public: FanTurnOnTrigger(Fan *state) { @@ -147,15 +154,51 @@ class FanTurnOffTrigger : public Trigger<> { bool last_on_; }; -class FanSpeedSetTrigger : public Trigger<> { +class FanDirectionSetTrigger : public Trigger { + public: + FanDirectionSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto direction = state->direction; + auto should_trigger = direction != this->last_direction_; + this->last_direction_ = direction; + if (should_trigger) { + this->trigger(direction); + } + }); + this->last_direction_ = state->direction; + } + + protected: + FanDirection last_direction_; +}; + +class FanOscillatingSetTrigger : public Trigger { + public: + FanOscillatingSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto oscillating = state->oscillating; + auto should_trigger = oscillating != this->last_oscillating_; + this->last_oscillating_ = oscillating; + if (should_trigger) { + this->trigger(oscillating); + } + }); + this->last_oscillating_ = state->oscillating; + } + + protected: + bool last_oscillating_; +}; + +class FanSpeedSetTrigger : public Trigger { public: FanSpeedSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { auto speed = state->speed; - auto should_trigger = speed != !this->last_speed_; + auto should_trigger = speed != this->last_speed_; this->last_speed_ = speed; if (should_trigger) { - this->trigger(); + this->trigger(speed); } }); this->last_speed_ = state->speed; @@ -165,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<> { int last_speed_; }; -class FanPresetSetTrigger : public Trigger<> { +class FanPresetSetTrigger : public Trigger { public: FanPresetSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { @@ -173,7 +216,7 @@ class FanPresetSetTrigger : public Trigger<> { auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { - this->trigger(); + this->trigger(preset_mode); } }); this->last_preset_mode_ = state->preset_mode; diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 4527e1eed1..5fbf011669 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -14,14 +14,12 @@ from esphome.const import ( from .. import speed_ns -AUTO_LOAD = ["output"] - SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), - cv.Optional(CONF_OUTPUT): cv.use_id(output.FloatOutput), + cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_SPEED): cv.invalid( @@ -38,9 +36,8 @@ async def to_code(config): await cg.register_component(var, config) await fan.register_fan(var, config) - if CONF_OUTPUT in config: - output_ = await cg.get_variable(config[CONF_OUTPUT]) - cg.add(var.set_output(output_)) + output_ = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(output_)) if CONF_OSCILLATION_OUTPUT in config: oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index bd0af7714d..57bd795416 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -36,10 +36,8 @@ void SpeedFan::control(const fan::FanCall &call) { } void SpeedFan::write_state_() { - if (this->output_ != nullptr) { - float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; - this->output_->set_level(speed); - } + float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; + this->output_->set_level(speed); if (this->oscillating_ != nullptr) this->oscillating_->set_state(this->oscillating); if (this->direction_ != nullptr) diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 57436d298b..6537bce3f6 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -25,7 +25,7 @@ class SpeedFan : public Component, public fan::Fan { void control(const fan::FanCall &call) override; void write_state_(); - output::FloatOutput *output_{nullptr}; + output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; diff --git a/esphome/components/template/fan/__init__.py b/esphome/components/template/fan/__init__.py new file mode 100644 index 0000000000..348ebd281f --- /dev/null +++ b/esphome/components/template/fan/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import fan +from esphome.components.fan import validate_preset_modes +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_PRESET_MODES, + CONF_SPEED_COUNT, +) + +from .. import template_ns + +CODEOWNERS = ["@ssieb"] + +TemplateFan = template_ns.class_("TemplateFan", cg.Component, fan.Fan) + +CONF_HAS_DIRECTION = "has_direction" +CONF_HAS_OSCILLATING = "has_oscillating" + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TemplateFan), + cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, + cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, + cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await cg.register_component(var, config) + await fan.register_fan(var, config) + + cg.add(var.set_has_direction(config[CONF_HAS_DIRECTION])) + cg.add(var.set_has_oscillating(config[CONF_HAS_OSCILLATING])) + + if CONF_SPEED_COUNT in config: + cg.add(var.set_speed_count(config[CONF_SPEED_COUNT])) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp new file mode 100644 index 0000000000..5f4a2ae8f7 --- /dev/null +++ b/esphome/components/template/fan/template_fan.cpp @@ -0,0 +1,38 @@ +#include "template_fan.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.fan"; + +void TemplateFan::setup() { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(*this); + } + + // Construct traits + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); +} + +void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } + +void TemplateFan::control(const fan::FanCall &call) { + if (call.get_state().has_value()) + this->state = *call.get_state(); + if (call.get_speed().has_value() && (this->speed_count_ > 0)) + this->speed = *call.get_speed(); + if (call.get_oscillating().has_value() && this->has_oscillating_) + this->oscillating = *call.get_oscillating(); + if (call.get_direction().has_value() && this->has_direction_) + this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); + + this->publish_state(); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h new file mode 100644 index 0000000000..7f5305ca48 --- /dev/null +++ b/esphome/components/template/fan/template_fan.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/fan/fan.h" + +namespace esphome { +namespace template_ { + +class TemplateFan : public Component, public fan::Fan { + public: + TemplateFan() {} + void setup() override; + void dump_config() override; + void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } + void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } + void set_speed_count(int count) { this->speed_count_ = count; } + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } + + protected: + void control(const fan::FanCall &call) override; + + bool has_oscillating_{false}; + bool has_direction_{false}; + int speed_count_{0}; + fan::FanTraits traits_; + std::set preset_modes_{}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 35f9b46194..4e53970cdf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -508,6 +508,7 @@ CONF_ON_CLIENT_CONNECTED = "on_client_connected" CONF_ON_CLIENT_DISCONNECTED = "on_client_disconnected" CONF_ON_CONNECT = "on_connect" CONF_ON_CONTROL = "on_control" +CONF_ON_DIRECTION_SET = "on_direction_set" CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" @@ -524,6 +525,7 @@ CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" CONF_ON_MULTI_CLICK = "on_multi_click" CONF_ON_OPEN = "on_open" +CONF_ON_OSCILLATING_SET = "on_oscillating_set" CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" From d4489ac373bcdc924c9facecfcd611e6ffe0d402 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 10 Mar 2024 23:43:33 +0100 Subject: [PATCH 422/436] dump config after logging CDC port is opened by host (#6169) --- esphome/components/logger/logger.cpp | 19 +++++++++++++++++++ esphome/components/logger/logger.h | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 740b12adc1..166fe2693d 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -3,6 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace logger { @@ -128,6 +129,24 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT } +#ifdef USE_LOGGER_USB_CDC +void Logger::loop() { +#ifdef USE_ARDUINO + if (this->uart_ != UART_SELECTION_USB_CDC) { + return; + } + static bool opened = false; + if (opened == Serial) { + return; + } + if (false == opened) { + App.schedule_dump_config(); + } + opened = !opened; +#endif +} +#endif + void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_.push_back(LogLevelOverride{tag, log_level}); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 9aabe88963..f6c1574ffb 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -56,7 +56,9 @@ enum UARTSelection { class Logger : public Component { public: explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); - +#ifdef USE_LOGGER_USB_CDC + void loop() override; +#endif /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); uint32_t get_baud_rate() const { return baud_rate_; } From 247baa414ab4611b0694996e12138ccf890142f0 Mon Sep 17 00:00:00 2001 From: chbmuc Date: Sun, 10 Mar 2024 23:58:50 +0100 Subject: [PATCH 423/436] Add IRK support to allow tracking of devices with random MAC addresses (#6335) * Add IRK support to allow tracking of devices with random MAC addresses * make CONF_IRK a local definition * Add tests --------- Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- .../components/ble_presence/binary_sensor.py | 11 +++- .../ble_presence/ble_presence_device.h | 61 ++++++++++++++++++- .../ble_presence/test.esp32-c3-idf.yaml | 4 ++ .../ble_presence/test.esp32-c3.yaml | 4 ++ .../ble_presence/test.esp32-idf.yaml | 4 ++ tests/components/ble_presence/test.esp32.yaml | 4 ++ 6 files changed, 86 insertions(+), 2 deletions(-) diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index 81878391bb..bd51cfbd0a 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_MIN_RSSI, ) +CONF_IRK = "irk" + DEPENDENCIES = ["esp32_ble_tracker"] ble_presence_ns = cg.esphome_ns.namespace("ble_presence") @@ -34,6 +36,7 @@ CONFIG_SCHEMA = cv.All( .extend( { cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_IRK): cv.uuid, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, @@ -45,7 +48,9 @@ CONFIG_SCHEMA = cv.All( ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA), - cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), + cv.has_exactly_one_key( + CONF_MAC_ADDRESS, CONF_IRK, CONF_SERVICE_UUID, CONF_IBEACON_UUID + ), _validate, ) @@ -61,6 +66,10 @@ async def to_code(config): if mac_address := config.get(CONF_MAC_ADDRESS): cg.add(var.set_address(mac_address.as_hex)) + if irk := config.get(CONF_IRK): + irk = esp32_ble_tracker.as_hex_array(str(irk)) + cg.add(var.set_irk(irk)) + if service_uuid := config.get(CONF_SERVICE_UUID): if len(service_uuid) == len(esp32_ble_tracker.bt_uuid16_format): cg.add(var.set_service_uuid16(esp32_ble_tracker.as_hex(service_uuid))) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 1be9adeb30..84753d5420 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -6,6 +6,16 @@ #ifdef USE_ESP32 +#ifdef USE_ARDUINO +#include "mbedtls/aes.h" +#include "mbedtls/base64.h" +#endif + +#ifdef USE_ESP_IDF +#define MBEDTLS_AES_ALT +#include +#endif + namespace esphome { namespace ble_presence { @@ -17,6 +27,10 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->match_by_ = MATCH_BY_MAC_ADDRESS; this->address_ = address; } + void set_irk(uint8_t *irk) { + this->match_by_ = MATCH_BY_IRK; + this->irk_ = irk; + } void set_service_uuid16(uint16_t uuid) { this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); @@ -62,6 +76,13 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, return true; } break; + case MATCH_BY_IRK: + if (resolve_irk_(device.address_uint64(), this->irk_)) { + this->publish_state(true); + this->found_ = true; + return true; + } + break; case MATCH_BY_SERVICE_UUID: for (auto uuid : device.get_service_uuids()) { if (this->uuid_ == uuid) { @@ -100,10 +121,11 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, float get_setup_priority() const override { return setup_priority::DATA; } protected: - enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; + enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; MatchType match_by_; uint64_t address_; + uint8_t *irk_; esp32_ble_tracker::ESPBTUUID uuid_; @@ -117,6 +139,43 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, bool check_ibeacon_minor_{false}; bool check_minimum_rssi_{false}; + bool resolve_irk_(uint64_t addr64, const uint8_t *irk) { + uint8_t ecb_key[16]; + uint8_t ecb_plaintext[16]; + uint8_t ecb_ciphertext[16]; + + memcpy(&ecb_key, irk, 16); + memset(&ecb_plaintext, 0, 16); + + ecb_plaintext[13] = (addr64 >> 40) & 0xff; + ecb_plaintext[14] = (addr64 >> 32) & 0xff; + ecb_plaintext[15] = (addr64 >> 24) & 0xff; + + mbedtls_aes_context ctx = {0, 0, {0}}; + mbedtls_aes_init(&ctx); + + if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) { + mbedtls_aes_free(&ctx); + return false; + } + + if (mbedtls_aes_crypt_ecb(&ctx, +#ifdef USE_ARDUINO + MBEDTLS_AES_ENCRYPT, +#elif defined(USE_ESP_IDF) + ESP_AES_ENCRYPT, +#endif + ecb_plaintext, ecb_ciphertext) != 0) { + mbedtls_aes_free(&ctx); + return false; + } + + mbedtls_aes_free(&ctx); + + return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && + ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); + } + bool found_{false}; }; diff --git a/tests/components/ble_presence/test.esp32-c3-idf.yaml b/tests/components/ble_presence/test.esp32-c3-idf.yaml index dde9215470..6e5173eed8 100644 --- a/tests/components/ble_presence/test.esp32-c3-idf.yaml +++ b/tests/components/ble_presence/test.esp32-c3-idf.yaml @@ -18,3 +18,7 @@ binary_sensor: ibeacon_major: 100 ibeacon_minor: 1 name: BLE Test iBeacon Presence + - platform: ble_presence + irk: 1234567890abcdef1234567890abcdef + name: "ESP32 BLE Tracker with Identity Resolving Key" + diff --git a/tests/components/ble_presence/test.esp32-c3.yaml b/tests/components/ble_presence/test.esp32-c3.yaml index dde9215470..6e5173eed8 100644 --- a/tests/components/ble_presence/test.esp32-c3.yaml +++ b/tests/components/ble_presence/test.esp32-c3.yaml @@ -18,3 +18,7 @@ binary_sensor: ibeacon_major: 100 ibeacon_minor: 1 name: BLE Test iBeacon Presence + - platform: ble_presence + irk: 1234567890abcdef1234567890abcdef + name: "ESP32 BLE Tracker with Identity Resolving Key" + diff --git a/tests/components/ble_presence/test.esp32-idf.yaml b/tests/components/ble_presence/test.esp32-idf.yaml index dde9215470..6e5173eed8 100644 --- a/tests/components/ble_presence/test.esp32-idf.yaml +++ b/tests/components/ble_presence/test.esp32-idf.yaml @@ -18,3 +18,7 @@ binary_sensor: ibeacon_major: 100 ibeacon_minor: 1 name: BLE Test iBeacon Presence + - platform: ble_presence + irk: 1234567890abcdef1234567890abcdef + name: "ESP32 BLE Tracker with Identity Resolving Key" + diff --git a/tests/components/ble_presence/test.esp32.yaml b/tests/components/ble_presence/test.esp32.yaml index dde9215470..6e5173eed8 100644 --- a/tests/components/ble_presence/test.esp32.yaml +++ b/tests/components/ble_presence/test.esp32.yaml @@ -18,3 +18,7 @@ binary_sensor: ibeacon_major: 100 ibeacon_minor: 1 name: BLE Test iBeacon Presence + - platform: ble_presence + irk: 1234567890abcdef1234567890abcdef + name: "ESP32 BLE Tracker with Identity Resolving Key" + From 8850b959e95bd6dbf4bd93ece7883fd4c6f36f64 Mon Sep 17 00:00:00 2001 From: alexborro Date: Mon, 11 Mar 2024 00:04:16 +0100 Subject: [PATCH 424/436] [Fingerprint_grow] Implements Sleep Mode feature (#6116) --- CODEOWNERS | 2 +- .../components/fingerprint_grow/__init__.py | 32 +++- .../fingerprint_grow/fingerprint_grow.cpp | 155 +++++++++++++++--- .../fingerprint_grow/fingerprint_grow.h | 16 ++ tests/test3.yaml | 5 + 5 files changed, 185 insertions(+), 25 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fd8afa27d2..2f964c2ef3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -118,7 +118,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi -esphome/components/fingerprint_grow/* @OnFreund @loongyh +esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh esphome/components/fs3000/* @kahrendt esphome/components/ft5x06/* @clydebarrow esphome/components/ft63x6/* @gpambrozio diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 26a01fc1d2..23651bd049 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -25,12 +25,14 @@ from esphome.const import ( CONF_TRIGGER_ID, ) -CODEOWNERS = ["@OnFreund", "@loongyh"] +CODEOWNERS = ["@OnFreund", "@loongyh", "@alexborro"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "sensor"] MULTI_CONF = True CONF_FINGERPRINT_GROW_ID = "fingerprint_grow_id" +CONF_SENSOR_POWER_PIN = "sensor_power_pin" +CONF_IDLE_PERIOD_TO_SLEEP = "idle_period_to_sleep" fingerprint_grow_ns = cg.esphome_ns.namespace("fingerprint_grow") FingerprintGrowComponent = fingerprint_grow_ns.class_( @@ -102,11 +104,26 @@ AURA_LED_COLORS = { } validate_aura_led_colors = cv.enum(AURA_LED_COLORS, upper=True) -CONFIG_SCHEMA = ( + +def validate(config): + if CONF_SENSOR_POWER_PIN in config and CONF_SENSING_PIN not in config: + raise cv.Invalid("You cannot use the Sensor Power Pin without a Sensing Pin") + if CONF_IDLE_PERIOD_TO_SLEEP in config and CONF_SENSOR_POWER_PIN not in config: + raise cv.Invalid( + "You cannot have an Idle Period to Sleep without a Sensor Power Pin" + ) + return config + + +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(FingerprintGrowComponent), cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_SENSOR_POWER_PIN): pins.gpio_output_pin_schema, + cv.Optional( + CONF_IDLE_PERIOD_TO_SLEEP + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_PASSWORD): cv.uint32_t, cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation( @@ -168,7 +185,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("500ms")) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + validate, ) @@ -188,6 +206,14 @@ async def to_code(config): sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN]) cg.add(var.set_sensing_pin(sensing_pin)) + if CONF_SENSOR_POWER_PIN in config: + sensor_power_pin = await cg.gpio_pin_expression(config[CONF_SENSOR_POWER_PIN]) + cg.add(var.set_sensor_power_pin(sensor_power_pin)) + + if CONF_IDLE_PERIOD_TO_SLEEP in config: + idle_period_to_sleep_ms = config[CONF_IDLE_PERIOD_TO_SLEEP] + cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms)) + for conf in config.get(CONF_ON_FINGER_SCAN_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index e8c6b2537f..bd0817350a 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -16,9 +16,14 @@ void FingerprintGrowComponent::update() { } if (this->has_sensing_pin_) { + // A finger touch results in a low level (digital_read() == false) if (this->sensing_pin_->digital_read()) { ESP_LOGV(TAG, "No touch sensing"); this->waiting_removal_ = false; + if ((this->enrollment_image_ == 0) && // Not in enrolment process + (millis() - this->last_transfer_ms_ > this->idle_period_to_sleep_ms_) && (this->is_sensor_awake_)) { + this->sensor_sleep_(); + } return; } else if (!this->waiting_removal_) { this->finger_scan_start_callback_.call(); @@ -53,7 +58,29 @@ void FingerprintGrowComponent::update() { void FingerprintGrowComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); + this->has_sensing_pin_ = (this->sensing_pin_ != nullptr); + this->has_power_pin_ = (this->sensor_power_pin_ != nullptr); + + // Call pins setup, so we effectively apply the config generated from the yaml file. + if (this->has_sensing_pin_) { + this->sensing_pin_->setup(); + } + if (this->has_power_pin_) { + // Starts with output low (disabling power) to avoid glitches in the sensor + this->sensor_power_pin_->digital_write(false); + this->sensor_power_pin_->setup(); + + // If the user didn't specify an idle period to sleep, applies the default. + if (this->idle_period_to_sleep_ms_ == UINT32_MAX) { + this->idle_period_to_sleep_ms_ = DEFAULT_IDLE_PERIOD_TO_SLEEP_MS; + } + } + + // Place the sensor in a known (sleep/off) state and sync internal var state. + this->sensor_sleep_(); + delay(20); // This delay guarantees the sensor will in fact be powered power. + if (this->check_password_()) { if (this->new_password_ != -1) { if (this->set_password_()) @@ -335,7 +362,7 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } } -uint8_t FingerprintGrowComponent::send_command_() { +uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) { while (this->available()) this->read(); this->write((uint8_t) (START_CODE >> 8)); @@ -346,12 +373,12 @@ uint8_t FingerprintGrowComponent::send_command_() { this->write(this->address_[3]); this->write(COMMAND); - uint16_t wire_length = this->data_.size() + 2; + uint16_t wire_length = p_data_buffer->size() + 2; this->write((uint8_t) (wire_length >> 8)); this->write((uint8_t) (wire_length & 0xFF)); uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND; - for (auto data : this->data_) { + for (auto data : *p_data_buffer) { this->write(data); sum += data; } @@ -359,7 +386,7 @@ uint8_t FingerprintGrowComponent::send_command_() { this->write((uint8_t) (sum >> 8)); this->write((uint8_t) (sum & 0xFF)); - this->data_.clear(); + p_data_buffer->clear(); uint8_t byte; uint16_t idx = 0, length = 0; @@ -369,7 +396,9 @@ uint8_t FingerprintGrowComponent::send_command_() { delay(1); continue; } + byte = this->read(); + switch (idx) { case 0: if (byte != (uint8_t) (START_CODE >> 8)) @@ -403,9 +432,9 @@ uint8_t FingerprintGrowComponent::send_command_() { length |= byte; break; default: - this->data_.push_back(byte); + p_data_buffer->push_back(byte); if ((idx - 8) == length) { - switch (this->data_[0]) { + switch ((*p_data_buffer)[0]) { case OK: case NO_FINGER: case IMAGE_FAIL: @@ -425,38 +454,122 @@ uint8_t FingerprintGrowComponent::send_command_() { ESP_LOGE(TAG, "Reader failed to process request"); break; default: - ESP_LOGE(TAG, "Unknown response received from reader: %d", this->data_[0]); + ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", (*p_data_buffer)[0]); break; } - return this->data_[0]; + this->last_transfer_ms_ = millis(); + return (*p_data_buffer)[0]; } break; } idx++; } ESP_LOGE(TAG, "No response received from reader"); - this->data_[0] = TIMEOUT; + (*p_data_buffer)[0] = TIMEOUT; + this->last_transfer_ms_ = millis(); return TIMEOUT; } +uint8_t FingerprintGrowComponent::send_command_() { + this->sensor_wakeup_(); + return this->transfer_(&this->data_); +} + +void FingerprintGrowComponent::sensor_wakeup_() { + // Immediately return if there is no power pin or the sensor is already on + if ((!this->has_power_pin_) || (this->is_sensor_awake_)) + return; + + this->sensor_power_pin_->digital_write(true); + this->is_sensor_awake_ = true; + + uint8_t byte = TIMEOUT; + + // Wait for the byte HANDSHAKE_SIGN from the sensor meaning it is operational. + for (uint16_t timer = 0; timer < WAIT_FOR_WAKE_UP_MS; timer++) { + if (this->available() > 0) { + byte = this->read(); + + /* If the received byte is zero, the UART probably misinterpreted a raising edge on + * the RX pin due the power up as byte "zero" - I verified this behaviour using + * the esp32-arduino lib. So here we just ignore this fake byte. + */ + if (byte != 0) + break; + } + delay(1); + } + + /* Lets check if the received by is a HANDSHAKE_SIGN, otherwise log an error + * message and try to continue on the best effort. + */ + if (byte == HANDSHAKE_SIGN) { + ESP_LOGD(TAG, "Sensor has woken up!"); + } else if (byte == TIMEOUT) { + ESP_LOGE(TAG, "Timed out waiting for sensor wake-up"); + } else { + ESP_LOGE(TAG, "Received wrong byte from the sensor during wake-up: 0x%.2X", byte); + } + + /* Next step, we must authenticate with the password. We cannot call check_password_ here + * neither use data_ to store the command because it might be already in use by the caller + * of send_command_() + */ + std::vector buffer = {VERIFY_PASSWORD, (uint8_t) (this->password_ >> 24), (uint8_t) (this->password_ >> 16), + (uint8_t) (this->password_ >> 8), (uint8_t) (this->password_ & 0xFF)}; + + if (this->transfer_(&buffer) != OK) { + ESP_LOGE(TAG, "Wrong password"); + } +} + +void FingerprintGrowComponent::sensor_sleep_() { + // Immediately return if the power pin feature is not implemented + if (!this->has_power_pin_) + return; + + this->sensor_power_pin_->digital_write(false); + this->is_sensor_awake_ = false; + ESP_LOGD(TAG, "Fingerprint sensor is now in sleep mode."); +} + void FingerprintGrowComponent::dump_config() { ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:"); ESP_LOGCONFIG(TAG, " System Identifier Code: 0x%.4X", this->system_identifier_code_); ESP_LOGCONFIG(TAG, " Touch Sensing Pin: %s", this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None"); + ESP_LOGCONFIG(TAG, " Sensor Power Pin: %s", + this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None"); + if (this->idle_period_to_sleep_ms_ < UINT32_MAX) { + ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %u ms", this->idle_period_to_sleep_ms_); + } else { + ESP_LOGCONFIG(TAG, " Idle Period to Sleep: Never"); + } LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); - ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->fingerprint_count_sensor_->get_state()); - LOG_SENSOR(" ", "Status", this->status_sensor_); - ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->status_sensor_->get_state()); - LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); - ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->capacity_sensor_->get_state()); - LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); - ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->security_level_sensor_->get_state()); - LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); - ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_finger_id_sensor_->get_state()); - LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); - ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_confidence_sensor_->get_state()); + if (this->fingerprint_count_sensor_) { + LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->fingerprint_count_sensor_->get_state()); + } + if (this->status_sensor_) { + LOG_SENSOR(" ", "Status", this->status_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->status_sensor_->get_state()); + } + if (this->capacity_sensor_) { + LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->capacity_sensor_->get_state()); + } + if (this->security_level_sensor_) { + LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->security_level_sensor_->get_state()); + } + if (this->last_finger_id_sensor_) { + LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_finger_id_sensor_->get_state()); + } + if (this->last_confidence_sensor_) { + LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_confidence_sensor_->get_state()); + } } } // namespace fingerprint_grow diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 1ab38d9fb5..20ff60997b 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -15,6 +15,11 @@ static const uint16_t START_CODE = 0xEF01; static const uint16_t ENROLLMENT_SLOT_UNUSED = 0xFFFF; +// The datasheet says a max wake up time of of 200ms. +static const uint8_t WAIT_FOR_WAKE_UP_MS = 200; + +static const uint32_t DEFAULT_IDLE_PERIOD_TO_SLEEP_MS = 5000; + enum GrowPacketType { COMMAND = 0x01, DATA = 0x02, @@ -63,6 +68,7 @@ enum GrowResponse { INVALID_IMAGE = 0x15, FLASH_ERR = 0x18, INVALID_REG = 0x1A, + HANDSHAKE_SIGN = 0x55, BAD_PACKET = 0xFE, TIMEOUT = 0xFF, }; @@ -99,8 +105,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic this->address_[3] = (uint8_t) (address & 0xFF); } void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } + void set_sensor_power_pin(GPIOPin *sensor_power_pin) { this->sensor_power_pin_ = sensor_power_pin; } void set_password(uint32_t password) { this->password_ = password; } void set_new_password(uint32_t new_password) { this->new_password_ = new_password; } + void set_idle_period_to_sleep_ms(uint32_t period_ms) { this->idle_period_to_sleep_ms_ = period_ms; } void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { this->fingerprint_count_sensor_ = fingerprint_count_sensor; } @@ -160,7 +168,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool set_password_(); bool get_parameters_(); void get_fingerprint_count_(); + uint8_t transfer_(std::vector *p_data_buffer); uint8_t send_command_(); + void sensor_wakeup_(); + void sensor_sleep_(); std::vector data_ = {}; uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; @@ -168,14 +179,19 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint32_t password_ = 0x0; uint32_t new_password_ = -1; GPIOPin *sensing_pin_{nullptr}; + GPIOPin *sensor_power_pin_{nullptr}; uint8_t enrollment_image_ = 0; uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; uint8_t enrollment_buffers_ = 5; bool waiting_removal_ = false; bool has_sensing_pin_ = false; + bool has_power_pin_ = false; + bool is_sensor_awake_ = false; + uint32_t last_transfer_ms_ = 0; uint32_t last_aura_led_control_ = 0; uint16_t last_aura_led_duration_ = 0; uint16_t system_identifier_code_ = 0; + uint32_t idle_period_to_sleep_ms_ = UINT32_MAX; sensor::Sensor *fingerprint_count_sensor_{nullptr}; sensor::Sensor *status_sensor_{nullptr}; sensor::Sensor *capacity_sensor_{nullptr}; diff --git a/tests/test3.yaml b/tests/test3.yaml index 1286315a7a..29ed40c52a 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1299,6 +1299,11 @@ fingerprint_grow: sensing_pin: allow_other_uses: true number: 4 + sensor_power_pin: + allow_other_uses: true + number: 5 + inverted: true + idle_period_to_sleep: 5s password: 0x12FE37DC new_password: 0xA65B9840 on_finger_scan_start: From 725b0c81e802c825b702ba730fd1060811d3da4e Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 11 Mar 2024 00:38:40 +0100 Subject: [PATCH 425/436] cleanup ili9xxx component by removing data rate define (#6350) --- esphome/components/ili9xxx/ili9xxx_display.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 7b92bd2336..7cbdb4569a 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -17,13 +17,9 @@ enum ILI9XXXColorMode { BITS_16 = 0x10, }; -#ifndef ILI9XXXDisplay_DATA_RATE -#define ILI9XXXDisplay_DATA_RATE spi::DATA_RATE_40MHZ -#endif // ILI9XXXDisplay_DATA_RATE - class ILI9XXXDisplay : public display::DisplayBuffer, public spi::SPIDevice { + spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_40MHZ> { public: ILI9XXXDisplay() = default; ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height, bool invert_colors) From c899a33d1af47207fc081e42ae46633d0743f0eb Mon Sep 17 00:00:00 2001 From: dentra Date: Mon, 11 Mar 2024 03:09:36 +0300 Subject: [PATCH 426/436] web_server_idf: support x-www-form-urlencoded POST requests (#6037) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/web_server_idf/utils.cpp | 93 +++++++++++ esphome/components/web_server_idf/utils.h | 17 ++ .../web_server_idf/web_server_idf.cpp | 149 ++++++++---------- .../web_server_idf/web_server_idf.h | 7 +- 4 files changed, 181 insertions(+), 85 deletions(-) create mode 100644 esphome/components/web_server_idf/utils.cpp create mode 100644 esphome/components/web_server_idf/utils.h diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp new file mode 100644 index 0000000000..6299937ce1 --- /dev/null +++ b/esphome/components/web_server_idf/utils.cpp @@ -0,0 +1,93 @@ +#ifdef USE_ESP_IDF +#include +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "http_parser.h" + +#include "utils.h" + +namespace esphome { +namespace web_server_idf { + +static const char *const TAG = "web_server_idf_utils"; + +void url_decode(char *str) { + char *ptr = str, buf; + for (; *str; str++, ptr++) { + if (*str == '%') { + str++; + if (parse_hex(str, 2, reinterpret_cast(&buf), 1) == 2) { + *ptr = buf; + str++; + } else { + str--; + *ptr = *str; + } + } else if (*str == '+') { + *ptr = ' '; + } else { + *ptr = *str; + } + } + *ptr = *str; +} + +bool request_has_header(httpd_req_t *req, const char *name) { return httpd_req_get_hdr_value_len(req, name); } + +optional request_get_header(httpd_req_t *req, const char *name) { + size_t len = httpd_req_get_hdr_value_len(req, name); + if (len == 0) { + return {}; + } + + std::string str; + str.resize(len); + + auto res = httpd_req_get_hdr_value_str(req, name, &str[0], len + 1); + if (res != ESP_OK) { + return {}; + } + + return {str}; +} + +optional request_get_url_query(httpd_req_t *req) { + auto len = httpd_req_get_url_query_len(req); + if (len == 0) { + return {}; + } + + std::string str; + str.resize(len); + + auto res = httpd_req_get_url_query_str(req, &str[0], len + 1); + if (res != ESP_OK) { + ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); + return {}; + } + + return {str}; +} + +optional query_key_value(const std::string &query_url, const std::string &key) { + if (query_url.empty()) { + return {}; + } + + auto val = std::unique_ptr(new char[query_url.size()]); + if (!val) { + ESP_LOGE(TAG, "Not enough memory to the query key value"); + return {}; + } + + if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) { + return {}; + } + + url_decode(val.get()); + return {val.get()}; +} + +} // namespace web_server_idf +} // namespace esphome +#endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h new file mode 100644 index 0000000000..9ed17c1d50 --- /dev/null +++ b/esphome/components/web_server_idf/utils.h @@ -0,0 +1,17 @@ +#pragma once +#ifdef USE_ESP_IDF + +#include +#include "esphome/core/helpers.h" + +namespace esphome { +namespace web_server_idf { + +bool request_has_header(httpd_req_t *req, const char *name); +optional request_get_header(httpd_req_t *req, const char *name); +optional request_get_url_query(httpd_req_t *req); +optional query_key_value(const std::string &query_url, const std::string &key); + +} // namespace web_server_idf +} // namespace esphome +#endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 8e67f3f169..cf187cd647 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -7,6 +7,7 @@ #include "esp_tls_crypto.h" +#include "utils.h" #include "web_server_idf.h" namespace esphome { @@ -47,7 +48,7 @@ void AsyncWebServer::begin() { const httpd_uri_t handler_post = { .uri = "", .method = HTTP_POST, - .handler = AsyncWebServer::request_handler, + .handler = AsyncWebServer::request_post_handler, .user_ctx = this, }; httpd_register_uri_handler(this->server_, &handler_post); @@ -62,20 +63,62 @@ void AsyncWebServer::begin() { } } +esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { + ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri); + auto content_type = request_get_header(r, "Content-Type"); + if (content_type.has_value() && *content_type != "application/x-www-form-urlencoded") { + ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request"); + // fallback to get handler to support backward compatibility + return AsyncWebServer::request_handler(r); + } + + if (!request_has_header(r, "Content-Length")) { + ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri); + httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr); + return ESP_OK; + } + + if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { + ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + + std::string post_query; + if (r->content_len > 0) { + post_query.resize(r->content_len); + const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1); + if (ret <= 0) { // 0 return value indicates connection closed + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr); + return ESP_ERR_TIMEOUT; + } + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + } + + AsyncWebServerRequest req(r, std::move(post_query)); + return static_cast(r->user_ctx)->request_handler_(&req); +} + esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) { - ESP_LOGV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri); + ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri); AsyncWebServerRequest req(r); - auto *server = static_cast(r->user_ctx); - for (auto *handler : server->handlers_) { - if (handler->canHandle(&req)) { + return static_cast(r->user_ctx)->request_handler_(&req); +} + +esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const { + for (auto *handler : this->handlers_) { + if (handler->canHandle(request)) { // At now process only basic requests. // OTA requires multipart request support and handleUpload for it - handler->handleRequest(&req); + handler->handleRequest(request); return ESP_OK; } } - if (server->on_not_found_) { - server->on_not_found_(&req); + if (this->on_not_found_) { + this->on_not_found_(request); return ESP_OK; } return ESP_ERR_NOT_FOUND; @@ -88,22 +131,10 @@ AsyncWebServerRequest::~AsyncWebServerRequest() { } } -bool AsyncWebServerRequest::hasHeader(const char *name) const { return httpd_req_get_hdr_value_len(*this, name); } +bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); } optional AsyncWebServerRequest::get_header(const char *name) const { - size_t buf_len = httpd_req_get_hdr_value_len(*this, name); - if (buf_len == 0) { - return {}; - } - auto buf = std::unique_ptr(new char[++buf_len]); - if (!buf) { - ESP_LOGE(TAG, "No enough memory for get header %s", name); - return {}; - } - if (httpd_req_get_hdr_value_str(*this, name, buf.get(), buf_len) != ESP_OK) { - return {}; - } - return {buf.get()}; + return request_get_header(*this, name); } std::string AsyncWebServerRequest::url() const { @@ -193,74 +224,25 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); } -static std::string url_decode(const std::string &in) { - std::string out; - out.reserve(in.size()); - for (std::size_t i = 0; i < in.size(); ++i) { - if (in[i] == '%') { - ++i; - if (i + 1 < in.size()) { - auto c = parse_hex(&in[i], 2); - if (c.has_value()) { - out += static_cast(*c); - ++i; - } else { - out += '%'; - out += in[i++]; - out += in[i]; - } - } else { - out += '%'; - out += in[i]; - } - } else if (in[i] == '+') { - out += ' '; - } else { - out += in[i]; - } - } - return out; -} - AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { auto find = this->params_.find(name); if (find != this->params_.end()) { return find->second; } - auto query_len = httpd_req_get_url_query_len(this->req_); - if (query_len == 0) { - return nullptr; + optional val = query_key_value(this->post_query_, name); + if (!val.has_value()) { + auto url_query = request_get_url_query(*this); + if (url_query.has_value()) { + val = query_key_value(url_query.value(), name); + } } - auto query_str = std::unique_ptr(new char[++query_len]); - if (!query_str) { - ESP_LOGE(TAG, "No enough memory for get query param"); - return nullptr; + AsyncWebParameter *param = nullptr; + if (val.has_value()) { + param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory) } - - auto res = httpd_req_get_url_query_str(*this, query_str.get(), query_len); - if (res != ESP_OK) { - ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); - return nullptr; - } - - auto query_val = std::unique_ptr(new char[query_len]); - if (!query_val) { - ESP_LOGE(TAG, "No enough memory for get query param value"); - return nullptr; - } - - res = httpd_query_key_value(query_str.get(), name.c_str(), query_val.get(), query_len); - if (res != ESP_OK) { - this->params_.insert({name, nullptr}); - return nullptr; - } - query_str.release(); - auto decoded = url_decode(query_val.get()); - query_val.release(); - auto *param = new AsyncWebParameter(decoded); // NOLINT(cppcoreguidelines-owning-memory) - this->params_.insert(std::make_pair(name, param)); + this->params_.insert({name, param}); return param; } @@ -271,14 +253,15 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) { void AsyncResponseStream::print(float value) { this->print(to_string(value)); } void AsyncResponseStream::printf(const char *fmt, ...) { - std::string str; va_list args; va_start(args, fmt); - size_t length = vsnprintf(nullptr, 0, fmt, args); + const int length = vsnprintf(nullptr, 0, fmt, args); va_end(args); + std::string str; str.resize(length); + va_start(args, fmt); vsnprintf(&str[0], length + 1, fmt, args); va_end(args); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 2fbc5cd2e9..750f890fc7 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -90,11 +90,10 @@ class AsyncWebServerResponseProgmem : public AsyncWebServerResponse { protected: const uint8_t *data_; - const size_t size_; + size_t size_; }; class AsyncWebServerRequest { - // FIXME friend class AsyncWebServerResponse; friend class AsyncWebServer; public: @@ -164,7 +163,9 @@ class AsyncWebServerRequest { httpd_req_t *req_; AsyncWebServerResponse *rsp_{}; std::map params_; + std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} + AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {} void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type); }; @@ -191,6 +192,8 @@ class AsyncWebServer { uint16_t port_{}; httpd_handle_t server_{}; static esp_err_t request_handler(httpd_req_t *r); + static esp_err_t request_post_handler(httpd_req_t *r); + esp_err_t request_handler_(AsyncWebServerRequest *request) const; std::vector handlers_; std::function on_not_found_{}; }; From 6a8a2aaefb582499da47948da0db21eedd8ce1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Mon, 11 Mar 2024 01:12:52 +0100 Subject: [PATCH 427/436] feat(MQTT): Add QoS option for each MQTT component (#6279) --- esphome/components/mqtt/__init__.py | 2 ++ esphome/components/mqtt/mqtt_client.cpp | 4 ++-- esphome/components/mqtt/mqtt_component.cpp | 12 ++++++++---- esphome/components/mqtt/mqtt_component.h | 5 +++++ esphome/config_validation.py | 2 ++ tests/test1.yaml | 1 + 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f804aee31a..e442eb9146 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -490,6 +490,8 @@ def get_default_topic_for(data, component_type, name, suffix): async def register_mqtt_component(var, config): await cg.register_component(var, {}) + if CONF_QOS in config: + cg.add(var.set_qos(config[CONF_QOS])) if CONF_RETAIN in config: cg.add(var.set_retain(config[CONF_RETAIN])) if not config.get(CONF_DISCOVERY, True): diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 0b18413928..0f5f49abc1 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -474,8 +474,8 @@ bool MQTTClientComponent::publish(const MQTTMessage &message) { if (!logging_topic) { if (ret) { - ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", message.topic.c_str(), message.payload.c_str(), - message.retain); + ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d qos=%d)", message.topic.c_str(), message.payload.c_str(), + message.retain, message.qos); } else { ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", message.topic.c_str(), message.payload.length()); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 8855b4d8e3..bb46ce732d 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -14,6 +14,8 @@ namespace mqtt { static const char *const TAG = "mqtt.component"; +void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } + void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { @@ -47,13 +49,13 @@ std::string MQTTComponent::get_command_topic_() const { bool MQTTComponent::publish(const std::string &topic, const std::string &payload) { if (topic.empty()) return false; - return global_mqtt_client->publish(topic, payload, 0, this->retain_); + return global_mqtt_client->publish(topic, payload, this->qos_, this->retain_); } bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) { if (topic.empty()) return false; - return global_mqtt_client->publish_json(topic, f, 0, this->retain_); + return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_); } bool MQTTComponent::send_discovery_() { @@ -61,7 +63,7 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.clean) { ESP_LOGV(TAG, "'%s': Cleaning discovery...", this->friendly_name().c_str()); - return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, 0, true); + return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); } ESP_LOGV(TAG, "'%s': Sending discovery...", this->friendly_name().c_str()); @@ -155,9 +157,11 @@ bool MQTTComponent::send_discovery_() { device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; }, - 0, discovery_info.retain); + this->qos_, discovery_info.retain); } +uint8_t MQTTComponent::get_qos() const { return this->qos_; } + bool MQTTComponent::get_retain() const { return this->retain_; } bool MQTTComponent::is_discovery_enabled() const { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 9fc8c795d3..147840d11f 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -77,6 +77,10 @@ class MQTTComponent : public Component { virtual bool is_internal(); + /// Set QOS for state messages. + void set_qos(uint8_t qos); + uint8_t get_qos() const; + /// Set whether state message should be retained. void set_retain(bool retain); bool get_retain() const; @@ -199,6 +203,7 @@ class MQTTComponent : public Component { bool command_retain_{false}; bool retain_{true}; + uint8_t qos_{0}; bool discovery_enabled_{true}; bool resend_state_{false}; }; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 848c5e4611..358608cd35 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_RETAIN, + CONF_QOS, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, @@ -1873,6 +1874,7 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema( MQTT_COMPONENT_SCHEMA = Schema( { + Optional(CONF_QOS): All(requires_component("mqtt"), int_range(min=0, max=2)), Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean), Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean), Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic), diff --git a/tests/test1.yaml b/tests/test1.yaml index 064924b864..505e839f5b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -693,6 +693,7 @@ sensor: internal: true address: 0x23 update_interval: 30s + qos: 2 retain: false availability: state_topic: livingroom/custom_state_topic From 9c95e570c7f0b286ea787c428c5ca331169d5d60 Mon Sep 17 00:00:00 2001 From: OdileVidrine <65249488+OdileVidrine@users.noreply.github.com> Date: Sun, 10 Mar 2024 20:33:31 -0400 Subject: [PATCH 428/436] Check permissions (#6255) --- esphome/__main__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index baa5ecde47..95d444ca9b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -297,8 +297,27 @@ def upload_using_platformio(config, port): return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) +def check_permissions(port): + if os.name == "posix" and get_port_type(port) == "SERIAL": + # Check if we can open selected serial port + if not os.access(port, os.F_OK): + raise EsphomeError( + "The selected serial port does not exist. To resolve this issue, " + "check that the device is connected to this computer with a USB cable and that " + "the USB cable can be used for data and is not a power-only cable." + ) + if not (os.access(port, os.R_OK | os.W_OK)): + raise EsphomeError( + "You do not have read or write permission on the selected serial port. " + "To resolve this issue, you can add your user to the dialout group " + f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. " + "You will need to log out & back in or reboot to activate the new group access." + ) + + def upload_program(config, args, host): if get_port_type(host) == "SERIAL": + check_permissions(host) if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): file = getattr(args, "file", None) return upload_using_esptool(config, host, file) @@ -344,6 +363,7 @@ def show_logs(config, args, port): if "logger" not in config: raise EsphomeError("Logger is not configured!") if get_port_type(port) == "SERIAL": + check_permissions(port) return run_miniterm(config, port) if get_port_type(port) == "NETWORK" and "api" in config: if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: From d6bcc465a849ce7c5b455f4c5f11d8138e155f45 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:34:46 +1100 Subject: [PATCH 429/436] Add CST816 touchscreen driver (#5941) * Add CST816 touchscreen --- CODEOWNERS | 1 + esphome/components/cst816/__init__.py | 6 + .../cst816/binary_sensor/__init__.py | 28 +++++ .../cst816/binary_sensor/cst816_button.h | 27 +++++ .../components/cst816/touchscreen/__init__.py | 34 ++++++ .../cst816/touchscreen/cst816_touchscreen.cpp | 113 ++++++++++++++++++ .../cst816/touchscreen/cst816_touchscreen.h | 60 ++++++++++ tests/components/cst816/test.esp32.yaml | 36 ++++++ 8 files changed, 305 insertions(+) create mode 100644 esphome/components/cst816/__init__.py create mode 100644 esphome/components/cst816/binary_sensor/__init__.py create mode 100644 esphome/components/cst816/binary_sensor/cst816_button.h create mode 100644 esphome/components/cst816/touchscreen/__init__.py create mode 100644 esphome/components/cst816/touchscreen/cst816_touchscreen.cpp create mode 100644 esphome/components/cst816/touchscreen/cst816_touchscreen.h create mode 100644 tests/components/cst816/test.esp32.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 2f964c2ef3..4f6ba3eed7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,6 +80,7 @@ esphome/components/copy/* @OttoWinter esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun esphome/components/cse7761/* @berfenger +esphome/components/cst816/* @clydebarrow esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx esphome/components/dac7678/* @NickB1 diff --git a/esphome/components/cst816/__init__.py b/esphome/components/cst816/__init__.py new file mode 100644 index 0000000000..674a80b7c1 --- /dev/null +++ b/esphome/components/cst816/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +cst816_ns = cg.esphome_ns.namespace("cst816") diff --git a/esphome/components/cst816/binary_sensor/__init__.py b/esphome/components/cst816/binary_sensor/__init__.py new file mode 100644 index 0000000000..b3fd5bb852 --- /dev/null +++ b/esphome/components/cst816/binary_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor + +from .. import cst816_ns +from ..touchscreen import CST816Touchscreen, CST816ButtonListener + +CONF_CST816_ID = "cst816_id" + +CST816Button = cst816_ns.class_( + "CST816Button", + binary_sensor.BinarySensor, + cg.Component, + CST816ButtonListener, + cg.Parented.template(CST816Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST816Button).extend( + { + cv.GenerateID(CONF_CST816_ID): cv.use_id(CST816Touchscreen), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_CST816_ID]) diff --git a/esphome/components/cst816/binary_sensor/cst816_button.h b/esphome/components/cst816/binary_sensor/cst816_button.h new file mode 100644 index 0000000000..4ae856d506 --- /dev/null +++ b/esphome/components/cst816/binary_sensor/cst816_button.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/cst816/touchscreen/cst816_touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace cst816 { + +class CST816Button : public binary_sensor::BinarySensor, + public Component, + public CST816ButtonListener, + public Parented { + public: + void setup() override { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); + } + + void dump_config() override { LOG_BINARY_SENSOR("", "CST816 Button", this); } + + void update_button(bool state) override { this->publish_state(state); } +}; + +} // namespace cst816 +} // namespace esphome diff --git a/esphome/components/cst816/touchscreen/__init__.py b/esphome/components/cst816/touchscreen/__init__.py new file mode 100644 index 0000000000..a3603ef575 --- /dev/null +++ b/esphome/components/cst816/touchscreen/__init__.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_RESET_PIN +from .. import cst816_ns + + +CST816Touchscreen = cst816_ns.class_( + "CST816Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CST816ButtonListener = cst816_ns.class_("CST816ButtonListener") +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CST816Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } +).extend(i2c.i2c_device_schema(0x15)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp new file mode 100644 index 0000000000..d2b8cc81f1 --- /dev/null +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -0,0 +1,113 @@ +#include "cst816_touchscreen.h" + +namespace esphome { +namespace cst816 { + +void CST816Touchscreen::continue_setup_() { + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + if (!this->read_byte(REG_CHIP_ID, &this->chip_id_)) { + this->mark_failed(); + esph_log_e(TAG, "Failed to read chip id"); + return; + } + switch (this->chip_id_) { + case CST820_CHIP_ID: + case CST716_CHIP_ID: + case CST816S_CHIP_ID: + case CST816D_CHIP_ID: + case CST816T_CHIP_ID: + break; + default: + this->mark_failed(); + esph_log_e(TAG, "Unknown chip ID 0x%02X", this->chip_id_); + return; + } + this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + esph_log_config(TAG, "CST816 Touchscreen setup complete"); +} + +void CST816Touchscreen::update_button_state_(bool state) { + if (this->button_touched_ == state) + return; + this->button_touched_ = state; + for (auto *listener : this->button_listeners_) + listener->update_button(state); +} + +void CST816Touchscreen::setup() { + esph_log_config(TAG, "Setting up CST816 Touchscreen..."); + 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 CST816Touchscreen::update_touches() { + uint8_t data[13]; + if (!this->read_bytes(REG_STATUS, data, sizeof data)) { + this->status_set_warning(); + return; + } + uint8_t num_of_touches = data[REG_TOUCH_NUM] & 3; + if (num_of_touches == 0) { + this->update_button_state_(false); + return; + } + + uint16_t x = encode_uint16(data[REG_XPOS_HIGH] & 0xF, data[REG_XPOS_LOW]); + uint16_t y = encode_uint16(data[REG_YPOS_HIGH] & 0xF, data[REG_YPOS_LOW]); + esph_log_v(TAG, "Read touch %d/%d", x, y); + if (x >= this->x_raw_max_) { + this->update_button_state_(true); + } else { + this->add_raw_touch_position_(0, x, y); + } +} + +void CST816Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "CST816 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + const char *name; + switch (this->chip_id_) { + case CST820_CHIP_ID: + name = "CST820"; + break; + case CST816S_CHIP_ID: + name = "CST816S"; + break; + case CST816D_CHIP_ID: + name = "CST816D"; + break; + case CST716_CHIP_ID: + name = "CST716"; + break; + case CST816T_CHIP_ID: + name = "CST816T"; + break; + default: + name = "Unknown"; + break; + } + ESP_LOGCONFIG(TAG, " Chip type: %s", name); +} + +} // namespace cst816 +} // namespace esphome diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.h b/esphome/components/cst816/touchscreen/cst816_touchscreen.h new file mode 100644 index 0000000000..0d987f2739 --- /dev/null +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cst816 { + +static const char *const TAG = "cst816.touchscreen"; + +static const uint8_t REG_STATUS = 0x00; +static const uint8_t REG_TOUCH_NUM = 0x02; +static const uint8_t REG_XPOS_HIGH = 0x03; +static const uint8_t REG_XPOS_LOW = 0x04; +static const uint8_t REG_YPOS_HIGH = 0x05; +static const uint8_t REG_YPOS_LOW = 0x06; +static const uint8_t REG_DIS_AUTOSLEEP = 0xFE; +static const uint8_t REG_CHIP_ID = 0xA7; +static const uint8_t REG_FW_VERSION = 0xA9; +static const uint8_t REG_SLEEP = 0xE5; +static const uint8_t REG_IRQ_CTL = 0xFA; +static const uint8_t IRQ_EN_MOTION = 0x70; + +static const uint8_t CST820_CHIP_ID = 0xB7; +static const uint8_t CST816S_CHIP_ID = 0xB4; +static const uint8_t CST816D_CHIP_ID = 0xB6; +static const uint8_t CST816T_CHIP_ID = 0xB5; +static const uint8_t CST716_CHIP_ID = 0x20; + +class CST816ButtonListener { + public: + virtual void update_button(bool state) = 0; +}; + +class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void update_touches() override; + void register_button_listener(CST816ButtonListener *listener) { this->button_listeners_.push_back(listener); } + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void continue_setup_(); + void update_button_state_(bool state); + + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; + uint8_t chip_id_{}; + std::vector button_listeners_; + bool button_touched_{}; +}; + +} // namespace cst816 +} // namespace esphome diff --git a/tests/components/cst816/test.esp32.yaml b/tests/components/cst816/test.esp32.yaml new file mode 100644 index 0000000000..f8deea6e98 --- /dev/null +++ b/tests/components/cst816/test.esp32.yaml @@ -0,0 +1,36 @@ +touchscreen: + - platform: cst816 + id: my_touchscreen + interrupt_pin: + number: 21 + reset_pin: GPIO16 + transform: + mirror_x: false + mirror_y: false + swap_xy: false + +i2c: + sda: 3 + scl: 2 + +display: + - id: my_display + platform: ili9xxx + dimensions: 480x320 + model: ST7796 + cs_pin: 15 + dc_pin: 20 + reset_pin: 22 + transform: + swap_xy: true + mirror_x: true + mirror_y: true + auto_clear_enabled: false + +spi: + clk_pin: 14 + mosi_pin: 13 + +binary_sensor: + - platform: cst816 + name: Home Button From c559ccbb831d1afd19863151fd3fa42a489dda48 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:52:05 +1100 Subject: [PATCH 430/436] ILI9XXX: Lazily allocate buffer (#6352) --- esphome/components/ili9xxx/ili9xxx_display.cpp | 7 ++++++- esphome/components/ili9xxx/ili9xxx_display.h | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 9f06c9ce0f..e292906a93 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -42,7 +42,9 @@ void ILI9XXXDisplay::setup() { this->y_low_ = this->height_; this->x_high_ = 0; this->y_high_ = 0; +} +void ILI9XXXDisplay::alloc_buffer_() { if (this->buffer_color_mode_ == BITS_16) { this->init_internal_(this->get_buffer_length_() * 2); if (this->buffer_ != nullptr) { @@ -107,6 +109,8 @@ void ILI9XXXDisplay::dump_config() { float ILI9XXXDisplay::get_setup_priority() const { return setup_priority::HARDWARE; } void ILI9XXXDisplay::fill(Color color) { + if (!this->check_buffer_()) + return; uint16_t new_color = 0; this->x_low_ = 0; this->y_low_ = 0; @@ -124,7 +128,6 @@ void ILI9XXXDisplay::fill(Color color) { // Upper and lower is equal can use quicker memset operation. Takes ~20ms. memset(this->buffer_, (uint8_t) new_color, buffer_length_16_bits); } else { - // Slower set of both buffers. Takes ~30ms. for (uint32_t i = 0; i < buffer_length_16_bits; i = i + 2) { this->buffer_[i] = (uint8_t) (new_color >> 8); this->buffer_[i + 1] = (uint8_t) new_color; @@ -144,6 +147,8 @@ void HOT ILI9XXXDisplay::draw_absolute_pixel_internal(int x, int y, Color color) if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { return; } + if (!this->check_buffer_()) + return; uint32_t pos = (y * width_) + x; uint16_t new_color; bool updated = false; diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 7cbdb4569a..41a1bbb528 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -86,6 +86,14 @@ class ILI9XXXDisplay : public display::DisplayBuffer, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; protected: + inline bool check_buffer_() { + if (this->buffer_ == nullptr) { + this->alloc_buffer_(); + return !this->is_failed(); + } + return true; + } + void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); @@ -116,6 +124,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer, void end_command_(); void start_data_(); void end_data_(); + void alloc_buffer_(); GPIOPin *reset_pin_{nullptr}; GPIOPin *dc_pin_{nullptr}; From 1662f833b04ce6dc17549effdfc10de2b081e483 Mon Sep 17 00:00:00 2001 From: swoboda1337 <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 11 Mar 2024 02:33:43 -0400 Subject: [PATCH 431/436] AM2315C Temperature + Humidity Sensor (#6266) Co-authored-by: Jonathan Swoboda Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/am2315c/__init__.py | 1 + esphome/components/am2315c/am2315c.cpp | 200 ++++++++++++++++++ esphome/components/am2315c/am2315c.h | 51 +++++ esphome/components/am2315c/sensor.py | 54 +++++ .../components/am2315c/test.esp32-c3-idf.yaml | 11 + tests/components/am2315c/test.esp32-c3.yaml | 11 + tests/components/am2315c/test.esp32-idf.yaml | 11 + tests/components/am2315c/test.esp32.yaml | 11 + tests/components/am2315c/test.esp8266.yaml | 11 + tests/components/am2315c/test.rp2040.yaml | 11 + 11 files changed, 373 insertions(+) create mode 100644 esphome/components/am2315c/__init__.py create mode 100644 esphome/components/am2315c/am2315c.cpp create mode 100644 esphome/components/am2315c/am2315c.h create mode 100644 esphome/components/am2315c/sensor.py create mode 100644 tests/components/am2315c/test.esp32-c3-idf.yaml create mode 100644 tests/components/am2315c/test.esp32-c3.yaml create mode 100644 tests/components/am2315c/test.esp32-idf.yaml create mode 100644 tests/components/am2315c/test.esp32.yaml create mode 100644 tests/components/am2315c/test.esp8266.yaml create mode 100644 tests/components/am2315c/test.rp2040.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 4f6ba3eed7..b2424cf5d1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -30,6 +30,7 @@ esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/alpha3/* @jan-hofmeier +esphome/components/am2315c/* @swoboda1337 esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/am43/sensor/* @buxtronix diff --git a/esphome/components/am2315c/__init__.py b/esphome/components/am2315c/__init__.py new file mode 100644 index 0000000000..2398e4fa23 --- /dev/null +++ b/esphome/components/am2315c/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@swoboda1337"] diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp new file mode 100644 index 0000000000..715251a9df --- /dev/null +++ b/esphome/components/am2315c/am2315c.cpp @@ -0,0 +1,200 @@ +// MIT License +// +// Copyright (c) 2023-2024 Rob Tillaart +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#include "am2315c.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace am2315c { + +static const char *const TAG = "am2315c"; + +uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) { + uint8_t crc = 0xFF; + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if (crc & 0x80) { + crc <<= 1; + crc ^= 0x31; + } else { + crc <<= 1; + } + } + } + return crc; +} + +bool AM2315C::reset_register_(uint8_t reg) { + // code based on demo code sent by www.aosong.com + // no further documentation. + // 0x1B returned 18, 0, 4 + // 0x1C returned 18, 65, 0 + // 0x1E returned 18, 8, 0 + // 18 seems to be status register + // other values unknown. + uint8_t data[3]; + data[0] = reg; + data[1] = 0; + data[2] = 0; + ESP_LOGD(TAG, "Reset register: 0x%02x", reg); + if (this->write(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Write failed!"); + this->mark_failed(); + return false; + } + delay(5); + if (this->read(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return false; + } + delay(10); + data[0] = 0xB0 | reg; + if (this->write(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Write failed!"); + this->mark_failed(); + return false; + } + delay(5); + return true; +} + +bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { + uint32_t raw; + raw = (data[1] << 12) | (data[2] << 4) | (data[3] >> 4); + humidity = raw * 9.5367431640625e-5; + raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; + temperature = raw * 1.9073486328125e-4 - 50; + return this->crc8_(data, 6) == data[6]; +} + +void AM2315C::setup() { + ESP_LOGCONFIG(TAG, "Setting up AM2315C..."); + + // get status + uint8_t status = 0; + if (this->read(&status, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return; + } + + // reset registers if required, according to the datasheet + // this can be required after power on, although this was + // never required during testing + if ((status & 0x18) != 0x18) { + ESP_LOGD(TAG, "Resetting AM2315C registers"); + if (!this->reset_register_(0x1B)) { + this->mark_failed(); + return; + } + if (!this->reset_register_(0x1C)) { + this->mark_failed(); + return; + } + if (!this->reset_register_(0x1E)) { + this->mark_failed(); + return; + } + } +} + +void AM2315C::update() { + // request measurement + uint8_t data[3]; + data[0] = 0xAC; + data[1] = 0x33; + data[2] = 0x00; + if (this->write(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Write failed!"); + this->mark_failed(); + return; + } + + // wait for hw to complete measurement + set_timeout(160, [this]() { + // check status + uint8_t status = 0; + if (this->read(&status, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return; + } + if ((status & 0x80) == 0x80) { + ESP_LOGE(TAG, "HW still busy!"); + this->mark_failed(); + return; + } + + // read + uint8_t data[7]; + if (this->read(data, 7) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return; + } + + // check for all zeros + bool zeros = true; + for (uint8_t i : data) { + zeros = zeros && (i == 0); + } + if (zeros) { + ESP_LOGW(TAG, "Data all zeros!"); + this->status_set_warning(); + return; + } + + // convert + float temperature = 0.0; + float humidity = 0.0; + if (this->convert_(data, humidity, temperature)) { + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(temperature); + } + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->publish_state(humidity); + } + this->status_clear_warning(); + } else { + ESP_LOGW(TAG, "CRC failed!"); + this->status_set_warning(); + } + }); +} + +void AM2315C::dump_config() { + ESP_LOGCONFIG(TAG, "AM2315C:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AM2315C failed!"); + } + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float AM2315C::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace am2315c +} // namespace esphome diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h new file mode 100644 index 0000000000..9cec40e4c2 --- /dev/null +++ b/esphome/components/am2315c/am2315c.h @@ -0,0 +1,51 @@ +// MIT License +// +// Copyright (c) 2023-2024 Rob Tillaart +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace am2315c { + +class AM2315C : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + void update() override; + void setup() override; + float get_setup_priority() const override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + uint8_t crc8_(uint8_t *data, uint8_t len); + bool convert_(uint8_t *data, float &humidity, float &temperature); + bool reset_register_(uint8_t reg); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace am2315c +} // namespace esphome diff --git a/esphome/components/am2315c/sensor.py b/esphome/components/am2315c/sensor.py new file mode 100644 index 0000000000..f3201b05a2 --- /dev/null +++ b/esphome/components/am2315c/sensor.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +am2315c_ns = cg.esphome_ns.namespace("am2315c") +AM2315C = am2315c_ns.class_("AM2315C", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AM2315C), + 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=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x38)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/components/am2315c/test.esp32-c3-idf.yaml b/tests/components/am2315c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp32-c3.yaml b/tests/components/am2315c/test.esp32-c3.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.esp32-c3.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp32-idf.yaml b/tests/components/am2315c/test.esp32-idf.yaml new file mode 100644 index 0000000000..ed6b65f787 --- /dev/null +++ b/tests/components/am2315c/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 16 + sda: 17 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp32.yaml b/tests/components/am2315c/test.esp32.yaml new file mode 100644 index 0000000000..ed6b65f787 --- /dev/null +++ b/tests/components/am2315c/test.esp32.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 16 + sda: 17 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp8266.yaml b/tests/components/am2315c/test.esp8266.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.esp8266.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.rp2040.yaml b/tests/components/am2315c/test.rp2040.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.rp2040.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity From 501973e07bbb984ad81dd416e7ed5f811913c149 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:38:47 +1100 Subject: [PATCH 432/436] Add ble_presence binary sensor timeout config value. (#6024) * Add binary sensor timeout config value. * Add test --- .../components/ble_presence/binary_sensor.py | 3 ++ .../ble_presence/ble_presence_device.h | 28 +++++++++++-------- tests/test2.yaml | 1 + 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index bd51cfbd0a..9c24a91a05 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_IBEACON_MINOR, CONF_IBEACON_UUID, CONF_MIN_RSSI, + CONF_TIMEOUT, ) CONF_IRK = "irk" @@ -41,6 +42,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, cv.Optional(CONF_IBEACON_UUID): cv.uuid, + cv.Optional(CONF_TIMEOUT, default="5min"): cv.positive_time_period, cv.Optional(CONF_MIN_RSSI): cv.All( cv.decibel, cv.int_range(min=-100, max=-30) ), @@ -60,6 +62,7 @@ async def to_code(config): await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) + cg.add(var.set_timeout(config[CONF_TIMEOUT].total_milliseconds)) if min_rssi := config.get(CONF_MIN_RSSI): cg.add(var.set_minimum_rssi(min_rssi)) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 84753d5420..0d86f6a40d 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -59,11 +59,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->check_minimum_rssi_ = true; this->minimum_rssi_ = rssi; } - void on_scan_end() override { - if (!this->found_) - this->publish_state(false); - this->found_ = false; - } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { if (this->check_minimum_rssi_ && this->minimum_rssi_ > device.get_rssi()) { return false; @@ -71,8 +67,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, switch (this->match_by_) { case MATCH_BY_MAC_ADDRESS: if (device.address_uint64() == this->address_) { - this->publish_state(true); - this->found_ = true; + this->set_found_(true); return true; } break; @@ -86,8 +81,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, case MATCH_BY_SERVICE_UUID: for (auto uuid : device.get_service_uuids()) { if (this->uuid_ == uuid) { - this->publish_state(true); - this->found_ = true; + this->set_found_(true); return true; } } @@ -111,16 +105,26 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, return false; } - this->publish_state(true); - this->found_ = true; + this->set_found_(true); return true; } return false; } + + void loop() override { + if (this->found_ && this->last_seen_ + this->timeout_ < millis()) + this->set_found_(false); + } void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } protected: + void set_found_(bool state) { + this->found_ = state; + if (state) + this->last_seen_ = millis(); + this->publish_state(state); + } enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; MatchType match_by_; @@ -177,6 +181,8 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } bool found_{false}; + uint32_t last_seen_{}; + uint32_t timeout_{}; }; } // namespace ble_presence diff --git a/tests/test2.yaml b/tests/test2.yaml index f7a690709a..217a4c8feb 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -501,6 +501,7 @@ binary_sensor: - platform: ble_presence mac_address: AC:37:43:77:5F:4C name: ESP32 BLE Tracker Google Home Mini + timeout: 30s - platform: ble_presence service_uuid: 11aa name: BLE Test Service 16 Presence From dfb14fc6ea90613cabf71c88c677933438afadfa Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:13:41 +1100 Subject: [PATCH 433/436] Add state listeners to `rotary_encoder` (#6035) --- esphome/components/rotary_encoder/rotary_encoder.cpp | 1 + esphome/components/rotary_encoder/rotary_encoder.h | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 7440214b1c..a3631ffe27 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -226,6 +226,7 @@ void RotaryEncoderSensor::loop() { } this->store_.last_read = counter; this->publish_state(counter); + this->listeners_.call(counter); this->publish_initial_value_ = false; } } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index deba3d952d..e88ee9152a 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -92,6 +92,8 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { this->on_anticlockwise_callback_.add(std::move(callback)); } + void register_listener(std::function listener) { this->listeners_.add(std::move(listener)); } + protected: InternalGPIOPin *pin_a_; InternalGPIOPin *pin_b_; @@ -102,8 +104,9 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { RotaryEncoderSensorStore store_{}; - CallbackManager on_clockwise_callback_; - CallbackManager on_anticlockwise_callback_; + CallbackManager on_clockwise_callback_{}; + CallbackManager on_anticlockwise_callback_{}; + CallbackManager listeners_{}; }; template class RotaryEncoderSetValueAction : public Action { From 221f04b9a594faff208b149911c4c366f7827097 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:19:35 +1100 Subject: [PATCH 434/436] ili9xxx: Add support for GC9A01A display (#6351) * Add support for GCA901A display --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ili9xxx/display.py | 1 + esphome/components/ili9xxx/ili9xxx_display.h | 5 ++ esphome/components/ili9xxx/ili9xxx_init.h | 54 ++++++++++++++++++++ tests/components/ili9xxx/test.esp32.yaml | 11 ++++ 4 files changed, 71 insertions(+) create mode 100644 tests/components/ili9xxx/test.esp32.yaml diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 0bd810ea16..3aaf76d6f8 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -51,6 +51,7 @@ ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode") ColorOrder = display.display_ns.enum("ColorMode") MODELS = { + "GC9A01A": ili9xxx_ns.class_("ILI9XXXGC9A01A", ILI9XXXDisplay), "M5STACK": ili9xxx_ns.class_("ILI9XXXM5Stack", ILI9XXXDisplay), "M5CORE": ili9xxx_ns.class_("ILI9XXXM5CORE", ILI9XXXDisplay), "TFT_2.4": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 41a1bbb528..11a90e142f 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -254,5 +254,10 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay { ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {} }; +class ILI9XXXGC9A01A : public ILI9XXXDisplay { + public: + ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {} +}; + } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index fe3f168c32..ea90f83f30 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -316,6 +316,60 @@ static const uint8_t PROGMEM INITCMD_ST7789V[] = { 0x00 // End of list }; +static const uint8_t PROGMEM INITCMD_GC9A01A[] = { + 0xEF, 0, + 0xEB, 1, 0x14, // ? + 0xFE, 0, + 0xEF, 0, + 0xEB, 1, 0x14, // ? + 0x84, 1, 0x40, // ? + 0x85, 1, 0xFF, // ? + 0x86, 1, 0xFF, // ? + 0x87, 1, 0xFF, // ? + 0x88, 1, 0x0A, // ? + 0x89, 1, 0x21, // ? + 0x8A, 1, 0x00, // ? + 0x8B, 1, 0x80, // ? + 0x8C, 1, 0x01, // ? + 0x8D, 1, 0x01, // ? + 0x8E, 1, 0xFF, // ? + 0x8F, 1, 0xFF, // ? + 0xB6, 2, 0x00, 0x00, // ? + 0x90, 4, 0x08, 0x08, 0x08, 0x08, // ? + ILI9XXX_PIXFMT , 1, 0x05, + ILI9XXX_MADCTL , 1, MADCTL_MX| MADCTL_BGR, // Memory Access Control + 0xBD, 1, 0x06, // ? + 0xBC, 1, 0x00, // ? + 0xFF, 3, 0x60, 0x01, 0x04, // ? + 0xC3, 1, 0x13, + 0xC4, 1, 0x13, + 0xF9, 1, 0x22, + 0xBE, 1, 0x11, // ? + 0xE1, 2, 0x10, 0x0E, // ? + 0xDF, 3, 0x21, 0x0c, 0x02, // ? + 0xF0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A, + 0xF1, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F, + 0xF2, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A, + 0xF3, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F, + 0xED, 2, 0x1B, 0x0B, // ? + 0xAE, 1, 0x77, // ? + 0xCD, 1, 0x63, // ? + 0xE8, 1, 0x34, + 0x62, 12, 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, // ? + 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70, + 0x63, 12, 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, // ? + 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70, + 0x64, 7, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07, // ? + 0x66, 10, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00, // ? + 0x67, 10, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98, // ? + 0x74, 7, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00, // ? + 0x98, 2, 0x3e, 0x07, // ? + 0x35, 0, + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + // clang-format on } // namespace ili9xxx } // namespace esphome diff --git a/tests/components/ili9xxx/test.esp32.yaml b/tests/components/ili9xxx/test.esp32.yaml new file mode 100644 index 0000000000..1095d565d2 --- /dev/null +++ b/tests/components/ili9xxx/test.esp32.yaml @@ -0,0 +1,11 @@ +spi: + mosi_pin: GPIO23 + clk_pin: GPIO18 + +display: + - platform: ili9xxx + model: gc9a01a + id: gca901_display + cs_pin: GPIO5 + dc_pin: GPIO22 + reset_pin: GPIO21 From 11b31483c3aeb1c4e52548eade8668bbd3cbe142 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:35:20 +1100 Subject: [PATCH 435/436] Touchscreen: add support for CST226 controller chip (#6151) --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/cst226/__init__.py | 6 ++ .../components/cst226/touchscreen/__init__.py | 38 ++++++++ .../cst226/touchscreen/cst226_touchscreen.cpp | 92 +++++++++++++++++++ .../cst226/touchscreen/cst226_touchscreen.h | 44 +++++++++ tests/components/cst226/test.esp32-c3.yaml | 24 +++++ 6 files changed, 205 insertions(+) create mode 100644 esphome/components/cst226/__init__.py create mode 100644 esphome/components/cst226/touchscreen/__init__.py create mode 100644 esphome/components/cst226/touchscreen/cst226_touchscreen.cpp create mode 100644 esphome/components/cst226/touchscreen/cst226_touchscreen.h create mode 100644 tests/components/cst226/test.esp32-c3.yaml diff --git a/CODEOWNERS b/CODEOWNERS index b2424cf5d1..b1c9c2ee2c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -81,6 +81,7 @@ esphome/components/copy/* @OttoWinter esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun esphome/components/cse7761/* @berfenger +esphome/components/cst226/* @clydebarrow esphome/components/cst816/* @clydebarrow esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx diff --git a/esphome/components/cst226/__init__.py b/esphome/components/cst226/__init__.py new file mode 100644 index 0000000000..847e44dbda --- /dev/null +++ b/esphome/components/cst226/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +cst226_ns = cg.esphome_ns.namespace("cst226") diff --git a/esphome/components/cst226/touchscreen/__init__.py b/esphome/components/cst226/touchscreen/__init__.py new file mode 100644 index 0000000000..76975ffe78 --- /dev/null +++ b/esphome/components/cst226/touchscreen/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_RESET_PIN +from .. import cst226_ns + + +CST226Touchscreen = cst226_ns.class_( + "CST226Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CST226ButtonListener = cst226_ns.class_("CST226ButtonListener") +CONFIG_SCHEMA = ( + touchscreen.touchscreen_schema("100ms") + .extend( + { + cv.GenerateID(): cv.declare_id(CST226Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x5A)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp new file mode 100644 index 0000000000..d4e43d30f5 --- /dev/null +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp @@ -0,0 +1,92 @@ +#include "cst226_touchscreen.h" + +namespace esphome { +namespace cst226 { + +void CST226Touchscreen::setup() { + esph_log_config(TAG, "Setting up CST226 Touchscreen..."); + 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() { + uint8_t data[28]; + if (!this->read_bytes(CST226_REG_STATUS, data, sizeof data)) { + this->status_set_warning(); + this->skip_update_ = true; + return; + } + this->status_clear_warning(); + if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) { + this->skip_update_ = true; + return; + } + uint8_t num_of_touches = data[5] & 0x7F; + if (num_of_touches == 0 || num_of_touches > 5) { + this->write_byte(0, 0xAB); + return; + } + + size_t index = 0; + for (uint8_t i = 0; i != num_of_touches; i++) { + uint8_t id = data[index] >> 4; + int16_t x = (data[index + 1] << 4) | ((data[index + 3] >> 4) & 0x0F); + int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F); + int16_t z = data[index + 4]; + this->add_raw_touch_position_(id, x, y, z); + esph_log_v(TAG, "Read touch %d: %d/%d", id, x, y); + index += 5; + if (i == 0) + index += 2; + } +} + +void CST226Touchscreen::continue_setup_() { + uint8_t buffer[8]; + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + buffer[0] = 0xD1; + if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) { + esph_log_e(TAG, "Write byte to 0xD1 failed"); + this->mark_failed(); + return; + } + delay(10); + if (this->read16_(0xD204, buffer, 4)) { + uint16_t chip_id = buffer[2] + (buffer[3] << 8); + uint16_t project_id = buffer[0] + (buffer[1] << 8); + esph_log_config(TAG, "Chip ID %X, project ID %x", chip_id, project_id); + } + if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { + if (this->read16_(0xD1F8, buffer, 4)) { + this->x_raw_max_ = buffer[0] + (buffer[1] << 8); + this->y_raw_max_ = buffer[2] + (buffer[3] << 8); + } else { + this->x_raw_max_ = this->display_->get_native_width(); + this->y_raw_max_ = this->display_->get_native_height(); + } + } + this->setup_complete_ = true; + esph_log_config(TAG, "CST226 Touchscreen setup complete"); +} + +void CST226Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "CST226 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace cst226 +} // namespace esphome diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.h b/esphome/components/cst226/touchscreen/cst226_touchscreen.h new file mode 100644 index 0000000000..9f518e5068 --- /dev/null +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cst226 { + +static const char *const TAG = "cst226.touchscreen"; + +static const uint8_t CST226_REG_STATUS = 0x00; + +class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void update_touches() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + bool can_proceed() override { return this->setup_complete_ || this->is_failed(); } + + protected: + bool read16_(uint16_t addr, uint8_t *data, size_t len) { + if (this->read_register16(addr, data, len) != i2c::ERROR_OK) { + esph_log_e(TAG, "Read data from 0x%04X failed", addr); + this->mark_failed(); + return false; + } + return true; + } + void continue_setup_(); + + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; + uint8_t chip_id_{}; + bool setup_complete_{}; +}; + +} // namespace cst226 +} // namespace esphome diff --git a/tests/components/cst226/test.esp32-c3.yaml b/tests/components/cst226/test.esp32-c3.yaml new file mode 100644 index 0000000000..4cbf38ef50 --- /dev/null +++ b/tests/components/cst226/test.esp32-c3.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_id_1 + clk_pin: GPIO7 + mosi_pin: GPIO6 + interface: any + +display: + - platform: ili9xxx + id: displ8 + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: + number: GPIO21 + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: cst226 + interrupt_pin: GPIO3 + reset_pin: GPIO20 + From e4df422798f31531e0c575d9dc1de09f6ad57d9b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:03:39 +1100 Subject: [PATCH 436/436] font: add anti-aliasing and other features (#6198) * Pack glyph bits * Use unsigned chars for unicode strings. * Implement multi-bit glyphs * clang-format * Allow extra glyphs to be added to a font * Allow .otf and .woff file extensions * Add printf versions with background color; Add tests * Whitespace... * Move font test to new framework * CI fix * CI fix * CODEOWNERS * File extensions tested as case-insensitive --- CODEOWNERS | 1 + esphome/components/display/display.cpp | 29 ++-- esphome/components/display/display.h | 26 +++- esphome/components/font/__init__.py | 208 ++++++++++++++++++------- esphome/components/font/font.cpp | 77 +++++---- esphome/components/font/font.h | 19 +-- tests/components/font/test.esp32.yaml | 27 ++++ tests/test8.yaml | 8 + 8 files changed, 282 insertions(+), 113 deletions(-) create mode 100644 tests/components/font/test.esp32.yaml diff --git a/CODEOWNERS b/CODEOWNERS index b1c9c2ee2c..3b2d1eeeed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -122,6 +122,7 @@ esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh +esphome/components/font/* @clydebarrow @esphome/core esphome/components/fs3000/* @kahrendt esphome/components/ft5x06/* @clydebarrow esphome/components/ft63x6/* @gpambrozio diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 4c764da02a..8ae1ee46aa 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -319,17 +319,19 @@ void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); } -void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) { +void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, Color background) { int x_start, y_start; int width, height; this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); - font->print(x_start, y_start, this, color, text); + font->print(x_start, y_start, this, color, text, background); } -void Display::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg) { + +void Display::vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, + va_list arg) { char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); if (ret > 0) - this->print(x, y, font, color, align, buffer); + this->print(x, y, font, color, align, buffer, background); } void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { @@ -423,8 +425,8 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te break; } } -void Display::print(int x, int y, BaseFont *font, Color color, const char *text) { - this->print(x, y, font, color, TextAlign::TOP_LEFT, text); +void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) { + this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background); } void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { this->print(x, y, font, COLOR_ON, align, text); @@ -432,28 +434,35 @@ void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *t void Display::print(int x, int y, BaseFont *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } +void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, + ...) { + va_list arg; + va_start(arg, format); + this->vprintf_(x, y, font, color, background, align, format, arg); + va_end(arg); +} void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); - this->vprintf_(x, y, font, color, align, format, arg); + this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg); va_end(arg); } void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); - this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); + this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, align, format, arg); + this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg); va_end(arg); } void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { va_list arg; va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); + this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index f67471a02d..21954ebb71 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -200,7 +200,7 @@ class BaseImage { class BaseFont { public: - virtual void print(int x, int y, Display *display, Color color, const char *text) = 0; + virtual void print(int x, int y, Display *display, Color color, const char *text, Color background) = 0; virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; }; @@ -327,8 +327,10 @@ class Display : public PollingComponent { * @param color The color to draw the text with. * @param align The alignment of the text. * @param text The text to draw. + * @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels */ - void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text); + void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, + Color background = COLOR_OFF); /** Print `text` with the top left at [x,y] with `font`. * @@ -337,8 +339,9 @@ class Display : public PollingComponent { * @param font The font to draw the text with. * @param color The color to draw the text with. * @param text The text to draw. + * @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels */ - void print(int x, int y, BaseFont *font, Color color, const char *text); + void print(int x, int y, BaseFont *font, Color color, const char *text, Color background = COLOR_OFF); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -359,6 +362,20 @@ class Display : public PollingComponent { */ void print(int x, int y, BaseFont *font, const char *text); + /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param background The background color to use for anti-aliasing + * @param align The alignment of the text. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...) + __attribute__((format(printf, 8, 9))); + /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. * * @param x The x coordinate of the text alignment anchor point. @@ -610,7 +627,8 @@ class Display : public PollingComponent { protected: bool clamp_x_(int x, int w, int &min_x, int &max_x); bool clamp_y_(int y, int h, int &min_y, int &max_y); - void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg); + void vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, + va_list arg); void do_update_(); void clear_clipping_(); diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 5b4682a808..6473ef53dc 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -10,7 +10,10 @@ import requests from esphome import core import esphome.config_validation as cv import esphome.codegen as cg -from esphome.helpers import copy_file_if_changed +from esphome.helpers import ( + copy_file_if_changed, + cpp_string_escape, +) from esphome.const import ( CONF_FAMILY, CONF_FILE, @@ -22,45 +25,75 @@ from esphome.const import ( CONF_PATH, CONF_WEIGHT, ) -from esphome.core import CORE, HexInt - +from esphome.core import ( + CORE, + HexInt, +) DOMAIN = "font" DEPENDENCIES = ["display"] MULTI_CONF = True +CODEOWNERS = ["@esphome/core", "@clydebarrow"] + font_ns = cg.esphome_ns.namespace("font") Font = font_ns.class_("Font") Glyph = font_ns.class_("Glyph") GlyphData = font_ns.struct("GlyphData") +CONF_BPP = "bpp" +CONF_EXTRAS = "extras" +CONF_FONTS = "fonts" + + +def glyph_comparator(x, y): + x_ = x.encode("utf-8") + y_ = y.encode("utf-8") + + for c in range(min(len(x_), len(y_))): + if x_[c] < y_[c]: + return -1 + if x_[c] > y_[c]: + return 1 + + if len(x_) < len(y_): + return -1 + if len(x_) > len(y_): + return 1 + raise cv.Invalid(f"Found duplicate glyph {x}") + def validate_glyphs(value): if isinstance(value, list): value = cv.Schema([cv.string])(value) value = cv.Schema([cv.string])(list(value)) - def comparator(x, y): - x_ = x.encode("utf-8") - y_ = y.encode("utf-8") - - for c in range(min(len(x_), len(y_))): - if x_[c] < y_[c]: - return -1 - if x_[c] > y_[c]: - return 1 - - if len(x_) < len(y_): - return -1 - if len(x_) > len(y_): - return 1 - raise cv.Invalid(f"Found duplicate glyph {x}") - - value.sort(key=functools.cmp_to_key(comparator)) + value.sort(key=functools.cmp_to_key(glyph_comparator)) return value +font_map = {} + + +def merge_glyphs(config): + glyphs = [] + glyphs.extend(config[CONF_GLYPHS]) + font_list = [(EFont(config[CONF_FILE], config[CONF_SIZE], config[CONF_GLYPHS]))] + if extras := config.get(CONF_EXTRAS): + extra_fonts = list( + map( + lambda x: EFont(x[CONF_FILE], config[CONF_SIZE], x[CONF_GLYPHS]), extras + ) + ) + font_list.extend(extra_fonts) + for extra in extras: + glyphs.extend(extra[CONF_GLYPHS]) + validate_glyphs(glyphs) + font_map[config[CONF_ID]] = font_list + return config + + def validate_pillow_installed(value): try: import PIL @@ -79,16 +112,16 @@ def validate_pillow_installed(value): return value +FONT_EXTENSIONS = (".ttf", ".woff", ".otf") + + def validate_truetype_file(value): - if value.endswith(".zip"): # for Google Fonts downloads + if value.lower().endswith(".zip"): # for Google Fonts downloads raise cv.Invalid( f"Please unzip the font archive '{value}' first and then use the .ttf files inside." ) - if not value.endswith(".ttf"): - raise cv.Invalid( - "Only truetype (.ttf) files are supported. Please make sure you're " - "using the correct format or rename the extension to .ttf" - ) + if not any(map(value.lower().endswith, FONT_EXTENSIONS)): + raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.") return cv.file_(value) @@ -233,7 +266,6 @@ def _file_schema(value): FILE_SCHEMA = cv.Schema(_file_schema) - DEFAULT_GLYPHS = ( ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) @@ -245,12 +277,22 @@ FONT_SCHEMA = cv.Schema( cv.Required(CONF_FILE): FILE_SCHEMA, cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), + cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8), + cv.Optional(CONF_EXTRAS): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_FILE): FILE_SCHEMA, + cv.Required(CONF_GLYPHS): validate_glyphs, + } + ) + ), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), } ) -CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA) +CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs) + # PIL doesn't provide a consistent interface for both TrueType and bitmap # fonts. So, we use our own wrappers to give us the consistency that we need. @@ -292,8 +334,32 @@ class BitmapFontWrapper: return (max_height, 0) +class EFont: + def __init__(self, file, size, glyphs): + self.glyphs = glyphs + ftype = file[CONF_TYPE] + if ftype == TYPE_LOCAL_BITMAP: + font = load_bitmap_font(CORE.relative_config_path(file[CONF_PATH])) + elif ftype == TYPE_LOCAL: + path = CORE.relative_config_path(file[CONF_PATH]) + font = load_ttf_font(path, size) + elif ftype == TYPE_GFONTS: + path = _compute_gfonts_local_path(file) + font = load_ttf_font(path, size) + else: + raise cv.Invalid(f"Could not load font: unknown type: {ftype}") + self.font = font + self.ascent, self.descent = font.getmetrics(glyphs) + + def has_glyph(self, glyph): + return glyph in self.glyphs + + def convert_bitmap_to_pillow_font(filepath): - from PIL import PcfFontFile, BdfFontFile + from PIL import ( + PcfFontFile, + BdfFontFile, + ) local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( filepath @@ -347,60 +413,82 @@ def load_ttf_font(path, size): return TrueTypeFontWrapper(font) +class GlyphInfo: + def __init__(self, data_len, offset_x, offset_y, width, height): + self.data_len = data_len + self.offset_x = offset_x + self.offset_y = offset_y + self.width = width + self.height = height + + async def to_code(config): - conf = config[CONF_FILE] - if conf[CONF_TYPE] == TYPE_LOCAL_BITMAP: - font = load_bitmap_font(CORE.relative_config_path(conf[CONF_PATH])) - elif conf[CONF_TYPE] == TYPE_LOCAL: - path = CORE.relative_config_path(conf[CONF_PATH]) - font = load_ttf_font(path, config[CONF_SIZE]) - elif conf[CONF_TYPE] == TYPE_GFONTS: - path = _compute_gfonts_local_path(conf) - font = load_ttf_font(path, config[CONF_SIZE]) - else: - raise core.EsphomeError(f"Could not load font: unknown type: {conf[CONF_TYPE]}") - - ascent, descent = font.getmetrics(config[CONF_GLYPHS]) - + glyph_to_font_map = {} + font_list = font_map[config[CONF_ID]] + glyphs = [] + for font in font_list: + glyphs.extend(font.glyphs) + for glyph in font.glyphs: + glyph_to_font_map[glyph] = font + glyphs.sort(key=functools.cmp_to_key(glyph_comparator)) glyph_args = {} data = [] - for glyph in config[CONF_GLYPHS]: - mask = font.getmask(glyph, mode="1") + bpp = config[CONF_BPP] + if bpp == 1: + mode = "1" + scale = 1 + else: + mode = "L" + scale = 256 // (1 << bpp) + for glyph in glyphs: + font = glyph_to_font_map[glyph].font + mask = font.getmask(glyph, mode=mode) offset_x, offset_y = font.getoffset(glyph) width, height = mask.size - width8 = ((width + 7) // 8) * 8 - glyph_data = [0] * (height * width8 // 8) + glyph_data = [0] * ((height * width * bpp + 7) // 8) + pos = 0 for y in range(height): for x in range(width): - if not mask.getpixel((x, y)): - continue - pos = x + y * width8 - glyph_data[pos // 8] |= 0x80 >> (pos % 8) - glyph_args[glyph] = (len(data), offset_x, offset_y, width, height) + pixel = mask.getpixel((x, y)) // scale + for bit_num in range(bpp): + if pixel & (1 << (bpp - bit_num - 1)): + glyph_data[pos // 8] |= 0x80 >> (pos % 8) + pos += 1 + glyph_args[glyph] = GlyphInfo(len(data), offset_x, offset_y, width, height) data += glyph_data rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) glyph_initializer = [] - for glyph in config[CONF_GLYPHS]: + for glyph in glyphs: glyph_initializer.append( cg.StructInitializer( GlyphData, - ("a_char", glyph), + ( + "a_char", + cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"), + ), ( "data", - cg.RawExpression(f"{str(prog_arr)} + {str(glyph_args[glyph][0])}"), + cg.RawExpression( + f"{str(prog_arr)} + {str(glyph_args[glyph].data_len)}" + ), ), - ("offset_x", glyph_args[glyph][1]), - ("offset_y", glyph_args[glyph][2]), - ("width", glyph_args[glyph][3]), - ("height", glyph_args[glyph][4]), + ("offset_x", glyph_args[glyph].offset_x), + ("offset_y", glyph_args[glyph].offset_y), + ("width", glyph_args[glyph].width), + ("height", glyph_args[glyph].height), ) ) glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) cg.new_Pvariable( - config[CONF_ID], glyphs, len(glyph_initializer), ascent, ascent + descent + config[CONF_ID], + glyphs, + len(glyph_initializer), + font_list[0].ascent, + font_list[0].ascent + font_list[0].descent, + bpp, ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index ef5b2b788d..5a18429789 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -10,29 +10,10 @@ namespace font { static const char *const TAG = "font"; -void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const { - int scan_x1, scan_y1, scan_width, scan_height; - this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - - const unsigned char *data = this->glyph_data_->data; - const int max_x = x_at + scan_x1 + scan_width; - const int max_y = y_start + scan_y1 + scan_height; - - for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) { - for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) { - uint8_t pixel_data = progmem_read_byte(data); - const int pixel_max_x = std::min(max_x, glyph_x + 8); - - for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) { - if (pixel_data & 0x80) { - display->draw_pixel_at(pixel_x, glyph_y, color); - } - } - } - } -} -const char *Glyph::get_char() const { return this->glyph_data_->a_char; } -bool Glyph::compare_to(const char *str) const { +const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; } +// Compare the char at the string position with this char. +// Return true if this char is less than or equal the other. +bool Glyph::compare_to(const uint8_t *str) const { // 1 -> this->char_ // 2 -> str for (uint32_t i = 0;; i++) { @@ -48,7 +29,7 @@ bool Glyph::compare_to(const char *str) const { // this should not happen return false; } -int Glyph::match_length(const char *str) const { +int Glyph::match_length(const uint8_t *str) const { for (uint32_t i = 0;; i++) { if (this->glyph_data_->a_char[i] == '\0') return i; @@ -65,12 +46,13 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { *height = this->glyph_data_->height; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { +Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) + : baseline_(baseline), height_(height), bpp_(bpp) { glyphs_.reserve(data_nr); for (int i = 0; i < data_nr; ++i) glyphs_.emplace_back(&data[i]); } -int Font::match_next_glyph(const char *str, int *match_length) { +int Font::match_next_glyph(const uint8_t *str, int *match_length) { int lo = 0; int hi = this->glyphs_.size() - 1; while (lo != hi) { @@ -95,7 +77,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in int x = 0; while (str[i] != '\0') { int match_length; - int glyph_n = this->match_next_glyph(str + i, &match_length); + int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length); if (glyph_n < 0) { // Unknown char, skip if (!this->get_glyphs().empty()) @@ -118,12 +100,13 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in *x_offset = min_x; *width = x - min_x; } -void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text) { +void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) { int i = 0; int x_at = x_start; + int scan_x1, scan_y1, scan_width, scan_height; while (text[i] != '\0') { int match_length; - int glyph_n = this->match_next_glyph(text + i, &match_length); + int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length); if (glyph_n < 0) { // Unknown char, skip ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); @@ -138,7 +121,41 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo } const Glyph &glyph = this->get_glyphs()[glyph_n]; - glyph.draw(x_at, y_start, display, color); + glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); + + const uint8_t *data = glyph.glyph_data_->data; + const int max_x = x_at + scan_x1 + scan_width; + const int max_y = y_start + scan_y1 + scan_height; + + uint8_t bitmask = 0; + uint8_t pixel_data = 0; + float bpp_max = (1 << this->bpp_) - 1; + for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { + for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { + uint8_t pixel = 0; + for (int bit_num = 0; bit_num != this->bpp_; bit_num++) { + if (bitmask == 0) { + pixel_data = progmem_read_byte(data++); + bitmask = 0x80; + } + pixel <<= 1; + if ((pixel_data & bitmask) != 0) + pixel |= 1; + bitmask >>= 1; + } + if (pixel == bpp_max) { + display->draw_pixel_at(glyph_x, glyph_y, color); + } else if (pixel != 0) { + float on = (float) pixel / bpp_max; + float off = 1.0 - on; + Color blended; + blended.r = color.r * on + background.r * off; + blended.g = color.r * on + background.g * off; + blended.b = color.r * on + background.b * off; + display->draw_pixel_at(glyph_x, glyph_y, blended); + } + } + } x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; i += match_length; diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 03171a6126..91bcd399ba 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -10,7 +10,7 @@ namespace font { class Font; struct GlyphData { - const char *a_char; + const uint8_t *a_char; const uint8_t *data; int offset_x; int offset_y; @@ -22,13 +22,11 @@ class Glyph { public: Glyph(const GlyphData *data) : glyph_data_(data) {} - void draw(int x, int y, display::Display *display, Color color) const; + const uint8_t *get_char() const; - const char *get_char() const; + bool compare_to(const uint8_t *str) const; - bool compare_to(const char *str) const; - - int match_length(const char *str) const; + int match_length(const uint8_t *str) const; void scan_area(int *x1, int *y1, int *width, int *height) const; @@ -46,14 +44,16 @@ class Font : public display::BaseFont { * @param baseline The y-offset from the top of the text to the baseline. * @param bottom The y-offset from the top of the text to the bottom (i.e. height). */ - Font(const GlyphData *data, int data_nr, int baseline, int height); + Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1); - int match_next_glyph(const char *str, int *match_length); + int match_next_glyph(const uint8_t *str, int *match_length); - void print(int x_start, int y_start, display::Display *display, Color color, const char *text) override; + void print(int x_start, int y_start, display::Display *display, Color color, const char *text, + Color background) override; void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } + inline int get_bpp() { return this->bpp_; } const std::vector> &get_glyphs() const { return glyphs_; } @@ -61,6 +61,7 @@ class Font : public display::BaseFont { std::vector> glyphs_; int baseline_; int height_; + uint8_t bpp_; // bits per pixel }; } // namespace font diff --git a/tests/components/font/test.esp32.yaml b/tests/components/font/test.esp32.yaml new file mode 100644 index 0000000000..9d699a1752 --- /dev/null +++ b/tests/components/font/test.esp32.yaml @@ -0,0 +1,27 @@ +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + glyphs: "0123456789." + extras: + - file: "gfonts://Roboto" + glyphs: ["\u00C4", "\u00C5", "\U000000C7"] + +spi: + clk_pin: 14 + mosi_pin: 13 + +display: + - id: my_display + platform: ili9xxx + dimensions: 480x320 + model: ST7796 + cs_pin: 15 + dc_pin: 21 + reset_pin: 22 + transform: + swap_xy: true + mirror_x: true + mirror_y: true + auto_clear_enabled: false + diff --git a/tests/test8.yaml b/tests/test8.yaml index 5618e23e25..5a8ae77468 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -52,6 +52,11 @@ spi_device: mode: 3 bit_order: lsb_first +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + display: - platform: ili9xxx id: displ8 @@ -61,6 +66,8 @@ display: reset_pin: number: GPIO48 allow_other_uses: true + lambda: |- + it.printf(10, 100, id(roboto), Color(0x123456), COLOR_OFF, display::TextAlign::BASELINE, "%f", id(heap_free).state); i2c: scl: GPIO18 @@ -85,6 +92,7 @@ binary_sensor: sensor: - platform: debug free: + id: heap_free name: "Heap Free" block: name: "Max Block Free"