mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 10:42:52 +00:00
Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d530f5c56 | ||
|
|
5a78f636b9 | ||
|
|
53d504137b | ||
|
|
bb0546fc0b | ||
|
|
b43630cf8e | ||
|
|
c9664d4e42 | ||
|
|
39ea6a3220 | ||
|
|
f4a6d6f18b | ||
|
|
6c24d4aa43 | ||
|
|
2c4b1f3ed5 | ||
|
|
0df7b0b366 | ||
|
|
bd4b32873a | ||
|
|
9e580db434 | ||
|
|
87187c5391 | ||
|
|
60402614cd | ||
|
|
2727d35655 | ||
|
|
fb1ef29d54 | ||
|
|
7c0f63ea20 | ||
|
|
5d75828ceb | ||
|
|
bd2e8d184d | ||
|
|
3c7ce71d6f | ||
|
|
58e03a875f | ||
|
|
41a9fca881 | ||
|
|
ec58e864f5 | ||
|
|
375fbd829e | ||
|
|
4bae89bd4a | ||
|
|
897b2f16a9 | ||
|
|
93c8796ae8 | ||
|
|
654afb536e | ||
|
|
97154f9e5f | ||
|
|
c9e49b8e8f | ||
|
|
3ae52e21cf | ||
|
|
f2395f9001 | ||
|
|
3d248496ab | ||
|
|
34eca948c4 | ||
|
|
58815575ef | ||
|
|
28b012389d | ||
|
|
54f86537ad | ||
|
|
3284d93a33 | ||
|
|
01686a5f67 | ||
|
|
3b0c485701 | ||
|
|
e61d348f0c | ||
|
|
22e594bc62 | ||
|
|
061733672f | ||
|
|
b1f5da9561 | ||
|
|
db517059a3 | ||
|
|
0b40892775 | ||
|
|
8e6ed71e2a | ||
|
|
cf569702c4 | ||
|
|
2affc04e62 | ||
|
|
a648822027 | ||
|
|
c6244fa4b9 | ||
|
|
f6746b4977 | ||
|
|
f1f1a77151 | ||
|
|
ba1af627fd | ||
|
|
b82dfd7e64 | ||
|
|
7de2bcb584 | ||
|
|
d5d11e1077 | ||
|
|
ab66b20944 | ||
|
|
baf1682635 | ||
|
|
eea11834f8 | ||
|
|
1d86cc39e9 | ||
|
|
686252f151 | ||
|
|
f5c15f66a5 | ||
|
|
9d6b50320d | ||
|
|
b903c54be4 | ||
|
|
63b4a59588 | ||
|
|
adc6255fd4 | ||
|
|
b28e7a1357 | ||
|
|
e7ba9a5726 | ||
|
|
2586e78127 | ||
|
|
23f6dedb42 | ||
|
|
10f30ddb59 | ||
|
|
96a66f3ddd | ||
|
|
5bf8ceac66 | ||
|
|
4d42352ec5 | ||
|
|
479232b6c0 |
@@ -893,14 +893,20 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 IDF 1/3
|
||||
options: --environment esp32-idf-tidy --split-num 3 --split-at 1
|
||||
name: Run script/clang-tidy for ESP32 IDF 1/5
|
||||
options: --environment esp32-idf-tidy --split-num 5 --split-at 1
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 IDF 2/3
|
||||
options: --environment esp32-idf-tidy --split-num 3 --split-at 2
|
||||
name: Run script/clang-tidy for ESP32 IDF 2/5
|
||||
options: --environment esp32-idf-tidy --split-num 5 --split-at 2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 IDF 3/3
|
||||
options: --environment esp32-idf-tidy --split-num 3 --split-at 3
|
||||
name: Run script/clang-tidy for ESP32 IDF 3/5
|
||||
options: --environment esp32-idf-tidy --split-num 5 --split-at 3
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 IDF 4/5
|
||||
options: --environment esp32-idf-tidy --split-num 5 --split-at 4
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 IDF 5/5
|
||||
options: --environment esp32-idf-tidy --split-num 5 --split-at 5
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
|
||||
+1
-1
@@ -568,7 +568,7 @@ esphome/components/tca9555/* @mobrembski
|
||||
esphome/components/tcl112/* @glmnet
|
||||
esphome/components/tcp_uart/* @Bascht74
|
||||
esphome/components/tee501/* @Stock-M
|
||||
esphome/components/teleinfo/* @0hax
|
||||
esphome/components/teleinfo/* @0hax @FredM67
|
||||
esphome/components/tem3200/* @bakerkj
|
||||
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
esphome/components/template/datetime/* @rfdarter
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.10.0b1
|
||||
PROJECT_NUMBER = 2026.10.0b4
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.21.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+31
-5
@@ -7,6 +7,7 @@ import importlib
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@@ -54,7 +55,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
from esphome.enum import StrEnum
|
||||
from esphome.helpers import get_bool_env, indent, is_ip_address
|
||||
from esphome.helpers import IS_MACOS, get_bool_env, indent, is_ip_address
|
||||
from esphome.log import AnsiFore, color, setup_log
|
||||
from esphome.stacktrace import LogLineProcessor
|
||||
from esphome.types import ConfigType
|
||||
@@ -1311,9 +1312,9 @@ def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
|
||||
# platform's final-validate hook merges duplicates anyway.
|
||||
available: dict[str, None] = {}
|
||||
for ota_item in config.get(CONF_OTA, []):
|
||||
platform = ota_item.get(CONF_PLATFORM)
|
||||
if platform in (CONF_ESPHOME, CONF_WEB_SERVER):
|
||||
available[platform] = None
|
||||
ota_platform = ota_item.get(CONF_PLATFORM)
|
||||
if ota_platform in (CONF_ESPHOME, CONF_WEB_SERVER):
|
||||
available[ota_platform] = None
|
||||
|
||||
if not available:
|
||||
raise EsphomeError(
|
||||
@@ -1945,7 +1946,10 @@ def run_multiple_configs(
|
||||
safe_print()
|
||||
|
||||
cmd = command_builder(f)
|
||||
rc = run_external_process(*cmd)
|
||||
# The parent already logged the Intel macOS warning; children skip it.
|
||||
rc = run_external_process(
|
||||
*cmd, env={**os.environ, _INTEL_MACOS_WARNED_ENV: "1"}
|
||||
)
|
||||
|
||||
if rc == 0:
|
||||
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
|
||||
@@ -2614,6 +2618,27 @@ def _warn_if_source_tree_mismatch() -> None:
|
||||
)
|
||||
|
||||
|
||||
_INTEL_MACOS_REMOVAL = "2027.6.0"
|
||||
_INTEL_MACOS_WARNED_ENV = "ESPHOME_INTEL_MACOS_WARNED"
|
||||
|
||||
|
||||
def _warn_if_intel_macos() -> None:
|
||||
"""Warn that Intel (x86_64) Python on macOS loses support by _INTEL_MACOS_REMOVAL."""
|
||||
if (
|
||||
not IS_MACOS
|
||||
or platform.machine() != "x86_64"
|
||||
or _INTEL_MACOS_WARNED_ENV in os.environ
|
||||
):
|
||||
return
|
||||
_LOGGER.warning(
|
||||
"Support for Intel Macs will end in ESPHome %s or earlier. The Python "
|
||||
"packages ESPHome depends on have stopped publishing Intel macOS builds, "
|
||||
"so future releases will not install on this machine. On an Apple "
|
||||
"Silicon Mac, switch to a native arm64 Python.",
|
||||
_INTEL_MACOS_REMOVAL,
|
||||
)
|
||||
|
||||
|
||||
def run_esphome(argv):
|
||||
from esphome.address_cache import AddressCache
|
||||
|
||||
@@ -2633,6 +2658,7 @@ def run_esphome(argv):
|
||||
|
||||
setup_log(log_level=args.log_level)
|
||||
_warn_if_source_tree_mismatch()
|
||||
_warn_if_intel_macos()
|
||||
|
||||
if args.command in PRE_CONFIG_ACTIONS:
|
||||
try:
|
||||
|
||||
@@ -49,9 +49,9 @@ class FrameworkRelease(NamedTuple):
|
||||
# Arduino core version -> its build in esphome-libs/arduino-esp8266
|
||||
FRAMEWORK_RELEASES: dict[Version, FrameworkRelease] = {
|
||||
Version(3, 1, 2): FrameworkRelease(
|
||||
"3.1.2-esphome.1",
|
||||
"e80751e3123676b967143e39c61f2d8693946db4c7806f2a83dcaaf797ecd582",
|
||||
37189311,
|
||||
"3.1.2-esphome.3",
|
||||
"3af533a85163804e164500a5b254dc23b922080e25ecb7dc37b3dd8ac84651db",
|
||||
37185650,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+6
-51
@@ -672,52 +672,20 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
|
||||
|
||||
|
||||
@register_condition("and", AndCondition, validate_condition_list)
|
||||
async def and_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("or", OrCondition, validate_condition_list)
|
||||
async def or_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("all", AndCondition, validate_condition_list)
|
||||
async def all_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("or", OrCondition, validate_condition_list)
|
||||
@register_condition("any", OrCondition, validate_condition_list)
|
||||
async def any_condition_to_code(
|
||||
@register_condition("xor", XorCondition, validate_condition_list)
|
||||
async def condition_group_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
# A group of one is that condition for and, or and xor; skip the wrapper.
|
||||
if len(conditions) == 1:
|
||||
return conditions[0]
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
@@ -734,19 +702,6 @@ async def not_condition_to_code(
|
||||
return cg.new_Pvariable(condition_id, template_arg, condition)
|
||||
|
||||
|
||||
@register_condition("xor", XorCondition, validate_condition_list)
|
||||
async def xor_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
|
||||
async def lambda_condition_to_code(
|
||||
config: ConfigType,
|
||||
|
||||
@@ -54,6 +54,7 @@ from esphome.components.esp8266.const import (
|
||||
KEY_ESP8266,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_SCANF_FLOAT,
|
||||
THROW_STUBS_HEADER,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.framework_helpers import get_project_cxx_compile_flags
|
||||
@@ -1056,7 +1057,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
cxx = (toolchain_tool(paths.toolchain, "g++"),)
|
||||
lines = [
|
||||
*tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache),
|
||||
*compile_rule_lines(),
|
||||
*compile_rule_lines(ccache),
|
||||
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
|
||||
*pch_rule_lines(),
|
||||
"rule link",
|
||||
@@ -1087,32 +1088,8 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
if "USE_ESP8266_WAVEFORM_STUBS" in flag_defines:
|
||||
core_exclude |= _CORE_EXCLUDE_WAVEFORM
|
||||
|
||||
archives = []
|
||||
# variant_dir existence was already enforced with the include dirs
|
||||
variant_sources = collect_sources(variant_dir)
|
||||
if variant_sources:
|
||||
objs = compile_edges(lines, variant_sources, variant_dir, "variant")
|
||||
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
|
||||
archives.append("libFrameworkArduinoVariant.a")
|
||||
|
||||
core_objs = compile_edges(
|
||||
lines, collect_sources(core_dir, core_exclude), core_dir, "core"
|
||||
)
|
||||
if not core_objs:
|
||||
# An empty archive would link into a wall of undefined references
|
||||
# (app_entry, the exception vectors) far from the cause
|
||||
raise EsphomeError(
|
||||
f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}"
|
||||
)
|
||||
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
|
||||
archives.append("libFrameworkArduino.a")
|
||||
|
||||
lib_archives, direct_objs = library_edges(lines, libraries)
|
||||
archives += lib_archives
|
||||
|
||||
# One source of truth with the PlatformIO path: esp8266/__init__ pins
|
||||
# build_src_flags (the throw_stubs force-include); -include paths
|
||||
# resolve against the source root
|
||||
# esp8266/__init__ pins build_src_flags (the throw_stubs force-include), as on
|
||||
# the PlatformIO path; -include paths resolve against the source root
|
||||
src_other: list[str] = []
|
||||
src_includes: list[str] = []
|
||||
src_it = iter(
|
||||
@@ -1131,7 +1108,45 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
src_includes.append(tok[len("-include") :])
|
||||
else:
|
||||
src_other.append(_shell_token(tok))
|
||||
# The throw stubs abort: none anywhere with exceptions on, else the core and
|
||||
# libraries get them too (src keeps them after its pch include)
|
||||
if config.exceptions:
|
||||
src_includes = [h for h in src_includes if h != THROW_STUBS_HEADER]
|
||||
include_flags = [f"-include {_q(src_dir / h)}" for h in src_includes]
|
||||
framework_flags = ""
|
||||
if THROW_STUBS_HEADER in src_includes:
|
||||
stubs_flag = include_flags[src_includes.index(THROW_STUBS_HEADER)]
|
||||
lines.append(f"frameworkflags = {stubs_flag}")
|
||||
framework_flags = "$frameworkflags"
|
||||
archives = []
|
||||
# variant_dir existence was already enforced with the include dirs
|
||||
variant_sources = collect_sources(variant_dir)
|
||||
if variant_sources:
|
||||
objs = compile_edges(
|
||||
lines, variant_sources, variant_dir, "variant", flags=framework_flags
|
||||
)
|
||||
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
|
||||
archives.append("libFrameworkArduinoVariant.a")
|
||||
|
||||
core_objs = compile_edges(
|
||||
lines,
|
||||
collect_sources(core_dir, core_exclude),
|
||||
core_dir,
|
||||
"core",
|
||||
flags=framework_flags,
|
||||
)
|
||||
if not core_objs:
|
||||
# An empty archive would link into a wall of undefined references
|
||||
# (app_entry, the exception vectors) far from the cause
|
||||
raise EsphomeError(
|
||||
f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}"
|
||||
)
|
||||
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
|
||||
archives.append("libFrameworkArduino.a")
|
||||
|
||||
lib_archives, direct_objs = library_edges(lines, libraries, framework_flags)
|
||||
archives += lib_archives
|
||||
|
||||
# One shared variable instead of repeating the flags line on every src
|
||||
# edge (hundreds of edges in a real project)
|
||||
lines.append(f"srcflags = {' '.join(src_other + include_flags)}")
|
||||
|
||||
@@ -240,7 +240,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
|
||||
|
||||
lines = [
|
||||
*tool_lines(compilers.cc, compilers.cxx, ccache),
|
||||
*compile_rule_lines(),
|
||||
*compile_rule_lines(ccache),
|
||||
*pch_rule_lines(),
|
||||
"rule link",
|
||||
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
|
||||
|
||||
@@ -74,33 +74,40 @@ def tool_lines(
|
||||
# path gets
|
||||
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
|
||||
f"buildtool = {_q(_BUILD_TOOL)}",
|
||||
f"ccache = {_q(ccache) if ccache else ''}",
|
||||
*([f"ccache = {_q(ccache)}"] if ccache else []),
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def compile_rule_lines() -> list[str]:
|
||||
"""The compile rules; ``$own_includes`` is empty unless an edge sets it."""
|
||||
def compile_rule_lines(ccache: str | None) -> list[str]:
|
||||
"""The compile rules; ``$own_includes`` is empty unless an edge sets it.
|
||||
|
||||
``ccache`` is the launcher ``tool_lines`` was given; the same test here
|
||||
keeps the variable and the rules in step. It is spliced in rather than
|
||||
left as an empty variable: a leading space on the command line makes
|
||||
CreateProcess fail on Windows.
|
||||
"""
|
||||
launcher = "$ccache " if ccache else ""
|
||||
return [
|
||||
"rule c",
|
||||
" command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
|
||||
f" command = {launcher}$cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CC $out",
|
||||
"rule cxx",
|
||||
" command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
|
||||
f" command = {launcher}$cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CXX $out",
|
||||
"rule aspp",
|
||||
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
|
||||
f" command = {launcher}$cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = AS $out",
|
||||
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
|
||||
# depfile and no $flags (defines/includes) either
|
||||
"rule asm",
|
||||
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
|
||||
f" command = {launcher}$cc -x assembler $asflags -c $in -o $out",
|
||||
" description = AS $out",
|
||||
]
|
||||
|
||||
@@ -207,10 +214,12 @@ def compile_edges(
|
||||
|
||||
|
||||
def library_edges(
|
||||
lines: list[str], libraries: list[ArduinoLibrary]
|
||||
lines: list[str], libraries: list[ArduinoLibrary], extra_flags: str = ""
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Emit every library's compile and archive edges.
|
||||
|
||||
``extra_flags`` (raw ninja text) precede each library's own flags.
|
||||
|
||||
Returns the archive names and the objects that link directly. A
|
||||
library's own include dirs lead its compile lines, as PlatformIO searched
|
||||
them first: the include path is one global list, so another library's
|
||||
@@ -232,7 +241,7 @@ def library_edges(
|
||||
lib.sources,
|
||||
common_parent(lib.sources),
|
||||
f"lib/{lib.name}",
|
||||
flags=" ".join(_shell_token(f) for f in lib.flags),
|
||||
flags=" ".join(filter(None, [extra_flags, *map(_shell_token, lib.flags)])),
|
||||
own_includes=" ".join(f"-I{_q(d)}" for d in lib.include_dirs),
|
||||
)
|
||||
if not lib.lib_archive:
|
||||
|
||||
@@ -81,6 +81,8 @@ service APIConnection {
|
||||
rpc subscribe_serial_proxy_identity(SubscribeSerialProxyIdentityRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||
|
||||
rpc sendspin_pairing_token(SendspinPairingTokenRequest) returns (SendspinPairingTokenResponse) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -398,6 +400,11 @@ message WizardCapabilities {
|
||||
bool configured = 1;
|
||||
}
|
||||
|
||||
message SendspinCapabilities {
|
||||
// Bitmask of the Sendspin features this device supports
|
||||
uint32 feature_flags = 1;
|
||||
}
|
||||
|
||||
message DeviceCapabilitiesResponse {
|
||||
option (id) = 150;
|
||||
option (source) = SOURCE_SERVER;
|
||||
@@ -408,6 +415,7 @@ message DeviceCapabilitiesResponse {
|
||||
repeated SerialProxyInfo serial_proxies = 4
|
||||
[(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
|
||||
WizardCapabilities wizard = 5 [(field_ifdef) = "USE_API_WIZARD"];
|
||||
SendspinCapabilities sendspin = 6 [(field_ifdef) = "USE_SENDSPIN"];
|
||||
}
|
||||
|
||||
// ==================== DEVICE WIZARD ====================
|
||||
@@ -436,7 +444,7 @@ message DeviceWizardRequest {
|
||||
// {"version": 1,
|
||||
// "pages": [{"title": "...", "description": "...",
|
||||
// "entities": [{"key": 123, "device_id": 456, "description": "..."}],
|
||||
// "inputs": [{"key": 789, "description": "...",
|
||||
// "inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
|
||||
// "entity_filters": [{"integration": "...", "domain": ["..."],
|
||||
// "device_class": ["..."], "supported_features": ["..."]}]}]}]}
|
||||
//
|
||||
@@ -460,6 +468,14 @@ message DeviceWizardRequest {
|
||||
// an entity matches a filter when it satisfies every field that is set, and an entity
|
||||
// that matches any one of the filters is accepted.
|
||||
//
|
||||
// An input with "multiple": true lets the user pick several entities. The client sends
|
||||
// them in one WizardInputSetRequest as a comma separated list with no spaces
|
||||
// ("calendar.a,calendar.b"); the device also accepts spaces around each id and stores the
|
||||
// list without them. "max_length" (only on a multiple input, 255 when left out) is the
|
||||
// longest list in bytes the device accepts. The version stays 1: a client that does not
|
||||
// know "multiple" ignores it, shows a single picker and sends one entity id, which a
|
||||
// multiple input accepts.
|
||||
//
|
||||
// The device keeps the entity id of an input in RAM only and does not store it across
|
||||
// restarts. The client therefore stores the choice itself and, after every connection
|
||||
// and before it sends SubscribeHomeAssistantStatesRequest, sends a WizardInputSetRequest
|
||||
@@ -468,17 +484,20 @@ message DeviceWizardResponse {
|
||||
option (id) = 157;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_WIZARD";
|
||||
// Not logged: the data is in flash, which ESP8266 cannot read for a dump
|
||||
option (log) = false;
|
||||
|
||||
// The data is in flash, and the device sends it from there
|
||||
bytes data = 1 [(pointer_to_buffer) = true];
|
||||
bytes data = 1 [(pointer_to_buffer) = true, (progmem) = true];
|
||||
}
|
||||
|
||||
// Sets the entity id of a wizard input (see WizardInputField.key). Clients send
|
||||
// this only when DeviceCapabilitiesResponse.wizard.configured is set. There is no
|
||||
// reply. The device ignores a request whose key matches no input or whose entity_id
|
||||
// is empty, longer than 255 bytes or has no '.'. Otherwise it keeps the id in RAM,
|
||||
// reply. The device ignores a request whose key matches no input. A single input is
|
||||
// ignored when the entity_id is empty, longer than 255 bytes or has no '.'. A multiple
|
||||
// input takes a comma separated list ("calendar.a,calendar.b"); the client sends no
|
||||
// spaces, the device trims spaces around each id and stores the list without them. The
|
||||
// whole request is ignored when any id is empty or has no '.', or when the list without
|
||||
// spaces does not fit the input's max_length (255 by default). Otherwise it keeps the id
|
||||
// or list in RAM,
|
||||
// not across restarts, and when the input belongs to a homeassistant platform entity
|
||||
// it subscribes to that entity's state. Clients send this for every input right
|
||||
// after connecting, before SubscribeHomeAssistantStatesRequest. A request that
|
||||
@@ -490,7 +509,7 @@ message WizardInputSetRequest {
|
||||
option (ifdef) = "USE_API_WIZARD_INPUTS";
|
||||
|
||||
fixed32 key = 1;
|
||||
string entity_id = 2 [(max_data_length) = 255];
|
||||
string entity_id = 2 [(max_data_length) = 4095];
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
@@ -3071,3 +3090,34 @@ message BluetoothSetConnectionParamsResponse {
|
||||
uint64 address = 1;
|
||||
int32 error = 2;
|
||||
}
|
||||
|
||||
// ==================== SENDSPIN ====================
|
||||
enum SendspinPairingTokenStatus {
|
||||
// The Sendspin client has not started yet, so the token does not exist yet. Ask again later.
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY = 0;
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_OK = 1;
|
||||
// The device has no API encryption key, so it will not send the token. Ask again once a key is set.
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED = 2;
|
||||
// Sendspin is turned off and has not started since boot, so the token does not exist yet.
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED = 3;
|
||||
// The Sendspin client failed to start, and will not start until the device reboots.
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_FAILED = 4;
|
||||
}
|
||||
|
||||
// Requests the Sendspin pairing token, which a Sendspin server uses to pair with this device
|
||||
message SendspinPairingTokenRequest {
|
||||
option (id) = 159;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SENDSPIN";
|
||||
// Empty
|
||||
}
|
||||
|
||||
message SendspinPairingTokenResponse {
|
||||
option (id) = 160;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SENDSPIN";
|
||||
|
||||
SendspinPairingTokenStatus status = 1;
|
||||
// The pairing token (Sendspin pairing.md "Pairing Token"). Set only when status is OK.
|
||||
string token = 2 [(max_data_length) = 127];
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
#include "esphome/components/zwave_proxy/zwave_proxy.h"
|
||||
#endif
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
@@ -1402,6 +1405,40 @@ void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
bool APIConnection::send_sendspin_pairing_token_response_() {
|
||||
SendspinPairingTokenResponse resp;
|
||||
#ifdef USE_API_NOISE
|
||||
// has_psk() stands in for "this session proved the key": with a key active, plaintext and zero-key sessions
|
||||
// never reach hello, and activating a key closes every session opened before it.
|
||||
if (!this->parent_->get_noise_ctx().has_psk()) {
|
||||
resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED;
|
||||
return this->send_message(resp);
|
||||
}
|
||||
auto token = sendspin_::global_sendspin_hub->get_pairing_token();
|
||||
if (token.has_value()) {
|
||||
resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_OK;
|
||||
resp.token = StringRef(*token);
|
||||
} else if (sendspin_::global_sendspin_hub->status_has_error()) {
|
||||
resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_FAILED;
|
||||
} else if (sendspin_::global_sendspin_hub->is_disabled()) {
|
||||
resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED;
|
||||
} else {
|
||||
resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY;
|
||||
}
|
||||
#else
|
||||
resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED;
|
||||
#endif
|
||||
return this->send_message(resp);
|
||||
}
|
||||
|
||||
void APIConnection::on_sendspin_pairing_token_request() {
|
||||
if (!this->send_sendspin_pairing_token_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
|
||||
@@ -1872,7 +1909,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 18;
|
||||
resp.api_version_minor = 19;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
@@ -2088,6 +2125,9 @@ bool APIConnection::send_device_capabilities_response_() {
|
||||
#endif
|
||||
#ifdef USE_API_WIZARD
|
||||
resp.wizard.configured = true;
|
||||
#endif
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
|
||||
#endif
|
||||
return this->send_message(resp);
|
||||
}
|
||||
|
||||
@@ -223,6 +223,10 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
void on_sendspin_pairing_token_request();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg);
|
||||
@@ -423,9 +427,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
bool send_ping_response_();
|
||||
bool send_device_info_response_();
|
||||
bool send_device_capabilities_response_();
|
||||
#ifdef USE_API_WIZARD
|
||||
bool send_device_wizard_response_();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg);
|
||||
#endif
|
||||
@@ -435,6 +436,9 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg);
|
||||
#endif
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
bool send_sendspin_pairing_token_response_();
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void try_send_camera_image_();
|
||||
|
||||
@@ -122,4 +122,9 @@ extend google.protobuf.FieldOptions {
|
||||
// to true when the field arrives, so an all-default submessage can be told apart
|
||||
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
|
||||
optional bool track_presence = 50020 [default=false];
|
||||
|
||||
// progmem: The pointer_to_buffer bytes field points into flash (PROGMEM).
|
||||
// The generated encode and dump copy it with progmem_memcpy, as ESP8266
|
||||
// cannot read flash with ordinary loads.
|
||||
optional bool progmem = 50021 [default=false];
|
||||
}
|
||||
|
||||
@@ -306,6 +306,19 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.feature_flags);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const DeviceCapabilitiesResponse *>(self);
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
@@ -324,6 +337,9 @@ uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__res
|
||||
#endif
|
||||
#ifdef USE_API_WIZARD
|
||||
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.wizard);
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.sendspin);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -346,13 +362,16 @@ uint32_t DeviceCapabilitiesResponse::calc_size_msg(const void *self) {
|
||||
#endif
|
||||
#ifdef USE_API_WIZARD
|
||||
size += ProtoSize::calc_message(1, msg.wizard.calculate_size());
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
size += ProtoSize::calc_message(1, msg.sendspin.calculate_size());
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_API_WIZARD
|
||||
uint8_t *DeviceWizardResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const DeviceWizardResponse *>(self);
|
||||
pos = ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.data, msg.data_len);
|
||||
pos = ProtoEncode::encode_progmem_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.data, msg.data_len);
|
||||
return pos;
|
||||
}
|
||||
uint32_t DeviceWizardResponse::calc_size_msg(const void *self) {
|
||||
@@ -4229,6 +4248,22 @@ uint32_t BluetoothSetConnectionParamsResponse::calc_size_msg(const void *self) {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
uint8_t *SendspinPairingTokenResponse::encode_msg(const void *self,
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const SendspinPairingTokenResponse *>(self);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(msg.status));
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.token);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SendspinPairingTokenResponse::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const SendspinPairingTokenResponse *>(self);
|
||||
uint32_t size = 0;
|
||||
size += msg.status ? 2 : 0;
|
||||
size += !msg.token.empty() ? 2 + msg.token.size() : 0;
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifndef HAS_PROTO_MESSAGE_DUMP
|
||||
static_assert(!std::is_polymorphic_v<HelloRequest>, "decodable messages carry no vtable");
|
||||
static_assert(!std::is_polymorphic_v<DisconnectRequest>, "decodable messages carry no vtable");
|
||||
|
||||
@@ -383,6 +383,15 @@ enum SerialProxyIdentityFlag : uint32_t {
|
||||
SERIAL_PROXY_IDENTITY_FLAG_CONNECTED = 1,
|
||||
SERIAL_PROXY_IDENTITY_FLAG_ERROR = 2,
|
||||
};
|
||||
#ifdef USE_SENDSPIN
|
||||
enum SendspinPairingTokenStatus : uint32_t {
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY = 0,
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_OK = 1,
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED = 2,
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED = 3,
|
||||
SENDSPIN_PAIRING_TOKEN_STATUS_FAILED = 4,
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace enums
|
||||
|
||||
@@ -737,10 +746,27 @@ class WizardCapabilities final : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
class SendspinCapabilities final : public ProtoMessage {
|
||||
public:
|
||||
uint32_t feature_flags{0};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
}
|
||||
static uint32_t calc_size_msg(const void *self);
|
||||
uint32_t calculate_size() const { return calc_size_msg(this); }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
class DeviceCapabilitiesResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 150;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 119;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 136;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("device_capabilities_response"); }
|
||||
#endif
|
||||
@@ -758,6 +784,9 @@ class DeviceCapabilitiesResponse final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_API_WIZARD
|
||||
WizardCapabilities wizard{};
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
SendspinCapabilities sendspin{};
|
||||
#endif
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -4138,5 +4167,28 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
class SendspinPairingTokenResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 160;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 11;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("sendspin_pairing_token_response"); }
|
||||
#endif
|
||||
enums::SendspinPairingTokenStatus status{};
|
||||
StringRef token{nullptr, 0}; // null until set, encode only
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
}
|
||||
static uint32_t calc_size_msg(const void *self);
|
||||
uint32_t calculate_size() const { return calc_size_msg(this); }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -114,15 +114,26 @@ template<typename T> static void dump_field(DumpBuffer &out, const char *field_n
|
||||
out.append("\n");
|
||||
}
|
||||
|
||||
// Bytes shown by a bytes field dump: 160 bytes is 480 chars with separators, to fit a typical log buffer
|
||||
static constexpr size_t DUMP_BYTES_MAX = 160;
|
||||
|
||||
// Helper for bytes fields - uses stack buffer to avoid heap allocation
|
||||
// Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer
|
||||
// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms)
|
||||
static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) {
|
||||
char hex_buf[format_hex_pretty_size(160)];
|
||||
char hex_buf[format_hex_pretty_size(DUMP_BYTES_MAX)];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
format_hex_pretty_to(hex_buf, data, len);
|
||||
out.append(hex_buf).append("\n");
|
||||
}
|
||||
|
||||
// Helper for bytes fields in flash: copies the shown bytes out with progmem_memcpy first
|
||||
static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len,
|
||||
int indent = 2) {
|
||||
uint8_t data_buf[DUMP_BYTES_MAX];
|
||||
len = std::min(len, sizeof(data_buf));
|
||||
progmem_memcpy(data_buf, data, len);
|
||||
dump_bytes_field(out, field_name, data_buf, len, indent);
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
|
||||
@@ -917,6 +928,25 @@ template<> const char *proto_enum_to_string<enums::SerialProxyIdentityFlag>(enum
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#ifdef USE_SENDSPIN
|
||||
template<>
|
||||
const char *proto_enum_to_string<enums::SendspinPairingTokenStatus>(enums::SendspinPairingTokenStatus value) {
|
||||
switch (value) {
|
||||
case enums::SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY:
|
||||
return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY");
|
||||
case enums::SENDSPIN_PAIRING_TOKEN_STATUS_OK:
|
||||
return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_OK");
|
||||
case enums::SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED:
|
||||
return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED");
|
||||
case enums::SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED:
|
||||
return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED");
|
||||
case enums::SENDSPIN_PAIRING_TOKEN_STATUS_FAILED:
|
||||
return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_FAILED");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("HelloRequest"));
|
||||
@@ -1085,6 +1115,13 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
|
||||
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceCapabilitiesResponse"));
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
@@ -1113,13 +1150,18 @@ const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
|
||||
out.append(2, ' ').append_p(ESPHOME_PSTR("wizard")).append(": ");
|
||||
this->wizard.dump_to(out);
|
||||
out.append("\n");
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
out.append(2, ' ').append_p(ESPHOME_PSTR("sendspin")).append(": ");
|
||||
this->sendspin.dump_to(out);
|
||||
out.append("\n");
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
#ifdef USE_API_WIZARD
|
||||
const char *DeviceWizardResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceWizardResponse"));
|
||||
dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
|
||||
dump_progmem_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
@@ -2933,6 +2975,14 @@ const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
const char *SendspinPairingTokenResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinPairingTokenResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SendspinPairingTokenStatus>(this->status));
|
||||
dump_field(out, ESPHOME_PSTR("token"), this->token);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
|
||||
@@ -751,6 +751,15 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
this->on_wizard_input_set_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENDSPIN
|
||||
case 159 /* SendspinPairingTokenRequest is empty */: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_sendspin_pairing_token_request"));
|
||||
#endif
|
||||
this->on_sendspin_pairing_token_request();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -252,6 +252,10 @@ class APIServerConnectionBase {
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENDSPIN
|
||||
void on_sendspin_pairing_token_request(){};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -13,53 +13,78 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.wizard";
|
||||
|
||||
uint8_t *wizard_encode_response(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const DeviceWizardResponse *>(self);
|
||||
if (msg.data_len == 0)
|
||||
return pos;
|
||||
pos = ProtoEncode::encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, 1, 2); // type 2: Length-delimited
|
||||
pos = ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, msg.data_len);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, msg.data_len);
|
||||
progmem_memcpy(pos, msg.data, msg.data_len);
|
||||
return pos + msg.data_len;
|
||||
}
|
||||
|
||||
#ifdef USE_API_WIZARD_INPUTS
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
// Walks the comma separated entity ids in value, without the spaces around each. Returns the length they take joined
|
||||
// by ',', or 0 when one is empty or has no '.'. Writes the joined list to out unless it is nullptr.
|
||||
static size_t wizard_join_entity_ids(const char *value, size_t length, char *out) {
|
||||
size_t joined = 0;
|
||||
const char *end = value + length;
|
||||
while (true) {
|
||||
const char *comma = static_cast<const char *>(memchr(value, ',', end - value));
|
||||
const char *id_end = comma != nullptr ? comma : end;
|
||||
while (value < id_end && *value == ' ')
|
||||
value++;
|
||||
while (id_end > value && id_end[-1] == ' ')
|
||||
id_end--;
|
||||
size_t id_length = id_end - value;
|
||||
if (id_length == 0 || memchr(value, '.', id_length) == nullptr)
|
||||
return 0;
|
||||
if (joined != 0) {
|
||||
if (out != nullptr)
|
||||
out[joined] = ',';
|
||||
joined++;
|
||||
}
|
||||
if (out != nullptr)
|
||||
memcpy(out + joined, value, id_length);
|
||||
joined += id_length;
|
||||
if (comma == nullptr)
|
||||
return joined;
|
||||
value = comma + 1;
|
||||
}
|
||||
}
|
||||
#endif // USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
|
||||
static bool wizard_entity_id_valid(const char *entity_id, size_t length) {
|
||||
return length > 0 && length < WIZARD_ENTITY_ID_BUFFER_SIZE && memchr(entity_id, '.', length) != nullptr;
|
||||
}
|
||||
|
||||
const char *wizard_set_input(const WizardInputSetRequest &msg) {
|
||||
if (!wizard_entity_id_valid(msg.entity_id.c_str(), msg.entity_id.size())) {
|
||||
ESP_LOGW(TAG, "Ignoring an invalid entity id for wizard input");
|
||||
return nullptr;
|
||||
}
|
||||
const char *value = msg.entity_id.c_str();
|
||||
const size_t length = msg.entity_id.size();
|
||||
for (size_t i = 0; i < API_WIZARD_INPUT_COUNT; i++) {
|
||||
// The table is in flash, which ESP8266 can only read through progmem_memcpy
|
||||
WizardInputEntry entry;
|
||||
progmem_memcpy(&entry, &API_WIZARD_INPUTS[i], sizeof(entry));
|
||||
if (entry.key != msg.key)
|
||||
continue;
|
||||
memcpy(entry.entity_id, msg.entity_id.c_str(), msg.entity_id.size());
|
||||
entry.entity_id[msg.entity_id.size()] = '\0';
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
if (entry.size != 0) {
|
||||
// Two passes, validate and measure then write, so a rejected request leaves the buffer unchanged
|
||||
const size_t joined = wizard_join_entity_ids(value, length, nullptr);
|
||||
if (joined == 0 || joined >= entry.size)
|
||||
break;
|
||||
wizard_join_entity_ids(value, length, entry.entity_id);
|
||||
entry.entity_id[joined] = '\0';
|
||||
return entry.entity_id;
|
||||
}
|
||||
#endif
|
||||
if (!wizard_entity_id_valid(value, length))
|
||||
break;
|
||||
memcpy(entry.entity_id, value, length);
|
||||
entry.entity_id[length] = '\0';
|
||||
return entry.entity_id;
|
||||
}
|
||||
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input");
|
||||
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input, or one that is not valid");
|
||||
return nullptr;
|
||||
}
|
||||
#endif // USE_API_WIZARD_INPUTS
|
||||
|
||||
bool APIConnection::send_device_wizard_response_() {
|
||||
void APIConnection::on_device_wizard_request() {
|
||||
DeviceWizardResponse resp;
|
||||
resp.data = API_WIZARD_DATA;
|
||||
resp.data_len = API_WIZARD_DATA_SIZE;
|
||||
// Not send_message: the data is in flash, so wizard_encode_response copies it out
|
||||
return this->send_message_(DeviceWizardResponse::calc_size_msg(&resp), DeviceWizardResponse::MESSAGE_TYPE,
|
||||
&wizard_encode_response, &resp);
|
||||
}
|
||||
|
||||
void APIConnection::on_device_wizard_request() {
|
||||
if (!this->send_device_wizard_response_()) {
|
||||
if (!this->send_message(resp)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "proto.h"
|
||||
@@ -22,15 +25,14 @@ static constexpr size_t WIZARD_ENTITY_ID_BUFFER_SIZE = 256;
|
||||
/// (components/api/wizard.py) and kept in flash. API_WIZARD_DATA_SIZE bytes long.
|
||||
extern const uint8_t API_WIZARD_DATA[] PROGMEM;
|
||||
|
||||
/// Encodes a DeviceWizardResponse like the generated encoder would. The data is in flash, which ESP8266 can only read
|
||||
/// with progmem_memcpy, so the generated encoder (a plain memcpy) cannot be used. Plain memcpy elsewhere.
|
||||
uint8_t *wizard_encode_response(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
|
||||
#ifdef USE_API_WIZARD_INPUTS
|
||||
/// Where the entity id of an input is kept, found by the key the client uses for it.
|
||||
struct WizardInputEntry {
|
||||
uint32_t key; // FNV-1 hash of the ESPHome id of the input
|
||||
char *entity_id; // RAM buffer of WIZARD_ENTITY_ID_BUFFER_SIZE bytes, shared with the homeassistant entity
|
||||
char *entity_id; // RAM buffer, WIZARD_ENTITY_ID_BUFFER_SIZE bytes or size, shared with the homeassistant entity
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
uint16_t size; // Buffer size of a multiple input, 0 for a single input
|
||||
#endif
|
||||
};
|
||||
|
||||
/// The inputs of the wizard, in flash. API_WIZARD_INPUT_COUNT entries long.
|
||||
@@ -48,9 +50,34 @@ const char *wizard_set_input(const WizardInputSetRequest &msg);
|
||||
class WizardInput {
|
||||
public:
|
||||
explicit WizardInput(const char *entity_id) : entity_id_(entity_id) {}
|
||||
/// The Home Assistant entity ID, empty until the wizard sets one.
|
||||
/// The Home Assistant entity ID, empty until the wizard sets one. For a multiple input this is the whole
|
||||
/// comma separated list, which homeassistant.action data can use as it is.
|
||||
StringRef entity_id() const { return StringRef(this->entity_id_); }
|
||||
bool has_entity_id() const { return this->entity_id_[0] != '\0'; }
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
/// The number of entity IDs in the list, 0 until the wizard sets one.
|
||||
size_t entity_id_count() const {
|
||||
if (!this->has_entity_id())
|
||||
return 0;
|
||||
size_t count = 1;
|
||||
for (const char *p = this->entity_id_; (p = strchr(p, ',')) != nullptr; p++)
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
/// Call callback(StringRef) for each entity ID in the list, in order. The StringRef points into the list and is
|
||||
/// not null terminated, so print it with "%.*s".
|
||||
template<typename F> void for_each_entity_id(F &&callback) const {
|
||||
// The device stores the list without spaces or empty entries
|
||||
const char *start = this->entity_id_;
|
||||
while (*start != '\0') {
|
||||
const char *end = strchr(start, ',');
|
||||
if (end == nullptr)
|
||||
end = start + strlen(start);
|
||||
callback(StringRef(start, end - start));
|
||||
start = *end == ',' ? end + 1 : end;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
const char *entity_id_;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "api_pb2_defines.h"
|
||||
#include "api_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
@@ -465,6 +466,21 @@ class ProtoEncode {
|
||||
uint32_t field_id, const uint8_t *data, size_t len) {
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
/// encode_bytes for data in flash (PROGMEM), which ESP8266 can only read with progmem_memcpy.
|
||||
[[nodiscard]] static inline uint8_t *encode_progmem_bytes(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const uint8_t *data, size_t len) {
|
||||
if (len == 0)
|
||||
return pos;
|
||||
return encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, data, len);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_progmem_bytes_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const uint8_t *data, size_t len) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited
|
||||
pos = encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
progmem_memcpy(pos, data, len);
|
||||
return pos + len;
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
|
||||
@@ -6,7 +6,6 @@ Home Assistant shows the wizard when the device is added. See api_wizard.h for t
|
||||
from collections.abc import Callable
|
||||
import importlib
|
||||
import json
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
@@ -20,6 +19,8 @@ from esphome.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_MAX_LENGTH,
|
||||
CONF_MULTIPLE,
|
||||
CONF_NAME,
|
||||
CONF_PAGES,
|
||||
CONF_PLATFORM,
|
||||
@@ -27,7 +28,12 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import fnv1_hash, fnv1_hash_object_id, fnv1a_32bit_hash
|
||||
from esphome.helpers import (
|
||||
fnv1_hash,
|
||||
fnv1_hash_object_id,
|
||||
fnv1a_32bit_hash,
|
||||
zstd_module,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
API_DOMAIN = "api"
|
||||
@@ -46,6 +52,10 @@ WizardInput = _API.class_("WizardInput")
|
||||
WIZARD_ENTITY_ID_BUFFER_SIZE = (
|
||||
256 # api_wizard.h; Home Assistant entity IDs are at most 255 bytes
|
||||
)
|
||||
WIZARD_ENTITY_ID_MAX_LENGTH = WIZARD_ENTITY_ID_BUFFER_SIZE - 1
|
||||
# The value of a multiple input fits one WizardInputSetRequest, which the device accepts up to 8 KiB on ESP8266;
|
||||
# api.proto max_data_length documents the same value
|
||||
WIZARD_MULTIPLE_MAX_LENGTH = 4095
|
||||
# One API message must fit APIBuffer::MAX_SIZE (65535) together with the largest frame header (7 bytes,
|
||||
# Noise) and footer (16 bytes, Noise MAC)
|
||||
WIZARD_RESPONSE_MAX_SIZE = 65535 - 7 - 16
|
||||
@@ -101,6 +111,21 @@ WIZARD_ENTITY_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _validate_multiple(conf: ConfigType) -> ConfigType:
|
||||
if conf[CONF_MULTIPLE] and CONF_ENTITY in conf:
|
||||
raise cv.Invalid(
|
||||
"A linked input follows the state of exactly one Home Assistant entity, so multiple "
|
||||
"is only for standalone inputs (id)",
|
||||
path=[CONF_MULTIPLE],
|
||||
)
|
||||
if CONF_MAX_LENGTH in conf and not conf[CONF_MULTIPLE]:
|
||||
raise cv.Invalid(
|
||||
"max_length only applies to an input with multiple: true",
|
||||
path=[CONF_MAX_LENGTH],
|
||||
)
|
||||
return conf
|
||||
|
||||
|
||||
# An input is either standalone (id declares a new WizardInput) or linked to a homeassistant entity that has no
|
||||
# entity_id of its own: Home Assistant sets it
|
||||
WIZARD_INPUT_SCHEMA = cv.All(
|
||||
@@ -109,6 +134,10 @@ WIZARD_INPUT_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ID): cv.declare_id(WizardInput),
|
||||
cv.Optional(CONF_ENTITY): cv.use_id(cg.EntityBase),
|
||||
cv.Optional(CONF_DESCRIPTION): _wizard_text(WIZARD_DESCRIPTION_MAX_LENGTH),
|
||||
cv.Optional(CONF_MULTIPLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_LENGTH): cv.int_range(
|
||||
min=WIZARD_ENTITY_ID_MAX_LENGTH, max=WIZARD_MULTIPLE_MAX_LENGTH
|
||||
),
|
||||
cv.Optional(CONF_TARGET): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ENTITY): cv.All(
|
||||
@@ -119,6 +148,7 @@ WIZARD_INPUT_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_ID, CONF_ENTITY),
|
||||
_validate_multiple,
|
||||
)
|
||||
|
||||
WIZARD_PAGE_SCHEMA = cv.All(
|
||||
@@ -243,6 +273,8 @@ def _wizard_defines(wizard: ConfigType) -> set[str]:
|
||||
defines.add("USE_API_WIZARD_LINKED_INPUTS")
|
||||
if any(CONF_ID in conf for conf in inputs):
|
||||
defines.add("USE_API_WIZARD_STANDALONE_INPUTS")
|
||||
if any(conf.get(CONF_MULTIPLE) for conf in inputs):
|
||||
defines.add("USE_API_WIZARD_MULTIPLE_INPUTS")
|
||||
return defines
|
||||
|
||||
|
||||
@@ -351,14 +383,6 @@ automation.register_apply_condition(
|
||||
)
|
||||
|
||||
|
||||
def zstd_module() -> ModuleType:
|
||||
"""The zstd module: the standard library one from Python 3.14, otherwise the backport."""
|
||||
try:
|
||||
return importlib.import_module("compression.zstd")
|
||||
except ImportError:
|
||||
return importlib.import_module("backports.zstd")
|
||||
|
||||
|
||||
def _entity_document(conf: ConfigType, config: fv.FinalValidateConfig) -> ConfigType:
|
||||
"""An entity of the device that the page shows, keyed as ListEntitiesResponse keys it."""
|
||||
_, declaration = _wizard_input_declaration(config, conf[CONF_ID])
|
||||
@@ -374,6 +398,10 @@ def _input_document(conf: ConfigType, config: fv.FinalValidateConfig) -> ConfigT
|
||||
document: ConfigType = {"key": fnv1_hash(_wizard_input_id(conf).id)}
|
||||
if description := conf.get(CONF_DESCRIPTION):
|
||||
document[CONF_DESCRIPTION] = description
|
||||
if conf.get(CONF_MULTIPLE):
|
||||
document["multiple"] = True
|
||||
if (max_length := conf.get(CONF_MAX_LENGTH)) is not None:
|
||||
document["max_length"] = max_length
|
||||
if filters := _wizard_input_filters(conf, config):
|
||||
document["entity_filters"] = [dict(entity_filter) for entity_filter in filters]
|
||||
return document
|
||||
@@ -387,7 +415,7 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
|
||||
{"version": 1,
|
||||
"pages": [{"title": "...", "description": "...",
|
||||
"entities": [{"key": 123, "device_id": 456, "description": "..."}],
|
||||
"inputs": [{"key": 789, "description": "...",
|
||||
"inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
|
||||
"entity_filters": [{"integration": "...", "domain": ["..."],
|
||||
"device_class": ["..."], "supported_features": ["..."]}]}]}]}
|
||||
|
||||
@@ -399,6 +427,14 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
|
||||
the device it belongs to (esphome/core/config.py), and is left out for the main device.
|
||||
- An input key is the FNV-1 hash of the ESPHome id of the input, or of the linked entity. A linked
|
||||
entity must not set entity_id, as that is a static entry kept apart from the ones the wizard sets.
|
||||
- multiple means the user may pick several entities. The client sends them in one
|
||||
WizardInputSetRequest as a comma separated list ("calendar.a,calendar.b") with no spaces; the
|
||||
device trims spaces around each id and stores the list without them. The whole request is
|
||||
ignored when any id is empty or has no '.', or when the list without spaces does not fit
|
||||
max_length (only on a multiple input, 255 when left out). A single input is ignored when the id
|
||||
is empty, longer than 255 bytes or has no '.'. The version stays 1: a client that does not know
|
||||
multiple ignores it, shows a single picker and sends one entity id, which a multiple input
|
||||
accepts.
|
||||
- entity_filters are the filters of the input, or the default filters of a linked switch,
|
||||
number, text, select or button.
|
||||
"""
|
||||
@@ -444,14 +480,15 @@ async def to_code(wizard: ConfigType) -> None:
|
||||
input_id = _wizard_input_id(conf)
|
||||
# Every buffer starts empty, until the wizard sets it. A linked homeassistant entity uses it as its entity id.
|
||||
buffer = _wizard_buffer_name(input_id)
|
||||
cg.add_global(
|
||||
cg.RawStatement(
|
||||
f'static char {buffer}[{WIZARD_ENTITY_ID_BUFFER_SIZE}] = "";'
|
||||
)
|
||||
)
|
||||
size = conf.get(CONF_MAX_LENGTH, WIZARD_ENTITY_ID_MAX_LENGTH) + 1
|
||||
cg.add_global(cg.RawStatement(f'static char {buffer}[{size}] = "";'))
|
||||
if CONF_ID in conf:
|
||||
cg.new_Pvariable(input_id, cg.RawExpression(buffer))
|
||||
entries.append(cg.RawExpression(f"{{{fnv1_hash(input_id.id)}u, {buffer}}}"))
|
||||
key = fnv1_hash(input_id.id)
|
||||
if conf[CONF_MULTIPLE]:
|
||||
entries.append(cg.RawExpression(f"{{{key}u, {buffer}, {size}}}"))
|
||||
else:
|
||||
entries.append(cg.RawExpression(f"{{{key}u, {buffer}}}"))
|
||||
if entries:
|
||||
cg.extern_progmem_array(
|
||||
"esphome::api::API_WIZARD_INPUTS",
|
||||
|
||||
@@ -391,7 +391,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Adds a define and IDF component for legacy `audio_decoder.cpp`.
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.2.0")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.3.0")
|
||||
_emit_memory_pair(
|
||||
data.flac.buffer_memory,
|
||||
"CONFIG_MICRO_FLAC_PREFER_PSRAM",
|
||||
|
||||
@@ -19,6 +19,9 @@ CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
|
||||
CONF_CANBUS_ID = "canbus_id"
|
||||
CONF_BIT_RATE = "bit_rate"
|
||||
CONF_ON_FRAME = "on_frame"
|
||||
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
|
||||
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
|
||||
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
|
||||
|
||||
|
||||
def validate_id(config: ConfigType) -> ConfigType:
|
||||
@@ -86,8 +89,13 @@ CANBUS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CanbusComponent),
|
||||
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
|
||||
cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True),
|
||||
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
|
||||
CAN_SPEEDS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_MAX_FRAMES_PER_LOOP, default=DEFAULT_MAX_FRAMES_PER_LOOP
|
||||
): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
|
||||
@@ -108,9 +116,13 @@ CANBUS_SCHEMA.add_extra(validate_id)
|
||||
|
||||
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_can_id([config[CONF_CAN_ID]]))
|
||||
cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]]))
|
||||
cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]]))
|
||||
cg.add(var.set_can_id(config[CONF_CAN_ID]))
|
||||
if config[CONF_USE_EXTENDED_ID]:
|
||||
cg.add(var.set_use_extended_id(True))
|
||||
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
|
||||
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
|
||||
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
|
||||
cg.add(var.set_max_frames_per_loop(max_frames))
|
||||
|
||||
for conf in config.get(CONF_ON_FRAME, []):
|
||||
can_id = conf[CONF_CAN_ID]
|
||||
|
||||
@@ -67,16 +67,18 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
|
||||
|
||||
void Canbus::loop() {
|
||||
struct CanFrame can_message;
|
||||
// read all messages until queue is empty
|
||||
int message_counter = 0;
|
||||
while (this->read_message(&can_message) == canbus::ERROR_OK) {
|
||||
message_counter++;
|
||||
uint32_t message_counter = 0;
|
||||
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
|
||||
if (this->read_message(&can_message) != canbus::ERROR_OK) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (can_message.use_extended_id) {
|
||||
ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%" PRIx32 " size=%d", message_counter,
|
||||
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
|
||||
can_message.can_id, can_message.can_data_length_code);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%" PRIx32 " size=%d", message_counter, can_message.can_id,
|
||||
can_message.can_data_length_code);
|
||||
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
|
||||
can_message.can_id, can_message.can_data_length_code);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
@@ -100,6 +102,12 @@ void Canbus::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message_counter == this->max_frames_per_loop_) {
|
||||
ESP_LOGW(TAG,
|
||||
"Reached max_frames_per_loop=%" PRIu32 ", deferring remaining CAN frames. Some frames might be dropped",
|
||||
this->max_frames_per_loop_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::canbus
|
||||
|
||||
@@ -78,6 +78,7 @@ class Canbus : public Component {
|
||||
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
|
||||
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
|
||||
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
|
||||
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
|
||||
|
||||
void add_trigger(CanbusTrigger *trigger);
|
||||
/**
|
||||
@@ -96,8 +97,9 @@ class Canbus : public Component {
|
||||
template<typename... Ts> friend class CanbusSendAction;
|
||||
std::vector<CanbusTrigger *> triggers_{};
|
||||
uint32_t can_id_;
|
||||
bool use_extended_id_;
|
||||
CanSpeed bit_rate_;
|
||||
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
|
||||
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
|
||||
bool use_extended_id_{false};
|
||||
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
|
||||
callback_manager_{};
|
||||
|
||||
|
||||
@@ -253,11 +253,17 @@ void CC1101Component::dump_config() {
|
||||
}
|
||||
|
||||
void CC1101Component::begin_tx() {
|
||||
// Read the mode before the PKTCTRL0 write below overwrites it in state_
|
||||
const bool packet_mode = this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO);
|
||||
// Ensure Packet Format is 3 (Async Serial)
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
// setup() only attaches the interrupt in packet mode. In async serial mode the pin may be
|
||||
// shared with remote_receiver, and detaching here would remove the receiver's interrupt.
|
||||
if (packet_mode) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
}
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,8 +16,6 @@ from esphome.const import (
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
|
||||
|
||||
combination_ns = cg.esphome_ns.namespace("combination")
|
||||
@@ -75,33 +71,19 @@ KALMAN_SOURCE_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _migrate_coeffecient(config: ConfigType) -> ConfigType:
|
||||
"""Migrate deprecated 'coeffecient' spelling to 'coefficient'."""
|
||||
if CONF_COEFFECIENT in config:
|
||||
if CONF_COEFFICIENT in config:
|
||||
raise cv.Invalid(
|
||||
f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'"
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0",
|
||||
CONF_COEFFECIENT,
|
||||
CONF_COEFFICIENT,
|
||||
)
|
||||
config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT)
|
||||
elif CONF_COEFFICIENT not in config:
|
||||
raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option")
|
||||
return config
|
||||
|
||||
|
||||
LINEAR_SOURCE_SCHEMA = cv.All(
|
||||
cv.rename_key(
|
||||
CONF_COEFFECIENT,
|
||||
CONF_COEFFICIENT,
|
||||
removed_in="2026.12.0",
|
||||
component="combination",
|
||||
),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
||||
cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_),
|
||||
cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_),
|
||||
cv.Required(CONF_COEFFICIENT): cv.templatable(cv.float_),
|
||||
}
|
||||
),
|
||||
_migrate_coeffecient,
|
||||
)
|
||||
|
||||
SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -2925,6 +2925,11 @@ async def to_code(config):
|
||||
# produce reproducible outputs and downstream tooling can reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
|
||||
# --undefined: libsrc.a is scanned before the IDF libraries that also register them.
|
||||
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
|
||||
cg.add_build_flag("-Wl,--undefined=__wrap___cxa_atexit")
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
@@ -3074,6 +3079,10 @@ async def to_code(config):
|
||||
# Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
|
||||
add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000)
|
||||
|
||||
# Main loop wakes use notification index 1; index 0 stays free for ESP-IDF waits
|
||||
# such as pthread_join(), which a stray main-loop wake would otherwise end early.
|
||||
add_idf_sdkconfig_option("CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES", 2)
|
||||
|
||||
# Place non-ISR FreeRTOS functions into flash instead of IRAM
|
||||
# This saves up to 8KB of IRAM. ISR-safe functions (FromISR variants) stay in IRAM.
|
||||
# In ESP-IDF 6.0 this becomes the default and CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Linker wrap stub for __cxa_atexit().
|
||||
*
|
||||
* GCC registers the destructor of every global and function local static
|
||||
* object with __cxa_atexit(). The firmware never returns from main or calls
|
||||
* exit(), so these destructors never run, but the first registration links
|
||||
* newlib's exit handler tables (about 400 bytes of RAM on ESP32).
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; }
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -531,6 +531,9 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
|
||||
// Xtensa: walk the backtrace using the public API
|
||||
if (info->frame != nullptr) {
|
||||
auto *xt_frame = (XtExcFrame *) info->frame;
|
||||
// IDF leaves addr unset for SoC-level panics (cache error, interrupt WDT)
|
||||
if (info->pseudo_excause)
|
||||
s_raw_crash_data.pc = xt_frame->pc;
|
||||
if (!g_panic_abort) {
|
||||
// Abort-class frames carry no useful cause/vaddr: TWDT task snapshots
|
||||
// never wrote them and abort() traps describe only the synthetic trap.
|
||||
|
||||
@@ -150,6 +150,8 @@ void BLEClientBase::connect() {
|
||||
// A registration whose event never arrived must not block this connection's release.
|
||||
this->services_released_ = false;
|
||||
this->pending_notify_regs_ = 0;
|
||||
// A CONNECT_EVT seen while idle can leave the id of a link this attempt does not own.
|
||||
this->conn_id_ = UNSET_CONN_ID;
|
||||
// Enable loop for state processing
|
||||
this->enable_loop();
|
||||
// Immediately transition to CONNECTING to prevent duplicate connection attempts
|
||||
@@ -350,7 +352,6 @@ 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_gattc_lifecycle_event_("OPEN");
|
||||
// conn_id was already set in ESP_GATTC_CONNECT_EVT
|
||||
this->service_count_ = 0;
|
||||
|
||||
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
|
||||
@@ -362,19 +363,24 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->state() != espbt::ClientState::CONNECTING) {
|
||||
// This should not happen but lets log it in case it does
|
||||
// because it means we have a bad assumption about how the
|
||||
// ESP BT stack works.
|
||||
const bool open_failed = param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN;
|
||||
// Bluedroid reports a failed open as DISCONNECT_EVT then a failing OPEN_EVT, with no CONNECT_EVT,
|
||||
// so DISCONNECTING with no id is expected for a failure. Anything else means a bad assumption
|
||||
// about how the ESP BT stack works, so log it.
|
||||
const bool expected_failure =
|
||||
open_failed && this->state() == espbt::ClientState::DISCONNECTING && this->conn_id_ == UNSET_CONN_ID;
|
||||
if (this->state() != espbt::ClientState::CONNECTING && !expected_failure) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
|
||||
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
|
||||
}
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
if (open_failed) {
|
||||
this->log_gattc_warning_("Connection open", param->open.status);
|
||||
// Connection was never established so CLOSE_EVT may not follow
|
||||
this->set_idle_();
|
||||
break;
|
||||
}
|
||||
// An open on a link that is already up sends no CONNECT_EVT, so take the id here.
|
||||
this->conn_id_ = param->open.conn_id;
|
||||
if (this->want_disconnect_) {
|
||||
// Disconnect was requested after connecting started,
|
||||
// but before the connection was established. Now that we have
|
||||
|
||||
@@ -53,6 +53,7 @@ from .const import (
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
KEY_WAVEFORM_REQUIRED,
|
||||
THROW_STUBS_HEADER,
|
||||
enable_serial,
|
||||
enable_serial1,
|
||||
esp8266_ns,
|
||||
@@ -114,9 +115,37 @@ _validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
|
||||
_resolve_toolchain = cv.resolve_toolchain("ESP8266", _TOOLCHAINS, Toolchain.ARDUINO)
|
||||
|
||||
|
||||
# Known boards keyed by spelling with case and "-" / "_" folded away
|
||||
_FOLDED_BOARDS = {board.lower().replace("-", "_"): board for board in BOARDS}
|
||||
|
||||
|
||||
def _resolve_board(config: ConfigType) -> ConfigType:
|
||||
"""Fix a misspelled board, else build an unknown one with PlatformIO."""
|
||||
board = config[CONF_BOARD]
|
||||
if board in BOARDS:
|
||||
return config
|
||||
if canonical := _FOLDED_BOARDS.get(board.lower().replace("-", "_")):
|
||||
_LOGGER.warning(
|
||||
"Board '%s' is not a known ESP8266 board; using '%s'", board, canonical
|
||||
)
|
||||
return {**config, CONF_BOARD: canonical}
|
||||
if CORE.toolchain is None and CONF_TOOLCHAIN not in config:
|
||||
return {**config, CONF_TOOLCHAIN: Toolchain.PLATFORMIO}
|
||||
return config
|
||||
|
||||
|
||||
def _warn_platformio_toolchain(config: ConfigType) -> ConfigType:
|
||||
# Remove before 2027.4.0
|
||||
if CORE.using_toolchain_platformio:
|
||||
if not CORE.using_toolchain_platformio:
|
||||
return config
|
||||
if config[CONF_BOARD] not in BOARDS:
|
||||
_LOGGER.warning(
|
||||
"Board '%s' is not supported by the native 'arduino' toolchain, so it "
|
||||
"builds with the deprecated 'platformio' toolchain, which will be "
|
||||
"removed in ESPHome 2027.4.0",
|
||||
config[CONF_BOARD],
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"The 'platformio' toolchain for ESP8266 is deprecated and will be "
|
||||
"removed in ESPHome 2027.4.0; the native 'arduino' toolchain is the "
|
||||
@@ -329,6 +358,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): _validate_toolchain,
|
||||
}
|
||||
),
|
||||
_resolve_board,
|
||||
_resolve_toolchain,
|
||||
_warn_platformio_toolchain,
|
||||
_validate_native_toolchain,
|
||||
@@ -448,11 +478,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
|
||||
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
|
||||
# See throw_stubs.h. Unconditional: the native build generator reads
|
||||
# the same option, keeping one source of truth.
|
||||
cg.add_platformio_option(
|
||||
"build_src_flags", "-include esphome/components/esp8266/throw_stubs.h"
|
||||
)
|
||||
# See throw_stubs.h. The native build generator reads this option, also
|
||||
# passes it to the core and libraries, and drops it with exceptions on.
|
||||
cg.add_platformio_option("build_src_flags", f"-include {THROW_STUBS_HEADER}")
|
||||
|
||||
# In testing mode, fake larger memory to allow linking grouped component tests
|
||||
# Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
|
||||
|
||||
@@ -17,6 +17,8 @@ KEY_SERIAL_REQUIRED = "serial_required"
|
||||
KEY_SERIAL1_REQUIRED = "serial1_required"
|
||||
# Set for the native (non-PlatformIO) toolchain's build generator
|
||||
KEY_SCANF_FLOAT = "scanf_float"
|
||||
# Force-included std::__throw_* overrides, relative to the src dir
|
||||
THROW_STUBS_HEADER = "esphome/components/esp8266/throw_stubs.h"
|
||||
# Per-board flash-layout override consumed by board_ld_script()
|
||||
KEY_LDSCRIPT = "ldscript"
|
||||
# Crash-decode tools, resolved once per run by __init__
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
* unused, dead-strip the load, and drop the string entirely -- no LTO needed.
|
||||
* Behavior is identical to today: a bare abort() (the message was never
|
||||
* printed). This header MUST be force-included before <string>, so it is
|
||||
* wired up via build_src_flags "-include ..." in this component's __init__.py.
|
||||
* wired up via build_src_flags "-include ..." in this component's __init__.py;
|
||||
* the native toolchain also force-includes it into the core and libraries.
|
||||
*
|
||||
* Note: this defines functions in namespace std (technically UB). It is safe
|
||||
* here because the definitions match the existing abort() behavior exactly.
|
||||
|
||||
@@ -37,7 +37,6 @@ void GreeClimate::transmit_state() {
|
||||
remote_state[1] = this->temperature_();
|
||||
|
||||
if (model == GREE_YAN) {
|
||||
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
|
||||
remote_state[3] = 0x50; // bits 4..7 always 0101
|
||||
remote_state[4] = this->vertical_swing_();
|
||||
}
|
||||
@@ -61,7 +60,6 @@ void GreeClimate::transmit_state() {
|
||||
}
|
||||
|
||||
if (model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
|
||||
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
|
||||
remote_state[3] = 0x50; // bits 4..7 always 0101
|
||||
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@ static constexpr uint8_t GREE_PRESET_NONE = 0x00;
|
||||
static constexpr uint8_t GREE_PRESET_SLEEP = 0x01;
|
||||
static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
|
||||
|
||||
// Mode bits stored in remote_state[2] bits 4..7 for YAN/YAA/YAC/YAC1FB9.
|
||||
static constexpr uint8_t GREE_MODE_BIT_TURBO = 0x10;
|
||||
static constexpr uint8_t GREE_MODE_BIT_LIGHT = 0x20;
|
||||
static constexpr uint8_t GREE_MODE_BIT_HEALTH = 0x40;
|
||||
static constexpr uint8_t GREE_MODE_BIT_XFAN = 0x80;
|
||||
|
||||
// Model codes
|
||||
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
|
||||
|
||||
@@ -103,7 +109,7 @@ class GreeClimate final : public climate_ir::ClimateIR {
|
||||
uint8_t preset_();
|
||||
|
||||
Model model_{};
|
||||
uint8_t mode_bits_{0}; // Combined mode bits for remote_state[2]
|
||||
uint8_t mode_bits_{GREE_MODE_BIT_LIGHT}; // The display light is on until a switch says otherwise
|
||||
};
|
||||
|
||||
} // namespace esphome::gree
|
||||
|
||||
@@ -17,12 +17,22 @@ CONF_HEALTH = "health"
|
||||
CONF_XFAN = "xfan"
|
||||
CONF_GREE_ID = "gree_id"
|
||||
|
||||
# Switch configurations: (config_key, display_name, bit_mask, icon)
|
||||
# Switch configurations: (config_key, display_name, mode_bit, icon)
|
||||
SWITCH_CONFIGS = (
|
||||
(CONF_TURBO, "Gree Turbo Switch", 0x10, "mdi:car-turbocharger"),
|
||||
(CONF_LIGHT, "Gree Light Switch", 0x20, "mdi:led-outline"),
|
||||
(CONF_HEALTH, "Gree Health Switch", 0x40, "mdi:pine-tree"),
|
||||
(CONF_XFAN, "Gree X-FAN Switch", 0x80, "mdi:wall-sconce-flat"),
|
||||
(
|
||||
CONF_TURBO,
|
||||
"Gree Turbo Switch",
|
||||
gree_ns.GREE_MODE_BIT_TURBO,
|
||||
"mdi:car-turbocharger",
|
||||
),
|
||||
(CONF_LIGHT, "Gree Light Switch", gree_ns.GREE_MODE_BIT_LIGHT, "mdi:led-outline"),
|
||||
(CONF_HEALTH, "Gree Health Switch", gree_ns.GREE_MODE_BIT_HEALTH, "mdi:pine-tree"),
|
||||
(
|
||||
CONF_XFAN,
|
||||
"Gree X-FAN Switch",
|
||||
gree_ns.GREE_MODE_BIT_XFAN,
|
||||
"mdi:wall-sconce-flat",
|
||||
),
|
||||
)
|
||||
|
||||
SUPPORTED_MODELS = {
|
||||
@@ -39,7 +49,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional(key): switch.switch_schema(
|
||||
GreeModeBitSwitch,
|
||||
icon=icon,
|
||||
default_restore_mode="RESTORE_DEFAULT_OFF",
|
||||
# The display light was on before the switches existed, so it stays on by default
|
||||
default_restore_mode="RESTORE_DEFAULT_ON"
|
||||
if key == CONF_LIGHT
|
||||
else "RESTORE_DEFAULT_OFF",
|
||||
device_class=DEVICE_CLASS_SWITCH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
|
||||
@@ -187,15 +187,18 @@ void HeatpumpIRClimate::transmit_state() {
|
||||
swing_h_cmd = HDIR_SWING;
|
||||
}
|
||||
|
||||
// MitsubishiMSCHeatpumpIR only knows FAN_1..FAN_3 and sends anything else as auto,
|
||||
// so low/medium/high have to start at FAN_1 there instead of FAN_2.
|
||||
const bool three_speed_fan = this->protocol_ == PROTOCOL_MITSUBISHI_MSC;
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
fan_speed_cmd = FAN_2;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_1 : FAN_2;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
fan_speed_cmd = FAN_3;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_2 : FAN_3;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
fan_speed_cmd = FAN_4;
|
||||
fan_speed_cmd = three_speed_fan ? FAN_3 : FAN_4;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace esphome::improv_serial {
|
||||
|
||||
static const char *const TAG = "improv_serial";
|
||||
|
||||
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_ID = 0;
|
||||
|
||||
void ImprovSerialComponent::setup() {
|
||||
global_improv_serial_component = this;
|
||||
#ifdef USE_IMPROV_SERIAL_UART
|
||||
@@ -73,7 +75,7 @@ void ImprovSerialComponent::loop() {
|
||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
||||
this->connecting_sta_.get_password());
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
this->cancel_timeout(WIFI_CONNECT_TIMEOUT_ID);
|
||||
this->set_state_(improv::STATE_PROVISIONED);
|
||||
|
||||
this->send_settings_response_(improv::WIFI_SETTINGS);
|
||||
@@ -302,7 +304,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
|
||||
this->set_timeout(WIFI_CONNECT_TIMEOUT_ID, switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
|
||||
[this]() { this->on_wifi_connect_timeout_(); });
|
||||
#else
|
||||
// No Wi-Fi support compiled in; there is nothing to provision.
|
||||
|
||||
@@ -491,6 +491,9 @@ async def component_to_code(config):
|
||||
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
|
||||
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
|
||||
|
||||
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
|
||||
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
|
||||
|
||||
# LN882x: a zero-size allocation must not trip the SDK's assert (see ln882x_zero_malloc.c).
|
||||
if config[CONF_COMPONENT_ID] == COMPONENT_LN882X:
|
||||
cg.add_build_flag("-Wl,--wrap=pvPortMalloc")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Linker wrap stub for __cxa_atexit().
|
||||
*
|
||||
* GCC registers the destructor of every global and function local static
|
||||
* object with __cxa_atexit(). The firmware never returns from main or calls
|
||||
* exit(), so these destructors never run, but the first registration links
|
||||
* newlib's exit handler tables.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
namespace esphome::libretiny {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; }
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
namespace esphome::mcp23xxx_base {
|
||||
|
||||
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
|
||||
this->pin_mode(flags_);
|
||||
// When interrupt_pin is configured, pin_mode() already auto-enables CHANGE
|
||||
|
||||
@@ -8,6 +8,10 @@ namespace esphome::mcp23xxx_base {
|
||||
|
||||
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
|
||||
|
||||
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
|
||||
// ISR in flash.
|
||||
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
|
||||
|
||||
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
|
||||
public:
|
||||
virtual void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
@@ -35,7 +39,8 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
|
||||
void setup_interrupt_pin_() {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->interrupt_pin_->attach_interrupt(&mcp23xxx_gpio_intr, static_cast<Component *>(this),
|
||||
gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
@@ -43,7 +48,6 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
// read a given register
|
||||
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
|
||||
|
||||
@@ -235,7 +235,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
ethernet.request_ethernet_ip_state_listener()
|
||||
|
||||
if CORE.is_esp32:
|
||||
add_idf_component(name="espressif/mdns", ref="1.12.0")
|
||||
add_idf_component(name="espressif/mdns", ref="1.14.0")
|
||||
# ESPHome only advertises; the browse APIs are unused
|
||||
add_idf_sdkconfig_option("CONFIG_MDNS_ENABLE_BROWSE", False)
|
||||
# The mdns console CLI is never used by ESPHome
|
||||
|
||||
@@ -273,6 +273,8 @@ class MQTTComponent : public Component {
|
||||
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
|
||||
|
||||
protected:
|
||||
static constexpr uint32_t SEND_DEFER_ID = 0;
|
||||
|
||||
/// Helper method to get the discovery topic for this component into a buffer.
|
||||
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
|
||||
const MQTTDiscoveryInfo &discovery_info) const;
|
||||
|
||||
@@ -121,7 +121,7 @@ void MQTTFanComponent::setup() {
|
||||
});
|
||||
}
|
||||
|
||||
this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
|
||||
this->state_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
|
||||
}
|
||||
|
||||
void MQTTFanComponent::dump_config() {
|
||||
|
||||
@@ -28,7 +28,7 @@ void MQTTJSONLightComponent::setup() {
|
||||
}
|
||||
|
||||
void MQTTJSONLightComponent::on_light_remote_values_update() {
|
||||
this->defer("send", [this]() { this->publish_state_(); });
|
||||
this->defer(SEND_DEFER_ID, [this]() { this->publish_state_(); });
|
||||
}
|
||||
|
||||
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
||||
|
||||
@@ -29,7 +29,7 @@ void MQTTLockComponent::setup() {
|
||||
}
|
||||
});
|
||||
this->lock_->add_on_state_callback(
|
||||
[this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); });
|
||||
[this](LockState /*state*/) { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
|
||||
}
|
||||
void MQTTLockComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str());
|
||||
|
||||
@@ -34,7 +34,7 @@ void MQTTSwitchComponent::setup() {
|
||||
}
|
||||
});
|
||||
this->switch_->add_on_state_callback(
|
||||
[this](bool enabled) { this->defer("send", [this, enabled]() { this->publish_state(enabled); }); });
|
||||
[this](bool enabled) { this->defer(SEND_DEFER_ID, [this, enabled]() { this->publish_state(enabled); }); });
|
||||
}
|
||||
void MQTTSwitchComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Switch '%s': ", this->switch_->get_name().c_str());
|
||||
|
||||
@@ -24,7 +24,7 @@ void MQTTUpdateComponent::setup() {
|
||||
}
|
||||
});
|
||||
|
||||
this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
|
||||
this->update_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
|
||||
}
|
||||
|
||||
bool MQTTUpdateComponent::publish_state() {
|
||||
|
||||
@@ -13,8 +13,8 @@ CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
# Keep in sync with platformio.ini and esphome/idf_component.yml.
|
||||
# LIBSODIUM_VERSION must match the version noise-c pins in its manifests.
|
||||
NOISE_C_VERSION = "0.1.30"
|
||||
LIBSODIUM_VERSION = "1.10021.11"
|
||||
NOISE_C_VERSION = "0.1.31"
|
||||
LIBSODIUM_VERSION = "1.10021.12"
|
||||
|
||||
noise_ns = cg.esphome_ns.namespace("noise")
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.build_helpers import pch
|
||||
from esphome.build_helpers.ccache import resolve_ccache_path
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import (
|
||||
Section,
|
||||
add_extra_script,
|
||||
copy_files as zephyr_copy_files,
|
||||
zephyr_add_overlay,
|
||||
@@ -125,11 +126,37 @@ def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
|
||||
|
||||
if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
|
||||
zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]])
|
||||
sections = BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]
|
||||
if CORE.testing_mode:
|
||||
sections = _testing_mode_sections(sections)
|
||||
zephyr_add_pm_static(sections)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# In testing mode, fake a larger flash to allow linking grouped component
|
||||
# tests. The nRF52840 has 1 MB and a mcumgr OTA config halves the app slot,
|
||||
# which an openthread config alone fills; CI images are never flashed.
|
||||
NRF52840_FLASH_SIZE = 0x100000
|
||||
TESTING_FLASH_SIZE = 0x400000
|
||||
|
||||
|
||||
def _testing_mode_sections(sections: list[Section]) -> list[Section]:
|
||||
"""Move a bootloader pinned to the end of the real flash to the end of the
|
||||
faked one, so the partition manager still sees a single gap for the app."""
|
||||
return [
|
||||
Section(
|
||||
section.name,
|
||||
section.address + TESTING_FLASH_SIZE - NRF52840_FLASH_SIZE,
|
||||
section.size,
|
||||
section.region,
|
||||
)
|
||||
if section.end_address == NRF52840_FLASH_SIZE
|
||||
else section
|
||||
for section in sections
|
||||
]
|
||||
|
||||
|
||||
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.SDK_NRF)
|
||||
_resolve_toolchain = cv.resolve_toolchain("nRF52", _TOOLCHAINS, Toolchain.SDK_NRF)
|
||||
|
||||
@@ -380,6 +407,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
zephyr_setup_preferences()
|
||||
zephyr_to_code(config)
|
||||
|
||||
if CORE.testing_mode:
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&flash0 {{
|
||||
reg = <0x0 {TESTING_FLASH_SIZE:#x}>;
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
||||
if dfu_config := config.get(CONF_DFU):
|
||||
CORE.add_job(_dfu_to_code, dfu_config)
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
@@ -695,13 +731,13 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||
return True # Handled: PYOCD upload
|
||||
|
||||
# Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths
|
||||
from .ble_logger import is_mac_address
|
||||
from .ble_logger import is_ble_address
|
||||
from .ota import smpmgr_scan, smpmgr_upload
|
||||
|
||||
if host == "BLE":
|
||||
mcumgr_device = asyncio.run(smpmgr_scan(CORE.name))
|
||||
|
||||
if is_mac_address(host):
|
||||
if is_ble_address(host):
|
||||
mcumgr_device = host
|
||||
|
||||
if mcumgr_device:
|
||||
@@ -716,7 +752,7 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||
|
||||
def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
|
||||
address = devices[0]
|
||||
from .ble_logger import is_mac_address, logger_connect, logger_scan
|
||||
from .ble_logger import is_ble_address, logger_connect, logger_scan
|
||||
|
||||
if devices[0] == "BLE":
|
||||
ble_device = asyncio.run(logger_scan(CORE.name))
|
||||
@@ -725,7 +761,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
|
||||
else:
|
||||
return True
|
||||
|
||||
if is_mac_address(address):
|
||||
if is_ble_address(address):
|
||||
asyncio.run(logger_connect(address))
|
||||
return True
|
||||
return False
|
||||
@@ -798,7 +834,7 @@ _PCH_CMAKE_LINES = [
|
||||
"set(esphome_kept_options)",
|
||||
"set(esphome_pch_headers)",
|
||||
"foreach(option IN LISTS esphome_options)",
|
||||
' if(option MATCHES "imacros> (.+)$")',
|
||||
' if(option MATCHES "imacros> ([^>]+)")',
|
||||
' list(APPEND esphome_pch_headers "${CMAKE_MATCH_1}")',
|
||||
" list(APPEND esphome_kept_options",
|
||||
' "$<$<NOT:$<AND:$<COMPILE_LANGUAGE:CXX>,$<STREQUAL:$<TARGET_PROPERTY:NAME>,app>>>:${option}>")',
|
||||
@@ -1003,11 +1039,13 @@ def run_compile(args, config: ConfigType) -> bool:
|
||||
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
bootloader = zephyr_data()[KEY_BOOTLOADER]
|
||||
|
||||
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
|
||||
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes.
|
||||
# The bootloader resets on a package whose sd_req does not list its SoftDevice,
|
||||
# so v7 accepts S140 7.0.1, 7.2.0 and 7.3.0 (e.g. Seeed XIAO nRF52840).
|
||||
_GENPKG_PARAMS = {
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
|
||||
}
|
||||
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
|
||||
_UF2_FAMILY_IDS = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
from typing import Final
|
||||
|
||||
from bleak import BleakClient, BleakScanner, BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from bleak.exc import (
|
||||
BleakCharacteristicNotFoundError,
|
||||
BleakDBusError,
|
||||
@@ -19,15 +20,31 @@ NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
MAC_ADDRESS_PATTERN: Final = re.compile(
|
||||
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
|
||||
)
|
||||
# macOS identifies BLE peripherals by a CoreBluetooth UUID instead of a MAC address
|
||||
UUID_PATTERN: Final = re.compile(
|
||||
r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def is_mac_address(value: str) -> bool:
|
||||
return MAC_ADDRESS_PATTERN.match(value)
|
||||
def is_ble_address(value: str) -> bool:
|
||||
return bool(MAC_ADDRESS_PATTERN.match(value) or UUID_PATTERN.match(value))
|
||||
|
||||
|
||||
def _name_matches(device: BLEDevice, adv: AdvertisementData, name: str) -> bool:
|
||||
# device.name can be a stale cached name on macOS; adv.local_name is what is on the air now
|
||||
return name in (device.name, adv.local_name)
|
||||
|
||||
|
||||
async def find_device_by_name(name: str, timeout: float = 10.0) -> BLEDevice | None:
|
||||
return await BleakScanner.find_device_by_filter(
|
||||
lambda device, adv: _name_matches(device, adv, name), timeout=timeout
|
||||
)
|
||||
|
||||
|
||||
async def logger_scan(name: str) -> BLEDevice | None:
|
||||
_LOGGER.info("Scanning bluetooth for %s...", name)
|
||||
device = await BleakScanner.find_device_by_name(name)
|
||||
device = await find_device_by_name(name)
|
||||
if not device:
|
||||
_LOGGER.error("%s Bluetooth LE device was not found!", name)
|
||||
return device
|
||||
|
||||
@@ -4,7 +4,6 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from bleak import BleakScanner
|
||||
from bleak.exc import BleakDBusError, BleakDeviceNotFoundError
|
||||
from smp.exceptions import SMPBadStartDelimiter
|
||||
from smpclient import SMPClient
|
||||
@@ -23,9 +22,8 @@ from smpclient.transport.serial import SMPSerialTransport
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.espota2 import ProgressBar
|
||||
|
||||
from .ble_logger import is_mac_address
|
||||
from .ble_logger import find_device_by_name, is_ble_address
|
||||
|
||||
SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84"
|
||||
BLE_SCAN_TIMEOUT = 10.0 # seconds
|
||||
RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute
|
||||
|
||||
@@ -45,12 +43,12 @@ def _json_state(o: object) -> object:
|
||||
|
||||
async def smpmgr_scan(name: str) -> str:
|
||||
_LOGGER.info("Scanning bluetooth for %s...", name)
|
||||
for device in await BleakScanner.discover(
|
||||
timeout=BLE_SCAN_TIMEOUT, service_uuids=[SMP_SERVICE_UUID]
|
||||
):
|
||||
if device.name == name:
|
||||
return device.address
|
||||
raise EsphomeError(f"BLE device {name} with OTA service not found")
|
||||
# No service filter: the SMP UUID only fits in the scan response, which macOS does not filter on.
|
||||
# The SMP service is checked on connect.
|
||||
device = await find_device_by_name(name, timeout=BLE_SCAN_TIMEOUT)
|
||||
if device is None:
|
||||
raise EsphomeError(f"BLE device {name} not found")
|
||||
return device.address
|
||||
|
||||
|
||||
async def smpmgr_upload(device: str, firmware: Path) -> None:
|
||||
@@ -88,7 +86,7 @@ def _get_image_tlv_sha256(file: Path) -> bytes:
|
||||
async def _smpmgr_upload(device: str, firmware: Path) -> None:
|
||||
image_tlv_sha256 = _get_image_tlv_sha256(firmware)
|
||||
|
||||
if is_mac_address(device):
|
||||
if is_ble_address(device):
|
||||
smp_client = SMPClient(SMPBLETransport(), device)
|
||||
else:
|
||||
smp_client = SMPClient(SMPSerialTransport(), device)
|
||||
@@ -105,7 +103,8 @@ async def _smpmgr_upload(device: str, firmware: Path) -> None:
|
||||
) from exc
|
||||
raise EsphomeError(f"BLE error connecting to {device}: {exc}") from exc
|
||||
except SMPBLETransportException as exc:
|
||||
raise EsphomeError(f"Connection error with {device}") from exc
|
||||
# Reached when a device with the right name has no SMP service, among other causes
|
||||
raise EsphomeError(f"Connection error with {device}: {exc}") from exc
|
||||
|
||||
_LOGGER.info("Connected %s...", device)
|
||||
try:
|
||||
|
||||
@@ -194,6 +194,8 @@ def _parse_platform_version(value: Any) -> str:
|
||||
# * The new version needs to be thoroughly validated before changing the
|
||||
# recommended version as otherwise a bunch of devices could be bricked
|
||||
# * For all constants below, update platformio.ini (in this repo)
|
||||
# * rp2_pio pins its own pioasm build (PIOASM_REPO_VERSION in rp2_pio/__init__.py); check
|
||||
# https://github.com/earlephilhower/pico-quick-toolchain/releases for a matching release
|
||||
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/earlephilhower/arduino-pico/releases
|
||||
|
||||
@@ -7,9 +7,9 @@ from esphome.types import ConfigType
|
||||
DEPENDENCIES = ["rp2"]
|
||||
|
||||
|
||||
PIOASM_REPO_VERSION = "1.5.0-b"
|
||||
PIOASM_REPO_VERSION = "4.1.0"
|
||||
PIOASM_REPO_BASE = f"https://github.com/earlephilhower/pico-quick-toolchain/releases/download/{PIOASM_REPO_VERSION}"
|
||||
PIOASM_VERSION = "pioasm-2e6142b.230216"
|
||||
PIOASM_VERSION = "pioasm-9fdfe11.250530"
|
||||
PIOASM_DOWNLOADS = {
|
||||
"linux": {
|
||||
"aarch64": f"aarch64-linux-gnu.{PIOASM_VERSION}.tar.gz",
|
||||
@@ -20,8 +20,8 @@ PIOASM_DOWNLOADS = {
|
||||
"amd64": f"x86_64-w64-mingw32.{PIOASM_VERSION}.zip",
|
||||
},
|
||||
"darwin": {
|
||||
"x86_64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
|
||||
"arm64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
|
||||
"x86_64": f"x86_64-apple-darwin20.4.{PIOASM_VERSION}.tar.gz",
|
||||
"arm64": f"aarch64-apple-darwin20.4.{PIOASM_VERSION}.tar.gz",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -295,8 +295,11 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
if (this->num_running_ == 0)
|
||||
if (this->num_running_ == 0) {
|
||||
// Nothing left to wait for, e.g. once the last queued item has run
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->script_->is_running())
|
||||
return;
|
||||
@@ -305,9 +308,10 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
|
||||
// Processing all items in a while loop causes infinite loops because
|
||||
// play_next_() can trigger more items to be queued
|
||||
if (!this->param_queue_.empty()) {
|
||||
auto ¶ms = this->param_queue_.front();
|
||||
this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
// Take the item out first: the chain may call stop(), which clears the queue
|
||||
auto params = std::move(this->param_queue_.front());
|
||||
this->param_queue_.pop_front();
|
||||
std::apply([this](auto &...args) { this->play_next_(args...); }, params);
|
||||
} else {
|
||||
// Queue is now empty - disable loop until next play_complex
|
||||
this->disable_loop();
|
||||
@@ -323,12 +327,8 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
this->play_next_(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
C *script_;
|
||||
std::list<std::tuple<Ts...>> param_queue_;
|
||||
std::list<std::tuple<stored_trigger_arg_t<Ts>...>> param_queue_;
|
||||
};
|
||||
|
||||
} // namespace esphome::script
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace esphome::seeed_mr24hpc1 {
|
||||
|
||||
static const char *const TAG = "seeed_mr24hpc1";
|
||||
|
||||
static constexpr uint32_t STARTUP_DELAY_MS = 1000;
|
||||
|
||||
// Prints the component's configuration data. dump_config() prints all of the component's configuration
|
||||
// items in an easy-to-read format, including the configuration key-value pairs.
|
||||
void MR24HPC1Component::dump_config() {
|
||||
@@ -76,8 +78,7 @@ void MR24HPC1Component::setup() {
|
||||
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->poll_time_base_func_check_ = false;
|
||||
this->check_dev_inf_sign_ = true;
|
||||
this->sg_start_query_data_ = STANDARD_FUNCTION_QUERY_PRODUCT_MODE;
|
||||
this->sg_data_len_ = 0;
|
||||
@@ -92,7 +93,13 @@ void MR24HPC1Component::setup() {
|
||||
memset(this->sg_frame_prase_buf_, 0, FRAME_BUF_MAX_SIZE);
|
||||
memset(this->sg_frame_buf_, 0, FRAME_BUF_MAX_SIZE);
|
||||
|
||||
this->set_interval(8000, [this]() { this->update_(); });
|
||||
// The module locks up when queried during its own boot, so hold all TX for a second
|
||||
this->set_timeout(STARTUP_DELAY_MS, [this]() {
|
||||
this->module_ready_ = true;
|
||||
this->set_custom_end_mode();
|
||||
this->poll_time_base_func_check_ = true;
|
||||
this->set_interval(8000, [this]() { this->update_(); });
|
||||
});
|
||||
}
|
||||
|
||||
// Timed polling of radar data
|
||||
@@ -686,6 +693,8 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) {
|
||||
|
||||
// Sending data frames
|
||||
void MR24HPC1Component::send_query_(const uint8_t *query, size_t string_length) {
|
||||
if (!this->module_ready_)
|
||||
return;
|
||||
this->write_array(query, string_length);
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ class MR24HPC1Component final : public Component,
|
||||
int sg_start_query_data_;
|
||||
bool check_dev_inf_sign_;
|
||||
bool poll_time_base_func_check_;
|
||||
bool module_ready_{false};
|
||||
|
||||
void update_();
|
||||
void r24_split_data_frame_(uint8_t value);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, mdns, network, psram, socket, wifi
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
from esphome.components.const import CONF_ENABLED, CONF_MANUFACTURER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
@@ -25,6 +27,8 @@ from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# mdns for autodiscovery
|
||||
AUTO_LOAD = ["mdns"]
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
@@ -46,6 +50,33 @@ CONF_FIXED_DELAY = "fixed_delay"
|
||||
CONF_DECODE_MEMORY = "decode_memory"
|
||||
CONF_CODECS = "codecs"
|
||||
|
||||
CONF_STATIC_PAIRING_CODE = "static_pairing_code"
|
||||
CONF_UNPAIRED_ACCESS = "unpaired_access"
|
||||
CONF_ON_OPEN_PAIRING_WINDOW = "on_open_pairing_window"
|
||||
CONF_ON_CLOSE_PAIRING_WINDOW = "on_close_pairing_window"
|
||||
CONF_ON_DISPLAY_PAIRING_CODE = "on_display_pairing_code"
|
||||
CONF_ON_CLEAR_PAIRING_CODE = "on_clear_pairing_code"
|
||||
CONF_ON_PAIRING_SUCCEEDED = "on_pairing_succeeded"
|
||||
CONF_ON_PAIRING_FAILED = "on_pairing_failed"
|
||||
|
||||
# A static pairing code is exactly 8 decimal digits.
|
||||
STATIC_PAIRING_CODE_DIGITS = 8
|
||||
|
||||
|
||||
def _validate_static_pairing_code(value: Any) -> str:
|
||||
# string_strict so leading zeros survive and `!secret` works.
|
||||
value = cv.string_strict(value)
|
||||
if len(value) != STATIC_PAIRING_CODE_DIGITS or not (
|
||||
value.isascii() and value.isdigit()
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_STATIC_PAIRING_CODE} must be exactly "
|
||||
f"{STATIC_PAIRING_CODE_DIGITS} decimal digits "
|
||||
'(quote the value so leading zeros are preserved, e.g. "01234567")'
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
# Matches ARTWORK_MAX_SLOTS in sendspin-cpp.
|
||||
MAX_ARTWORK_SLOTS = 4
|
||||
|
||||
@@ -76,7 +107,6 @@ OPUS_SAMPLE_RATE = 48000
|
||||
SendspinImageFormat = sendspin_library_ns.enum("SendspinImageFormat", is_class=True)
|
||||
IMAGE_FORMAT_JPEG = SendspinImageFormat.enum("JPEG")
|
||||
IMAGE_FORMAT_PNG = SendspinImageFormat.enum("PNG")
|
||||
IMAGE_FORMAT_BMP = SendspinImageFormat.enum("BMP")
|
||||
|
||||
SendspinImageSource = sendspin_library_ns.enum("SendspinImageSource", is_class=True)
|
||||
IMAGE_SOURCE_ALBUM = SendspinImageSource.enum("ALBUM")
|
||||
@@ -109,6 +139,34 @@ SendspinHub = sendspin_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_OPEN_PAIRING_WINDOW, "add_on_open_pairing_window_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CLOSE_PAIRING_WINDOW, "add_on_close_pairing_window_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DISPLAY_PAIRING_CODE,
|
||||
"add_on_display_pairing_code_callback",
|
||||
[(cg.std_string, "code")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CLEAR_PAIRING_CODE, "add_on_clear_pairing_code_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PAIRING_SUCCEEDED,
|
||||
"add_on_pairing_succeeded_callback",
|
||||
[(cg.std_string, "server_id")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PAIRING_FAILED,
|
||||
"add_on_pairing_failed_callback",
|
||||
[(cg.std_string, "server_id"), (cg.StringRef, "reason")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendspinConfiguration:
|
||||
artwork_support: bool = False
|
||||
@@ -116,6 +174,8 @@ class SendspinConfiguration:
|
||||
metadata_support: bool = False
|
||||
player_support: bool = False
|
||||
visualizer_support: bool = False
|
||||
pairing_code_display_support: bool = False
|
||||
switch_types: set[str] = field(default_factory=set)
|
||||
|
||||
artwork_preferences: list[ConfigType] = field(default_factory=list)
|
||||
player_config: ConfigType | None = None
|
||||
@@ -152,6 +212,16 @@ def request_visualizer_support() -> None:
|
||||
_get_data().visualizer_support = True
|
||||
|
||||
|
||||
def request_pairing_code_display_support() -> None:
|
||||
"""Mark that the device can emit a dynamic pairing code (e.g. a pairing_code text sensor)."""
|
||||
_get_data().pairing_code_display_support = True
|
||||
|
||||
|
||||
def request_switch(switch_type: str) -> None:
|
||||
"""Mark that a sendspin switch of this type drives the matching hub setting."""
|
||||
_get_data().switch_types.add(switch_type)
|
||||
|
||||
|
||||
def register_artwork_preference(config: ConfigType) -> int:
|
||||
"""Register an artwork slot preference and return the slot it was given.
|
||||
|
||||
@@ -208,28 +278,85 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_STATIC_PAIRING_CODE): cv.sensitive(
|
||||
_validate_static_pairing_code
|
||||
),
|
||||
# No default: the unpaired access switch rejects this key when it is set.
|
||||
cv.Optional(CONF_UNPAIRED_ACCESS): cv.boolean,
|
||||
cv.Optional(CONF_ON_OPEN_PAIRING_WINDOW): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLOSE_PAIRING_WINDOW): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISPLAY_PAIRING_CODE): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLEAR_PAIRING_CODE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PAIRING_SUCCEEDED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PAIRING_FAILED): automation.validate_automation({}),
|
||||
}
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
# sendspin-cpp needs noise-c as an ESP-IDF component, which Arduino below IDF 6.0 cannot use.
|
||||
cv.only_with_framework("esp-idf"),
|
||||
_request_high_performance_networking,
|
||||
)
|
||||
|
||||
|
||||
def _offers_dynamic_pairing_code(config: ConfigType) -> bool:
|
||||
"""Whether the device can show a dynamic pairing code, so dynamic_pairing_code is advertised."""
|
||||
return bool(
|
||||
config.get(CONF_ON_DISPLAY_PAIRING_CODE)
|
||||
or _get_data().pairing_code_display_support
|
||||
)
|
||||
|
||||
|
||||
def _has_pairing_method(config: ConfigType) -> bool:
|
||||
"""Whether the config gives a server any way to pair with the device."""
|
||||
return CONF_STATIC_PAIRING_CODE in config or _offers_dynamic_pairing_code(config)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
dynamic_code = _offers_dynamic_pairing_code(config)
|
||||
# The protocol allows only one pairing code method.
|
||||
if dynamic_code and CONF_STATIC_PAIRING_CODE in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_STATIC_PAIRING_CODE}' cannot be used with a dynamic pairing code "
|
||||
f"({CONF_ON_DISPLAY_PAIRING_CODE} or a pairing_code text sensor), since only "
|
||||
"one pairing code method can be offered",
|
||||
path=[CONF_STATIC_PAIRING_CODE],
|
||||
)
|
||||
if not config.get(CONF_UNPAIRED_ACCESS, True) and not _has_pairing_method(config):
|
||||
_LOGGER.warning(
|
||||
"'%s' is off but there is no pairing method (%s or a dynamic pairing code), so no "
|
||||
"new server can pair with this device",
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def _request_controller_role(config: ConfigType) -> ConfigType:
|
||||
"""Request the controller role for the sendspin.switch action."""
|
||||
request_controller_support()
|
||||
return config
|
||||
|
||||
|
||||
# Selects the hub. sendspin.switch adds the controller role it needs; the pairing window actions need no role.
|
||||
SENDSPIN_HUB_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SendspinHub),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
SENDSPIN_SIMPLE_ACTION_SCHEMA = cv.All(
|
||||
automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SendspinHub),
|
||||
}
|
||||
)
|
||||
),
|
||||
_request_controller_role,
|
||||
SENDSPIN_HUB_ACTION_SCHEMA, _request_controller_role
|
||||
)
|
||||
|
||||
|
||||
@@ -240,6 +367,19 @@ automation.register_apply_action(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"sendspin.confirm_pairing_window",
|
||||
SENDSPIN_HUB_ACTION_SCHEMA,
|
||||
automation.ApplyCall("confirm_pairing_window()"),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"sendspin.cancel_pairing_window",
|
||||
SENDSPIN_HUB_ACTION_SCHEMA,
|
||||
automation.ApplyCall("cancel_pairing_window()"),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -264,10 +404,27 @@ async def to_code(config: ConfigType) -> None:
|
||||
if value:
|
||||
cg.add(setter(value))
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.8.0")
|
||||
if (code := config.get(CONF_STATIC_PAIRING_CODE)) is not None:
|
||||
cg.add(var.set_static_pairing_code(code))
|
||||
|
||||
cg.add_define("USE_SENDSPIN", True) # for MDNS
|
||||
# The client starts once both are set: here, or by the switch that drives the setting.
|
||||
switch_types = _get_data().switch_types
|
||||
if CONF_ENABLED not in switch_types:
|
||||
cg.add(var.set_enabled(True))
|
||||
if CONF_UNPAIRED_ACCESS not in switch_types:
|
||||
cg.add(var.set_unpaired_access_enabled(config.get(CONF_UNPAIRED_ACCESS, True)))
|
||||
|
||||
if _offers_dynamic_pairing_code(config):
|
||||
cg.add(var.set_pairing_code_display_supported(True))
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.9.3")
|
||||
# esp_websocket_client links esp_tls even for ws:// connections.
|
||||
esp32.request_tls()
|
||||
|
||||
cg.add_define("USE_SENDSPIN", True) # for MDNS and the native API
|
||||
|
||||
# Service starts disabled and the hub enables it; always advertised where unsupported
|
||||
if mdns.request_service_enable_disable():
|
||||
@@ -276,8 +433,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
data = _get_data()
|
||||
|
||||
# The color role is not yet wired up in ESPHome; disable it in the library for now.
|
||||
# The color and source roles are not yet wired up in ESPHome; disable them in the library for now.
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_COLOR", False)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_SOURCE", False)
|
||||
|
||||
# Configure Sendspin roles based on requested features (ESPHome internally via USE_SENDSPIN_*)
|
||||
# and disable building unused code paths in the sendspin-cpp library (IDF SDKConfig via CONFIG_SENDSPIN_ENABLE_*).
|
||||
@@ -351,7 +509,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
("audio_formats", audio_format_structs),
|
||||
("audio_buffer_capacity", player_cfg[CONF_BUFFER_SIZE]),
|
||||
("fixed_delay_us", player_cfg[CONF_FIXED_DELAY]),
|
||||
("initial_static_delay_ms", player_cfg[CONF_INITIAL_STATIC_DELAY]),
|
||||
# The released YAML key name is kept for compatibility.
|
||||
("initial_output_delay_ms", player_cfg[CONF_INITIAL_STATIC_DELAY]),
|
||||
("psram_stack", psram_stack),
|
||||
]
|
||||
if (decode_memory := player_cfg.get(CONF_DECODE_MEMORY)) is not None:
|
||||
@@ -366,6 +525,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
else:
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_PLAYER", False)
|
||||
|
||||
if not data.player_support or CODEC_OPUS not in data.player_config[CONF_CODECS]:
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_OPUS", False)
|
||||
|
||||
if data.visualizer_support:
|
||||
cg.add_define("USE_SENDSPIN_VISUALIZER", True)
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Sendspin image platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import runtime_image
|
||||
@@ -21,7 +23,6 @@ from esphome.types import ConfigType
|
||||
from .. import (
|
||||
CONF_DISPLAY_OFFSET,
|
||||
CONF_SENDSPIN_ID,
|
||||
IMAGE_FORMAT_BMP,
|
||||
IMAGE_FORMAT_JPEG,
|
||||
IMAGE_FORMAT_PNG,
|
||||
IMAGE_SOURCE_ALBUM,
|
||||
@@ -57,9 +58,16 @@ _FORMAT_TO_SENDSPIN_ENUM = {
|
||||
"JPEG": IMAGE_FORMAT_JPEG,
|
||||
"JPG": IMAGE_FORMAT_JPEG,
|
||||
"PNG": IMAGE_FORMAT_PNG,
|
||||
"BMP": IMAGE_FORMAT_BMP,
|
||||
}
|
||||
|
||||
|
||||
# Remove before 2027.4.0
|
||||
def _reject_bmp(value: Any) -> Any:
|
||||
if isinstance(value, str) and value.upper() == "BMP":
|
||||
raise cv.Invalid("BMP artwork is no longer supported, use JPEG or PNG instead.")
|
||||
return value
|
||||
|
||||
|
||||
# The library's SendspinImageSource::NONE is its internal "unset" sentinel; a slot advertising it
|
||||
# would never receive artwork while still paying for two frame buffers, so it is not offered here.
|
||||
IMAGE_SOURCES = {
|
||||
@@ -121,7 +129,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub),
|
||||
# Narrow runtime_image's format list to what the library can request, so the
|
||||
# accepted set and the enum map below cannot drift apart.
|
||||
cv.Required(CONF_FORMAT): cv.one_of(*_FORMAT_TO_SENDSPIN_ENUM, upper=True),
|
||||
cv.Required(CONF_FORMAT): cv.All(
|
||||
_reject_bmp, cv.one_of(*_FORMAT_TO_SENDSPIN_ENUM, upper=True)
|
||||
),
|
||||
cv.Required(CONF_RESIZE): cv.dimensions,
|
||||
cv.Required(CONF_CURRENT_IMAGE): _IMAGE_SCHEMA,
|
||||
cv.Optional(CONF_TRANSITION_IMAGE): _IMAGE_SCHEMA,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "sendspin_image.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_ARTWORK)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_ARTWORK)
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -125,7 +125,7 @@ bool SendspinImageSlot::decode_frame_(const uint8_t *data, size_t length, const
|
||||
}
|
||||
|
||||
// A decode that asked for other dimensions had the buffer taken away from it, so it painted
|
||||
// nothing (or stopped partway). JPEG and BMP report that as an error above; PNG carries on
|
||||
// nothing (or stopped partway). JPEG reports that as an error above; PNG carries on
|
||||
// regardless, so the frame is dropped here.
|
||||
return this->decode_sink_.decoded_into(target);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_ARTWORK)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_ARTWORK)
|
||||
|
||||
#include "esphome/components/image/image.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
@@ -29,7 +29,7 @@ class ArtworkDecodeSink : public runtime_image::RuntimeImage {
|
||||
/// @brief True when the decode ended with the given buffer still in place.
|
||||
///
|
||||
/// An external buffer is dropped rather than resized, so a decode that wanted other dimensions
|
||||
/// leaves the sink holding nothing. The JPEG and BMP decoders report that as a decode error, but
|
||||
/// leaves the sink holding nothing. The JPEG decoder reports that as a decode error, but
|
||||
/// the PNG decoder ignores it and reports success, so the outcome is checked here as well.
|
||||
bool decoded_into(const uint8_t *buffer) const { return this->buffer_ == buffer; }
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "sendspin_media_player.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER)
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER)
|
||||
|
||||
#include "esphome/components/media_player/media_player.h"
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
|
||||
@@ -11,7 +11,9 @@ from esphome.const import (
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CODEC_FLAC,
|
||||
CODEC_OPUS,
|
||||
CODEC_PCM,
|
||||
CODECS,
|
||||
CONF_CODECS,
|
||||
CONF_DECODE_MEMORY,
|
||||
@@ -58,6 +60,11 @@ def _resolve_codecs(config: ConfigType) -> ConfigType:
|
||||
f"Codec '{CODEC_OPUS}' requires a {CONF_SAMPLE_RATE} of {OPUS_SAMPLE_RATE}",
|
||||
path=[CONF_CODECS],
|
||||
)
|
||||
if CODEC_FLAC not in codecs and CODEC_PCM not in codecs:
|
||||
raise cv.Invalid(
|
||||
f"Codecs must include '{CODEC_FLAC}' or '{CODEC_PCM}'",
|
||||
path=[CONF_CODECS],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "sendspin_media_source.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER)
|
||||
|
||||
#include "esphome/components/audio/audio.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -24,7 +24,7 @@ void SendspinMediaSource::setup() {
|
||||
// Push cached states to player role. They may have been set before setup() ran.
|
||||
this->player_role_->update_volume(std::roundf(this->cached_volume_ * 100.0f));
|
||||
this->player_role_->update_muted(this->cached_muted_);
|
||||
this->player_role_->set_static_delay_adjustable(this->static_delay_adjustable_);
|
||||
this->player_role_->set_output_delay_adjustable(this->static_delay_adjustable_);
|
||||
}
|
||||
|
||||
void SendspinMediaSource::dump_config() {
|
||||
@@ -35,7 +35,7 @@ void SendspinMediaSource::dump_config() {
|
||||
void SendspinMediaSource::set_static_delay_adjustable(bool adjustable) {
|
||||
this->static_delay_adjustable_ = adjustable;
|
||||
if (this->player_role_) {
|
||||
this->player_role_->set_static_delay_adjustable(adjustable);
|
||||
this->player_role_->set_output_delay_adjustable(adjustable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ void SendspinMediaSource::handle_command(media_source::MediaSourceCommand comman
|
||||
if (!this->pending_start_) {
|
||||
// Ignore stop commands if we have a pending start, since the orchestrator may send a stop command before
|
||||
// play_uri
|
||||
ESP_LOGD(TAG, "Received STOP command, updating Sendspin state to EXTERNAL_SOURCE");
|
||||
this->parent_->update_state(sendspin::SendspinClientState::EXTERNAL_SOURCE);
|
||||
ESP_LOGD(TAG, "Received STOP command, leaving the Sendspin group");
|
||||
this->parent_->leave_group();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -185,8 +185,6 @@ size_t SendspinMediaSource::on_audio_write(uint8_t *data, size_t length, uint32_
|
||||
|
||||
// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback)
|
||||
void SendspinMediaSource::on_stream_start() {
|
||||
this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED);
|
||||
|
||||
if (!this->pending_start_) {
|
||||
// Dedup rapid on_stream_start() calls
|
||||
this->pending_start_ = true;
|
||||
@@ -211,4 +209,4 @@ void SendspinMediaSource::on_mute_changed(bool muted) { this->request_mute_(mute
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32 && USE_SENDSPIN_PLAYER && USE_SENDSPIN_CONTROLLER
|
||||
#endif // USE_ESP_IDF && USE_SENDSPIN_PLAYER && USE_SENDSPIN_CONTROLLER
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER)
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "sendspin_hub.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#ifdef USE_ETHERNET
|
||||
@@ -17,26 +17,122 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.hub";
|
||||
|
||||
namespace {
|
||||
|
||||
// The reason passed to on_pairing_failed automations, in the protocol's own spelling.
|
||||
StringRef pair_abort_reason_to_string(sendspin::SendspinPairAbortReason reason) {
|
||||
using sendspin::SendspinPairAbortReason;
|
||||
switch (reason) {
|
||||
case SendspinPairAbortReason::ATTEMPT_TIMEOUT:
|
||||
return StringRef::from_lit("attempt_timeout");
|
||||
case SendspinPairAbortReason::CONCURRENT_ATTEMPT:
|
||||
return StringRef::from_lit("concurrent_attempt");
|
||||
case SendspinPairAbortReason::METHOD_NOT_SUPPORTED:
|
||||
return StringRef::from_lit("method_not_supported");
|
||||
case SendspinPairAbortReason::PAIRING_CODE_MISMATCH:
|
||||
return StringRef::from_lit("pairing_code_mismatch");
|
||||
case SendspinPairAbortReason::USER_CANCELLED:
|
||||
return StringRef::from_lit("user_cancelled");
|
||||
case SendspinPairAbortReason::UNKNOWN:
|
||||
break;
|
||||
}
|
||||
return StringRef::from_lit("unknown");
|
||||
}
|
||||
|
||||
// Zeroes through a volatile pointer so dead-store elimination cannot drop it.
|
||||
void secure_wipe(void *data, size_t len) {
|
||||
volatile auto *p = static_cast<volatile uint8_t *>(data);
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
p[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Call once per key, from setup().
|
||||
ESPPreferenceObject make_blob_pref(const std::string &name, size_t size) {
|
||||
return global_preferences->make_preference(size, fnv1a_hash("sendspin_" + name));
|
||||
}
|
||||
|
||||
// Loads straight into the returned buffer, which the library wipes after use. A failed load can
|
||||
// leave part of a stored private key or PSK behind, so that path wipes it here. This only covers
|
||||
// this buffer: the preference backend keeps its own copy of a pending write until it syncs.
|
||||
std::optional<std::vector<uint8_t>> read_blob(ESPPreferenceObject &pref, size_t size) {
|
||||
std::vector<uint8_t> out(size);
|
||||
if (!pref.load(out.data(), out.size())) {
|
||||
secure_wipe(out.data(), out.size());
|
||||
return std::nullopt;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The library always writes a key's fixed size; any other length is misuse and is not stored.
|
||||
bool write_blob(ESPPreferenceObject &pref, const char *key, size_t size, const uint8_t *data, size_t len) {
|
||||
if (len != size) {
|
||||
ESP_LOGW(TAG, "\"%s\" blob of %zu bytes does not match its size of %zu; rejecting write", key, len, size);
|
||||
return false;
|
||||
}
|
||||
if (!pref.save(data, len)) {
|
||||
ESP_LOGW(TAG, "Failed to persist \"%s\" blob (%zu bytes)", key, len);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// "rec_<n>" to n; nullopt for any other key or a slot with no storage here.
|
||||
std::optional<size_t> parse_record_slot(const std::string &key) {
|
||||
const char *const prefix = sendspin::persistence_keys::RECORD_SLOT_PREFIX;
|
||||
const size_t prefix_len = std::strlen(prefix);
|
||||
if (key.size() <= prefix_len || key.compare(0, prefix_len, prefix) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
size_t slot = 0;
|
||||
for (size_t i = prefix_len; i < key.size(); i++) {
|
||||
const char c = key[i];
|
||||
if (c < '0' || c > '9') {
|
||||
return std::nullopt;
|
||||
}
|
||||
slot = slot * 10 + static_cast<size_t>(c - '0');
|
||||
if (slot >= SENDSPIN_RECORD_SLOTS) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
// Indexed by the library enums, which start at zero and are contiguous.
|
||||
static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"};
|
||||
static const char *const IMAGE_FORMAT_NAMES[] = {"JPEG", "PNG", "BMP"};
|
||||
static const char *const IMAGE_FORMAT_NAMES[] = {"JPEG", "PNG"};
|
||||
#endif
|
||||
|
||||
SendspinHub *global_sendspin_hub = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
SendspinHub::SendspinHub() { global_sendspin_hub = this; }
|
||||
|
||||
void SendspinHub::setup() {
|
||||
auto config = this->build_client_config_();
|
||||
this->client_ = std::make_unique<sendspin::SendspinClient>(std::move(config));
|
||||
|
||||
// Set up persistence (preferences must be initialized before providers are added to the client)
|
||||
this->last_played_server_pref_ =
|
||||
global_preferences->make_preference<LastPlayedServerPref>(fnv1a_hash("sendspin_last_played"));
|
||||
#ifdef USE_SENDSPIN_PLAYER
|
||||
this->static_delay_pref_ = global_preferences->make_preference<StaticDelayPref>(fnv1a_hash("sendspin_static_delay"));
|
||||
#endif
|
||||
// Set up persistence (preferences must be initialized before providers are added to the client). These key names
|
||||
// are frozen; see PREFERENCE KEYS in sendspin_hub.h.
|
||||
namespace keys = sendspin::persistence_keys;
|
||||
this->keypair_pref_ = make_blob_pref("keypair", keys::KEYPAIR_SIZE);
|
||||
this->pairing_psk_pref_ = make_blob_pref("pair_psk", keys::PAIRING_PSK_SIZE);
|
||||
this->last_played_pref_ = make_blob_pref("last_played", keys::LAST_PLAYED_SIZE);
|
||||
// Released firmware's name, so a calibrated delay survives.
|
||||
this->output_delay_pref_ = make_blob_pref("static_delay", keys::OUTPUT_DELAY_SIZE);
|
||||
for (size_t slot = 0; slot < SENDSPIN_RECORD_SLOTS; slot++) {
|
||||
this->record_slot_prefs_[slot] = make_blob_pref(keys::record_slot_key(slot), keys::RECORD_SLOT_SIZE);
|
||||
}
|
||||
this->record_order_pref_ = make_blob_pref(keys::RECORD_ORDER, SENDSPIN_RECORD_SLOTS);
|
||||
|
||||
// Wire providers and client listener
|
||||
this->client_->set_listener(this);
|
||||
@@ -62,14 +158,15 @@ void SendspinHub::setup() {
|
||||
this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
|
||||
#endif
|
||||
|
||||
#ifndef USE_SENDSPIN_SWITCH
|
||||
this->enabled_ = true;
|
||||
#endif
|
||||
// Set before setup() by codegen; an unpaired access switch sets it later, from its own setup().
|
||||
if (this->unpaired_access_.has_value()) {
|
||||
this->client_->set_unpaired_access_enabled(*this->unpaired_access_);
|
||||
}
|
||||
}
|
||||
|
||||
void SendspinHub::loop() {
|
||||
if (this->enabled_.has_value() && this->enabled_.value() != this->client_->is_started() &&
|
||||
!this->status_has_error()) {
|
||||
if (this->enabled_.has_value() && this->unpaired_access_.has_value() &&
|
||||
this->enabled_.value() != this->client_->is_started() && !this->status_has_error()) {
|
||||
if (!this->enabled_.value()) {
|
||||
this->client_->stop();
|
||||
} else if (!this->client_->start()) {
|
||||
@@ -83,17 +180,37 @@ void SendspinHub::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Sends each server a goodbye and writes what the client still owed its provider. Wi-Fi sets up
|
||||
// before this hub, so it shuts down after it and the goodbyes can still go out.
|
||||
void SendspinHub::on_shutdown() {
|
||||
// Keeps loop() from starting the client again.
|
||||
this->enabled_ = false;
|
||||
if (this->is_client_running()) {
|
||||
this->client_->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void SendspinHub::dump_config() {
|
||||
// client_id exists only once start() has run.
|
||||
const char *client_id = "(unavailable)";
|
||||
if (this->client_ != nullptr && !this->client_->client_id().empty()) {
|
||||
client_id = this->client_->client_id().c_str();
|
||||
}
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Sendspin Hub:\n"
|
||||
" Client ID: %s\n"
|
||||
" MAC address: %s\n"
|
||||
" Manufacturer: %s\n"
|
||||
" Model: %s\n"
|
||||
" Firmware version: %s\n"
|
||||
" Task stack in PSRAM: %s",
|
||||
get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
|
||||
this->firmware_version_, YESNO(this->task_stack_in_psram_));
|
||||
" Task stack in PSRAM: %s\n"
|
||||
" Unpaired access: %s\n"
|
||||
" Pairing code method: %s",
|
||||
client_id, get_mac_address_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
|
||||
this->firmware_version_, YESNO(this->task_stack_in_psram_),
|
||||
YESNO(this->client_ != nullptr && this->client_->is_unpaired_access_enabled()),
|
||||
this->pairing_code_method_());
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
// Slot indices come from the order the image platform entries were declared, so the log is the
|
||||
@@ -108,7 +225,7 @@ void SendspinHub::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
|
||||
void SendspinHub::set_enabled(bool enabled) {
|
||||
if (this->status_has_error()) {
|
||||
ESP_LOGE(TAG, "Cannot %s: Sendspin failed to start, reboot to retry",
|
||||
@@ -118,6 +235,23 @@ void SendspinHub::set_enabled(bool enabled) {
|
||||
this->enabled_ = enabled;
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop
|
||||
std::optional<std::string> SendspinHub::get_pairing_token() const {
|
||||
if (this->client_ == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return this->client_->pairing_token();
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
|
||||
void SendspinHub::set_unpaired_access_enabled(bool enabled) {
|
||||
this->unpaired_access_ = enabled;
|
||||
// Before setup() there is no client yet; setup() applies the stored value.
|
||||
if (this->client_ != nullptr) {
|
||||
this->client_->set_unpaired_access_enabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinHub::update_mdns_service_() {
|
||||
@@ -154,22 +288,58 @@ void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason)
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::update_state(sendspin::SendspinClientState state) {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->update_state(state);
|
||||
void SendspinHub::leave_group() {
|
||||
if (this->is_client_running() &&
|
||||
this->client_->get_group_state().playback_state == sendspin::SendspinPlaybackState::PLAYING) {
|
||||
this->client_->leave();
|
||||
}
|
||||
}
|
||||
|
||||
const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
// The server matches client_id against the L2 source MAC of the device's multicast traffic.
|
||||
// THREAD CONTEXT: Main loop (invoked from the sendspin.confirm_pairing_window action)
|
||||
void SendspinHub::confirm_pairing_window() {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->confirm_pairing_window();
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from the sendspin.cancel_pairing_window action)
|
||||
void SendspinHub::cancel_pairing_window() {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->cancel_pairing_window();
|
||||
}
|
||||
}
|
||||
|
||||
const char *SendspinHub::get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
// The server matches this MAC against the L2 source MAC of the device's multicast traffic.
|
||||
// ESP-IDF derives the ethernet MAC as base+3 by default on ESP32-S3, so we cannot use the
|
||||
// eFuse base MAC when ethernet is the active interface.
|
||||
#ifdef USE_ETHERNET
|
||||
if (ethernet::global_eth_component != nullptr) {
|
||||
return ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf);
|
||||
ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf);
|
||||
} else {
|
||||
get_mac_address_pretty_into_buffer(buf);
|
||||
}
|
||||
#else
|
||||
get_mac_address_pretty_into_buffer(buf);
|
||||
#endif
|
||||
return get_mac_address_pretty_into_buffer(buf);
|
||||
// The pretty format is uppercase, but SendspinClientConfig::mac_address must be lowercase.
|
||||
for (char &c : buf) {
|
||||
if (c >= 'A' && c <= 'F') {
|
||||
c += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
// Validation rejects a static code together with the dynamic code, so at most one is set.
|
||||
const char *SendspinHub::pairing_code_method_() const {
|
||||
if (this->pairing_code_display_supported_) {
|
||||
return LOG_STR_LITERAL("dynamic");
|
||||
}
|
||||
if (this->static_pairing_code_ != nullptr) {
|
||||
return LOG_STR_LITERAL("static");
|
||||
}
|
||||
return LOG_STR_LITERAL("none");
|
||||
}
|
||||
|
||||
const char *SendspinHub::get_product_name_() const {
|
||||
@@ -180,12 +350,26 @@ sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
|
||||
sendspin::SendspinClientConfig config;
|
||||
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
|
||||
config.mac_address = SendspinHub::get_mac_address_into_buffer(mac_buf);
|
||||
config.name = App.get_friendly_name();
|
||||
config.product_name = this->get_product_name_();
|
||||
config.manufacturer = this->manufacturer_;
|
||||
config.software_version = this->firmware_version_;
|
||||
config.httpd_psram_stack = this->task_stack_in_psram_;
|
||||
config.protocol_task_psram_stack = this->task_stack_in_psram_;
|
||||
config.max_pairing_records = SENDSPIN_RECORD_SLOTS;
|
||||
|
||||
// The dynamic code needs a channel and a format. Only digits, since automations get the bare string and could not
|
||||
// tell a QR code token apart.
|
||||
if (this->pairing_code_display_supported_) {
|
||||
config.pairing_code_out_channels = {sendspin::SendspinPairingCodeChannel::DISPLAY};
|
||||
config.pairing_code_formats = {sendspin::SendspinPairingCodeFormat::DIGITS};
|
||||
}
|
||||
|
||||
if (this->static_pairing_code_ != nullptr) {
|
||||
config.static_pairing_code_locations = {"operator"};
|
||||
config.static_pairing_code = this->static_pairing_code_;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -215,35 +399,77 @@ void SendspinHub::on_release_high_performance() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void SendspinHub::on_open_pairing_window() { this->open_pairing_window_callbacks_.call(); }
|
||||
|
||||
void SendspinHub::on_close_pairing_window() { this->close_pairing_window_callbacks_.call(); }
|
||||
|
||||
// Only digits are offered, and the library refuses an activation in any other format.
|
||||
void SendspinHub::on_display_pairing_code(const std::string &code, sendspin::SendspinPairingCodeFormat /*format*/) {
|
||||
this->display_pairing_code_callbacks_.call(code);
|
||||
}
|
||||
|
||||
void SendspinHub::on_clear_pairing_code() { this->clear_pairing_code_callbacks_.call(); }
|
||||
|
||||
void SendspinHub::on_pairing_succeeded(const std::string &server_id) {
|
||||
this->pairing_succeeded_callbacks_.call(server_id);
|
||||
}
|
||||
|
||||
void SendspinHub::on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) {
|
||||
this->pairing_failed_callbacks_.call(server_id, pair_abort_reason_to_string(reason));
|
||||
}
|
||||
|
||||
// --- SendspinNetworkProvider override ---
|
||||
|
||||
// THREAD CONTEXT: Main loop (polled by client_->loop())
|
||||
// THREAD CONTEXT: Main loop (polled by start() and client_->loop())
|
||||
bool SendspinHub::is_network_ready() { return network::is_connected(); }
|
||||
|
||||
// --- SendspinPersistenceProvider overrides ---
|
||||
// THREAD CONTEXT: Main loop (the library makes every provider call there, including from start() and stop())
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events)
|
||||
bool SendspinHub::save_last_server_hash(uint32_t hash) {
|
||||
LastPlayedServerPref pref{.server_id_hash = hash};
|
||||
bool ok = this->last_played_server_pref_.save(&pref);
|
||||
if (ok) {
|
||||
ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to persist last played server hash");
|
||||
std::pair<ESPPreferenceObject *, size_t> SendspinHub::pref_for_key_(const std::string &key) {
|
||||
namespace keys = sendspin::persistence_keys;
|
||||
if (auto slot = parse_record_slot(key); slot.has_value()) {
|
||||
return {&this->record_slot_prefs_[*slot], keys::RECORD_SLOT_SIZE};
|
||||
}
|
||||
return ok;
|
||||
if (key == keys::RECORD_ORDER) {
|
||||
return {&this->record_order_pref_, SENDSPIN_RECORD_SLOTS};
|
||||
}
|
||||
if (key == keys::KEYPAIR) {
|
||||
return {&this->keypair_pref_, keys::KEYPAIR_SIZE};
|
||||
}
|
||||
if (key == keys::PAIRING_PSK) {
|
||||
return {&this->pairing_psk_pref_, keys::PAIRING_PSK_SIZE};
|
||||
}
|
||||
if (key == keys::LAST_PLAYED) {
|
||||
return {&this->last_played_pref_, keys::LAST_PLAYED_SIZE};
|
||||
}
|
||||
if (key == keys::OUTPUT_DELAY) {
|
||||
return {&this->output_delay_pref_, keys::OUTPUT_DELAY_SIZE};
|
||||
}
|
||||
return {nullptr, 0};
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events)
|
||||
std::optional<uint32_t> SendspinHub::load_last_server_hash() {
|
||||
LastPlayedServerPref pref{};
|
||||
if (this->last_played_server_pref_.load(&pref)) {
|
||||
ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash);
|
||||
return pref.server_id_hash;
|
||||
std::optional<std::vector<uint8_t>> SendspinHub::load_blob(const std::string &key) {
|
||||
auto [pref, size] = this->pref_for_key_(key);
|
||||
if (pref == nullptr) {
|
||||
ESP_LOGW(TAG, "load_blob: unknown key \"%s\"", key.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
return read_blob(*pref, size);
|
||||
}
|
||||
|
||||
bool SendspinHub::save_blob(const std::string &key, const uint8_t *data, size_t len) {
|
||||
auto [pref, size] = this->pref_for_key_(key);
|
||||
if (pref == nullptr) {
|
||||
ESP_LOGW(TAG, "save_blob: unknown key \"%s\"", key.c_str());
|
||||
return false;
|
||||
}
|
||||
return write_blob(*pref, key.c_str(), size, data, len);
|
||||
}
|
||||
|
||||
// Writes every component's queued preferences, so a failure may belong to another component.
|
||||
bool SendspinHub::commit() { return global_preferences->sync(); }
|
||||
|
||||
// --- Sendspin role specific methods/overrides ---
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -285,11 +511,7 @@ void SendspinHub::send_client_command(sendspin::SendspinControllerCommand comman
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from the sendspin.switch action)
|
||||
void SendspinHub::switch_client() {
|
||||
// Clear any EXTERNAL_SOURCE state so the switch command is followed
|
||||
this->update_state(sendspin::SendspinClientState::SYNCHRONIZED);
|
||||
this->send_client_command(sendspin::SendspinControllerCommand::SWITCH);
|
||||
}
|
||||
void SendspinHub::switch_client() { this->send_client_command(sendspin::SendspinControllerCommand::SWITCH); }
|
||||
|
||||
// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
|
||||
void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) {
|
||||
@@ -332,31 +554,8 @@ sendspin::PlayerRole *SendspinHub::get_player_role() {
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override)
|
||||
bool SendspinHub::save_static_delay(uint16_t delay_ms) {
|
||||
StaticDelayPref pref{.delay_ms = delay_ms};
|
||||
bool ok = this->static_delay_pref_.save(&pref);
|
||||
if (ok) {
|
||||
ESP_LOGD(TAG, "Persisted static delay: %u ms", delay_ms);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to persist static delay");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override)
|
||||
std::optional<uint16_t> SendspinHub::load_static_delay() {
|
||||
StaticDelayPref pref{};
|
||||
if (this->static_delay_pref_.load(&pref)) {
|
||||
ESP_LOGI(TAG, "Loaded static delay: %u ms", pref.delay_ms);
|
||||
return pref.delay_ms;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ESP_IDF
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
@@ -31,9 +32,15 @@
|
||||
#include <sendspin/player_role.h>
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
@@ -44,22 +51,21 @@ namespace esphome::sendspin_ {
|
||||
/// one step later than hub so they can assume hub's setup() has already completed.
|
||||
namespace sendspin_priority {
|
||||
// AFTER_WIFI so the hub runs after the wifi/ethernet drivers are up and we can read the active
|
||||
// interface's MAC for client_id.
|
||||
// interface's MAC address for the device info.
|
||||
inline constexpr float HUB = esphome::setup_priority::AFTER_WIFI;
|
||||
inline constexpr float CHILD = HUB - 1.0f;
|
||||
} // namespace sendspin_priority
|
||||
|
||||
/// @brief Persistent storage structure for last played server hash.
|
||||
struct LastPlayedServerPref {
|
||||
uint32_t server_id_hash;
|
||||
};
|
||||
// PREFERENCE KEYS: each library persistence key has its own preference, hashed from "sendspin_" + name. Renaming
|
||||
// one erases it.
|
||||
|
||||
#ifdef USE_SENDSPIN_PLAYER
|
||||
/// @brief Persistent storage structure for player static delay.
|
||||
struct StaticDelayPref {
|
||||
uint16_t delay_ms;
|
||||
/// Pairing-record slots, handed to the client as max_pairing_records.
|
||||
inline constexpr size_t SENDSPIN_RECORD_SLOTS = sendspin::SendspinClientConfig::DEFAULT_MAX_PAIRING_RECORDS;
|
||||
|
||||
/// Feature flags reported to API clients in DeviceCapabilitiesResponse.
|
||||
enum SendspinFeature : uint32_t {
|
||||
SENDSPIN_FEATURE_PAIRING_TOKEN = 1 << 0,
|
||||
};
|
||||
#endif
|
||||
|
||||
/// @brief Thin adapter over sendspin::SendspinClient.
|
||||
///
|
||||
@@ -90,9 +96,12 @@ class SendspinHub final : public Component,
|
||||
public sendspin::SendspinNetworkProvider,
|
||||
public sendspin::SendspinPersistenceProvider {
|
||||
public:
|
||||
SendspinHub();
|
||||
|
||||
float get_setup_priority() const override { return sendspin_priority::HUB; }
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void on_shutdown() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// @brief Connects the underlying client to the given Sendspin server.
|
||||
@@ -113,14 +122,22 @@ class SendspinHub final : public Component,
|
||||
/// - `USER_REQUEST`: user explicitly requested disconnect.
|
||||
void disconnect_from_server(sendspin::SendspinGoodbyeReason reason);
|
||||
|
||||
/// @brief Updates the client's reported playback state on the server.
|
||||
/// @brief Leaves the current group so another source can use the speaker.
|
||||
///
|
||||
/// Sends `client/leave` while the group is playing. A stopped group is left alone, so the device stays grouped.
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
void leave_group();
|
||||
|
||||
/// @brief Confirms a pairing attempt on the device. With no attempt waiting, it opens the pairing window for the
|
||||
/// next one.
|
||||
///
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
/// @param state New client state:
|
||||
/// - `SYNCHRONIZED`: client is synchronized and playing from the server.
|
||||
/// - `ERROR`: client encountered a playback error.
|
||||
/// - `EXTERNAL_SOURCE`: client is playing from a non-Sendspin source.
|
||||
void update_state(sendspin::SendspinClientState state);
|
||||
void confirm_pairing_window();
|
||||
|
||||
/// @brief Closes an open pairing window, so a waiting pairing attempt is not confirmed.
|
||||
///
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
void cancel_pairing_window();
|
||||
|
||||
// --- Configuration setters (called from codegen) ---
|
||||
|
||||
@@ -128,19 +145,59 @@ class SendspinHub final : public Component,
|
||||
this->group_update_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_open_pairing_window_callback(F &&callback) {
|
||||
this->open_pairing_window_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_close_pairing_window_callback(F &&callback) {
|
||||
this->close_pairing_window_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_display_pairing_code_callback(F &&callback) {
|
||||
this->display_pairing_code_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_clear_pairing_code_callback(F &&callback) {
|
||||
this->clear_pairing_code_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_pairing_succeeded_callback(F &&callback) {
|
||||
this->pairing_succeeded_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_pairing_failed_callback(F &&callback) {
|
||||
this->pairing_failed_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
/// @brief Requests the Sendspin client, including the server, the roles and the mDNS advertisement, to start or
|
||||
/// stop.
|
||||
///
|
||||
/// Applied from the hub's loop(). Stopping blocks until the client is fully stopped; the roles' clear callbacks
|
||||
/// fire from inside that call. With a sendspin switch configured the client stays stopped until the switch has
|
||||
/// called this once. Must be called from the main loop thread.
|
||||
/// fire from inside that call. The client stays stopped until this has been called once, by codegen or by the
|
||||
/// enabled switch. Must be called from the main loop thread.
|
||||
void set_enabled(bool enabled);
|
||||
|
||||
/// Turns unpaired (Sentinel) access on or off. The client waits for this call, by codegen or by the unpaired
|
||||
/// access switch, before its first start. May be called before setup(). Main loop only.
|
||||
void set_unpaired_access_enabled(bool enabled);
|
||||
|
||||
/// @brief Returns whether the Sendspin client is running.
|
||||
bool is_client_running() const { return this->client_ != nullptr && this->client_->is_started(); }
|
||||
|
||||
/// @brief Returns whether the client has been turned off with set_enabled().
|
||||
bool is_disabled() const { return this->enabled_.has_value() && !this->enabled_.value(); }
|
||||
|
||||
/// @brief Returns the SendspinFeature flags reported to API clients.
|
||||
uint32_t get_feature_flags() const { return SENDSPIN_FEATURE_PAIRING_TOKEN; }
|
||||
|
||||
/// @brief Returns the pairing token a Sendspin server uses to pair with this device.
|
||||
///
|
||||
/// The token is a long-lived secret, so this component never logs it. Returns std::nullopt until the client has
|
||||
/// started once; it stays available after the client stops. Main loop only.
|
||||
std::optional<std::string> get_pairing_token() const;
|
||||
|
||||
/// @brief Sets the device information reported to the server in the `client/hello` message.
|
||||
///
|
||||
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
|
||||
@@ -154,6 +211,13 @@ class SendspinHub final : public Component,
|
||||
void set_mdns(mdns::MDNSComponent *mdns) { this->mdns_ = mdns; }
|
||||
#endif
|
||||
|
||||
/// The static pairing code, used on every boot.
|
||||
void set_static_pairing_code(const char *code) { this->static_pairing_code_ = code; }
|
||||
|
||||
/// Set when on_display_pairing_code or a pairing_code text sensor is configured, so the device offers the dynamic
|
||||
/// pairing code.
|
||||
void set_pairing_code_display_supported(bool supported) { this->pairing_code_display_supported_ = supported; }
|
||||
|
||||
// --- Sendspin role specific methods ---
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -219,12 +283,18 @@ class SendspinHub final : public Component,
|
||||
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
|
||||
sendspin::SendspinClientConfig build_client_config_();
|
||||
|
||||
/// The preference a library key is stored in and its blob size, or {nullptr, 0} for a key with
|
||||
/// no storage here.
|
||||
std::pair<ESPPreferenceObject *, size_t> pref_for_key_(const std::string &key);
|
||||
|
||||
/// @brief Returns the product name reported to the server: the configured model, or the device name.
|
||||
const char *get_product_name_() const;
|
||||
|
||||
/// @brief Writes the active network interface's MAC into @p buf and returns its data pointer.
|
||||
const char *pairing_code_method_() const;
|
||||
|
||||
/// @brief Writes the active network interface's MAC, in lowercase, into @p buf and returns its data pointer.
|
||||
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
|
||||
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
static const char *get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
/// @brief Keeps the `_sendspin` mDNS service advertised while the client is running.
|
||||
@@ -238,12 +308,26 @@ class SendspinHub final : public Component,
|
||||
|
||||
void on_release_high_performance() override;
|
||||
|
||||
void on_open_pairing_window() override;
|
||||
|
||||
void on_close_pairing_window() override;
|
||||
|
||||
void on_display_pairing_code(const std::string &code, sendspin::SendspinPairingCodeFormat format) override;
|
||||
|
||||
void on_clear_pairing_code() override;
|
||||
|
||||
void on_pairing_succeeded(const std::string &server_id) override;
|
||||
|
||||
void on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) override;
|
||||
|
||||
// --- SendspinNetworkProvider override ---
|
||||
bool is_network_ready() override;
|
||||
|
||||
// --- SendspinPersistenceProvider overrides ---
|
||||
bool save_last_server_hash(uint32_t hash) override;
|
||||
std::optional<uint32_t> load_last_server_hash() override;
|
||||
// The library calls commit() after writing pairing secrets, so they reach flash right away.
|
||||
std::optional<std::vector<uint8_t>> load_blob(const std::string &key) override;
|
||||
bool save_blob(const std::string &key, const uint8_t *data, size_t len) override;
|
||||
bool commit() override;
|
||||
|
||||
// --- Sendspin role specific methods/overrides/member variables ---
|
||||
|
||||
@@ -291,29 +375,41 @@ class SendspinHub final : public Component,
|
||||
#ifdef USE_SENDSPIN_PLAYER
|
||||
sendspin::PlayerRoleListener *player_listener_{nullptr};
|
||||
sendspin::PlayerRoleConfig player_config_{};
|
||||
|
||||
// Part of SendspinPersistenceProvider overrides
|
||||
ESPPreferenceObject static_delay_pref_;
|
||||
std::optional<uint16_t> load_static_delay() override;
|
||||
bool save_static_delay(uint16_t delay_ms) override;
|
||||
#endif
|
||||
|
||||
// --- Core member variables ---
|
||||
|
||||
ESPPreferenceObject last_played_server_pref_;
|
||||
// Built once in setup(): make_preference() allocates a backend that is never freed.
|
||||
ESPPreferenceObject keypair_pref_;
|
||||
ESPPreferenceObject pairing_psk_pref_;
|
||||
ESPPreferenceObject last_played_pref_;
|
||||
ESPPreferenceObject output_delay_pref_;
|
||||
std::array<ESPPreferenceObject, SENDSPIN_RECORD_SLOTS> record_slot_prefs_;
|
||||
ESPPreferenceObject record_order_pref_;
|
||||
|
||||
std::unique_ptr<sendspin::SendspinClient> client_;
|
||||
|
||||
// Callback fan-out to child components
|
||||
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
|
||||
|
||||
// Lazy: each pairing callback is fed by an optional YAML surface.
|
||||
LazyCallbackManager<void()> open_pairing_window_callbacks_{};
|
||||
LazyCallbackManager<void()> close_pairing_window_callbacks_{};
|
||||
LazyCallbackManager<void(const std::string &)> display_pairing_code_callbacks_{};
|
||||
LazyCallbackManager<void()> clear_pairing_code_callbacks_{};
|
||||
LazyCallbackManager<void(const std::string &)> pairing_succeeded_callbacks_{};
|
||||
LazyCallbackManager<void(const std::string &, StringRef)> pairing_failed_callbacks_{};
|
||||
|
||||
const char *static_pairing_code_{nullptr}; // Codegen string literal, or nullptr when not configured
|
||||
bool pairing_code_display_supported_{false};
|
||||
bool task_stack_in_psram_{false};
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
bool mdns_advertised_{false}; // Last state requested from mdns
|
||||
#endif
|
||||
|
||||
// Requested client state, applied from loop(). Empty until the switch restores its state.
|
||||
// Requested client state, applied from loop(). The client does not start until both are set.
|
||||
std::optional<bool> enabled_;
|
||||
std::optional<bool> unpaired_access_;
|
||||
|
||||
// Device information sent in the `client/hello` message. Defaults apply when neither the
|
||||
// sendspin configuration nor the project information supplies a value.
|
||||
@@ -347,6 +443,8 @@ class SendspinPollingChild : public PollingComponent, public Parented<SendspinHu
|
||||
float get_setup_priority() const override { return sendspin_priority::CHILD; }
|
||||
};
|
||||
|
||||
extern SendspinHub *global_sendspin_hub; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ESP_IDF
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "sendspin_sensor.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR)
|
||||
|
||||
#include <sendspin/metadata_role.h>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR)
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
@@ -1,35 +1,101 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.const import CONF_PLATFORM, CONF_SWITCH, CONF_TYPE, ENTITY_CATEGORY_CONFIG
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SENDSPIN_ID, SendspinHub, sendspin_ns
|
||||
from .. import (
|
||||
CONF_SENDSPIN_ID,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
DOMAIN,
|
||||
SendspinHub,
|
||||
_has_pairing_method,
|
||||
request_switch,
|
||||
sendspin_ns,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["sendspin"]
|
||||
|
||||
SendspinSwitch = sendspin_ns.class_("SendspinSwitch", switch.Switch, cg.Component)
|
||||
SendspinEnabledSwitch = sendspin_ns.class_(
|
||||
"SendspinEnabledSwitch", switch.Switch, cg.Component
|
||||
)
|
||||
SendspinUnpairedAccessSwitch = sendspin_ns.class_(
|
||||
"SendspinUnpairedAccessSwitch", switch.Switch, cg.Component
|
||||
)
|
||||
|
||||
|
||||
def _switch_schema(class_: cg.MockObjClass) -> cv.Schema:
|
||||
return (
|
||||
switch.switch_schema(
|
||||
class_,
|
||||
block_inverted=True,
|
||||
default_restore_mode="RESTORE_DEFAULT_ON",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def _request_switch(config: ConfigType) -> ConfigType:
|
||||
"""Tell the hub to wait for this switch before the client's first start."""
|
||||
request_switch(config[CONF_TYPE])
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.typed_schema(
|
||||
{
|
||||
CONF_ENABLED: switch.switch_schema(
|
||||
SendspinSwitch,
|
||||
block_inverted=True,
|
||||
default_restore_mode="RESTORE_DEFAULT_ON",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
CONF_ENABLED: _switch_schema(SendspinEnabledSwitch),
|
||||
CONF_UNPAIRED_ACCESS: _switch_schema(SendspinUnpairedAccessSwitch),
|
||||
},
|
||||
key=CONF_TYPE,
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
_request_switch,
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
full_config = fv.full_config.get()
|
||||
switch_type = config[CONF_TYPE]
|
||||
same_type = [
|
||||
conf
|
||||
for conf in full_config.get(CONF_SWITCH, [])
|
||||
if conf.get(CONF_PLATFORM) == DOMAIN and conf.get(CONF_TYPE) == switch_type
|
||||
]
|
||||
# Two switches of one type would each drive the same hub setting.
|
||||
if len(same_type) > 1:
|
||||
raise cv.Invalid(f"Only one sendspin '{switch_type}' switch is allowed")
|
||||
if switch_type != CONF_UNPAIRED_ACCESS:
|
||||
return config
|
||||
hub_config = full_config.get(DOMAIN, {})
|
||||
if CONF_UNPAIRED_ACCESS in hub_config:
|
||||
raise cv.Invalid(
|
||||
f"'{DOMAIN}: {CONF_UNPAIRED_ACCESS}' cannot be set together with an "
|
||||
f"{CONF_UNPAIRED_ACCESS} switch; set the switch's restore_mode instead"
|
||||
)
|
||||
if not _has_pairing_method(hub_config):
|
||||
_LOGGER.warning(
|
||||
"The %s switch has no pairing method (%s or a dynamic pairing code), so "
|
||||
"while it is off no new server can pair with this device",
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -1,26 +1,49 @@
|
||||
#include "sendspin_switch.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_SWITCH)
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.switch";
|
||||
|
||||
void SendspinSwitch::setup() {
|
||||
// --- SendspinEnabledSwitch ---
|
||||
|
||||
void SendspinEnabledSwitch::setup() {
|
||||
// The hub waits for this request, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
}
|
||||
|
||||
void SendspinSwitch::dump_config() { LOG_SWITCH("", "Sendspin Switch", this); }
|
||||
void SendspinEnabledSwitch::dump_config() { LOG_SWITCH("", "Sendspin Enabled Switch", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinSwitch::write_state(bool state) {
|
||||
void SendspinEnabledSwitch::write_state(bool state) {
|
||||
this->parent_->set_enabled(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
// --- SendspinUnpairedAccessSwitch ---
|
||||
|
||||
void SendspinUnpairedAccessSwitch::setup() {
|
||||
// The hub holds the client's first start for this, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
}
|
||||
|
||||
void SendspinUnpairedAccessSwitch::dump_config() { LOG_SWITCH("", "Sendspin Unpaired Access Switch", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinUnpairedAccessSwitch::write_state(bool state) {
|
||||
const bool turning_off = !state && this->has_state() && this->state;
|
||||
this->parent_->set_unpaired_access_enabled(state);
|
||||
this->publish_state(state);
|
||||
// Write the change to flash now: losing it in a power cut would reopen unpaired access.
|
||||
if (turning_off) {
|
||||
global_preferences->sync();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_SWITCH)
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
@@ -10,7 +10,19 @@
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
/// @brief Switch that starts and stops the Sendspin client through the hub (see SendspinHub::set_enabled()).
|
||||
class SendspinSwitch final : public switch_::Switch, public SendspinChild {
|
||||
class SendspinEnabledSwitch final : public switch_::Switch, public SendspinChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
/// @brief Switch that turns unpaired (Sentinel) access on and off through the hub (see
|
||||
/// SendspinHub::set_unpaired_access_enabled()). The library does not persist the setting, so the switch's restore
|
||||
/// mode does.
|
||||
class SendspinUnpairedAccessSwitch final : public switch_::Switch, public SendspinChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -21,4 +33,4 @@ class SendspinSwitch final : public switch_::Switch, public SendspinChild {
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif
|
||||
|
||||
@@ -4,16 +4,29 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TYPE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SENDSPIN_ID, SendspinHub, request_metadata_support, sendspin_ns
|
||||
from .. import (
|
||||
CONF_SENDSPIN_ID,
|
||||
SendspinHub,
|
||||
request_metadata_support,
|
||||
request_pairing_code_display_support,
|
||||
sendspin_ns,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["sendspin"]
|
||||
|
||||
CONF_PAIRING_CODE = "pairing_code"
|
||||
|
||||
SendspinTextSensor = sendspin_ns.class_(
|
||||
"SendspinTextSensor",
|
||||
text_sensor.TextSensor,
|
||||
cg.Component,
|
||||
)
|
||||
SendspinPairingCodeTextSensor = sendspin_ns.class_(
|
||||
"SendspinPairingCodeTextSensor",
|
||||
text_sensor.TextSensor,
|
||||
cg.Component,
|
||||
)
|
||||
|
||||
SendspinTextMetadataTypes = sendspin_ns.enum("SendspinTextMetadataTypes", is_class=True)
|
||||
SENDSPIN_TEXT_METADATA_TYPES = {
|
||||
@@ -26,18 +39,29 @@ SENDSPIN_TEXT_METADATA_TYPES = {
|
||||
|
||||
def _request_roles(config: ConfigType) -> ConfigType:
|
||||
"""Request the necessary Sendspin roles for the text sensor."""
|
||||
request_metadata_support()
|
||||
if config[CONF_TYPE] == CONF_PAIRING_CODE:
|
||||
# A display surface for the dynamic code, so advertise dynamic_pairing_code.
|
||||
request_pairing_code_display_support()
|
||||
else:
|
||||
request_metadata_support()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_HUB_ID_SCHEMA = cv.Schema({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
_METADATA_SCHEMA = text_sensor.text_sensor_schema(SendspinTextSensor).extend(
|
||||
_HUB_ID_SCHEMA
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
text_sensor.text_sensor_schema().extend(
|
||||
cv.typed_schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SendspinTextSensor),
|
||||
cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub),
|
||||
cv.Required(CONF_TYPE): cv.enum(SENDSPIN_TEXT_METADATA_TYPES),
|
||||
}
|
||||
**dict.fromkeys(SENDSPIN_TEXT_METADATA_TYPES, _METADATA_SCHEMA),
|
||||
CONF_PAIRING_CODE: text_sensor.text_sensor_schema(
|
||||
SendspinPairingCodeTextSensor
|
||||
).extend(_HUB_ID_SCHEMA),
|
||||
},
|
||||
key=CONF_TYPE,
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
_request_roles,
|
||||
@@ -50,4 +74,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_parented(var, config[CONF_SENDSPIN_ID])
|
||||
await text_sensor.register_text_sensor(var, config)
|
||||
|
||||
cg.add(var.set_metadata_type(config[CONF_TYPE]))
|
||||
if (
|
||||
metadata_type := SENDSPIN_TEXT_METADATA_TYPES.get(config[CONF_TYPE])
|
||||
) is not None:
|
||||
cg.add(var.set_metadata_type(metadata_type))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "sendspin_text_sensor.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_TEXT_SENSOR)
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
#include <sendspin/metadata_role.h>
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -10,6 +12,7 @@ namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.text_sensor";
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
void SendspinTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sendspin", this); }
|
||||
|
||||
// A field is nullopt when the server has not provided it or has explicitly cleared it. Both mean there is nothing to
|
||||
@@ -50,6 +53,16 @@ void SendspinTextSensor::publish_if_changed_(const char *value) {
|
||||
this->publish_state(value);
|
||||
}
|
||||
}
|
||||
#endif // USE_SENDSPIN_METADATA
|
||||
|
||||
void SendspinPairingCodeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Pairing Code", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop. The registered callbacks also fire on the main loop (SendspinHub dispatches them from
|
||||
// its loop()).
|
||||
void SendspinPairingCodeTextSensor::setup() {
|
||||
this->parent_->add_on_display_pairing_code_callback([this](const std::string &code) { this->publish_state(code); });
|
||||
this->parent_->add_on_clear_pairing_code_callback([this]() { this->publish_state(""); });
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_TEXT_SENSOR)
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
#include <sendspin/metadata_role.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
|
||||
enum class SendspinTextMetadataTypes {
|
||||
TITLE,
|
||||
ARTIST,
|
||||
@@ -31,6 +35,13 @@ class SendspinTextSensor final : public SendspinChild, public text_sensor::TextS
|
||||
|
||||
SendspinTextMetadataTypes metadata_type_;
|
||||
};
|
||||
#endif // USE_SENDSPIN_METADATA
|
||||
|
||||
class SendspinPairingCodeTextSensor final : public SendspinChild, public text_sensor::TextSensor {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
#endif
|
||||
|
||||
@@ -15,9 +15,12 @@ static const uint8_t STARTUP_SEQUENCE[][2] PROGMEM = {
|
||||
{0x60, 0x01}, // ADR_PIN_CTRL: output
|
||||
{0x7D, 0x11},
|
||||
{0x7E, 0xFF},
|
||||
{0x00, 0x01},
|
||||
{0x00, 0x01}, // Page 1
|
||||
{0x51, 0x05},
|
||||
{0x00, 0x00},
|
||||
{0x00, 0x00}, // Page 0
|
||||
|
||||
// Additional register configuration
|
||||
{0x53, 0x60}, // ANA_CTRL: for high audio performance use 175kHz bandwidth with Fsw=768kHz
|
||||
};
|
||||
|
||||
static const LogString *model_name() { return LOG_STR("TAS5805M"); }
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
|
||||
namespace esphome::tas58xx {
|
||||
|
||||
// Remainder of the startup sequence, run after the reset. Register 0x00 selects the page.
|
||||
// All writes except starting and last three register writes are exactly the startup sequence from TI PurePath Console.
|
||||
// Registers 0x46, 0x7D, 0x7E, page 1 register 0x51 and Page 2 registers 0x1D, 0x19 are not documented in the datasheet.
|
||||
|
||||
// clang-format off
|
||||
// Remainder of the startup sequence, run after the reset in TAS58xx::init_(). Register 0x00 selects the page.
|
||||
// Based on the TI PurePath Console (PPC) export for "TAS5825M 2-Band DRC&AGL 2.0 96k".
|
||||
// Registers 0x46, 0x7D, 0x7E, page 1 register 0x51 and page 2 registers 0x1D, 0x19 are not documented in datasheet.
|
||||
static const uint8_t STARTUP_SEQUENCE[][2] PROGMEM = {
|
||||
{0x03, 0x00}, // Deep Sleep
|
||||
{0x03, 0x00}, // DEVICE_CTRL_2: deep sleep, the state the PPC sequence starts from
|
||||
{0x7D, 0x11},
|
||||
{0x7E, 0xFF},
|
||||
{0x00, 0x01}, // Page 1
|
||||
@@ -20,15 +18,16 @@ static const uint8_t STARTUP_SEQUENCE[][2] PROGMEM = {
|
||||
{0x19, 0x80},
|
||||
{0x00, 0x00}, // Page 0
|
||||
{0x46, 0x11},
|
||||
{0x02, 0x00}, // DEVICE_CTRL_1 - BD MODE, Damp to PBL MODE
|
||||
{0x53, 0x01}, // ANA_CTRL PWM Phase Control - in phase
|
||||
{0x54, 0x00}, // AGAIN 0dB
|
||||
{0x03, 0x02}, // Hi-Z
|
||||
{0x61, 0x0B}, // GPIO0_SEL - GPIO0 as FAULTZ output
|
||||
{0x60, 0x01}, // GPIO0_OE - Enable GPIO0 as output
|
||||
{0x77, 0x07}, // CBC_CONTROL enabling CBC function for warnings and faults
|
||||
// PPC also writes 0x02 (DEVICE_CTRL_1) and 0x54 (AGAIN) here; init_() writes both from YAML
|
||||
{0x53, 0x01}, // ANA_CTRL: in phase, 100kHz bandwidth for high audio performance with Fsw=384kHz
|
||||
{0x03, 0x02}, // DEVICE_CTRL_2: Hi-Z
|
||||
|
||||
// Additional register configuration
|
||||
// GPIO1 is a dedicated pin on TAS5825M (separate from ADR), used as FAULTZ
|
||||
{0x62, 0x0B}, // GPIO1_SEL: GPIO1 as FAULTZ
|
||||
{0x60, 0x02}, // GPIO_CTRL: GPIO1 is output
|
||||
{0x77, 0x07}, // CBC_CONTROL: enable cycle-by-cycle current limit for warnings and faults
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static const LogString *model_name() { return LOG_STR("TAS5825M"); }
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@0hax"]
|
||||
CODEOWNERS = ["@0hax", "@FredM67"]
|
||||
DOMAIN = "teleinfo"
|
||||
MULTI_CONF = True
|
||||
|
||||
teleinfo_ns = cg.esphome_ns.namespace("teleinfo")
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_FLASH,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOWATT,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MILLIAMP,
|
||||
UNIT_MILLIVOLT,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
@@ -13,19 +35,125 @@ from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, telein
|
||||
|
||||
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
TeleInfoSensor,
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
icon=ICON_FLASH,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
).extend(TELEINFO_LISTENER_SCHEMA)
|
||||
|
||||
def _preset(unit: str, device_class: str, state_class: str) -> dict[str, str]:
|
||||
return {
|
||||
CONF_UNIT_OF_MEASUREMENT: unit,
|
||||
CONF_DEVICE_CLASS: device_class,
|
||||
CONF_STATE_CLASS: state_class,
|
||||
}
|
||||
|
||||
|
||||
_ENERGY = _preset(UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING)
|
||||
_REACTIVE_ENERGY = _preset(
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
)
|
||||
_CURRENT = _preset(UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT)
|
||||
_VOLTAGE = _preset(UNIT_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT)
|
||||
_APPARENT_POWER_VA = _preset(
|
||||
UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_APPARENT_POWER_KVA = _preset(
|
||||
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
|
||||
|
||||
# Legacy defaults, kept for tags without a known preset
|
||||
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
|
||||
|
||||
# Units that are valid for each device class, used to decide whether a preset's
|
||||
# device class can be kept when the user overrides the unit (and vice versa)
|
||||
_COMPATIBLE_UNITS = {
|
||||
DEVICE_CLASS_ENERGY: {UNIT_WATT_HOURS, UNIT_KILOWATT_HOURS},
|
||||
DEVICE_CLASS_REACTIVE_ENERGY: {
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
},
|
||||
DEVICE_CLASS_CURRENT: {UNIT_AMPERE, UNIT_MILLIAMP},
|
||||
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
|
||||
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
|
||||
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
|
||||
}
|
||||
|
||||
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
|
||||
TIC_TAG_CONFIGS = {
|
||||
# Standard mode
|
||||
"EA": _ENERGY,
|
||||
"ER": _REACTIVE_ENERGY,
|
||||
"IRMS": _CURRENT,
|
||||
"URMS": _VOLTAGE,
|
||||
"UMOY": _VOLTAGE,
|
||||
"SINST": _APPARENT_POWER_VA,
|
||||
"SMAX": _APPARENT_POWER_VA,
|
||||
"CC": _POWER,
|
||||
"PREF": _APPARENT_POWER_KVA,
|
||||
"PCOUP": _APPARENT_POWER_KVA,
|
||||
# Historical mode
|
||||
"BASE": _ENERGY,
|
||||
"HCH": _ENERGY,
|
||||
"EJP": _ENERGY,
|
||||
"BBRH": _ENERGY,
|
||||
"IINST": _CURRENT,
|
||||
"IMAX": _CURRENT,
|
||||
"ISOUSC": _CURRENT,
|
||||
"ADPS": _CURRENT,
|
||||
"ADIR": _CURRENT,
|
||||
"PAPP": _APPARENT_POWER_VA,
|
||||
"PMAX": _POWER,
|
||||
}
|
||||
|
||||
|
||||
def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
"""Apply preset configurations based on the tag name.
|
||||
|
||||
Only keys not set by the user are filled in. For known tags, a preset unit or
|
||||
device class is dropped if it is incompatible with the one set by the user.
|
||||
Unknown tags get the legacy defaults, merged key by key as before.
|
||||
"""
|
||||
if CONF_TAG_NAME not in config:
|
||||
return config
|
||||
|
||||
# Runs before cv.string, so the raw value may not be a string yet
|
||||
tag_name = str(config[CONF_TAG_NAME])
|
||||
preset = next(
|
||||
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
|
||||
None,
|
||||
)
|
||||
|
||||
config = dict(config)
|
||||
if preset is None:
|
||||
for key, value in _LEGACY_DEFAULT.items():
|
||||
config.setdefault(key, value)
|
||||
return config
|
||||
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
|
||||
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
|
||||
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
|
||||
|
||||
for key, value in preset.items():
|
||||
if key in config:
|
||||
continue
|
||||
if key in (CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE_CLASS) and not compatible:
|
||||
continue
|
||||
config[key] = value
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
apply_tag_config,
|
||||
sensor.sensor_schema(
|
||||
TeleInfoSensor,
|
||||
accuracy_decimals=0,
|
||||
).extend(TELEINFO_LISTENER_SCHEMA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID])
|
||||
cg.add(teleinfo.register_teleinfo_listener(var))
|
||||
|
||||
@@ -116,8 +116,7 @@ void TeleInfo::loop() {
|
||||
* 0xa | Tag | 0x9 | Timestamp | 0x9 | 0x9 | CRC | 0xd
|
||||
*
|
||||
*/
|
||||
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) &&
|
||||
((buf_finger - buf_) < buf_index_)) { // NOLINT(clang-diagnostic-sign-compare)
|
||||
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_end - buf_finger)))) {
|
||||
/*
|
||||
* Make sure timesamp is nullified between each tag as some tags don't
|
||||
* have a timestamp
|
||||
@@ -168,8 +167,8 @@ void TeleInfo::loop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Advance buf_finger to end of group */
|
||||
buf_finger += field_len + 1 + 1 + 1;
|
||||
/* Resume after the group's 0xd; grp_end < buf_end, so this stays in bounds. */
|
||||
buf_finger = grp_end + 1;
|
||||
|
||||
publish_value_(std::string(tag_), std::string(val_));
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ from esphome.const import (
|
||||
CONF_SECONDS,
|
||||
CONF_TIMEZONE,
|
||||
CONF_TRIGGER_ID,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
@@ -462,6 +464,12 @@ async def register_time(time_var, config):
|
||||
async def to_code(config):
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("POSIX_CLOCK", True)
|
||||
# From sdk-nrf 2.8.0 newlib time() gets the clock through gettimeofday(),
|
||||
# which Zephyr only provides with XSI_SINGLE_PROCESS. Without it time()
|
||||
# always returns -1.
|
||||
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(2, 8, 0):
|
||||
zephyr_add_prj_conf("POSIX_SINGLE_PROCESS", True)
|
||||
zephyr_add_prj_conf("XSI_SINGLE_PROCESS", True)
|
||||
cg.add_define("USE_TIME")
|
||||
cg.add_global(time_ns.using)
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
/// Software serial RX decoder: on_edge() counts bits from the time between edges;
|
||||
/// finalize() completes a byte whose idle high tail has no closing edge.
|
||||
/// Times are in any steady tick unit.
|
||||
class SoftwareSerialRxDecoder {
|
||||
public:
|
||||
static constexpr uint8_t RX_IDLE = 0xFF;
|
||||
|
||||
/// Configure framing and buffer; drops all state. Follow with reset().
|
||||
void setup(uint32_t bit_ticks, uint8_t data_bits, bool parity, uint8_t stop_bits, uint8_t *buffer,
|
||||
size_t buffer_size) {
|
||||
this->bit_ticks_ = bit_ticks;
|
||||
this->data_bits_ = data_bits;
|
||||
this->stop_bit_ = data_bits + (parity ? 1 : 0);
|
||||
this->max_run_ticks_ = bit_ticks * (this->stop_bit_ + stop_bits + 2);
|
||||
this->buffer_ = buffer;
|
||||
this->buffer_size_ = buffer_size;
|
||||
this->in_pos_ = 0;
|
||||
this->out_pos_ = 0;
|
||||
this->reset(0, true);
|
||||
}
|
||||
|
||||
/// Forget any partial frame; `level` is the current line level.
|
||||
void reset(uint32_t now, bool level) {
|
||||
this->bit_ = RX_IDLE;
|
||||
this->cur_byte_ = 0;
|
||||
this->last_level_ = level;
|
||||
this->last_edge_ = now;
|
||||
}
|
||||
|
||||
/// ISR. True to wake the loop: a byte was pushed, or the line went high in an
|
||||
/// open frame (a data 1 and the idle tail look alike) so finalize() may be needed.
|
||||
bool ESPHOME_ALWAYS_INLINE on_edge(uint32_t now, bool level) {
|
||||
const bool last_level = this->last_level_;
|
||||
// Collapsed edge
|
||||
if (level == last_level)
|
||||
return false;
|
||||
// LX106 has no divider, so count
|
||||
uint32_t delta = now - this->last_edge_;
|
||||
if (delta > this->max_run_ticks_)
|
||||
delta = this->max_run_ticks_;
|
||||
delta += this->bit_ticks_ / 2;
|
||||
uint32_t bits = 0;
|
||||
while (delta >= this->bit_ticks_) {
|
||||
delta -= this->bit_ticks_;
|
||||
bits++;
|
||||
}
|
||||
const bool pushed = this->consume_run_(bits, last_level);
|
||||
this->last_edge_ = now;
|
||||
this->last_level_ = level;
|
||||
return pushed || (level && this->bit_ != RX_IDLE);
|
||||
}
|
||||
|
||||
bool pending() const { return this->bit_ != RX_IDLE && this->last_level_; }
|
||||
|
||||
bool finalize_due(uint32_t now) const {
|
||||
const uint8_t bit = this->bit_;
|
||||
if (bit == RX_IDLE)
|
||||
return false;
|
||||
return now - this->last_edge_ >= this->tail_ticks_(bit);
|
||||
}
|
||||
|
||||
/// Call with the ISR masked.
|
||||
void finalize(uint32_t now) {
|
||||
if (!this->last_level_ || !this->finalize_due(now))
|
||||
return;
|
||||
// Remaining bits are all ones
|
||||
const uint8_t bit = this->bit_;
|
||||
uint8_t cur = this->cur_byte_;
|
||||
if (bit < this->data_bits_)
|
||||
cur |= ((1U << this->data_bits_) - 1) & ~((1U << bit) - 1);
|
||||
this->push_byte(cur);
|
||||
this->bit_ = RX_IDLE;
|
||||
}
|
||||
|
||||
/// Drops the byte when full; holds buffer_size - 1 bytes.
|
||||
bool ESPHOME_ALWAYS_INLINE push_byte(uint8_t data) {
|
||||
size_t in = this->in_pos_;
|
||||
size_t next = in + 1;
|
||||
if (next == this->buffer_size_)
|
||||
next = 0;
|
||||
if (next == this->out_pos_)
|
||||
return false;
|
||||
this->buffer_[in] = data;
|
||||
this->in_pos_ = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t *buffer() const { return this->buffer_; }
|
||||
size_t buffer_size() const { return this->buffer_size_; }
|
||||
|
||||
size_t available() const {
|
||||
// Read volatile in_pos_ once to avoid TOCTOU race with ISR.
|
||||
size_t in = this->in_pos_;
|
||||
if (in >= this->out_pos_)
|
||||
return in - this->out_pos_;
|
||||
return this->buffer_size_ - this->out_pos_ + in;
|
||||
}
|
||||
uint8_t peek_byte() const {
|
||||
if (this->in_pos_ == this->out_pos_)
|
||||
return 0;
|
||||
return this->buffer_[this->out_pos_];
|
||||
}
|
||||
uint8_t read_byte() {
|
||||
if (this->in_pos_ == this->out_pos_)
|
||||
return 0;
|
||||
uint8_t data = this->buffer_[this->out_pos_];
|
||||
size_t next = this->out_pos_ + 1;
|
||||
this->out_pos_ = next == this->buffer_size_ ? 0 : next;
|
||||
return data;
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t tail_ticks_(uint8_t bit) const {
|
||||
return (this->stop_bit_ + 1 - bit) * this->bit_ticks_ + this->bit_ticks_ / 2;
|
||||
}
|
||||
|
||||
bool ESPHOME_ALWAYS_INLINE consume_run_(uint32_t bits, bool level) {
|
||||
uint8_t bit = this->bit_;
|
||||
uint8_t cur = this->cur_byte_;
|
||||
bool pushed = false;
|
||||
while (bits > 0) {
|
||||
if (bit == RX_IDLE) {
|
||||
if (level)
|
||||
break;
|
||||
// Start bit
|
||||
bit = 0;
|
||||
cur = 0;
|
||||
bits--;
|
||||
} else if (bit < this->data_bits_) {
|
||||
uint8_t n = this->data_bits_ - bit;
|
||||
if (n > bits)
|
||||
n = bits;
|
||||
if (level)
|
||||
cur |= ((1U << n) - 1) << bit;
|
||||
bit += n;
|
||||
bits -= n;
|
||||
} else if (bit < this->stop_bit_) {
|
||||
// Parity, not checked
|
||||
bit++;
|
||||
bits--;
|
||||
} else {
|
||||
// Stop bit; low is a framing error
|
||||
if (level)
|
||||
pushed = this->push_byte(cur);
|
||||
bit = RX_IDLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->bit_ = bit;
|
||||
this->cur_byte_ = cur;
|
||||
return pushed;
|
||||
}
|
||||
|
||||
// Members ordered largest to smallest to minimize padding
|
||||
uint32_t bit_ticks_{0};
|
||||
uint32_t max_run_ticks_{0};
|
||||
volatile uint32_t last_edge_{0};
|
||||
uint8_t *buffer_{nullptr};
|
||||
size_t buffer_size_{0};
|
||||
volatile size_t in_pos_{0};
|
||||
size_t out_pos_{0}; // reader only
|
||||
/// Next frame bit (data, parity, then stop at stop_bit_) or RX_IDLE.
|
||||
volatile uint8_t bit_{RX_IDLE};
|
||||
volatile uint8_t cur_byte_{0};
|
||||
volatile bool last_level_{true};
|
||||
uint8_t data_bits_{8};
|
||||
uint8_t stop_bit_{8};
|
||||
};
|
||||
|
||||
} // namespace esphome::uart
|
||||
@@ -10,8 +10,8 @@ namespace esphome::uart {
|
||||
|
||||
static const char *const TAG = "uart";
|
||||
|
||||
// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD
|
||||
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN");
|
||||
// UART parity strings indexed by UARTParityOptions enum (0-4): NONE, EVEN, ODD, MARK, SPACE
|
||||
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "MARK", "SPACE", "UNKNOWN");
|
||||
|
||||
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
|
||||
uint8_t data_bits) {
|
||||
|
||||
@@ -18,6 +18,9 @@ enum UARTParityOptions {
|
||||
UART_CONFIG_PARITY_NONE,
|
||||
UART_CONFIG_PARITY_EVEN,
|
||||
UART_CONFIG_PARITY_ODD,
|
||||
// Only USB serial adapters (usb_uart) support these
|
||||
UART_CONFIG_PARITY_MARK,
|
||||
UART_CONFIG_PARITY_SPACE,
|
||||
};
|
||||
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
static const char *const TAG = "uart";
|
||||
ESPHOME_LOG_TAG(TAG, "uart");
|
||||
// Edge decoder up to this baud rate; at 38400 WiFi interrupt latency corrupts bytes.
|
||||
static constexpr uint32_t SW_SERIAL_EDGE_MODE_MAX_BAUD = 19200;
|
||||
// Edge decoder time base: 1/16 us from the 1 MHz system timer, which keeps its rate during a
|
||||
// CpuFrequencyBoost; the cycle counter does not.
|
||||
static constexpr uint32_t RX_TICKS_PER_SECOND = 16000000;
|
||||
__attribute__((always_inline)) static inline uint32_t rx_now() { return micros() << 4; }
|
||||
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
uint32_t ESP8266UartComponent::get_config() {
|
||||
@@ -158,6 +164,11 @@ void ESP8266UartComponent::dump_config() {
|
||||
"\n Wake on data RX: ENABLED"
|
||||
#endif
|
||||
);
|
||||
if (this->rx_pin_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " RX decoder: %s",
|
||||
this->baud_rate_ <= SW_SERIAL_EDGE_MODE_MAX_BAUD ? LOG_STR_LITERAL("edge")
|
||||
: LOG_STR_LITERAL("start bit sampler"));
|
||||
}
|
||||
}
|
||||
this->check_logger_conflict();
|
||||
}
|
||||
@@ -239,23 +250,39 @@ UARTFlushResult ESP8266UartComponent::flush() {
|
||||
void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate,
|
||||
uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity,
|
||||
size_t rx_buffer_size) {
|
||||
// load_settings() re-enters here
|
||||
if (this->gpio_rx_pin_ != nullptr)
|
||||
this->gpio_rx_pin_->detach_interrupt();
|
||||
this->bit_time_ = F_CPU / baud_rate;
|
||||
this->rx_buffer_size_ = rx_buffer_size;
|
||||
this->stop_bits_ = stop_bits;
|
||||
this->data_bits_ = data_bits;
|
||||
this->parity_ = parity;
|
||||
// to_isr() allocates, so call it only for a new or changed pin
|
||||
if (tx_pin != nullptr) {
|
||||
if (tx_pin != this->gpio_tx_pin_)
|
||||
this->tx_pin_ = tx_pin->to_isr();
|
||||
gpio_tx_pin_ = tx_pin;
|
||||
gpio_tx_pin_->setup();
|
||||
tx_pin_ = gpio_tx_pin_->to_isr();
|
||||
tx_pin_.digital_write(true);
|
||||
}
|
||||
if (rx_pin != nullptr) {
|
||||
if (rx_pin != this->gpio_rx_pin_)
|
||||
this->rx_pin_ = rx_pin->to_isr();
|
||||
gpio_rx_pin_ = rx_pin;
|
||||
gpio_rx_pin_->setup();
|
||||
rx_pin_ = gpio_rx_pin_->to_isr();
|
||||
rx_buffer_ = new uint8_t[this->rx_buffer_size_]; // NOLINT
|
||||
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
uint8_t *buffer = this->rx_.buffer();
|
||||
if (this->rx_.buffer_size() != rx_buffer_size) {
|
||||
delete[] buffer; // NOLINT
|
||||
buffer = new uint8_t[rx_buffer_size]; // NOLINT
|
||||
}
|
||||
this->rx_.setup((RX_TICKS_PER_SECOND + baud_rate / 2) / baud_rate, data_bits, parity != UART_CONFIG_PARITY_NONE,
|
||||
stop_bits, buffer, rx_buffer_size);
|
||||
this->rx_.reset(rx_now(), this->rx_pin_.digital_read());
|
||||
if (baud_rate <= SW_SERIAL_EDGE_MODE_MAX_BAUD) {
|
||||
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr_edge, this, gpio::INTERRUPT_ANY_EDGE);
|
||||
} else {
|
||||
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A byte can arrive while a CpuFrequencyBoost has an 80 MHz build at 160 MHz; the clock select bit doubles
|
||||
@@ -288,8 +315,7 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
|
||||
if (arg->stop_bits_ == 2)
|
||||
arg->wait_(&wait, start, bit_time);
|
||||
|
||||
arg->rx_buffer_[arg->rx_in_pos_] = rec;
|
||||
arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_;
|
||||
arg->rx_.push_byte(rec);
|
||||
// Clear RX pin so that the interrupt doesn't re-trigger right away again.
|
||||
arg->rx_pin_.clear_interrupt();
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
@@ -299,6 +325,27 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
|
||||
wake_loop_isrsafe();
|
||||
#endif
|
||||
}
|
||||
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr_edge(ESP8266SoftwareSerial *arg) {
|
||||
const uint32_t now = rx_now();
|
||||
const bool level = arg->rx_pin_.digital_read();
|
||||
[[maybe_unused]] const bool wake = arg->rx_.on_edge(now, level);
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
if (wake)
|
||||
wake_loop_isrsafe();
|
||||
#endif
|
||||
}
|
||||
void ESP8266SoftwareSerial::rx_finalize_pending_() {
|
||||
if (!this->rx_.finalize_due(rx_now())) {
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// Tail not elapsed; wake again only once drained
|
||||
if (this->rx_.available() == 0)
|
||||
wake_loop_threadsafe();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
InterruptLock lock;
|
||||
this->rx_.finalize(rx_now());
|
||||
}
|
||||
void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
|
||||
if (this->gpio_tx_pin_ == nullptr) {
|
||||
ESP_LOGE(TAG, "UART doesn't have TX pins set!");
|
||||
@@ -350,28 +397,19 @@ void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const
|
||||
this->wait_(wait, start, bit_time);
|
||||
}
|
||||
uint8_t ESP8266SoftwareSerial::read_byte() {
|
||||
if (this->rx_in_pos_ == this->rx_out_pos_)
|
||||
return 0;
|
||||
uint8_t data = this->rx_buffer_[this->rx_out_pos_];
|
||||
this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_;
|
||||
return data;
|
||||
this->rx_sync_();
|
||||
return this->rx_.read_byte();
|
||||
}
|
||||
uint8_t ESP8266SoftwareSerial::peek_byte() {
|
||||
if (this->rx_in_pos_ == this->rx_out_pos_)
|
||||
return 0;
|
||||
return this->rx_buffer_[this->rx_out_pos_];
|
||||
this->rx_sync_();
|
||||
return this->rx_.peek_byte();
|
||||
}
|
||||
void ESP8266SoftwareSerial::flush() {
|
||||
// Flush is a NO-OP with software serial, all bytes are written immediately.
|
||||
}
|
||||
size_t ESP8266SoftwareSerial::available() {
|
||||
// Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR.
|
||||
// When in >= out, data is contiguous: [out..in).
|
||||
// When in < out, data wraps: [out..buf_size) + [0..in).
|
||||
size_t in = this->rx_in_pos_;
|
||||
if (in >= this->rx_out_pos_)
|
||||
return in - this->rx_out_pos_;
|
||||
return this->rx_buffer_size_ - this->rx_out_pos_ + in;
|
||||
this->rx_sync_();
|
||||
return this->rx_.available();
|
||||
}
|
||||
|
||||
} // namespace esphome::uart
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "software_serial_rx_decoder.h"
|
||||
#include "uart_component.h"
|
||||
|
||||
namespace esphome::uart {
|
||||
@@ -26,24 +27,30 @@ class ESP8266SoftwareSerial {
|
||||
size_t available();
|
||||
|
||||
protected:
|
||||
/// Above SW_SERIAL_EDGE_MODE_MAX_BAUD; reads the whole byte in the ISR.
|
||||
static void gpio_intr(ESP8266SoftwareSerial *arg);
|
||||
static void gpio_intr_edge(ESP8266SoftwareSerial *arg);
|
||||
|
||||
void wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
|
||||
bool read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
|
||||
void write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time);
|
||||
|
||||
void rx_finalize_pending_();
|
||||
void ESPHOME_ALWAYS_INLINE rx_sync_() {
|
||||
if (this->rx_.pending())
|
||||
this->rx_finalize_pending_();
|
||||
}
|
||||
|
||||
// Members ordered largest to smallest to minimize padding
|
||||
uint32_t bit_time_{0};
|
||||
uint8_t *rx_buffer_{nullptr};
|
||||
size_t rx_buffer_size_;
|
||||
volatile size_t rx_in_pos_{0};
|
||||
size_t rx_out_pos_{0};
|
||||
uint8_t stop_bits_;
|
||||
uint8_t data_bits_;
|
||||
UARTParityOptions parity_;
|
||||
InternalGPIOPin *gpio_tx_pin_{nullptr};
|
||||
ISRInternalGPIOPin tx_pin_;
|
||||
InternalGPIOPin *gpio_rx_pin_{nullptr};
|
||||
ISRInternalGPIOPin rx_pin_;
|
||||
SoftwareSerialRxDecoder rx_;
|
||||
UARTParityOptions parity_;
|
||||
uint8_t stop_bits_;
|
||||
uint8_t data_bits_;
|
||||
};
|
||||
|
||||
class ESP8266UartComponent final : public UARTComponent, public Component {
|
||||
|
||||
@@ -158,6 +158,8 @@ void HostUartComponent::setup() {
|
||||
options.c_cflag |= PARENB;
|
||||
options.c_cflag |= PARODD;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
// Set stop bits
|
||||
if (this->stop_bits_ == 2) {
|
||||
|
||||
@@ -38,6 +38,8 @@ uint16_t LibreTinyUARTComponent::get_config() {
|
||||
case UART_CONFIG_PARITY_ODD:
|
||||
config |= SERIAL_PARITY_ODD;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
config |= (this->data_bits_ - 4) << 8;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from itertools import combinations
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
@@ -22,6 +23,8 @@ import esphome.final_validate as fv
|
||||
from esphome.helpers import cpp_u16string_escape
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["bytebuffer"]
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
@@ -71,15 +74,19 @@ def usb_device_schema(
|
||||
)
|
||||
|
||||
|
||||
def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
|
||||
def _clients_overlap(first: ConfigType, second: ConfigType) -> bool:
|
||||
# Two entries overlap when no field they both constrain tells them apart
|
||||
for first, second in combinations(configs, 2):
|
||||
for key, wildcard in _FILTER_WILDCARDS.items():
|
||||
a = first.get(key)
|
||||
b = second.get(key)
|
||||
if wildcard not in (a, b) and a != b:
|
||||
break
|
||||
else:
|
||||
for key, wildcard in _FILTER_WILDCARDS.items():
|
||||
a = first.get(key)
|
||||
b = second.get(key)
|
||||
if wildcard not in (a, b) and a != b:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
|
||||
for first, second in itertools.combinations(configs, 2):
|
||||
if _clients_overlap(first, second):
|
||||
raise cv.Invalid(
|
||||
f"USB configs overlap: {first[CONF_ID]}, {second[CONF_ID]}"
|
||||
)
|
||||
@@ -87,11 +94,29 @@ def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
# Every USB client on the bus, whichever component configured it: any two could
|
||||
# otherwise open the same device
|
||||
clients = list(config.get(CONF_DEVICES) or ())
|
||||
clients.extend(fv.full_config.get().get("usb_uart") or ())
|
||||
validate_usb_clients(clients)
|
||||
# Any two overlapping clients could otherwise open the same device
|
||||
devices = config.get(CONF_DEVICES) or []
|
||||
uarts = fv.full_config.get().get("usb_uart") or []
|
||||
validate_usb_clients(devices)
|
||||
validate_usb_clients(uarts)
|
||||
# Remove before 2027.10.0, then pool devices and uarts in one validate_usb_clients call
|
||||
for device, uart in itertools.product(devices, uarts):
|
||||
if not _clients_overlap(device, uart):
|
||||
continue
|
||||
# The device matches nothing the usb_uart entry does not already claim
|
||||
redundant = all(
|
||||
uart.get(key) in (wildcard, device.get(key))
|
||||
for key, wildcard in _FILTER_WILDCARDS.items()
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"usb_host device '%s' matches the same USB device as usb_uart '%s'; %s. "
|
||||
"This will be an error in 2027.10.0",
|
||||
device[CONF_ID],
|
||||
uart[CONF_ID],
|
||||
"remove it from usb_host devices"
|
||||
if redundant
|
||||
else "narrow its filter so it no longer matches that device",
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
|
||||
from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent
|
||||
from esphome.components.uart import (
|
||||
CONF_DEBUG_PREFIX,
|
||||
CONF_FLUSH_TIMEOUT,
|
||||
UARTComponent,
|
||||
UARTParityOptions,
|
||||
)
|
||||
from esphome.components.usb_host import (
|
||||
get_max_packet_size,
|
||||
register_usb_client,
|
||||
@@ -38,7 +43,6 @@ def is_usb_uart_channel(uart_id: ID, full_config: ConfigType) -> bool:
|
||||
)
|
||||
|
||||
|
||||
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
|
||||
UART_PARITY_OPTIONS = {
|
||||
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
|
||||
"EVEN": UARTParityOptions.UART_CONFIG_PARITY_EVEN,
|
||||
|
||||
@@ -132,15 +132,12 @@ bool USBUartTypeCH34X::config_step(USBUartChannelBase *channel, uint8_t step, bo
|
||||
factor = 256 - factor;
|
||||
|
||||
uint16_t value = 0xC0;
|
||||
if (channel->stop_bits_ == UART_CONFIG_STOP_BITS_2)
|
||||
if (channel->line_stop_bits_() == LineStopBits::LINE_STOP_BITS_2)
|
||||
value |= 4;
|
||||
switch (channel->parity_) {
|
||||
case UART_CONFIG_PARITY_NONE:
|
||||
break;
|
||||
default:
|
||||
value |= 8 | ((channel->parity_ - 1) << 4);
|
||||
break;
|
||||
}
|
||||
// Parity enable, then 0 = odd, 1 = even, 2 = mark, 3 = space
|
||||
const LineParity parity = channel->line_parity_();
|
||||
if (parity != LineParity::LINE_PARITY_NONE)
|
||||
value |= 8 | ((static_cast<uint8_t>(parity) - 1) << 4);
|
||||
value |= channel->data_bits_ - 5;
|
||||
value <<= 8;
|
||||
value |= 0x8C;
|
||||
|
||||
@@ -107,8 +107,8 @@ bool USBUartTypeCP210X::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_);
|
||||
return true;
|
||||
case 1: {
|
||||
uint16_t line_control = channel->stop_bits_;
|
||||
line_control |= static_cast<uint8_t>(channel->parity_) << 4;
|
||||
uint16_t line_control = static_cast<uint8_t>(channel->line_stop_bits_());
|
||||
line_control |= static_cast<uint8_t>(channel->line_parity_()) << 4;
|
||||
line_control |= channel->data_bits_ << 8;
|
||||
ESP_LOGD(TAG, "Line control value 0x%X", line_control);
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_LINE_CTL, line_control, channel->index_);
|
||||
|
||||
@@ -362,34 +362,8 @@ bool USBUartTypeFT23XX::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
}
|
||||
case 2: { // set line properties (data bits / parity / stop bits)
|
||||
uint16_t value = channel->data_bits_;
|
||||
switch (channel->parity_) {
|
||||
case UART_CONFIG_PARITY_NONE:
|
||||
value |= (0x00 << 8);
|
||||
break;
|
||||
case UART_CONFIG_PARITY_ODD:
|
||||
value |= (0x01 << 8);
|
||||
break;
|
||||
case UART_CONFIG_PARITY_EVEN:
|
||||
value |= (0x02 << 8);
|
||||
break;
|
||||
case UART_CONFIG_PARITY_MARK:
|
||||
value |= (0x03 << 8);
|
||||
break;
|
||||
case UART_CONFIG_PARITY_SPACE:
|
||||
value |= (0x04 << 8);
|
||||
break;
|
||||
}
|
||||
switch (channel->stop_bits_) {
|
||||
default: // 1 bit
|
||||
value |= (0x00 << 11);
|
||||
break;
|
||||
case UART_CONFIG_STOP_BITS_1_5:
|
||||
value |= (0x01 << 11);
|
||||
break;
|
||||
case UART_CONFIG_STOP_BITS_2:
|
||||
value |= (0x02 << 11);
|
||||
break;
|
||||
}
|
||||
value |= static_cast<uint8_t>(channel->line_parity_()) << 8;
|
||||
value |= static_cast<uint8_t>(channel->line_stop_bits_()) << 11;
|
||||
value |= (0x00 << 14);
|
||||
this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x04, value,
|
||||
channel->cdc_dev_.bulk_interface_number + 1);
|
||||
|
||||
@@ -261,34 +261,9 @@ bool USBUartTypePL2303::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
encode_baud_divisor(line_coding, baud);
|
||||
}
|
||||
|
||||
// Stop bits: 0=1, 1=1.5, 2=2
|
||||
switch (channel->get_stop_bits()) {
|
||||
case 2:
|
||||
line_coding[4] = 2;
|
||||
break;
|
||||
default:
|
||||
line_coding[4] = 0;
|
||||
break;
|
||||
}
|
||||
line_coding[4] = static_cast<uint8_t>(channel->line_stop_bits_());
|
||||
|
||||
// Parity: 0=none, 1=odd, 2=even, 3=mark, 4=space
|
||||
switch (channel->parity_) {
|
||||
case UART_CONFIG_PARITY_ODD:
|
||||
line_coding[5] = 1;
|
||||
break;
|
||||
case UART_CONFIG_PARITY_EVEN:
|
||||
line_coding[5] = 2;
|
||||
break;
|
||||
case UART_CONFIG_PARITY_MARK:
|
||||
line_coding[5] = 3;
|
||||
break;
|
||||
case UART_CONFIG_PARITY_SPACE:
|
||||
line_coding[5] = 4;
|
||||
break;
|
||||
default:
|
||||
line_coding[5] = 0;
|
||||
break;
|
||||
}
|
||||
line_coding[5] = static_cast<uint8_t>(channel->line_parity_());
|
||||
|
||||
// Data bits
|
||||
line_coding[6] = channel->get_data_bits();
|
||||
|
||||
@@ -270,9 +270,10 @@ void USBUartComponent::dump_config() {
|
||||
" Flush Timeout: %" PRIu32 " ms\n"
|
||||
" Debug: %s\n"
|
||||
" Dummy receiver: %s",
|
||||
channel->index_, channel->baud_rate_, channel->data_bits_, PARITY_NAMES[channel->parity_],
|
||||
STOP_BITS_NAMES[channel->stop_bits_], channel->flush_timeout_ms_, YESNO(channel->debug_),
|
||||
YESNO(channel->dummy_receiver_));
|
||||
channel->index_, channel->baud_rate_, channel->data_bits_,
|
||||
PARITY_NAMES[static_cast<uint8_t>(channel->line_parity_())],
|
||||
STOP_BITS_NAMES[static_cast<uint8_t>(channel->line_stop_bits_())], channel->flush_timeout_ms_,
|
||||
YESNO(channel->debug_), YESNO(channel->dummy_receiver_));
|
||||
}
|
||||
}
|
||||
void USBUartComponent::start_input(USBUartChannelBase *channel) {
|
||||
@@ -515,14 +516,17 @@ bool USBUartTypeCdcAcm::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
// sends RSTACK.
|
||||
uint32_t baud = channel->baud_rate_;
|
||||
std::vector<uint8_t> line_coding = {
|
||||
static_cast<uint8_t>(baud & 0xFF), static_cast<uint8_t>((baud >> 8) & 0xFF),
|
||||
static_cast<uint8_t>((baud >> 16) & 0xFF), static_cast<uint8_t>((baud >> 24) & 0xFF),
|
||||
static_cast<uint8_t>(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop
|
||||
static_cast<uint8_t>(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space
|
||||
static_cast<uint8_t>(channel->data_bits_), // bDataBits
|
||||
static_cast<uint8_t>(baud & 0xFF),
|
||||
static_cast<uint8_t>((baud >> 8) & 0xFF),
|
||||
static_cast<uint8_t>((baud >> 16) & 0xFF),
|
||||
static_cast<uint8_t>((baud >> 24) & 0xFF),
|
||||
static_cast<uint8_t>(channel->line_stop_bits_()), // bCharFormat
|
||||
static_cast<uint8_t>(channel->line_parity_()), // bParityType
|
||||
static_cast<uint8_t>(channel->data_bits_), // bDataBits
|
||||
};
|
||||
ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_,
|
||||
(unsigned) channel->parity_, channel->data_bits_);
|
||||
ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%s parity=%s data=%u", (unsigned) baud,
|
||||
STOP_BITS_NAMES[static_cast<uint8_t>(channel->line_stop_bits_())],
|
||||
PARITY_NAMES[static_cast<uint8_t>(channel->line_parity_())], channel->data_bits_);
|
||||
this->config_transfer_(CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number,
|
||||
line_coding);
|
||||
return true;
|
||||
@@ -675,6 +679,32 @@ bool USBUartComponent::run_config_machine_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
LineStopBits USBUartChannelBase::line_stop_bits_() const {
|
||||
switch (this->stop_bits_) {
|
||||
case UART_CONFIG_STOP_BITS_1_5:
|
||||
return LineStopBits::LINE_STOP_BITS_1_5;
|
||||
case UART_CONFIG_STOP_BITS_2:
|
||||
return LineStopBits::LINE_STOP_BITS_2;
|
||||
default:
|
||||
return LineStopBits::LINE_STOP_BITS_1;
|
||||
}
|
||||
}
|
||||
|
||||
LineParity USBUartChannelBase::line_parity_() const {
|
||||
switch (this->parity_) {
|
||||
case uart::UART_CONFIG_PARITY_ODD:
|
||||
return LineParity::LINE_PARITY_ODD;
|
||||
case uart::UART_CONFIG_PARITY_EVEN:
|
||||
return LineParity::LINE_PARITY_EVEN;
|
||||
case uart::UART_CONFIG_PARITY_MARK:
|
||||
return LineParity::LINE_PARITY_MARK;
|
||||
case uart::UART_CONFIG_PARITY_SPACE:
|
||||
return LineParity::LINE_PARITY_SPACE;
|
||||
default:
|
||||
return LineParity::LINE_PARITY_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
void USBUartChannelBase::load_settings(bool /*dump_config*/) {
|
||||
// The per-channel control transfers already log their values at debug level.
|
||||
this->parent_->apply_channel_settings(this);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user