mirror of
https://github.com/esphome/esphome.git
synced 2025-11-03 16:41:50 +00:00
Compare commits
30 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa6cd3dae | ||
|
|
4ec9ab944d | ||
|
|
58a088e06b | ||
|
|
49a46883ed | ||
|
|
bc03538e25 | ||
|
|
969034b61a | ||
|
|
06eb1b6014 | ||
|
|
589d00f17f | ||
|
|
68c0aa4d6d | ||
|
|
2fddb061e1 | ||
|
|
c85eb448e4 | ||
|
|
396c02c6de | ||
|
|
52c4509208 | ||
|
|
d29cae9c3b | ||
|
|
532e3e370f | ||
|
|
da573a217d | ||
|
|
a9b27d1966 | ||
|
|
0aa3c9685e | ||
|
|
93b28447ee | ||
|
|
52634dac2a | ||
|
|
64c94c1440 | ||
|
|
f7bf1ef52c | ||
|
|
fa8c5e880c | ||
|
|
27ba90ea95 | ||
|
|
469246b8d8 | ||
|
|
50f15735dc | ||
|
|
83d9c02a1b | ||
|
|
701e6099aa | ||
|
|
d59476d0e1 | ||
|
|
fbbb791b0d |
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -102,12 +102,12 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -182,13 +182,13 @@ jobs:
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.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.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
21
CODEOWNERS
21
CODEOWNERS
@@ -40,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi
|
||||
esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/api/* @OttoWinter
|
||||
esphome/components/api/* @esphome/core
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
esphome/components/async_tcp/* @OttoWinter
|
||||
esphome/components/async_tcp/* @esphome/core
|
||||
esphome/components/at581x/* @X-Ryl669
|
||||
esphome/components/atc_mithermometer/* @ahpohl
|
||||
esphome/components/atm90e26/* @danieltwagner
|
||||
@@ -69,7 +69,7 @@ esphome/components/bl0939/* @ziceva
|
||||
esphome/components/bl0940/* @tobias-
|
||||
esphome/components/bl0942/* @dbuezas @dwmw2
|
||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||
esphome/components/bluetooth_proxy/* @jesserockz
|
||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
||||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
@@ -91,7 +91,7 @@ esphome/components/bytebuffer/* @clydebarrow
|
||||
esphome/components/camera/* @DT-art1 @bdraco
|
||||
esphome/components/canbus/* @danielschramm @mvturnho
|
||||
esphome/components/cap1188/* @mreditor97
|
||||
esphome/components/captive_portal/* @OttoWinter
|
||||
esphome/components/captive_portal/* @esphome/core
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
@@ -118,7 +118,7 @@ esphome/components/dallas_temp/* @ssieb
|
||||
esphome/components/daly_bms/* @s1lvi0
|
||||
esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/datetime/* @jesserockz @rfdarter
|
||||
esphome/components/debug/* @OttoWinter
|
||||
esphome/components/debug/* @esphome/core
|
||||
esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
@@ -144,9 +144,10 @@ esphome/components/es8156/* @kbx81
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
esphome/components/es8388/* @P4uLT
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @Rapsssito @jesserockz
|
||||
esphome/components/esp32_ble_client/* @jesserockz
|
||||
esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz
|
||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
|
||||
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
|
||||
esphome/components/esp32_ble_tracker/* @bdraco
|
||||
esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
@@ -237,7 +238,7 @@ esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/jsn_sr04t/* @Mafus1
|
||||
esphome/components/json/* @OttoWinter
|
||||
esphome/components/json/* @esphome/core
|
||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||
esphome/components/key_collector/* @ssieb
|
||||
esphome/components/key_provider/* @ssieb
|
||||
@@ -467,7 +468,7 @@ esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @OttoWinter
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tlc5947/* @rnauber
|
||||
esphome/components/tlc5971/* @IJIJI
|
||||
esphome/components/tm1621/* @Philippe12
|
||||
@@ -511,7 +512,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/web_server/ota/* @esphome/core
|
||||
esphome/components/web_server_base/* @OttoWinter
|
||||
esphome/components/web_server_base/* @esphome/core
|
||||
esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
esphome/components/weikai_i2c/* @DrCoolZic
|
||||
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Protocol
|
||||
|
||||
import argcomplete
|
||||
|
||||
@@ -44,6 +45,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
from esphome.helpers import get_bool_env, indent, is_ip_address
|
||||
from esphome.log import AnsiFore, color, setup_log
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import (
|
||||
get_serial_ports,
|
||||
list_yaml_files,
|
||||
@@ -55,6 +57,23 @@ from esphome.util import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ArgsProtocol(Protocol):
|
||||
device: list[str] | None
|
||||
reset: bool
|
||||
username: str | None
|
||||
password: str | None
|
||||
client_id: str | None
|
||||
topic: str | None
|
||||
file: str | None
|
||||
no_logs: bool
|
||||
only_generate: bool
|
||||
show_secrets: bool
|
||||
dashboard: bool
|
||||
configuration: str
|
||||
name: str
|
||||
upload_speed: str | None
|
||||
|
||||
|
||||
def choose_prompt(options, purpose: str = None):
|
||||
if not options:
|
||||
raise EsphomeError(
|
||||
@@ -88,30 +107,54 @@ def choose_prompt(options, purpose: str = None):
|
||||
|
||||
|
||||
def choose_upload_log_host(
|
||||
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
||||
):
|
||||
default: list[str] | str | None,
|
||||
check_default: str | None,
|
||||
show_ota: bool,
|
||||
show_mqtt: bool,
|
||||
show_api: bool,
|
||||
purpose: str | None = None,
|
||||
) -> list[str]:
|
||||
# Convert to list for uniform handling
|
||||
defaults = [default] if isinstance(default, str) else default or []
|
||||
|
||||
# If devices specified, resolve them
|
||||
if defaults:
|
||||
resolved: list[str] = []
|
||||
for device in defaults:
|
||||
if device == "SERIAL":
|
||||
serial_ports = get_serial_ports()
|
||||
if not serial_ports:
|
||||
_LOGGER.warning("No serial ports found, skipping SERIAL device")
|
||||
continue
|
||||
options = [
|
||||
(f"{port.path} ({port.description})", port.path)
|
||||
for port in serial_ports
|
||||
]
|
||||
resolved.append(choose_prompt(options, purpose=purpose))
|
||||
elif device == "OTA":
|
||||
if (show_ota and "ota" in CORE.config) or (
|
||||
show_api and "api" in CORE.config
|
||||
):
|
||||
resolved.append(CORE.address)
|
||||
elif show_mqtt and has_mqtt_logging():
|
||||
resolved.append("MQTT")
|
||||
else:
|
||||
resolved.append(device)
|
||||
return resolved
|
||||
|
||||
# No devices specified, show interactive chooser
|
||||
options = [
|
||||
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
|
||||
]
|
||||
if default == "SERIAL":
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if default == "OTA":
|
||||
return CORE.address
|
||||
if (
|
||||
show_mqtt
|
||||
and (mqtt_config := CORE.config.get(CONF_MQTT))
|
||||
and mqtt_logging_enabled(mqtt_config)
|
||||
):
|
||||
if show_mqtt and has_mqtt_logging():
|
||||
mqtt_config = CORE.config[CONF_MQTT]
|
||||
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
|
||||
if default == "OTA":
|
||||
return "MQTT"
|
||||
if default is not None:
|
||||
return default
|
||||
|
||||
if check_default is not None and check_default in [opt[1] for opt in options]:
|
||||
return check_default
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
return [check_default]
|
||||
return [choose_prompt(options, purpose=purpose)]
|
||||
|
||||
|
||||
def mqtt_logging_enabled(mqtt_config):
|
||||
@@ -123,7 +166,14 @@ def mqtt_logging_enabled(mqtt_config):
|
||||
return log_topic.get(CONF_LEVEL, None) != "NONE"
|
||||
|
||||
|
||||
def get_port_type(port):
|
||||
def has_mqtt_logging() -> bool:
|
||||
"""Check if MQTT logging is available."""
|
||||
return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
|
||||
mqtt_config
|
||||
)
|
||||
|
||||
|
||||
def get_port_type(port: str) -> str:
|
||||
if port.startswith("/") or port.startswith("COM"):
|
||||
return "SERIAL"
|
||||
if port == "MQTT":
|
||||
@@ -131,7 +181,7 @@ def get_port_type(port):
|
||||
return "NETWORK"
|
||||
|
||||
|
||||
def run_miniterm(config, port, args):
|
||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
from aioesphomeapi import LogParser
|
||||
import serial
|
||||
|
||||
@@ -208,7 +258,7 @@ def wrap_to_code(name, comp):
|
||||
return wrapped
|
||||
|
||||
|
||||
def write_cpp(config):
|
||||
def write_cpp(config: ConfigType) -> int:
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
@@ -216,7 +266,7 @@ def write_cpp(config):
|
||||
return write_cpp_file()
|
||||
|
||||
|
||||
def generate_cpp_contents(config):
|
||||
def generate_cpp_contents(config: ConfigType) -> None:
|
||||
_LOGGER.info("Generating C++ source...")
|
||||
|
||||
for name, component, conf in iter_component_configs(CORE.config):
|
||||
@@ -227,7 +277,7 @@ def generate_cpp_contents(config):
|
||||
CORE.flush_tasks()
|
||||
|
||||
|
||||
def write_cpp_file():
|
||||
def write_cpp_file() -> int:
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
writer.write_cpp(code_s)
|
||||
|
||||
@@ -238,7 +288,7 @@ def write_cpp_file():
|
||||
return 0
|
||||
|
||||
|
||||
def compile_program(args, config):
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome import platformio_api
|
||||
|
||||
_LOGGER.info("Compiling app...")
|
||||
@@ -249,7 +299,9 @@ def compile_program(args, config):
|
||||
return 0 if idedata is not None else 1
|
||||
|
||||
|
||||
def upload_using_esptool(config, port, file, speed):
|
||||
def upload_using_esptool(
|
||||
config: ConfigType, port: str, file: str, speed: int
|
||||
) -> str | int:
|
||||
from esphome import platformio_api
|
||||
|
||||
first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
|
||||
@@ -314,7 +366,7 @@ def upload_using_esptool(config, port, file, speed):
|
||||
return run_esptool(115200)
|
||||
|
||||
|
||||
def upload_using_platformio(config, port):
|
||||
def upload_using_platformio(config: ConfigType, port: str):
|
||||
from esphome import platformio_api
|
||||
|
||||
upload_args = ["-t", "upload", "-t", "nobuild"]
|
||||
@@ -323,7 +375,7 @@ def upload_using_platformio(config, port):
|
||||
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
|
||||
|
||||
|
||||
def check_permissions(port):
|
||||
def check_permissions(port: str):
|
||||
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):
|
||||
@@ -341,7 +393,7 @@ def check_permissions(port):
|
||||
)
|
||||
|
||||
|
||||
def upload_program(config, args, host):
|
||||
def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
if getattr(module, "upload_program")(config, args, host):
|
||||
@@ -356,7 +408,7 @@ def upload_program(config, args, host):
|
||||
return upload_using_esptool(config, host, file, args.upload_speed)
|
||||
|
||||
if CORE.target_platform in (PLATFORM_RP2040):
|
||||
return upload_using_platformio(config, args.device)
|
||||
return upload_using_platformio(config, host)
|
||||
|
||||
if CORE.is_libretiny:
|
||||
return upload_using_platformio(config, host)
|
||||
@@ -379,9 +431,12 @@ def upload_program(config, args, host):
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD, "")
|
||||
|
||||
# Check if we should use MQTT for address resolution
|
||||
# This happens when no device was specified, or the current host is "MQTT"/"OTA"
|
||||
devices: list[str] = args.device or []
|
||||
if (
|
||||
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
|
||||
and (not args.device or args.device in ("MQTT", "OTA"))
|
||||
and (not devices or host in ("MQTT", "OTA"))
|
||||
and (
|
||||
((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
|
||||
or get_port_type(host) == "MQTT"
|
||||
@@ -399,23 +454,28 @@ def upload_program(config, args, host):
|
||||
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
|
||||
|
||||
|
||||
def show_logs(config, args, port):
|
||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||
if "logger" not in config:
|
||||
raise EsphomeError("Logger is not configured!")
|
||||
|
||||
port = devices[0]
|
||||
|
||||
if get_port_type(port) == "SERIAL":
|
||||
check_permissions(port)
|
||||
return run_miniterm(config, port, args)
|
||||
if get_port_type(port) == "NETWORK" and "api" in config:
|
||||
addresses_to_use = devices
|
||||
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
|
||||
from esphome import mqtt
|
||||
|
||||
port = mqtt.get_esphome_device_ip(
|
||||
mqtt_address = mqtt.get_esphome_device_ip(
|
||||
config, args.username, args.password, args.client_id
|
||||
)[0]
|
||||
addresses_to_use = [mqtt_address]
|
||||
|
||||
from esphome.components.api.client import run_logs
|
||||
|
||||
return run_logs(config, port)
|
||||
return run_logs(config, addresses_to_use)
|
||||
if get_port_type(port) == "MQTT" and "mqtt" in config:
|
||||
from esphome import mqtt
|
||||
|
||||
@@ -426,7 +486,7 @@ def show_logs(config, args, port):
|
||||
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
|
||||
|
||||
|
||||
def clean_mqtt(config, args):
|
||||
def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.clear_topic(
|
||||
@@ -434,13 +494,13 @@ def clean_mqtt(config, args):
|
||||
)
|
||||
|
||||
|
||||
def command_wizard(args):
|
||||
def command_wizard(args: ArgsProtocol) -> int | None:
|
||||
from esphome import wizard
|
||||
|
||||
return wizard.wizard(args.configuration)
|
||||
|
||||
|
||||
def command_config(args, config):
|
||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if not CORE.verbose:
|
||||
config = strip_default_ids(config)
|
||||
output = yaml_util.dump(config, args.show_secrets)
|
||||
@@ -455,7 +515,7 @@ def command_config(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_vscode(args):
|
||||
def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
from esphome import vscode
|
||||
|
||||
logging.disable(logging.INFO)
|
||||
@@ -463,7 +523,7 @@ def command_vscode(args):
|
||||
vscode.read_config(args)
|
||||
|
||||
|
||||
def command_compile(args, config):
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -477,8 +537,9 @@ def command_compile(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_upload(args, config):
|
||||
port = choose_upload_log_host(
|
||||
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
@@ -486,14 +547,22 @@ def command_upload(args, config):
|
||||
show_api=False,
|
||||
purpose="uploading",
|
||||
)
|
||||
exit_code = upload_program(config, args, port)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
return 0
|
||||
|
||||
# Try each device until one succeeds
|
||||
exit_code = 1
|
||||
for device in devices:
|
||||
_LOGGER.info("Uploading to %s", device)
|
||||
exit_code = upload_program(config, args, device)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
return 0
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Failed to upload to %s", device)
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
def command_discover(args, config):
|
||||
def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if "mqtt" in config:
|
||||
from esphome import mqtt
|
||||
|
||||
@@ -502,8 +571,9 @@ def command_discover(args, config):
|
||||
raise EsphomeError("No discover method configured (mqtt)")
|
||||
|
||||
|
||||
def command_logs(args, config):
|
||||
port = choose_upload_log_host(
|
||||
def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
@@ -511,10 +581,10 @@ def command_logs(args, config):
|
||||
show_api=True,
|
||||
purpose="logging",
|
||||
)
|
||||
return show_logs(config, args, port)
|
||||
return show_logs(config, args, devices)
|
||||
|
||||
|
||||
def command_run(args, config):
|
||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -531,7 +601,8 @@ def command_run(args, config):
|
||||
program_path = idedata.raw["prog_path"]
|
||||
return run_external_process(program_path)
|
||||
|
||||
port = choose_upload_log_host(
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
@@ -539,39 +610,53 @@ def command_run(args, config):
|
||||
show_api=True,
|
||||
purpose="uploading",
|
||||
)
|
||||
exit_code = upload_program(config, args, port)
|
||||
if exit_code != 0:
|
||||
|
||||
# Try each device for upload until one succeeds
|
||||
successful_device: str | None = None
|
||||
for device in devices:
|
||||
_LOGGER.info("Uploading to %s", device)
|
||||
exit_code = upload_program(config, args, device)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
successful_device = device
|
||||
break
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Failed to upload to %s", device)
|
||||
|
||||
if successful_device is None:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
|
||||
if args.no_logs:
|
||||
return 0
|
||||
port = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=port,
|
||||
|
||||
# For logs, prefer the device we successfully uploaded to
|
||||
devices = choose_upload_log_host(
|
||||
default=successful_device,
|
||||
check_default=successful_device,
|
||||
show_ota=False,
|
||||
show_mqtt=True,
|
||||
show_api=True,
|
||||
purpose="logging",
|
||||
)
|
||||
return show_logs(config, args, port)
|
||||
return show_logs(config, args, devices)
|
||||
|
||||
|
||||
def command_clean_mqtt(args, config):
|
||||
def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return clean_mqtt(config, args)
|
||||
|
||||
|
||||
def command_mqtt_fingerprint(args, config):
|
||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.get_fingerprint(config)
|
||||
|
||||
|
||||
def command_version(args):
|
||||
def command_version(args: ArgsProtocol) -> int | None:
|
||||
safe_print(f"Version: {const.__version__}")
|
||||
return 0
|
||||
|
||||
|
||||
def command_clean(args, config):
|
||||
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
try:
|
||||
writer.clean_build()
|
||||
except OSError as err:
|
||||
@@ -581,13 +666,13 @@ def command_clean(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_dashboard(args):
|
||||
def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
from esphome.dashboard import dashboard
|
||||
|
||||
return dashboard.start_dashboard(args)
|
||||
|
||||
|
||||
def command_update_all(args):
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
import click
|
||||
|
||||
success = {}
|
||||
@@ -634,7 +719,7 @@ def command_update_all(args):
|
||||
return failed
|
||||
|
||||
|
||||
def command_idedata(args, config):
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
from esphome import platformio_api
|
||||
@@ -650,7 +735,7 @@ def command_idedata(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_rename(args, config):
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
for c in args.name:
|
||||
if c not in ALLOWED_NAME_CHARS:
|
||||
print(
|
||||
@@ -767,6 +852,12 @@ POST_CONFIG_ACTIONS = {
|
||||
"discover": command_discover,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
"clean",
|
||||
"clean-mqtt",
|
||||
"config",
|
||||
]
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
options_parser = argparse.ArgumentParser(add_help=False)
|
||||
@@ -854,7 +945,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -876,7 +968,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--reset",
|
||||
@@ -905,7 +998,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -1032,6 +1126,13 @@ def parse_args(argv):
|
||||
arguments = argv[1:]
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS:
|
||||
args, unknown_args = parser.parse_known_args(arguments)
|
||||
if unknown_args:
|
||||
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
|
||||
return args
|
||||
|
||||
return parser.parse_args(arguments)
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from esphome.core import CORE, coroutine_with_priority
|
||||
DOMAIN = "api"
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
api_ns = cg.esphome_ns.namespace("api")
|
||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||
|
||||
@@ -371,8 +371,21 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
if (call.is_event) {
|
||||
// For events, send to only one client to prevent duplicates
|
||||
// Events represent "something that happened" and should only be sent once total
|
||||
for (auto &client : this->clients_) {
|
||||
if (client->is_authenticated() && client->flags_.service_call_subscription) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
return; // Send to only the first authenticated client with service call subscription
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For service calls, send to all clients (existing behavior)
|
||||
// Service calls represent "actions to take" and may need to be sent to multiple Home Assistant instances
|
||||
for (auto &client : this->clients_) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
"""Run the logs command in the event loop."""
|
||||
conf = config["api"]
|
||||
name = config["esphome"]["name"]
|
||||
@@ -39,13 +39,21 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
noise_psk: str | None = None
|
||||
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
|
||||
noise_psk = key
|
||||
_LOGGER.info("Starting log output from %s using esphome API", address)
|
||||
|
||||
if len(addresses) == 1:
|
||||
_LOGGER.info("Starting log output from %s using esphome API", addresses[0])
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Starting log output from %s using esphome API", " or ".join(addresses)
|
||||
)
|
||||
|
||||
cli = APIClient(
|
||||
address,
|
||||
addresses[0], # Primary address for compatibility
|
||||
port,
|
||||
password,
|
||||
client_info=f"ESPHome Logs {__version__}",
|
||||
noise_psk=noise_psk,
|
||||
addresses=addresses, # Pass all addresses for automatic retry
|
||||
)
|
||||
dashboard = CORE.dashboard
|
||||
|
||||
@@ -66,7 +74,7 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
await stop()
|
||||
|
||||
|
||||
def run_logs(config: dict[str, Any], address: str) -> None:
|
||||
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
"""Run the logs command."""
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(async_run_logs(config, address))
|
||||
asyncio.run(async_run_logs(config, addresses))
|
||||
|
||||
@@ -56,6 +56,14 @@ class CustomAPIDevice {
|
||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#else
|
||||
template<typename T, typename... Ts>
|
||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Register a custom native API service that will show up in Home Assistant.
|
||||
@@ -81,6 +89,12 @@ class CustomAPIDevice {
|
||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#else
|
||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
@@ -135,6 +149,22 @@ class CustomAPIDevice {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||
}
|
||||
#else
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -222,6 +252,28 @@ class CustomAPIDevice {
|
||||
}
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
#else
|
||||
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
|
||||
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void>
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
|
||||
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void>
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema({}),
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ACTIVE, CONF_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.log import AnsiFore, color
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
|
||||
DEPENDENCIES = ["api", "esp32"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CONNECTION_SLOTS = "connection_slots"
|
||||
CONF_CACHE_SERVICES = "cache_services"
|
||||
@@ -41,6 +47,27 @@ def validate_connections(config):
|
||||
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
|
||||
config
|
||||
)
|
||||
|
||||
# Warn about connection slot waste when using Arduino framework
|
||||
if CORE.using_arduino and connection_slots:
|
||||
_LOGGER.warning(
|
||||
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
|
||||
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
|
||||
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
|
||||
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
|
||||
"\n"
|
||||
"To switch to ESP-IDF, add this to your YAML:\n"
|
||||
" esp32:\n"
|
||||
" framework:\n"
|
||||
" type: esp-idf\n"
|
||||
"\n"
|
||||
"For detailed migration instructions, see:\n"
|
||||
"%s",
|
||||
color(
|
||||
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
**config,
|
||||
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
||||
|
||||
@@ -35,8 +35,8 @@ void BluetoothProxy::setup() {
|
||||
// Don't pre-allocate pool - let it grow only if needed in busy environments
|
||||
// Many devices in quiet areas will never need the overflow pool
|
||||
|
||||
this->connections_free_response_.limit = this->connections_.size();
|
||||
this->connections_free_response_.free = this->connections_.size();
|
||||
this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||
this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||
|
||||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
@@ -134,12 +134,13 @@ void BluetoothProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Active: %s\n"
|
||||
" Connections: %d",
|
||||
YESNO(this->active_), this->connections_.size());
|
||||
YESNO(this->active_), this->connection_count_);
|
||||
}
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
@@ -162,7 +163,8 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
|
||||
}
|
||||
|
||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == address)
|
||||
return connection;
|
||||
}
|
||||
@@ -170,7 +172,8 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
if (!reserve)
|
||||
return nullptr;
|
||||
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == 0) {
|
||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
||||
connection->set_address(address);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
@@ -63,8 +64,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
void register_connection(BluetoothConnection *connection) {
|
||||
this->connections_.push_back(connection);
|
||||
connection->proxy_ = this;
|
||||
if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
|
||||
this->connections_[this->connection_count_++] = connection;
|
||||
connection->proxy_ = this;
|
||||
}
|
||||
}
|
||||
|
||||
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
|
||||
@@ -138,8 +141,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
api::APIConnection *api_connection_{nullptr};
|
||||
|
||||
// Group 2: Container types (typically 12 bytes on 32-bit)
|
||||
std::vector<BluetoothConnection *> connections_{};
|
||||
// Group 2: Fixed-size array of connection pointers
|
||||
std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
|
||||
|
||||
// BLE advertisement batching
|
||||
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
||||
@@ -154,7 +157,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
// Group 4: 1-byte types grouped together
|
||||
bool active_;
|
||||
uint8_t advertisement_count_{0};
|
||||
// 2 bytes used, 2 bytes padding
|
||||
uint8_t connection_count_{0};
|
||||
// 3 bytes used, 1 byte padding
|
||||
};
|
||||
|
||||
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -14,7 +14,7 @@ from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
AUTO_LOAD = ["web_server_base", "ota.web_server"]
|
||||
DEPENDENCIES = ["wifi"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
captive_portal_ns = cg.esphome_ns.namespace("captive_portal")
|
||||
CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component)
|
||||
|
||||
@@ -13,7 +13,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["logger"]
|
||||
|
||||
CONF_DEBUG_ID = "debug_id"
|
||||
|
||||
@@ -11,7 +11,7 @@ from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
|
||||
import esphome.final_validate as fv
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
||||
|
||||
|
||||
class BTLoggers(Enum):
|
||||
|
||||
@@ -23,21 +23,14 @@
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
// Maximum number of BLE scan results to buffer
|
||||
// Sized to handle bursts of advertisements while allowing for processing delays
|
||||
// With 16 advertisements per batch and some safety margin:
|
||||
// - Without PSRAM: 24 entries (1.5× batch size)
|
||||
// - With PSRAM: 36 entries (2.25× batch size)
|
||||
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
|
||||
// Maximum size of the BLE event queue
|
||||
// Increased to absorb the ring buffer capacity from esp32_ble_tracker
|
||||
#ifdef USE_PSRAM
|
||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
|
||||
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM)
|
||||
#else
|
||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
|
||||
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM)
|
||||
#endif
|
||||
|
||||
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
|
||||
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
|
||||
|
||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
|
||||
|
||||
// NOLINTNEXTLINE(modernize-use-using)
|
||||
|
||||
@@ -2,7 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble_tracker
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_tracker"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client")
|
||||
|
||||
@@ -145,6 +145,36 @@ void BLEClientBase::connect() {
|
||||
this->remote_addr_type_);
|
||||
this->paired_ = false;
|
||||
|
||||
// Set preferred connection parameters before connecting
|
||||
// Use FAST for all V3 connections (better latency and reliability)
|
||||
// Use MEDIUM for V1/legacy connections (balanced performance)
|
||||
uint16_t min_interval, max_interval, timeout;
|
||||
const char *param_type;
|
||||
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
min_interval = FAST_MIN_CONN_INTERVAL;
|
||||
max_interval = FAST_MAX_CONN_INTERVAL;
|
||||
timeout = FAST_CONN_TIMEOUT;
|
||||
param_type = "fast";
|
||||
} else {
|
||||
min_interval = MEDIUM_MIN_CONN_INTERVAL;
|
||||
max_interval = MEDIUM_MAX_CONN_INTERVAL;
|
||||
timeout = MEDIUM_CONN_TIMEOUT;
|
||||
param_type = "medium";
|
||||
}
|
||||
|
||||
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
|
||||
0, // latency: 0
|
||||
timeout);
|
||||
if (param_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param_ret);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
|
||||
}
|
||||
|
||||
// Now open the connection
|
||||
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
|
||||
@@ -152,35 +182,6 @@ void BLEClientBase::connect() {
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
} else {
|
||||
this->set_state(espbt::ClientState::CONNECTING);
|
||||
|
||||
// Always set connection parameters to ensure stable operation
|
||||
// Use FAST for all V3 connections (better latency and reliability)
|
||||
// Use MEDIUM for V1/legacy connections (balanced performance)
|
||||
uint16_t min_interval, max_interval, timeout;
|
||||
const char *param_type;
|
||||
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
min_interval = FAST_MIN_CONN_INTERVAL;
|
||||
max_interval = FAST_MAX_CONN_INTERVAL;
|
||||
timeout = FAST_CONN_TIMEOUT;
|
||||
param_type = "fast";
|
||||
} else {
|
||||
min_interval = MEDIUM_MIN_CONN_INTERVAL;
|
||||
max_interval = MEDIUM_MAX_CONN_INTERVAL;
|
||||
timeout = MEDIUM_CONN_TIMEOUT;
|
||||
param_type = "medium";
|
||||
}
|
||||
|
||||
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
|
||||
0, // latency: 0
|
||||
timeout);
|
||||
if (param_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param_ret);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +256,19 @@ void BLEClientBase::log_event_(const char *name) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
|
||||
}
|
||||
|
||||
void BLEClientBase::restore_medium_conn_params_() {
|
||||
// Restore to medium connection parameters after initial connection phase
|
||||
// This balances performance with bandwidth usage for normal operation
|
||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
|
||||
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
|
||||
conn_params.latency = 0;
|
||||
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
|
||||
ESP_LOGD(TAG, "[%d] [%s] Restoring medium conn params", this->connection_index_, this->address_str_.c_str());
|
||||
esp_ble_gap_update_conn_params(&conn_params);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -283,7 +297,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (!this->check_addr(param->open.remote_bda))
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_OPEN_EVT");
|
||||
this->conn_id_ = param->open.conn_id;
|
||||
// conn_id was already set in ESP_GATTC_CONNECT_EVT
|
||||
this->service_count_ = 0;
|
||||
if (this->state_ != espbt::ClientState::CONNECTING) {
|
||||
// This should not happen but lets log it in case it does
|
||||
@@ -317,15 +331,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->conn_id_ = UNSET_CONN_ID;
|
||||
break;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// MTU negotiation already started in ESP_GATTC_CONNECT_EVT
|
||||
this->set_state(espbt::ClientState::CONNECTED);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str());
|
||||
|
||||
// Restore to medium connection parameters for cached connections too
|
||||
this->restore_medium_conn_params_();
|
||||
|
||||
// only set our state, subclients might have more stuff to do yet.
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
@@ -338,6 +352,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (!this->check_addr(param->connect.remote_bda))
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_CONNECT_EVT");
|
||||
this->conn_id_ = param->connect.conn_id;
|
||||
// Start MTU negotiation immediately as recommended by ESP-IDF examples
|
||||
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
|
||||
// ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
|
||||
// This saves ~3ms in the connection process.
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
@@ -413,15 +437,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
// This balances performance with bandwidth usage after the critical discovery phase
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
|
||||
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
|
||||
conn_params.latency = 0;
|
||||
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
|
||||
ESP_LOGD(TAG, "[%d] [%s] Restored medium conn params after service discovery", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
esp_ble_gap_update_conn_params(&conn_params);
|
||||
this->restore_medium_conn_params_();
|
||||
}
|
||||
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
|
||||
@@ -66,7 +66,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
(uint8_t) (this->address_ >> 0) & 0xff);
|
||||
}
|
||||
}
|
||||
std::string address_str() const { return this->address_str_; }
|
||||
const std::string &address_str() const { return this->address_str_; }
|
||||
|
||||
BLEService *get_service(espbt::ESPBTUUID uuid);
|
||||
BLEService *get_service(uint16_t uuid);
|
||||
@@ -127,6 +127,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
// 6 bytes used, 2 bytes padding
|
||||
|
||||
void log_event_(const char *name);
|
||||
void restore_medium_conn_params_();
|
||||
};
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
|
||||
@@ -36,6 +36,7 @@ from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["esp32_ble"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@bdraco"]
|
||||
|
||||
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
|
||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
|
||||
|
||||
@@ -49,13 +49,6 @@ void ESP32BLETracker::setup() {
|
||||
ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
|
||||
return;
|
||||
}
|
||||
RAMAllocator<BLEScanResult> allocator;
|
||||
this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
|
||||
|
||||
if (this->scan_ring_buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
global_esp32_ble_tracker = this;
|
||||
|
||||
@@ -117,74 +110,8 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
bool promote_to_connecting = discovered && !searching && !connecting;
|
||||
|
||||
// Process scan results from lock-free SPSC ring buffer
|
||||
// Consumer side: This runs in the main loop thread
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
// Load our own index with relaxed ordering (we're the only writer)
|
||||
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
|
||||
|
||||
// Load producer's index with acquire to see their latest writes
|
||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
|
||||
|
||||
while (read_idx != write_idx) {
|
||||
// Calculate how many contiguous results we can process in one batch
|
||||
// If write > read: process all results from read to write
|
||||
// If write <= read (wraparound): process from read to end of buffer first
|
||||
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
|
||||
|
||||
// Process the batch for raw advertisements
|
||||
if (this->raw_advertisements_) {
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||
}
|
||||
for (auto *client : this->clients_) {
|
||||
client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||
}
|
||||
}
|
||||
|
||||
// Process individual results for parsed advertisements
|
||||
if (this->parse_advertisements_) {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
for (size_t i = 0; i < batch_size; i++) {
|
||||
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
|
||||
ESPBTDevice device;
|
||||
device.parse_scan_rst(scan_result);
|
||||
|
||||
bool found = false;
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->parse_device(device))
|
||||
found = true;
|
||||
}
|
||||
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->parse_device(device)) {
|
||||
found = true;
|
||||
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
||||
promote_to_connecting = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && !this->scan_continuous_) {
|
||||
this->print_bt_device_info(device);
|
||||
}
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
}
|
||||
|
||||
// Update read index for entire batch
|
||||
read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
|
||||
|
||||
// Store with release to ensure reads complete before index update
|
||||
this->ring_read_index_.store(read_idx, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Log dropped results periodically
|
||||
size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
|
||||
}
|
||||
}
|
||||
// All scan result processing is now done immediately in gap_scan_event_handler
|
||||
// No ring buffer processing needed here
|
||||
if (this->scanner_state_ == ScannerState::FAILED ||
|
||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||
this->stop_scan_();
|
||||
@@ -229,8 +156,10 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
// If there is a discovered client and no connecting
|
||||
// clients and no clients using the scanner to search for
|
||||
// devices, then stop scanning and promote the discovered
|
||||
// client to ready to connect.
|
||||
// devices, then promote the discovered client to ready to connect.
|
||||
// Note: Scanning is already stopped by gap_scan_event_handler when
|
||||
// a discovered client is found, so we only need to handle promotion
|
||||
// when the scanner is IDLE.
|
||||
if (promote_to_connecting &&
|
||||
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
|
||||
for (auto *client : this->clients_) {
|
||||
@@ -238,19 +167,21 @@ void ESP32BLETracker::loop() {
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGD(TAG, "Stopping scan to make connection");
|
||||
this->stop_scan_();
|
||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||
ESP_LOGD(TAG, "Promoting client to connect");
|
||||
// We only want to promote one client at a time.
|
||||
// once the scanner is fully stopped.
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||
if (!this->coex_prefer_ble_) {
|
||||
this->coex_prefer_ble_ = true;
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
|
||||
}
|
||||
#endif
|
||||
client->set_state(ClientState::READY_TO_CONNECT);
|
||||
// Don't wait for scan stop complete - promote immediately.
|
||||
// This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
|
||||
// This guarantees that the stop scan command will be fully processed before any subsequent connect command,
|
||||
// preventing race conditions or overlapping operations.
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Promoting client to connect");
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||
if (!this->coex_prefer_ble_) {
|
||||
this->coex_prefer_ble_ = true;
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
|
||||
}
|
||||
#endif
|
||||
client->set_state(ClientState::READY_TO_CONNECT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -390,31 +321,18 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
|
||||
|
||||
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
|
||||
// Note: This handler is called from the main loop context via esp32_ble's event queue.
|
||||
// However, we still use a lock-free ring buffer to batch results efficiently.
|
||||
// We process advertisements immediately instead of buffering them.
|
||||
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
|
||||
|
||||
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
|
||||
// Ring buffer write (Producer side)
|
||||
// Even though we're in the main loop, the ring buffer design allows efficient batching
|
||||
// IMPORTANT: Only this thread writes to ring_write_index_
|
||||
// Process the scan result immediately
|
||||
bool found_discovered_client = this->process_scan_result_(scan_result);
|
||||
|
||||
// Load our own index with relaxed ordering (we're the only writer)
|
||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
|
||||
uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
|
||||
|
||||
// Load consumer's index with acquire to see their latest updates
|
||||
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
|
||||
|
||||
// Check if buffer is full
|
||||
if (next_write_idx != read_idx) {
|
||||
// Write to ring buffer
|
||||
this->scan_ring_buffer_[write_idx] = scan_result;
|
||||
|
||||
// Store with release to ensure the write is visible before index update
|
||||
this->ring_write_index_.store(next_write_idx, std::memory_order_release);
|
||||
} else {
|
||||
// Buffer full, track dropped results
|
||||
this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
|
||||
// If we found a discovered client that needs promotion, stop scanning
|
||||
// This replaces the promote_to_connecting logic from loop()
|
||||
if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGD(TAG, "Found discovered client, stopping scan for connection");
|
||||
this->stop_scan_();
|
||||
}
|
||||
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
|
||||
// Scan finished on its own
|
||||
@@ -859,8 +777,66 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
||||
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
|
||||
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
|
||||
}
|
||||
|
||||
bool ESP32BLETracker::has_connecting_clients_() const {
|
||||
for (auto *client : this->clients_) {
|
||||
auto state = client->state();
|
||||
if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
|
||||
bool found_discovered_client = false;
|
||||
|
||||
// Process raw advertisements
|
||||
if (this->raw_advertisements_) {
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->parse_devices(&scan_result, 1);
|
||||
}
|
||||
for (auto *client : this->clients_) {
|
||||
client->parse_devices(&scan_result, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Process parsed advertisements
|
||||
if (this->parse_advertisements_) {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
ESPBTDevice device;
|
||||
device.parse_scan_rst(scan_result);
|
||||
|
||||
bool found = false;
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->parse_device(device))
|
||||
found = true;
|
||||
}
|
||||
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->parse_device(device)) {
|
||||
found = true;
|
||||
// Check if this client is discovered and needs promotion
|
||||
if (client->state() == ClientState::DISCOVERED) {
|
||||
// Only check for connecting clients if we found a discovered client
|
||||
// This matches the original logic: !connecting && client->state() == DISCOVERED
|
||||
if (!this->has_connecting_clients_()) {
|
||||
found_discovered_client = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && !this->scan_continuous_) {
|
||||
this->print_bt_device_info(device);
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
}
|
||||
|
||||
return found_discovered_client;
|
||||
}
|
||||
|
||||
void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
|
||||
ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
|
||||
this->already_discovered_.clear();
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -21,6 +20,7 @@
|
||||
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/components/esp32_ble/ble_scan_result.h"
|
||||
|
||||
namespace esphome::esp32_ble_tracker {
|
||||
|
||||
@@ -272,6 +272,13 @@ class ESP32BLETracker : public Component,
|
||||
void set_scanner_state_(ScannerState state);
|
||||
/// Common cleanup logic when transitioning scanner to IDLE state
|
||||
void cleanup_scan_state_(bool is_stop_complete);
|
||||
/// Process a single scan result immediately
|
||||
/// Returns true if a discovered client needs promotion to READY_TO_CONNECT
|
||||
bool process_scan_result_(const BLEScanResult &scan_result);
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
/// Check if any clients are in connecting or ready to connect state
|
||||
bool has_connecting_clients_() const;
|
||||
#endif
|
||||
|
||||
uint8_t app_id_{0};
|
||||
|
||||
@@ -295,15 +302,6 @@ class ESP32BLETracker : public Component,
|
||||
bool raw_advertisements_{false};
|
||||
bool parse_advertisements_{false};
|
||||
|
||||
// Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
|
||||
// Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
|
||||
// Consumer: ESPHome main loop (loop() method)
|
||||
// This design ensures zero blocking in the BT callback and prevents scan result loss
|
||||
BLEScanResult *scan_ring_buffer_;
|
||||
std::atomic<uint8_t> ring_write_index_{0}; // Written only by BT callback (producer)
|
||||
std::atomic<uint8_t> ring_read_index_{0}; // Written only by main loop (consumer)
|
||||
std::atomic<uint16_t> scan_results_dropped_{0}; // Tracks buffer overflow events
|
||||
|
||||
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
int connecting_{0};
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#include <esp32-hal-dac.h>
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_dac {
|
||||
@@ -23,18 +19,12 @@ void ESP32DAC::setup() {
|
||||
this->pin_->setup();
|
||||
this->turn_off();
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
|
||||
const dac_oneshot_config_t oneshot_cfg{channel};
|
||||
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32DAC::on_safe_shutdown() {
|
||||
#ifdef USE_ESP_IDF
|
||||
dac_oneshot_del_channel(this->dac_handle_);
|
||||
#endif
|
||||
}
|
||||
void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); }
|
||||
|
||||
void ESP32DAC::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ESP32 DAC:");
|
||||
@@ -48,15 +38,10 @@ void ESP32DAC::write_state(float state) {
|
||||
|
||||
state = state * 255;
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
dac_oneshot_output_voltage(this->dac_handle_, state);
|
||||
#endif
|
||||
#ifdef USE_ARDUINO
|
||||
dacWrite(this->pin_->get_pin(), state);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esp32_dac
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include <driver/dac_oneshot.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_dac {
|
||||
@@ -29,12 +27,10 @@ class ESP32DAC : public output::FloatOutput, public Component {
|
||||
void write_state(float state) override;
|
||||
|
||||
InternalGPIOPin *pin_;
|
||||
#ifdef USE_ESP_IDF
|
||||
dac_oneshot_handle_t dac_handle_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esp32_dac
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
|
||||
|
||||
@@ -49,7 +49,7 @@ class ESPNowPacket {
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
// Constructor for sent data
|
||||
ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
|
||||
this->init_sent_data(info->src_addr, status);
|
||||
this->init_sent_data_(info->src_addr, status);
|
||||
}
|
||||
#else
|
||||
// Constructor for sent data
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gpio_expander {
|
||||
namespace esphome::gpio_expander {
|
||||
|
||||
/// @brief A class to cache the read state of a GPIO expander.
|
||||
/// This class caches reads between GPIO Pins which are on the same bank.
|
||||
@@ -17,12 +18,22 @@ namespace gpio_expander {
|
||||
/// N - Number of pins
|
||||
template<typename T, T N> class CachedGpioExpander {
|
||||
public:
|
||||
/// @brief Read the state of the given pin. This will invalidate the cache for the given pin number.
|
||||
/// @param pin Pin number to read
|
||||
/// @return Pin state
|
||||
bool digital_read(T pin) {
|
||||
uint8_t bank = pin / (sizeof(T) * BITS_PER_BYTE);
|
||||
if (this->read_cache_invalidated_[bank]) {
|
||||
this->read_cache_invalidated_[bank] = false;
|
||||
const uint8_t bank = pin / BANK_SIZE;
|
||||
const T pin_mask = (1 << (pin % BANK_SIZE));
|
||||
// Check if specific pin cache is valid
|
||||
if (this->read_cache_valid_[bank] & pin_mask) {
|
||||
// Invalidate pin
|
||||
this->read_cache_valid_[bank] &= ~pin_mask;
|
||||
} else {
|
||||
// Read whole bank from hardware
|
||||
if (!this->digital_read_hw(pin))
|
||||
return false;
|
||||
// Mark bank cache as valid except the pin that is being returned now
|
||||
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~pin_mask;
|
||||
}
|
||||
return this->digital_read_cache(pin);
|
||||
}
|
||||
@@ -36,18 +47,16 @@ template<typename T, T N> class CachedGpioExpander {
|
||||
virtual bool digital_read_cache(T pin) = 0;
|
||||
/// @brief Call component low level function to write GPIO state to device
|
||||
virtual void digital_write_hw(T pin, bool value) = 0;
|
||||
const uint8_t cache_byte_size_ = N / (sizeof(T) * BITS_PER_BYTE);
|
||||
|
||||
/// @brief Invalidate cache. This function should be called in component loop().
|
||||
void reset_pin_cache_() {
|
||||
for (T i = 0; i < this->cache_byte_size_; i++) {
|
||||
this->read_cache_invalidated_[i] = true;
|
||||
}
|
||||
}
|
||||
void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); }
|
||||
|
||||
static const uint8_t BITS_PER_BYTE = 8;
|
||||
std::array<bool, N / (sizeof(T) * BITS_PER_BYTE)> read_cache_invalidated_{};
|
||||
static constexpr uint8_t BITS_PER_BYTE = 8;
|
||||
static constexpr uint8_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
|
||||
static constexpr size_t BANKS = N / BANK_SIZE;
|
||||
static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T);
|
||||
|
||||
T read_cache_valid_[BANKS]{0};
|
||||
};
|
||||
|
||||
} // namespace gpio_expander
|
||||
} // namespace esphome
|
||||
} // namespace esphome::gpio_expander
|
||||
|
||||
@@ -20,12 +20,11 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
|
||||
|
||||
#define ERROR_CHECK(err) \
|
||||
if ((err) != i2c::ERROR_OK) { \
|
||||
this->status_set_warning("Communication failure"); \
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \
|
||||
return; \
|
||||
}
|
||||
|
||||
void GT911Touchscreen::setup() {
|
||||
i2c::ErrorCode err;
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
@@ -35,9 +34,14 @@ void GT911Touchscreen::setup() {
|
||||
this->interrupt_pin_->digital_write(false);
|
||||
}
|
||||
delay(2);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(50); // NOLINT
|
||||
this->reset_pin_->digital_write(true); // wait 50ms after reset
|
||||
this->set_timeout(50, [this] { this->setup_internal_(); });
|
||||
return;
|
||||
}
|
||||
this->setup_internal_();
|
||||
}
|
||||
|
||||
void GT911Touchscreen::setup_internal_() {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// set pre-configured input mode
|
||||
this->interrupt_pin_->setup();
|
||||
@@ -45,7 +49,7 @@ void GT911Touchscreen::setup() {
|
||||
|
||||
// check the configuration of the int line.
|
||||
uint8_t data[4];
|
||||
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||
i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
|
||||
this->address_ = SECONDARY_ADDRESS;
|
||||
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||
@@ -53,7 +57,7 @@ void GT911Touchscreen::setup() {
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->read(data, 1);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
|
||||
ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]);
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->attach_interrupt_(this->interrupt_pin_,
|
||||
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
|
||||
@@ -75,16 +79,24 @@ void GT911Touchscreen::setup() {
|
||||
}
|
||||
}
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->mark_failed("Failed to read calibration");
|
||||
this->mark_failed("Calibration error");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->mark_failed("Failed to communicate");
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
this->setup_done_ = true;
|
||||
}
|
||||
|
||||
void GT911Touchscreen::update_touches() {
|
||||
this->skip_update_ = true; // skip send touch events by default, set to false after successful error checks
|
||||
if (!this->setup_done_) {
|
||||
return;
|
||||
}
|
||||
|
||||
i2c::ErrorCode err;
|
||||
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
|
||||
@@ -97,7 +109,6 @@ void GT911Touchscreen::update_touches() {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -107,6 +118,7 @@ void GT911Touchscreen::update_touches() {
|
||||
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
|
||||
ERROR_CHECK(err);
|
||||
|
||||
this->skip_update_ = false; // All error checks passed, send touch events
|
||||
for (uint8_t i = 0; i != num_of_touches; i++) {
|
||||
uint16_t id = data[i][0];
|
||||
uint16_t x = encode_uint16(data[i][2], data[i][1]);
|
||||
|
||||
@@ -15,8 +15,20 @@ class GT911ButtonListener {
|
||||
|
||||
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
/// @brief Initialize the GT911 touchscreen.
|
||||
///
|
||||
/// If @ref reset_pin_ is set, the touchscreen will be hardware reset,
|
||||
/// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout()
|
||||
/// to allow the device to stabilize after reset.
|
||||
///
|
||||
/// If @ref interrupt_pin_ is set, it will be temporarily configured during reset
|
||||
/// to control I2C address selection.
|
||||
///
|
||||
/// After the timeout, or immediately if no reset is performed, @ref setup_internal_()
|
||||
/// is called to complete the initialization.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
bool can_proceed() override { return this->setup_done_; }
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
@@ -25,8 +37,20 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
||||
protected:
|
||||
void update_touches() override;
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
/// @brief Perform the internal setup routine for the GT911 touchscreen.
|
||||
///
|
||||
/// This function checks the I2C address, configures the interrupt pin (if available),
|
||||
/// reads the touchscreen mode from the controller, and attempts to read calibration
|
||||
/// data (maximum X and Y values) if not already set.
|
||||
///
|
||||
/// On success, sets @ref setup_done_ to true.
|
||||
/// On failure, calls @ref mark_failed() with an appropriate error message.
|
||||
void setup_internal_();
|
||||
/// @brief True if the touchscreen setup has completed successfully.
|
||||
bool setup_done_{false};
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
std::vector<GT911ButtonListener *> button_listeners_;
|
||||
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
|
||||
};
|
||||
|
||||
@@ -24,9 +24,6 @@ static const uint32_t READ_DURATION_MS = 16;
|
||||
static const size_t TASK_STACK_SIZE = 4096;
|
||||
static const ssize_t TASK_PRIORITY = 23;
|
||||
|
||||
// Use an exponential moving average to correct a DC offset with weight factor 1/1000
|
||||
static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
|
||||
|
||||
static const char *const TAG = "i2s_audio.microphone";
|
||||
|
||||
enum MicrophoneEventGroupBits : uint32_t {
|
||||
@@ -381,26 +378,57 @@ void I2SAudioMicrophone::mic_task(void *params) {
|
||||
}
|
||||
|
||||
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
|
||||
/**
|
||||
* From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html:
|
||||
*
|
||||
* y(n) = x(n) - x(n-1) + R * y(n-1)
|
||||
* R = 1 - (pi * 2 * frequency / samplerate)
|
||||
*
|
||||
* From https://en.wikipedia.org/wiki/Hearing_range:
|
||||
* The human range is commonly given as 20Hz up.
|
||||
*
|
||||
* From https://en.wikipedia.org/wiki/High-resolution_audio:
|
||||
* A reasonable upper bound for sample rate seems to be 96kHz.
|
||||
*
|
||||
* Calculate R value for 20Hz on a 96kHz sample rate:
|
||||
* R = 1 - (pi * 2 * 20 / 96000)
|
||||
* R = 0.9986910031
|
||||
*
|
||||
* Transform floating point to bit-shifting approximation:
|
||||
* output = input - prev_input + R * prev_output
|
||||
* output = input - prev_input + (prev_output - (prev_output >> S))
|
||||
*
|
||||
* Approximate bit-shift value S from R:
|
||||
* R = 1 - (1 >> S)
|
||||
* R = 1 - (1 / 2^S)
|
||||
* R = 1 - 2^-S
|
||||
* 0.9986910031 = 1 - 2^-S
|
||||
* S = 9.57732 ~= 10
|
||||
*
|
||||
* Actual R from S:
|
||||
* R = 1 - 2^-10 = 0.9990234375
|
||||
*
|
||||
* Confirm this has effect outside human hearing on 96000kHz sample:
|
||||
* 0.9990234375 = 1 - (pi * 2 * f / 96000)
|
||||
* f = 14.9208Hz
|
||||
*
|
||||
* Confirm this has effect outside human hearing on PDM 16kHz sample:
|
||||
* 0.9990234375 = 1 - (pi * 2 * f / 16000)
|
||||
* f = 2.4868Hz
|
||||
*
|
||||
*/
|
||||
const uint8_t dc_filter_shift = 10;
|
||||
const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
|
||||
const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
|
||||
|
||||
if (total_samples == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t offset_accumulator = 0;
|
||||
for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
|
||||
const uint32_t byte_index = sample_index * bytes_per_sample;
|
||||
int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
|
||||
offset_accumulator += sample;
|
||||
sample -= this->dc_offset_;
|
||||
audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample);
|
||||
int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
|
||||
int32_t output = input - this->dc_offset_prev_input_ +
|
||||
(this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift));
|
||||
this->dc_offset_prev_input_ = input;
|
||||
this->dc_offset_prev_output_ = output;
|
||||
audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample);
|
||||
}
|
||||
|
||||
const int32_t new_offset = offset_accumulator / total_samples;
|
||||
this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR +
|
||||
(DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ /
|
||||
DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR;
|
||||
}
|
||||
|
||||
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
|
||||
|
||||
@@ -82,7 +82,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
|
||||
bool correct_dc_offset_;
|
||||
bool locked_driver_{false};
|
||||
int32_t dc_offset_{0};
|
||||
int32_t dc_offset_prev_input_{0};
|
||||
int32_t dc_offset_prev_output_{0};
|
||||
};
|
||||
|
||||
} // namespace i2s_audio
|
||||
|
||||
@@ -2,7 +2,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
json_ns = cg.esphome_ns.namespace("json")
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
|
||||
@@ -77,6 +77,7 @@ BRIGHTNESS = 0x51
|
||||
WRDISBV = 0x51
|
||||
RDDISBV = 0x52
|
||||
WRCTRLD = 0x53
|
||||
WCE = 0x58
|
||||
SWIRE1 = 0x5A
|
||||
SWIRE2 = 0x5B
|
||||
IFMODE = 0xB0
|
||||
@@ -91,6 +92,7 @@ PWCTR2 = 0xC1
|
||||
PWCTR3 = 0xC2
|
||||
PWCTR4 = 0xC3
|
||||
PWCTR5 = 0xC4
|
||||
SPIMODESEL = 0xC4
|
||||
VMCTR1 = 0xC5
|
||||
IFCTR = 0xC6
|
||||
VMCTR2 = 0xC7
|
||||
|
||||
@@ -5,10 +5,13 @@ from esphome.components.mipi import (
|
||||
PAGESEL,
|
||||
PIXFMT,
|
||||
SLPOUT,
|
||||
SPIMODESEL,
|
||||
SWIRE1,
|
||||
SWIRE2,
|
||||
TEON,
|
||||
WCE,
|
||||
WRAM,
|
||||
WRCTRLD,
|
||||
DriverChip,
|
||||
delay,
|
||||
)
|
||||
@@ -87,4 +90,19 @@ T4_S3_AMOLED = RM690B0.extend(
|
||||
bus_mode=TYPE_QUAD,
|
||||
)
|
||||
|
||||
CO5300 = DriverChip(
|
||||
"CO5300",
|
||||
brightness=0xD0,
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
initsequence=(
|
||||
(SLPOUT,), # Requires early SLPOUT
|
||||
(PAGESEL, 0x00),
|
||||
(SPIMODESEL, 0x80),
|
||||
(WRCTRLD, 0x20),
|
||||
(WCE, 0x00),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .amoled import CO5300
|
||||
from .ili import ILI9488_A
|
||||
|
||||
DriverChip(
|
||||
@@ -140,3 +141,14 @@ ILI9488_A.extend(
|
||||
data_rate="20MHz",
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
CO5300.extend(
|
||||
"WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75",
|
||||
width=466,
|
||||
height=466,
|
||||
pixel_mode="16bit",
|
||||
offset_height=0,
|
||||
offset_width=6,
|
||||
cs_pin=12,
|
||||
reset_pin=39,
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
CONF_RMT_SYMBOLS,
|
||||
CONF_USE_DMA,
|
||||
CONF_VALUE,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
@@ -22,11 +23,17 @@ AUTO_LOAD = ["remote_base"]
|
||||
CONF_EOT_LEVEL = "eot_level"
|
||||
CONF_ON_TRANSMIT = "on_transmit"
|
||||
CONF_ON_COMPLETE = "on_complete"
|
||||
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
|
||||
|
||||
remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter")
|
||||
RemoteTransmitterComponent = remote_transmitter_ns.class_(
|
||||
"RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component
|
||||
)
|
||||
DigitalWriteAction = remote_transmitter_ns.class_(
|
||||
"DigitalWriteAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(RemoteTransmitterComponent),
|
||||
)
|
||||
|
||||
MULTI_CONF = True
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
@@ -63,6 +70,25 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent),
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.boolean),
|
||||
},
|
||||
key=CONF_VALUE,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"remote_transmitter.digital_write", DigitalWriteAction, DIGITAL_WRITE_ACTION_SCHEMA
|
||||
)
|
||||
async def digital_write_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_TRANSMITTER_ID])
|
||||
template_ = await cg.templatable(config[CONF_VALUE], args, bool)
|
||||
cg.add(var.set_value(template_))
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
|
||||
18
esphome/components/remote_transmitter/automation.h
Normal file
18
esphome/components/remote_transmitter/automation.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace remote_transmitter {
|
||||
|
||||
template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public Parented<RemoteTransmitterComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(bool, value)
|
||||
void play(Ts... x) override { this->parent_->digital_write(this->value_.value(x...)); }
|
||||
};
|
||||
|
||||
} // namespace remote_transmitter
|
||||
} // namespace esphome
|
||||
@@ -30,10 +30,11 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
||||
|
||||
void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; }
|
||||
|
||||
void digital_write(bool value);
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
|
||||
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
|
||||
void digital_write(bool value);
|
||||
#endif
|
||||
|
||||
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };
|
||||
|
||||
@@ -73,6 +73,8 @@ void RemoteTransmitterComponent::space_(uint32_t usec) {
|
||||
this->target_time_ += usec;
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
|
||||
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
ESP_LOGD(TAG, "Sending remote code");
|
||||
uint32_t on_time, off_time;
|
||||
|
||||
@@ -75,6 +75,8 @@ void RemoteTransmitterComponent::space_(uint32_t usec) {
|
||||
this->target_time_ += usec;
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
|
||||
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
ESP_LOGD(TAG, "Sending remote code");
|
||||
uint32_t on_time, off_time;
|
||||
|
||||
@@ -64,21 +64,15 @@ void MR24HPC1Component::dump_config() {
|
||||
void MR24HPC1Component::setup() {
|
||||
this->check_uart_settings(115200);
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
if (this->custom_mode_number_ != nullptr) {
|
||||
this->custom_mode_number_->publish_state(0); // Zero out the custom mode
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (this->custom_mode_num_sensor_ != nullptr) {
|
||||
this->custom_mode_num_sensor_->publish_state(0);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->custom_mode_end_text_sensor_ != nullptr) {
|
||||
this->custom_mode_end_text_sensor_->publish_state("Not in custom mode");
|
||||
}
|
||||
#endif
|
||||
this->set_custom_end_mode();
|
||||
this->poll_time_base_func_check_ = true;
|
||||
this->check_dev_inf_sign_ = true;
|
||||
@@ -361,7 +355,6 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
|
||||
void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) {
|
||||
uint16_t product_len = encode_uint16(data[FRAME_COMMAND_WORD_INDEX + 1], data[FRAME_COMMAND_WORD_INDEX + 2]);
|
||||
if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_PRODUCT_MODE) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if ((this->product_model_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) {
|
||||
memset(this->c_product_mode_, 0, PRODUCT_BUF_MAX_SIZE);
|
||||
memcpy(this->c_product_mode_, &data[FRAME_DATA_INDEX], product_len);
|
||||
@@ -369,9 +362,7 @@ void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) {
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Reply: get product_mode error!");
|
||||
}
|
||||
#endif
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_PRODUCT_ID) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if ((this->product_id_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) {
|
||||
memset(this->c_product_id_, 0, PRODUCT_BUF_MAX_SIZE);
|
||||
memcpy(this->c_product_id_, &data[FRAME_DATA_INDEX], product_len);
|
||||
@@ -379,9 +370,7 @@ void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) {
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Reply: get productId error!");
|
||||
}
|
||||
#endif
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_HARDWARE_MODEL) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if ((this->hardware_model_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) {
|
||||
memset(this->c_hardware_model_, 0, PRODUCT_BUF_MAX_SIZE);
|
||||
memcpy(this->c_hardware_model_, &data[FRAME_DATA_INDEX], product_len);
|
||||
@@ -390,9 +379,7 @@ void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) {
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Reply: get hardwareModel error!");
|
||||
}
|
||||
#endif
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_FIRMWARE_VERSION) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if ((this->firware_version_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) {
|
||||
memset(this->c_firmware_version_, 0, PRODUCT_BUF_MAX_SIZE);
|
||||
memcpy(this->c_firmware_version_, &data[FRAME_DATA_INDEX], product_len);
|
||||
@@ -400,26 +387,22 @@ void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) {
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Reply: get firmwareVersion error!");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing the underlying open parameters
|
||||
void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *data) {
|
||||
if (data[FRAME_COMMAND_WORD_INDEX] == 0x00) {
|
||||
#ifdef USE_SWITCH
|
||||
if (this->underlying_open_function_switch_ != nullptr) {
|
||||
this->underlying_open_function_switch_->publish_state(
|
||||
data[FRAME_DATA_INDEX]); // Underlying Open Parameter Switch Status Updates
|
||||
}
|
||||
#endif
|
||||
if (data[FRAME_DATA_INDEX]) {
|
||||
this->s_output_info_switch_flag_ = OUTPUT_SWITCH_ON;
|
||||
} else {
|
||||
this->s_output_info_switch_flag_ = OUTPUT_SWTICH_OFF;
|
||||
}
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x01) {
|
||||
#ifdef USE_SENSOR
|
||||
if (this->custom_spatial_static_value_sensor_ != nullptr) {
|
||||
this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
}
|
||||
@@ -435,30 +418,20 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da
|
||||
if (this->custom_motion_speed_sensor_ != nullptr) {
|
||||
this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX + 4] - 10) * 0.5f);
|
||||
}
|
||||
#endif
|
||||
} else if ((data[FRAME_COMMAND_WORD_INDEX] == 0x06) || (data[FRAME_COMMAND_WORD_INDEX] == 0x86)) {
|
||||
// none:0x00 close_to:0x01 far_away:0x02
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if ((this->keep_away_text_sensor_ != nullptr) && (data[FRAME_DATA_INDEX] < 3)) {
|
||||
this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]);
|
||||
}
|
||||
#endif
|
||||
} else if ((this->movement_signs_sensor_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x07) || (data[FRAME_COMMAND_WORD_INDEX] == 0x87))) {
|
||||
#ifdef USE_SENSOR
|
||||
this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
#endif
|
||||
} else if ((this->existence_threshold_number_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x08) || (data[FRAME_COMMAND_WORD_INDEX] == 0x88))) {
|
||||
#ifdef USE_NUMBER
|
||||
this->existence_threshold_number_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
#endif
|
||||
} else if ((this->motion_threshold_number_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x09) || (data[FRAME_COMMAND_WORD_INDEX] == 0x89))) {
|
||||
#ifdef USE_NUMBER
|
||||
this->motion_threshold_number_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
} else if ((this->existence_boundary_select_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) {
|
||||
if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) {
|
||||
@@ -469,68 +442,48 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da
|
||||
if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) {
|
||||
this->motion_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]);
|
||||
}
|
||||
#endif
|
||||
} else if ((this->motion_trigger_number_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x0c) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8c))) {
|
||||
#ifdef USE_NUMBER
|
||||
uint32_t motion_trigger_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1],
|
||||
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]);
|
||||
this->motion_trigger_number_->publish_state(motion_trigger_time);
|
||||
#endif
|
||||
} else if ((this->motion_to_rest_number_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x0d) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8d))) {
|
||||
#ifdef USE_NUMBER
|
||||
uint32_t move_to_rest_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1],
|
||||
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]);
|
||||
this->motion_to_rest_number_->publish_state(move_to_rest_time);
|
||||
#endif
|
||||
} else if ((this->custom_unman_time_number_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x0e) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8e))) {
|
||||
#ifdef USE_NUMBER
|
||||
uint32_t enter_unmanned_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1],
|
||||
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]);
|
||||
float custom_unmanned_time = enter_unmanned_time / 1000.0;
|
||||
this->custom_unman_time_number_->publish_state(custom_unmanned_time);
|
||||
#endif
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x80) {
|
||||
if (data[FRAME_DATA_INDEX]) {
|
||||
this->s_output_info_switch_flag_ = OUTPUT_SWITCH_ON;
|
||||
} else {
|
||||
this->s_output_info_switch_flag_ = OUTPUT_SWTICH_OFF;
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
if (this->underlying_open_function_switch_ != nullptr) {
|
||||
this->underlying_open_function_switch_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
}
|
||||
#endif
|
||||
} else if ((this->custom_spatial_static_value_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x81)) {
|
||||
#ifdef USE_SENSOR
|
||||
this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
#endif
|
||||
} else if ((this->custom_spatial_motion_value_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x82)) {
|
||||
#ifdef USE_SENSOR
|
||||
this->custom_spatial_motion_value_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
#endif
|
||||
} else if ((this->custom_presence_of_detection_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x83)) {
|
||||
#ifdef USE_SENSOR
|
||||
this->custom_presence_of_detection_sensor_->publish_state(
|
||||
S_PRESENCE_OF_DETECTION_RANGE_STR[data[FRAME_DATA_INDEX]]);
|
||||
#endif
|
||||
} else if ((this->custom_motion_distance_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x84)) {
|
||||
#ifdef USE_SENSOR
|
||||
this->custom_motion_distance_sensor_->publish_state(data[FRAME_DATA_INDEX] * 0.5f);
|
||||
#endif
|
||||
} else if ((this->custom_motion_speed_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x85)) {
|
||||
#ifdef USE_SENSOR
|
||||
this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX] - 10) * 0.5f);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void MR24HPC1Component::r24_parse_data_frame_(uint8_t *data, uint8_t len) {
|
||||
switch (data[FRAME_CONTROL_WORD_INDEX]) {
|
||||
case 0x01: {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if ((this->heartbeat_state_text_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x01)) {
|
||||
this->heartbeat_state_text_sensor_->publish_state("Equipment Normal");
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x02) {
|
||||
@@ -538,11 +491,6 @@ void MR24HPC1Component::r24_parse_data_frame_(uint8_t *data, uint8_t len) {
|
||||
} else if (this->heartbeat_state_text_sensor_ != nullptr) {
|
||||
this->heartbeat_state_text_sensor_->publish_state("Equipment Abnormal");
|
||||
}
|
||||
#else
|
||||
if (data[FRAME_COMMAND_WORD_INDEX] == 0x02) {
|
||||
ESP_LOGD(TAG, "Reply: query restart packet");
|
||||
}
|
||||
#endif
|
||||
} break;
|
||||
case 0x02: {
|
||||
this->r24_frame_parse_product_information_(data);
|
||||
@@ -566,73 +514,51 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) {
|
||||
if (data[FRAME_COMMAND_WORD_INDEX] == 0x01) {
|
||||
ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]);
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) {
|
||||
#ifdef USE_SELECT
|
||||
if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) {
|
||||
this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]);
|
||||
}
|
||||
#endif
|
||||
} else if ((this->sensitivity_number_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x08) || (data[FRAME_COMMAND_WORD_INDEX] == 0x88))) {
|
||||
// 1-3
|
||||
#ifdef USE_NUMBER
|
||||
this->sensitivity_number_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
#endif
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x09) {
|
||||
// 1-4
|
||||
#ifdef USE_SENSOR
|
||||
if (this->custom_mode_num_sensor_ != nullptr) {
|
||||
this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
if (this->custom_mode_number_ != nullptr) {
|
||||
this->custom_mode_number_->publish_state(0);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->custom_mode_end_text_sensor_ != nullptr) {
|
||||
this->custom_mode_end_text_sensor_->publish_state("Setup in progress");
|
||||
}
|
||||
#endif
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x81) {
|
||||
ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]);
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) {
|
||||
#ifdef USE_SELECT
|
||||
if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) {
|
||||
this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]);
|
||||
}
|
||||
#endif
|
||||
} else if ((this->custom_mode_end_text_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x0A)) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
this->custom_mode_end_text_sensor_->publish_state("Set Success!");
|
||||
#endif
|
||||
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x89) {
|
||||
if (data[FRAME_DATA_INDEX] == 0) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->custom_mode_end_text_sensor_ != nullptr) {
|
||||
this->custom_mode_end_text_sensor_->publish_state("Not in custom mode");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
if (this->custom_mode_number_ != nullptr) {
|
||||
this->custom_mode_number_->publish_state(0);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (this->custom_mode_num_sensor_ != nullptr) {
|
||||
this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#ifdef USE_SENSOR
|
||||
if (this->custom_mode_num_sensor_ != nullptr) {
|
||||
this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]);
|
||||
@@ -642,37 +568,27 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) {
|
||||
void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) {
|
||||
if ((this->has_target_binary_sensor_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x01) || (data[FRAME_COMMAND_WORD_INDEX] == 0x81))) {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
this->has_target_binary_sensor_->publish_state(S_SOMEONE_EXISTS_STR[data[FRAME_DATA_INDEX]]);
|
||||
#endif
|
||||
} else if ((this->motion_status_text_sensor_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x02) || (data[FRAME_COMMAND_WORD_INDEX] == 0x82))) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (data[FRAME_DATA_INDEX] < 3) {
|
||||
this->motion_status_text_sensor_->publish_state(S_MOTION_STATUS_STR[data[FRAME_DATA_INDEX]]);
|
||||
}
|
||||
#endif
|
||||
} else if ((this->movement_signs_sensor_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x03) || (data[FRAME_COMMAND_WORD_INDEX] == 0x83))) {
|
||||
#ifdef USE_SENSOR
|
||||
this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
} else if ((this->unman_time_select_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) {
|
||||
// none:0x00 1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08
|
||||
if (data[FRAME_DATA_INDEX] < 9) {
|
||||
this->unman_time_select_->publish_state(S_UNMANNED_TIME_STR[data[FRAME_DATA_INDEX]]);
|
||||
}
|
||||
#endif
|
||||
} else if ((this->keep_away_text_sensor_ != nullptr) &&
|
||||
((data[FRAME_COMMAND_WORD_INDEX] == 0x0B) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8B))) {
|
||||
// none:0x00 close_to:0x01 far_away:0x02
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (data[FRAME_DATA_INDEX] < 3) {
|
||||
this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]);
|
||||
}
|
||||
@@ -779,15 +695,12 @@ void MR24HPC1Component::set_underlying_open_function(bool enable) {
|
||||
} else {
|
||||
this->send_query_(UNDERLYING_SWITCH_OFF, sizeof(UNDERLYING_SWITCH_OFF));
|
||||
}
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->keep_away_text_sensor_ != nullptr) {
|
||||
this->keep_away_text_sensor_->publish_state("");
|
||||
}
|
||||
if (this->motion_status_text_sensor_ != nullptr) {
|
||||
this->motion_status_text_sensor_->publish_state("");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (this->custom_spatial_static_value_sensor_ != nullptr) {
|
||||
this->custom_spatial_static_value_sensor_->publish_state(NAN);
|
||||
}
|
||||
@@ -803,7 +716,6 @@ void MR24HPC1Component::set_underlying_open_function(bool enable) {
|
||||
if (this->custom_motion_speed_sensor_ != nullptr) {
|
||||
this->custom_motion_speed_sensor_->publish_state(NAN);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_scene_mode(uint8_t value) {
|
||||
@@ -811,16 +723,12 @@ void MR24HPC1Component::set_scene_mode(uint8_t value) {
|
||||
uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x07, 0x00, 0x01, value, 0x00, 0x54, 0x43};
|
||||
send_data[7] = get_frame_crc_sum(send_data, send_data_len);
|
||||
this->send_query_(send_data, send_data_len);
|
||||
#ifdef USE_NUMBER
|
||||
if (this->custom_mode_number_ != nullptr) {
|
||||
this->custom_mode_number_->publish_state(0);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (this->custom_mode_num_sensor_ != nullptr) {
|
||||
this->custom_mode_num_sensor_->publish_state(0);
|
||||
}
|
||||
#endif
|
||||
this->get_scene_mode();
|
||||
this->get_sensitivity();
|
||||
this->get_custom_mode();
|
||||
@@ -860,11 +768,9 @@ void MR24HPC1Component::set_unman_time(uint8_t value) {
|
||||
void MR24HPC1Component::set_custom_mode(uint8_t mode) {
|
||||
if (mode == 0) {
|
||||
this->set_custom_end_mode(); // Equivalent to end setting
|
||||
#ifdef USE_NUMBER
|
||||
if (this->custom_mode_number_ != nullptr) {
|
||||
this->custom_mode_number_->publish_state(0);
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
uint8_t send_data_len = 10;
|
||||
@@ -887,11 +793,9 @@ void MR24HPC1Component::set_custom_end_mode() {
|
||||
uint8_t send_data_len = 10;
|
||||
uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x0a, 0x00, 0x01, 0x0F, 0xCB, 0x54, 0x43};
|
||||
this->send_query_(send_data, send_data_len);
|
||||
#ifdef USE_NUMBER
|
||||
if (this->custom_mode_number_ != nullptr) {
|
||||
this->custom_mode_number_->publish_state(0); // Clear setpoints
|
||||
}
|
||||
#endif
|
||||
this->get_existence_boundary();
|
||||
this->get_motion_boundary();
|
||||
this->get_existence_threshold();
|
||||
@@ -905,10 +809,8 @@ void MR24HPC1Component::set_custom_end_mode() {
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_existence_boundary(uint8_t value) {
|
||||
#ifdef USE_SENSOR
|
||||
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
|
||||
return; // You'll have to check that you're in custom mode to set it up
|
||||
#endif
|
||||
uint8_t send_data_len = 10;
|
||||
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0A, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43};
|
||||
send_data[7] = get_frame_crc_sum(send_data, send_data_len);
|
||||
@@ -917,10 +819,8 @@ void MR24HPC1Component::set_existence_boundary(uint8_t value) {
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_motion_boundary(uint8_t value) {
|
||||
#ifdef USE_SENSOR
|
||||
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
|
||||
return; // You'll have to check that you're in custom mode to set it up
|
||||
#endif
|
||||
uint8_t send_data_len = 10;
|
||||
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0B, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43};
|
||||
send_data[7] = get_frame_crc_sum(send_data, send_data_len);
|
||||
@@ -929,10 +829,8 @@ void MR24HPC1Component::set_motion_boundary(uint8_t value) {
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_existence_threshold(uint8_t value) {
|
||||
#ifdef USE_SENSOR
|
||||
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
|
||||
return; // You'll have to check that you're in custom mode to set it up
|
||||
#endif
|
||||
uint8_t send_data_len = 10;
|
||||
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x08, 0x00, 0x01, value, 0x00, 0x54, 0x43};
|
||||
send_data[7] = get_frame_crc_sum(send_data, send_data_len);
|
||||
@@ -941,10 +839,8 @@ void MR24HPC1Component::set_existence_threshold(uint8_t value) {
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_motion_threshold(uint8_t value) {
|
||||
#ifdef USE_SENSOR
|
||||
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
|
||||
return; // You'll have to check that you're in custom mode to set it up
|
||||
#endif
|
||||
uint8_t send_data_len = 10;
|
||||
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x09, 0x00, 0x01, value, 0x00, 0x54, 0x43};
|
||||
send_data[7] = get_frame_crc_sum(send_data, send_data_len);
|
||||
@@ -953,10 +849,8 @@ void MR24HPC1Component::set_motion_threshold(uint8_t value) {
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_motion_trigger_time(uint8_t value) {
|
||||
#ifdef USE_SENSOR
|
||||
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
|
||||
return; // You'll have to check that you're in custom mode to set it up
|
||||
#endif
|
||||
uint8_t send_data_len = 13;
|
||||
uint8_t send_data[13] = {0x53, 0x59, 0x08, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, value, 0x00, 0x54, 0x43};
|
||||
send_data[10] = get_frame_crc_sum(send_data, send_data_len);
|
||||
@@ -965,10 +859,8 @@ void MR24HPC1Component::set_motion_trigger_time(uint8_t value) {
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_motion_to_rest_time(uint16_t value) {
|
||||
#ifdef USE_SENSOR
|
||||
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
|
||||
return; // You'll have to check that you're in custom mode to set it up
|
||||
#endif
|
||||
uint8_t h8_num = (value >> 8) & 0xff;
|
||||
uint8_t l8_num = value & 0xff;
|
||||
uint8_t send_data_len = 13;
|
||||
@@ -979,10 +871,8 @@ void MR24HPC1Component::set_motion_to_rest_time(uint16_t value) {
|
||||
}
|
||||
|
||||
void MR24HPC1Component::set_custom_unman_time(uint16_t value) {
|
||||
#ifdef USE_SENSOR
|
||||
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
|
||||
return; // You'll have to check that you're in custom mode to set it up
|
||||
#endif
|
||||
uint32_t value_ms = value * 1000;
|
||||
uint8_t h24_num = (value_ms >> 24) & 0xff;
|
||||
uint8_t h16_num = (value_ms >> 16) & 0xff;
|
||||
|
||||
@@ -30,7 +30,7 @@ from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
time_ns = cg.esphome_ns.namespace("time")
|
||||
|
||||
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
|
||||
}
|
||||
|
||||
// Handle regular form data
|
||||
if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) {
|
||||
if (r->content_len > CONFIG_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;
|
||||
|
||||
@@ -324,39 +324,47 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
||||
configuration = json_message["configuration"]
|
||||
config_file = settings.rel_path(configuration)
|
||||
port = json_message["port"]
|
||||
addresses: list[str] = [port]
|
||||
if (
|
||||
port == "OTA" # pylint: disable=too-many-boolean-expressions
|
||||
and (entry := entries.get(config_file))
|
||||
and entry.loaded_integrations
|
||||
and "api" in entry.loaded_integrations
|
||||
):
|
||||
if (mdns := dashboard.mdns_status) and (
|
||||
address_list := 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 = sort_ip_addresses(address_list)[0]
|
||||
elif (
|
||||
entry.address
|
||||
addresses = []
|
||||
# First priority: entry.address AKA use_address
|
||||
if (
|
||||
(use_address := entry.address)
|
||||
and (
|
||||
address_list := await dashboard.dns_cache.async_resolve(
|
||||
entry.address, time.monotonic()
|
||||
use_address, time.monotonic()
|
||||
)
|
||||
)
|
||||
and not isinstance(address_list, Exception)
|
||||
):
|
||||
# If mdns is not available, try to use the DNS cache
|
||||
port = sort_ip_addresses(address_list)[0]
|
||||
addresses.extend(sort_ip_addresses(address_list))
|
||||
|
||||
return [
|
||||
*DASHBOARD_COMMAND,
|
||||
*args,
|
||||
config_file,
|
||||
"--device",
|
||||
port,
|
||||
# Second priority: mDNS
|
||||
if (
|
||||
(mdns := dashboard.mdns_status)
|
||||
and (address_list := await mdns.async_resolve_host(entry.name))
|
||||
and (
|
||||
new_addresses := [
|
||||
addr for addr in address_list if addr not in addresses
|
||||
]
|
||||
)
|
||||
):
|
||||
# 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
|
||||
addresses.extend(sort_ip_addresses(new_addresses))
|
||||
|
||||
device_args: list[str] = [
|
||||
arg for address in addresses for arg in ("--device", address)
|
||||
]
|
||||
|
||||
return [*DASHBOARD_COMMAND, *args, config_file, *device_args]
|
||||
|
||||
|
||||
class EsphomeLogsHandler(EsphomePortCommandWebSocket):
|
||||
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from esphome import const
|
||||
|
||||
@@ -110,7 +111,7 @@ class RedirectText:
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._out, item)
|
||||
|
||||
def _write_color_replace(self, s):
|
||||
def _write_color_replace(self, s: str | bytes) -> None:
|
||||
from esphome.core import CORE
|
||||
|
||||
if CORE.dashboard:
|
||||
@@ -121,7 +122,7 @@ class RedirectText:
|
||||
s = s.replace("\033", "\\033")
|
||||
self._out.write(s)
|
||||
|
||||
def write(self, s):
|
||||
def write(self, s: str | bytes) -> int:
|
||||
# s is usually a str already (self._out is of type TextIOWrapper)
|
||||
# However, s is sometimes also a bytes object in python3. Let's make sure it's a
|
||||
# str
|
||||
@@ -223,7 +224,7 @@ def run_external_command(
|
||||
return retval
|
||||
|
||||
|
||||
def run_external_process(*cmd, **kwargs):
|
||||
def run_external_process(*cmd: str, **kwargs: Any) -> int | str:
|
||||
full_cmd = " ".join(shlex_quote(x) for x in cmd)
|
||||
_LOGGER.debug("Running: %s", full_cmd)
|
||||
filter_lines = kwargs.get("filter_lines")
|
||||
@@ -266,7 +267,7 @@ class OrderedDict(collections.OrderedDict):
|
||||
return dict(self).__repr__()
|
||||
|
||||
|
||||
def list_yaml_files(folders):
|
||||
def list_yaml_files(folders: list[str]) -> list[str]:
|
||||
files = filter_yaml_files(
|
||||
[os.path.join(folder, p) for folder in folders for p in os.listdir(folder)]
|
||||
)
|
||||
@@ -274,7 +275,7 @@ def list_yaml_files(folders):
|
||||
return files
|
||||
|
||||
|
||||
def filter_yaml_files(files):
|
||||
def filter_yaml_files(files: list[str]) -> list[str]:
|
||||
return [
|
||||
f
|
||||
for f in files
|
||||
|
||||
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.0.2
|
||||
click==8.1.7
|
||||
esphome-dashboard==20250514.0
|
||||
aioesphomeapi==37.2.4
|
||||
aioesphomeapi==37.2.5
|
||||
zeroconf==0.147.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.14 # dashboard_import
|
||||
|
||||
@@ -204,3 +204,9 @@ button:
|
||||
command: 0xEC
|
||||
rc_code_1: 0x0D
|
||||
rc_code_2: 0x0D
|
||||
- platform: template
|
||||
name: Digital Write
|
||||
on_press:
|
||||
- remote_transmitter.digital_write: true
|
||||
- remote_transmitter.digital_write:
|
||||
value: false
|
||||
|
||||
5
tests/components/seeed_mr24hpc1/.gitignore
vendored
5
tests/components/seeed_mr24hpc1/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
# Gitignore settings for ESPHome
|
||||
# This is an example and may include too much for your use-case.
|
||||
# You can modify this file to suit your needs.
|
||||
/.esphome/
|
||||
/secrets.yaml
|
||||
@@ -1,13 +1,3 @@
|
||||
esphome:
|
||||
name: test-esp8266
|
||||
friendly_name: "Test ESP8266"
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
uart:
|
||||
- id: seeed_mr24hpc1_uart
|
||||
tx_pin: ${uart_tx_pin}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
esphome:
|
||||
name: test-no-select-esp8266
|
||||
friendly_name: "Test No Select ESP8266"
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
uart:
|
||||
- id: seeed_mr24hpc1_uart
|
||||
tx_pin: ${uart_tx_pin}
|
||||
rx_pin: ${uart_rx_pin}
|
||||
baud_rate: 115200
|
||||
parity: NONE
|
||||
stop_bits: 1
|
||||
|
||||
seeed_mr24hpc1:
|
||||
id: my_seeed_mr24hpc1
|
||||
uart_id: seeed_mr24hpc1_uart
|
||||
|
||||
sensor:
|
||||
- platform: seeed_mr24hpc1
|
||||
custom_presence_of_detection:
|
||||
name: "Static Distance"
|
||||
|
||||
binary_sensor:
|
||||
- platform: seeed_mr24hpc1
|
||||
has_target:
|
||||
name: "Presence Information"
|
||||
|
||||
text_sensor:
|
||||
- platform: seeed_mr24hpc1
|
||||
heart_beat:
|
||||
name: "Heartbeat"
|
||||
|
||||
# Note: NO select components included - this should work without compilation errors
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
uart_tx_pin: GPIO1
|
||||
uart_rx_pin: GPIO3
|
||||
|
||||
<<: !include common_no_select.yaml
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
uart_tx_pin: GPIO1
|
||||
uart_rx_pin: GPIO3
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,53 @@
|
||||
esphome:
|
||||
name: test-duplicate-events
|
||||
friendly_name: Duplicate Events Test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: trigger_doorbell_test
|
||||
then:
|
||||
- logger.log: "Triggering doorbell test event"
|
||||
- homeassistant.event:
|
||||
event: esphome.doorbell_pressed
|
||||
data:
|
||||
device_id: test_device_123
|
||||
- logger.log: "Doorbell event sent"
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Simulate the user's binary sensor setup
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
name: "Test Doorbell"
|
||||
id: test_doorbell
|
||||
# Simulate doorbell behavior - starts false, becomes true when pressed
|
||||
lambda: |-
|
||||
static bool pressed = false;
|
||||
return pressed;
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Doorbell pressed - sending HA event"
|
||||
- homeassistant.event:
|
||||
event: esphome.doorbell_pressed
|
||||
data:
|
||||
device_id: test_device_123
|
||||
- logger.log: "Doorbell HA event sent"
|
||||
|
||||
button:
|
||||
# Button to simulate doorbell press for testing
|
||||
- platform: template
|
||||
name: "Simulate Doorbell Press"
|
||||
id: simulate_doorbell
|
||||
on_press:
|
||||
- logger.log: "Simulating doorbell press"
|
||||
- binary_sensor.template.publish:
|
||||
id: test_doorbell
|
||||
state: true
|
||||
- delay: 100ms
|
||||
- binary_sensor.template.publish:
|
||||
id: test_doorbell
|
||||
state: false
|
||||
- logger.log: "Doorbell simulation complete"
|
||||
@@ -0,0 +1,25 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
AUTO_LOAD = ["gpio_expander"]
|
||||
|
||||
gpio_expander_test_component_ns = cg.esphome_ns.namespace(
|
||||
"gpio_expander_test_component"
|
||||
)
|
||||
|
||||
GPIOExpanderTestComponent = gpio_expander_test_component_ns.class_(
|
||||
"GPIOExpanderTestComponent", cg.Component
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(GPIOExpanderTestComponent),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "gpio_expander_test_component.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::gpio_expander_test_component {
|
||||
|
||||
static const char *const TAG = "gpio_expander_test";
|
||||
|
||||
void GPIOExpanderTestComponent::setup() {
|
||||
for (uint8_t pin = 0; pin < 32; pin++) {
|
||||
this->digital_read(pin);
|
||||
}
|
||||
|
||||
this->digital_read(3);
|
||||
this->digital_read(3);
|
||||
this->digital_read(4);
|
||||
this->digital_read(3);
|
||||
this->digital_read(10);
|
||||
this->reset_pin_cache_(); // Reset cache to ensure next read is from hardware
|
||||
this->digital_read(15);
|
||||
this->digital_read(14);
|
||||
this->digital_read(14);
|
||||
|
||||
ESP_LOGD(TAG, "DONE");
|
||||
}
|
||||
|
||||
bool GPIOExpanderTestComponent::digital_read_hw(uint8_t pin) {
|
||||
ESP_LOGD(TAG, "digital_read_hw pin=%d", pin);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GPIOExpanderTestComponent::digital_read_cache(uint8_t pin) {
|
||||
ESP_LOGD(TAG, "digital_read_cache pin=%d", pin);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::gpio_expander_test_component
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::gpio_expander_test_component {
|
||||
|
||||
class GPIOExpanderTestComponent : public Component, public esphome::gpio_expander::CachedGpioExpander<uint8_t, 32> {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
protected:
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override{};
|
||||
};
|
||||
|
||||
} // namespace esphome::gpio_expander_test_component
|
||||
17
tests/integration/fixtures/gpio_expander_cache.yaml
Normal file
17
tests/integration/fixtures/gpio_expander_cache.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: gpio-expander-cache
|
||||
host:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
|
||||
# External component that uses gpio_expander::CachedGpioExpander
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
components: [gpio_expander_test_component]
|
||||
|
||||
gpio_expander_test_component:
|
||||
@@ -0,0 +1,33 @@
|
||||
esphome:
|
||||
name: test-service-calls
|
||||
friendly_name: Service Calls Test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: trigger_service_call_test
|
||||
then:
|
||||
- logger.log: "Triggering service call test"
|
||||
- homeassistant.service:
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id: light.test_light
|
||||
brightness: "255"
|
||||
- logger.log: "Service call sent"
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
button:
|
||||
# Button to trigger a service call for testing
|
||||
- platform: template
|
||||
name: "Test Service Call"
|
||||
id: test_service_call
|
||||
on_press:
|
||||
- logger.log: "Sending homeassistant service call"
|
||||
- homeassistant.service:
|
||||
service: switch.turn_on
|
||||
data:
|
||||
entity_id: switch.test_switch
|
||||
- logger.log: "Service call complete"
|
||||
@@ -373,3 +373,20 @@ button:
|
||||
name: "Test Button"
|
||||
on_press:
|
||||
- logger.log: "Button pressed"
|
||||
|
||||
# Date, Time, and DateTime entities
|
||||
datetime:
|
||||
- platform: template
|
||||
type: date
|
||||
name: "Test Date"
|
||||
initial_value: "2023-05-13"
|
||||
optimistic: true
|
||||
- platform: template
|
||||
type: time
|
||||
name: "Test Time"
|
||||
initial_value: "12:30:00"
|
||||
optimistic: true
|
||||
- platform: template
|
||||
type: datetime
|
||||
name: "Test DateTime"
|
||||
optimistic: true
|
||||
|
||||
129
tests/integration/test_duplicate_homeassistant_events.py
Normal file
129
tests/integration/test_duplicate_homeassistant_events.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Integration test for duplicate Home Assistant events issue.
|
||||
|
||||
Tests that homeassistant.event actions don't generate duplicate events
|
||||
when multiple API clients are connected to the same ESPHome device.
|
||||
|
||||
This addresses the issue where binary_sensor on_press/on_click triggers
|
||||
would fire duplicate events due to the API server sending the event
|
||||
to all connected clients instead of just one.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
|
||||
from aioesphomeapi import HomeassistantServiceCall
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_homeassistant_events(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that homeassistant.event actions don't generate duplicate events with multiple clients."""
|
||||
|
||||
# Track events received by each client
|
||||
client1_events: list[HomeassistantServiceCall] = []
|
||||
client2_events: list[HomeassistantServiceCall] = []
|
||||
all_events: list[HomeassistantServiceCall] = []
|
||||
|
||||
# Track events by event name for easier counting
|
||||
event_counts = defaultdict(int)
|
||||
|
||||
def on_service_call_client1(service_call: HomeassistantServiceCall) -> None:
|
||||
"""Track events received by first client."""
|
||||
if service_call.is_event:
|
||||
client1_events.append(service_call)
|
||||
all_events.append(service_call)
|
||||
event_counts[service_call.service] += 1
|
||||
|
||||
def on_service_call_client2(service_call: HomeassistantServiceCall) -> None:
|
||||
"""Track events received by second client."""
|
||||
if service_call.is_event:
|
||||
client2_events.append(service_call)
|
||||
all_events.append(service_call)
|
||||
event_counts[service_call.service] += 1
|
||||
|
||||
# Connect TWO API clients to simulate the real-world scenario
|
||||
# where Home Assistant might have multiple connections
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client1,
|
||||
api_client_connected() as client2,
|
||||
):
|
||||
# Subscribe both clients to service calls
|
||||
client1.subscribe_service_calls(on_service_call_client1)
|
||||
client2.subscribe_service_calls(on_service_call_client2)
|
||||
|
||||
# Get device info to ensure both clients are connected
|
||||
device_info1 = await client1.device_info()
|
||||
device_info2 = await client2.device_info()
|
||||
assert device_info1 is not None
|
||||
assert device_info2 is not None
|
||||
assert device_info1.name == device_info2.name
|
||||
|
||||
# List entities and services
|
||||
entities1, services1 = await client1.list_entities_services()
|
||||
entities2, services2 = await client2.list_entities_services()
|
||||
|
||||
# Find the test button service
|
||||
test_service = next(
|
||||
(s for s in services1 if s.name == "trigger_doorbell_test"), None
|
||||
)
|
||||
assert test_service is not None, "trigger_doorbell_test service not found"
|
||||
|
||||
# Clear any initial events
|
||||
client1_events.clear()
|
||||
client2_events.clear()
|
||||
all_events.clear()
|
||||
event_counts.clear()
|
||||
|
||||
# Trigger the doorbell event multiple times to simulate real usage
|
||||
for i in range(3):
|
||||
# Execute the service that triggers the doorbell event
|
||||
client1.execute_service(test_service, {})
|
||||
|
||||
# Wait a bit for the event to be processed
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Wait for all events to be received
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Now check the results
|
||||
print(f"Client1 received {len(client1_events)} events")
|
||||
print(f"Client2 received {len(client2_events)} events")
|
||||
print(f"Total events seen: {len(all_events)}")
|
||||
|
||||
# Print event details for debugging
|
||||
for i, event in enumerate(all_events):
|
||||
print(f"Event {i}: {event.service} (is_event: {event.is_event})")
|
||||
|
||||
# Each button press should generate exactly ONE event total,
|
||||
# not one per client. The current bug would show 6 total events
|
||||
# (3 button presses × 2 clients = 6 events) instead of 3.
|
||||
|
||||
expected_doorbell_events = 3
|
||||
actual_doorbell_events = event_counts.get("esphome.doorbell_pressed", 0)
|
||||
|
||||
assert actual_doorbell_events == expected_doorbell_events, (
|
||||
f"Expected {expected_doorbell_events} doorbell events, got {actual_doorbell_events}. "
|
||||
f"This indicates duplicate events are being sent to multiple API clients. "
|
||||
f"Client1 events: {len(client1_events)}, Client2 events: {len(client2_events)}"
|
||||
)
|
||||
|
||||
# Additionally, verify that not both clients received the same events
|
||||
# (events should be sent to only one client, not duplicated)
|
||||
if len(all_events) == expected_doorbell_events:
|
||||
# Events are not duplicated - this is the correct behavior
|
||||
print("✓ Events are properly deduplicated")
|
||||
else:
|
||||
# This is the bug - events are duplicated across clients
|
||||
pytest.fail(
|
||||
f"Events appear to be duplicated across API clients. "
|
||||
f"Expected {expected_doorbell_events} total events, got {len(all_events)}"
|
||||
)
|
||||
123
tests/integration/test_gpio_expander_cache.py
Normal file
123
tests/integration/test_gpio_expander_cache.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Integration test for CachedGPIOExpander to ensure correct behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gpio_expander_cache(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test gpio_expander::CachedGpioExpander correctly calls hardware functions."""
|
||||
# Get the path to the external components directory
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
|
||||
# Replace the placeholder in the YAML config with the actual path
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
logs_done = asyncio.Event()
|
||||
|
||||
# Patterns to match in logs
|
||||
digital_read_hw_pattern = re.compile(r"digital_read_hw pin=(\d+)")
|
||||
digital_read_cache_pattern = re.compile(r"digital_read_cache pin=(\d+)")
|
||||
|
||||
# ensure logs are in the expected order
|
||||
log_order = [
|
||||
(digital_read_hw_pattern, 0),
|
||||
[(digital_read_cache_pattern, i) for i in range(0, 8)],
|
||||
(digital_read_hw_pattern, 8),
|
||||
[(digital_read_cache_pattern, i) for i in range(8, 16)],
|
||||
(digital_read_hw_pattern, 16),
|
||||
[(digital_read_cache_pattern, i) for i in range(16, 24)],
|
||||
(digital_read_hw_pattern, 24),
|
||||
[(digital_read_cache_pattern, i) for i in range(24, 32)],
|
||||
(digital_read_hw_pattern, 3),
|
||||
(digital_read_cache_pattern, 3),
|
||||
(digital_read_hw_pattern, 3),
|
||||
(digital_read_cache_pattern, 3),
|
||||
(digital_read_cache_pattern, 4),
|
||||
(digital_read_hw_pattern, 3),
|
||||
(digital_read_cache_pattern, 3),
|
||||
(digital_read_hw_pattern, 10),
|
||||
(digital_read_cache_pattern, 10),
|
||||
# full cache reset here for testing
|
||||
(digital_read_hw_pattern, 15),
|
||||
(digital_read_cache_pattern, 15),
|
||||
(digital_read_cache_pattern, 14),
|
||||
(digital_read_hw_pattern, 14),
|
||||
(digital_read_cache_pattern, 14),
|
||||
]
|
||||
# Flatten the log order for easier processing
|
||||
log_order: list[tuple[re.Pattern, int]] = [
|
||||
item
|
||||
for sublist in log_order
|
||||
for item in (sublist if isinstance(sublist, list) else [sublist])
|
||||
]
|
||||
|
||||
index = 0
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
nonlocal index
|
||||
if logs_done.is_set():
|
||||
return
|
||||
|
||||
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||
|
||||
if "digital_read" in clean_line:
|
||||
if index >= len(log_order):
|
||||
print(f"Received unexpected log line: {clean_line}")
|
||||
logs_done.set()
|
||||
return
|
||||
|
||||
pattern, expected_pin = log_order[index]
|
||||
match = pattern.search(clean_line)
|
||||
|
||||
if not match:
|
||||
print(f"Log line did not match next expected pattern: {clean_line}")
|
||||
logs_done.set()
|
||||
return
|
||||
|
||||
pin = int(match.group(1))
|
||||
if pin != expected_pin:
|
||||
print(f"Unexpected pin number. Expected {expected_pin}, got {pin}")
|
||||
logs_done.set()
|
||||
return
|
||||
|
||||
index += 1
|
||||
|
||||
elif "DONE" in clean_line:
|
||||
# Check if we reached the end of the expected log entries
|
||||
logs_done.set()
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "gpio-expander-cache"
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(logs_done.wait(), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for logs to complete")
|
||||
|
||||
assert index == len(log_order), (
|
||||
f"Expected {len(log_order)} log entries, but got {index}"
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Integration test to verify that homeassistant.service calls are still sent to all clients.
|
||||
|
||||
This test ensures that our fix for duplicate events doesn't break the expected
|
||||
behavior for service calls, which should be sent to all connected clients.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import HomeassistantServiceCall
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_homeassistant_service_calls_to_all_clients(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that homeassistant.service calls are sent to all connected clients."""
|
||||
|
||||
# Track service calls received by each client
|
||||
client1_service_calls: list[HomeassistantServiceCall] = []
|
||||
client2_service_calls: list[HomeassistantServiceCall] = []
|
||||
|
||||
def on_service_call_client1(service_call: HomeassistantServiceCall) -> None:
|
||||
"""Track service calls received by first client."""
|
||||
if not service_call.is_event: # Only track service calls, not events
|
||||
client1_service_calls.append(service_call)
|
||||
|
||||
def on_service_call_client2(service_call: HomeassistantServiceCall) -> None:
|
||||
"""Track service calls received by second client."""
|
||||
if not service_call.is_event: # Only track service calls, not events
|
||||
client2_service_calls.append(service_call)
|
||||
|
||||
# Connect TWO API clients
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client1,
|
||||
api_client_connected() as client2,
|
||||
):
|
||||
# Subscribe both clients to service calls
|
||||
client1.subscribe_service_calls(on_service_call_client1)
|
||||
client2.subscribe_service_calls(on_service_call_client2)
|
||||
|
||||
# Get device info to ensure both clients are connected
|
||||
device_info1 = await client1.device_info()
|
||||
device_info2 = await client2.device_info()
|
||||
assert device_info1 is not None
|
||||
assert device_info2 is not None
|
||||
assert device_info1.name == device_info2.name
|
||||
|
||||
# List entities and services
|
||||
entities1, services1 = await client1.list_entities_services()
|
||||
|
||||
# Find the test service
|
||||
test_service = next(
|
||||
(s for s in services1 if s.name == "trigger_service_call_test"), None
|
||||
)
|
||||
assert test_service is not None, "trigger_service_call_test service not found"
|
||||
|
||||
# Clear any initial service calls
|
||||
client1_service_calls.clear()
|
||||
client2_service_calls.clear()
|
||||
|
||||
# Trigger the service call
|
||||
client1.execute_service(test_service, {})
|
||||
|
||||
# Wait for service calls to be processed
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Now check the results
|
||||
print(f"Client1 received {len(client1_service_calls)} service calls")
|
||||
print(f"Client2 received {len(client2_service_calls)} service calls")
|
||||
|
||||
# For service calls (not events), both clients should receive the call
|
||||
# This is the correct behavior for service calls
|
||||
assert len(client1_service_calls) > 0, (
|
||||
"Client1 should have received service calls"
|
||||
)
|
||||
assert len(client2_service_calls) > 0, (
|
||||
"Client2 should have received service calls"
|
||||
)
|
||||
|
||||
# Both clients should have received the same service calls
|
||||
assert len(client1_service_calls) == len(client2_service_calls), (
|
||||
f"Both clients should receive the same number of service calls. "
|
||||
f"Client1: {len(client1_service_calls)}, Client2: {len(client2_service_calls)}"
|
||||
)
|
||||
|
||||
# Verify the service call details
|
||||
for call1, call2 in zip(client1_service_calls, client2_service_calls):
|
||||
assert call1.service == call2.service, "Service names should match"
|
||||
assert not call1.is_event, "These should be service calls, not events"
|
||||
assert not call2.is_event, "These should be service calls, not events"
|
||||
@@ -4,7 +4,17 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import ClimateInfo, EntityState, SensorState
|
||||
from aioesphomeapi import (
|
||||
ClimateInfo,
|
||||
DateInfo,
|
||||
DateState,
|
||||
DateTimeInfo,
|
||||
DateTimeState,
|
||||
EntityState,
|
||||
SensorState,
|
||||
TimeInfo,
|
||||
TimeState,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
@@ -22,34 +32,56 @@ async def test_host_mode_many_entities(
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Subscribe to state changes
|
||||
states: dict[int, EntityState] = {}
|
||||
sensor_count_future: asyncio.Future[int] = loop.create_future()
|
||||
minimum_states_future: asyncio.Future[None] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
# Count sensor states specifically
|
||||
# Check if we have received minimum expected states
|
||||
sensor_states = [
|
||||
s
|
||||
for s in states.values()
|
||||
if isinstance(s, SensorState) and isinstance(s.state, float)
|
||||
]
|
||||
# When we have received states from at least 50 sensors, resolve the future
|
||||
if len(sensor_states) >= 50 and not sensor_count_future.done():
|
||||
sensor_count_future.set_result(len(sensor_states))
|
||||
date_states = [s for s in states.values() if isinstance(s, DateState)]
|
||||
time_states = [s for s in states.values() if isinstance(s, TimeState)]
|
||||
datetime_states = [
|
||||
s for s in states.values() if isinstance(s, DateTimeState)
|
||||
]
|
||||
|
||||
# We expect at least 50 sensors and 1 of each datetime entity type
|
||||
if (
|
||||
len(sensor_states) >= 50
|
||||
and len(date_states) >= 1
|
||||
and len(time_states) >= 1
|
||||
and len(datetime_states) >= 1
|
||||
and not minimum_states_future.done()
|
||||
):
|
||||
minimum_states_future.set_result(None)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Wait for states from at least 50 sensors with timeout
|
||||
# Wait for minimum states with timeout
|
||||
try:
|
||||
sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0)
|
||||
await asyncio.wait_for(minimum_states_future, timeout=10.0)
|
||||
except TimeoutError:
|
||||
sensor_states = [
|
||||
s
|
||||
for s in states.values()
|
||||
if isinstance(s, SensorState) and isinstance(s.state, float)
|
||||
]
|
||||
date_states = [s for s in states.values() if isinstance(s, DateState)]
|
||||
time_states = [s for s in states.values() if isinstance(s, TimeState)]
|
||||
datetime_states = [
|
||||
s for s in states.values() if isinstance(s, DateTimeState)
|
||||
]
|
||||
|
||||
pytest.fail(
|
||||
f"Did not receive states from at least 50 sensors within 10 seconds. "
|
||||
f"Received {len(sensor_states)} sensor states out of {len(states)} total states"
|
||||
f"Did not receive expected states within 10 seconds. "
|
||||
f"Received: {len(sensor_states)} sensor states (expected >=50), "
|
||||
f"{len(date_states)} date states (expected >=1), "
|
||||
f"{len(time_states)} time states (expected >=1), "
|
||||
f"{len(datetime_states)} datetime states (expected >=1). "
|
||||
f"Total states: {len(states)}"
|
||||
)
|
||||
|
||||
# Verify we received a good number of entity states
|
||||
@@ -64,13 +96,25 @@ async def test_host_mode_many_entities(
|
||||
if isinstance(s, SensorState) and isinstance(s.state, float)
|
||||
]
|
||||
|
||||
assert sensor_count >= 50, (
|
||||
f"Expected at least 50 sensor states, got {sensor_count}"
|
||||
)
|
||||
assert len(sensor_states) >= 50, (
|
||||
f"Expected at least 50 sensor states, got {len(sensor_states)}"
|
||||
)
|
||||
|
||||
# Verify we received datetime entity states
|
||||
date_states = [s for s in states.values() if isinstance(s, DateState)]
|
||||
time_states = [s for s in states.values() if isinstance(s, TimeState)]
|
||||
datetime_states = [s for s in states.values() if isinstance(s, DateTimeState)]
|
||||
|
||||
assert len(date_states) >= 1, (
|
||||
f"Expected at least 1 date state, got {len(date_states)}"
|
||||
)
|
||||
assert len(time_states) >= 1, (
|
||||
f"Expected at least 1 time state, got {len(time_states)}"
|
||||
)
|
||||
assert len(datetime_states) >= 1, (
|
||||
f"Expected at least 1 datetime state, got {len(datetime_states)}"
|
||||
)
|
||||
|
||||
# Get entity info to verify climate entity details
|
||||
entities = await client.list_entities_services()
|
||||
climate_infos = [e for e in entities[0] if isinstance(e, ClimateInfo)]
|
||||
@@ -89,3 +133,28 @@ async def test_host_mode_many_entities(
|
||||
assert "HOME" in preset_names, f"Expected 'HOME' preset, got {preset_names}"
|
||||
assert "AWAY" in preset_names, f"Expected 'AWAY' preset, got {preset_names}"
|
||||
assert "SLEEP" in preset_names, f"Expected 'SLEEP' preset, got {preset_names}"
|
||||
|
||||
# Verify datetime entities exist
|
||||
date_infos = [e for e in entities[0] if isinstance(e, DateInfo)]
|
||||
time_infos = [e for e in entities[0] if isinstance(e, TimeInfo)]
|
||||
datetime_infos = [e for e in entities[0] if isinstance(e, DateTimeInfo)]
|
||||
|
||||
assert len(date_infos) >= 1, "Expected at least 1 date entity"
|
||||
assert len(time_infos) >= 1, "Expected at least 1 time entity"
|
||||
assert len(datetime_infos) >= 1, "Expected at least 1 datetime entity"
|
||||
|
||||
# Verify the entity names
|
||||
date_info = date_infos[0]
|
||||
assert date_info.name == "Test Date", (
|
||||
f"Expected date entity name 'Test Date', got {date_info.name}"
|
||||
)
|
||||
|
||||
time_info = time_infos[0]
|
||||
assert time_info.name == "Test Time", (
|
||||
f"Expected time entity name 'Test Time', got {time_info.name}"
|
||||
)
|
||||
|
||||
datetime_info = datetime_infos[0]
|
||||
assert datetime_info.name == "Test DateTime", (
|
||||
f"Expected datetime entity name 'Test DateTime', got {datetime_info.name}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user