mirror of
https://github.com/esphome/esphome.git
synced 2025-10-12 14:53:49 +01:00
Merge branch 'integration' of https://github.com/esphome/esphome into integration
This commit is contained in:
@@ -1 +1 @@
|
|||||||
4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9
|
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f
|
||||||
|
1
.github/workflows/ci-clang-tidy-hash.yml
vendored
1
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
- ".clang-tidy"
|
- ".clang-tidy"
|
||||||
- "platformio.ini"
|
- "platformio.ini"
|
||||||
- "requirements_dev.txt"
|
- "requirements_dev.txt"
|
||||||
|
- "sdkconfig.defaults"
|
||||||
- ".clang-tidy.hash"
|
- ".clang-tidy.hash"
|
||||||
- "script/clang_tidy_hash.py"
|
- "script/clang_tidy_hash.py"
|
||||||
- ".github/workflows/ci-clang-tidy-hash.yml"
|
- ".github/workflows/ci-clang-tidy-hash.yml"
|
||||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -466,7 +466,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
- uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||||
env:
|
env:
|
||||||
SKIP: pylint,clang-tidy-hash
|
SKIP: pylint,clang-tidy-hash
|
||||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -86,6 +86,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Stale
|
- name: Stale
|
||||||
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
|
@@ -11,7 +11,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.13.2
|
rev: v0.14.0
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
@@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
|
|||||||
esphome/components/ens160_i2c/* @latonita
|
esphome/components/ens160_i2c/* @latonita
|
||||||
esphome/components/ens160_spi/* @latonita
|
esphome/components/ens160_spi/* @latonita
|
||||||
esphome/components/ens210/* @itn3rd77
|
esphome/components/ens210/* @itn3rd77
|
||||||
|
esphome/components/epaper_spi/* @esphome/core
|
||||||
esphome/components/es7210/* @kahrendt
|
esphome/components/es7210/* @kahrendt
|
||||||
esphome/components/es7243e/* @kbx81
|
esphome/components/es7243e/* @kbx81
|
||||||
esphome/components/es8156/* @kbx81
|
esphome/components/es8156/* @kbx81
|
||||||
@@ -256,6 +257,7 @@ esphome/components/libretiny_pwm/* @kuba2k2
|
|||||||
esphome/components/light/* @esphome/core
|
esphome/components/light/* @esphome/core
|
||||||
esphome/components/lightwaverf/* @max246
|
esphome/components/lightwaverf/* @max246
|
||||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
||||||
|
esphome/components/lm75b/* @beormund
|
||||||
esphome/components/ln882x/* @lamauny
|
esphome/components/ln882x/* @lamauny
|
||||||
esphome/components/lock/* @esphome/core
|
esphome/components/lock/* @esphome/core
|
||||||
esphome/components/logger/* @esphome/core
|
esphome/components/logger/* @esphome/core
|
||||||
@@ -428,6 +430,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
|||||||
esphome/components/spi/* @clydebarrow @esphome/core
|
esphome/components/spi/* @clydebarrow @esphome/core
|
||||||
esphome/components/spi_device/* @clydebarrow
|
esphome/components/spi_device/* @clydebarrow
|
||||||
esphome/components/spi_led_strip/* @clydebarrow
|
esphome/components/spi_led_strip/* @clydebarrow
|
||||||
|
esphome/components/split_buffer/* @jesserockz
|
||||||
esphome/components/sprinkler/* @kbx81
|
esphome/components/sprinkler/* @kbx81
|
||||||
esphome/components/sps30/* @martgras
|
esphome/components/sps30/* @martgras
|
||||||
esphome/components/ssd1322_base/* @kbx81
|
esphome/components/ssd1322_base/* @kbx81
|
||||||
|
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2025.10.0-dev
|
PROJECT_NUMBER = 2025.11.0-dev
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
@@ -14,9 +14,11 @@ from typing import Protocol
|
|||||||
|
|
||||||
import argcomplete
|
import argcomplete
|
||||||
|
|
||||||
|
# Note: Do not import modules from esphome.components here, as this would
|
||||||
|
# cause them to be loaded before external components are processed, resulting
|
||||||
|
# in the built-in version being used instead of the external component one.
|
||||||
from esphome import const, writer, yaml_util
|
from esphome import const, writer, yaml_util
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.mqtt import CONF_DISCOVER_IP
|
|
||||||
from esphome.config import iter_component_configs, read_config, strip_default_ids
|
from esphome.config import iter_component_configs, read_config, strip_default_ids
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
ALLOWED_NAME_CHARS,
|
ALLOWED_NAME_CHARS,
|
||||||
@@ -240,6 +242,8 @@ def has_ota() -> bool:
|
|||||||
|
|
||||||
def has_mqtt_ip_lookup() -> bool:
|
def has_mqtt_ip_lookup() -> bool:
|
||||||
"""Check if MQTT is available and IP lookup is supported."""
|
"""Check if MQTT is available and IP lookup is supported."""
|
||||||
|
from esphome.components.mqtt import CONF_DISCOVER_IP
|
||||||
|
|
||||||
if CONF_MQTT not in CORE.config:
|
if CONF_MQTT not in CORE.config:
|
||||||
return False
|
return False
|
||||||
# Default Enabled
|
# Default Enabled
|
||||||
|
@@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f
|
|||||||
int Animation::get_current_frame() const { return this->current_frame_; }
|
int Animation::get_current_frame() const { return this->current_frame_; }
|
||||||
void Animation::next_frame() {
|
void Animation::next_frame() {
|
||||||
this->current_frame_++;
|
this->current_frame_++;
|
||||||
if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
|
if (loop_count_ && static_cast<uint32_t>(this->current_frame_) == loop_end_frame_ &&
|
||||||
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
|
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
|
||||||
this->current_frame_ = loop_start_frame_;
|
this->current_frame_ = loop_start_frame_;
|
||||||
this->loop_current_iteration_++;
|
this->loop_current_iteration_++;
|
||||||
}
|
}
|
||||||
if (this->current_frame_ >= animation_frame_count_) {
|
if (static_cast<uint32_t>(this->current_frame_) >= animation_frame_count_) {
|
||||||
this->loop_current_iteration_ = 1;
|
this->loop_current_iteration_ = 1;
|
||||||
this->current_frame_ = 0;
|
this->current_frame_ = 0;
|
||||||
}
|
}
|
||||||
|
@@ -9,37 +9,59 @@ import esphome.config_validation as cv
|
|||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ACTION,
|
CONF_ACTION,
|
||||||
CONF_ACTIONS,
|
CONF_ACTIONS,
|
||||||
|
CONF_CAPTURE_RESPONSE,
|
||||||
CONF_DATA,
|
CONF_DATA,
|
||||||
CONF_DATA_TEMPLATE,
|
CONF_DATA_TEMPLATE,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_KEY,
|
CONF_KEY,
|
||||||
|
CONF_MAX_CONNECTIONS,
|
||||||
CONF_ON_CLIENT_CONNECTED,
|
CONF_ON_CLIENT_CONNECTED,
|
||||||
CONF_ON_CLIENT_DISCONNECTED,
|
CONF_ON_CLIENT_DISCONNECTED,
|
||||||
|
CONF_ON_ERROR,
|
||||||
|
CONF_ON_SUCCESS,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_REBOOT_TIMEOUT,
|
CONF_REBOOT_TIMEOUT,
|
||||||
|
CONF_RESPONSE_TEMPLATE,
|
||||||
CONF_SERVICE,
|
CONF_SERVICE,
|
||||||
CONF_SERVICES,
|
CONF_SERVICES,
|
||||||
CONF_TAG,
|
CONF_TAG,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_VARIABLES,
|
CONF_VARIABLES,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||||
|
from esphome.cpp_generator import TemplateArgsType
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "api"
|
DOMAIN = "api"
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
AUTO_LOAD = ["socket"]
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
|
|
||||||
|
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||||
|
"""Conditionally auto-load json only when capture_response is used."""
|
||||||
|
base = ["socket"]
|
||||||
|
|
||||||
|
# Check if any homeassistant.action/homeassistant.service has capture_response: true
|
||||||
|
# This flag is set during config validation in _validate_response_config
|
||||||
|
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
|
||||||
|
return base + ["json"]
|
||||||
|
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
api_ns = cg.esphome_ns.namespace("api")
|
api_ns = cg.esphome_ns.namespace("api")
|
||||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||||
HomeAssistantServiceCallAction = api_ns.class_(
|
HomeAssistantServiceCallAction = api_ns.class_(
|
||||||
"HomeAssistantServiceCallAction", automation.Action
|
"HomeAssistantServiceCallAction", automation.Action
|
||||||
)
|
)
|
||||||
|
ActionResponse = api_ns.class_("ActionResponse")
|
||||||
|
HomeAssistantActionResponseTrigger = api_ns.class_(
|
||||||
|
"HomeAssistantActionResponseTrigger", automation.Trigger
|
||||||
|
)
|
||||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
||||||
|
|
||||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
||||||
@@ -60,7 +82,6 @@ CONF_CUSTOM_SERVICES = "custom_services"
|
|||||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||||
CONF_MAX_CONNECTIONS = "max_connections"
|
|
||||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||||
|
|
||||||
|
|
||||||
@@ -155,8 +176,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
|
||||||
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
|
||||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||||
single=True
|
single=True
|
||||||
),
|
),
|
||||||
@@ -290,6 +309,29 @@ async def to_code(config):
|
|||||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_response_config(config: ConfigType) -> ConfigType:
|
||||||
|
# Validate dependencies:
|
||||||
|
# - response_template requires capture_response: true
|
||||||
|
# - capture_response: true requires on_success
|
||||||
|
if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
|
||||||
|
path=[CONF_RESPONSE_TEMPLATE],
|
||||||
|
)
|
||||||
|
|
||||||
|
if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
|
||||||
|
path=[CONF_CAPTURE_RESPONSE],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track if any action uses capture_response for AUTO_LOAD
|
||||||
|
if config[CONF_CAPTURE_RESPONSE]:
|
||||||
|
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -305,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
||||||
{cv.string: cv.returning_lambda}
|
{cv.string: cv.returning_lambda}
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
|
||||||
|
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
|
||||||
|
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
||||||
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
||||||
|
_validate_response_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -322,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
|||||||
HomeAssistantServiceCallAction,
|
HomeAssistantServiceCallAction,
|
||||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||||
)
|
)
|
||||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
async def homeassistant_service_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
):
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||||
@@ -337,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
|||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(key, templ))
|
||||||
|
|
||||||
|
if on_error := config.get(CONF_ON_ERROR):
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
|
||||||
|
cg.add(var.set_wants_status())
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_error_trigger(),
|
||||||
|
[(cg.std_string, "error"), *args],
|
||||||
|
on_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
if on_success := config.get(CONF_ON_SUCCESS):
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||||
|
cg.add(var.set_wants_status())
|
||||||
|
if config[CONF_CAPTURE_RESPONSE]:
|
||||||
|
cg.add(var.set_wants_response())
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_success_trigger_with_response(),
|
||||||
|
[(cg.JsonObjectConst, "response"), *args],
|
||||||
|
on_success,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
|
||||||
|
templ = await cg.templatable(response_template, args, cg.std_string)
|
||||||
|
cg.add(var.set_response_template(templ))
|
||||||
|
|
||||||
|
else:
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_success_trigger(),
|
||||||
|
args,
|
||||||
|
on_success,
|
||||||
|
)
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@@ -780,6 +780,22 @@ message HomeassistantActionRequest {
|
|||||||
repeated HomeassistantServiceMap data_template = 3;
|
repeated HomeassistantServiceMap data_template = 3;
|
||||||
repeated HomeassistantServiceMap variables = 4;
|
repeated HomeassistantServiceMap variables = 4;
|
||||||
bool is_event = 5;
|
bool is_event = 5;
|
||||||
|
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
||||||
|
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||||
|
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message sent by Home Assistant to ESPHome with service call response data
|
||||||
|
message HomeassistantActionResponse {
|
||||||
|
option (id) = 130;
|
||||||
|
option (source) = SOURCE_CLIENT;
|
||||||
|
option (no_delay) = true;
|
||||||
|
option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
|
||||||
|
|
||||||
|
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
|
||||||
|
bool success = 2; // Whether the service call succeeded
|
||||||
|
string error_message = 3; // Error message if success = false
|
||||||
|
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
||||||
|
@@ -8,9 +8,9 @@
|
|||||||
#endif
|
#endif
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <utility>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
#include "esphome/components/network/util.h"
|
#include "esphome/components/network/util.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
@@ -116,8 +116,7 @@ void APIConnection::start() {
|
|||||||
|
|
||||||
APIError err = this->helper_->init();
|
APIError err = this->helper_->init();
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
|
||||||
this->log_warning_(LOG_STR("Helper init failed"), err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->client_info_.peername = helper_->getpeername();
|
this->client_info_.peername = helper_->getpeername();
|
||||||
@@ -147,8 +146,7 @@ void APIConnection::loop() {
|
|||||||
|
|
||||||
APIError err = this->helper_->loop();
|
APIError err = this->helper_->loop();
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
|
||||||
this->log_socket_operation_failed_(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,17 +161,13 @@ void APIConnection::loop() {
|
|||||||
// No more data available
|
// No more data available
|
||||||
break;
|
break;
|
||||||
} else if (err != APIError::OK) {
|
} else if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
|
||||||
this->log_warning_(LOG_STR("Reading failed"), err);
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this->last_traffic_ = now;
|
this->last_traffic_ = now;
|
||||||
// read a packet
|
// read a packet
|
||||||
if (buffer.data_len > 0) {
|
this->read_message(buffer.data_len, buffer.type,
|
||||||
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
|
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
|
||||||
} else {
|
|
||||||
this->read_message(0, buffer.type, nullptr);
|
|
||||||
}
|
|
||||||
if (this->flags_.remove)
|
if (this->flags_.remove)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1389,11 @@ void APIConnection::complete_authentication_() {
|
|||||||
this->send_time_request();
|
this->send_time_request();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_ZWAVE_PROXY
|
||||||
|
if (zwave_proxy::global_zwave_proxy != nullptr) {
|
||||||
|
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||||
@@ -1550,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
if (msg.response_data_len > 0) {
|
||||||
|
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
|
||||||
|
msg.response_data_len);
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
||||||
NoiseEncryptionSetKeyResponse resp;
|
NoiseEncryptionSetKeyResponse resp;
|
||||||
@@ -1580,8 +1593,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
|||||||
delay(0);
|
delay(0);
|
||||||
APIError err = this->helper_->loop();
|
APIError err = this->helper_->loop();
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
|
||||||
this->log_socket_operation_failed_(err);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this->helper_->can_write_without_blocking())
|
if (this->helper_->can_write_without_blocking())
|
||||||
@@ -1600,8 +1612,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
|||||||
if (err == APIError::WOULD_BLOCK)
|
if (err == APIError::WOULD_BLOCK)
|
||||||
return false;
|
return false;
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
|
||||||
this->log_warning_(LOG_STR("Packet write failed"), err);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Do not set last_traffic_ on send
|
// Do not set last_traffic_ on send
|
||||||
@@ -1787,8 +1798,7 @@ void APIConnection::process_batch_() {
|
|||||||
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
|
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
|
||||||
std::span<const PacketInfo>(packet_info, packet_count));
|
std::span<const PacketInfo>(packet_info, packet_count));
|
||||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||||
on_fatal_error();
|
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||||
this->log_warning_(LOG_STR("Batch write failed"), err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1871,9 +1881,5 @@ void APIConnection::log_warning_(const LogString *message, APIError err) {
|
|||||||
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::log_socket_operation_failed_(APIError err) {
|
|
||||||
this->log_warning_(LOG_STR("Socket operation failed"), err);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
#endif
|
#endif
|
||||||
|
@@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection {
|
|||||||
return;
|
return;
|
||||||
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
#endif
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
@@ -732,8 +735,11 @@ class APIConnection final : public APIServerConnection {
|
|||||||
|
|
||||||
// Helper function to log API errors with errno
|
// Helper function to log API errors with errno
|
||||||
void log_warning_(const LogString *message, APIError err);
|
void log_warning_(const LogString *message, APIError err);
|
||||||
// Specific helper for duplicated error message
|
// Helper to handle fatal errors with logging
|
||||||
void log_socket_operation_failed_(APIError err);
|
inline void fatal_error_with_log_(const LogString *message, APIError err) {
|
||||||
|
this->on_fatal_error();
|
||||||
|
this->log_warning_(message, err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
@@ -19,13 +19,14 @@ namespace esphome::api {
|
|||||||
//#define HELPER_LOG_PACKETS
|
//#define HELPER_LOG_PACKETS
|
||||||
|
|
||||||
// Maximum message size limits to prevent OOM on constrained devices
|
// Maximum message size limits to prevent OOM on constrained devices
|
||||||
// Voice Assistant is our largest user at 1024 bytes per audio chunk
|
// Handshake messages are limited to a small size for security
|
||||||
// Using 2048 + 256 bytes overhead = 2304 bytes total to support voice and future needs
|
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
|
||||||
// ESP8266 has very limited RAM and cannot support voice assistant
|
|
||||||
|
// Data message limits vary by platform based on available memory
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 512; // Keep small for memory constrained ESP8266
|
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
|
||||||
#else
|
#else
|
||||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 2304; // Support voice (1024) + headroom for larger messages
|
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declaration
|
||||||
|
@@ -132,26 +132,16 @@ APIError APINoiseFrameHelper::loop() {
|
|||||||
return APIFrameHelper::loop();
|
return APIFrameHelper::loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
/** Read a packet into the rx_buf_.
|
||||||
*
|
*
|
||||||
* @param frame: The struct to hold the frame information in.
|
* @return APIError::OK if a full packet is in rx_buf_
|
||||||
* msg_start: points to the start of the payload - this pointer is only valid until the next
|
|
||||||
* try_receive_raw_ call
|
|
||||||
*
|
|
||||||
* @return 0 if a full packet is in rx_buf_
|
|
||||||
* @return -1 if error, check errno.
|
|
||||||
*
|
*
|
||||||
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
|
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
|
||||||
* errno ENOMEM: Not enough memory for reading packet.
|
* errno ENOMEM: Not enough memory for reading packet.
|
||||||
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||||
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
|
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
|
||||||
*/
|
*/
|
||||||
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
APIError APINoiseFrameHelper::try_read_frame_() {
|
||||||
if (frame == nullptr) {
|
|
||||||
HELPER_LOG("Bad argument for try_read_frame_");
|
|
||||||
return APIError::BAD_ARG;
|
|
||||||
}
|
|
||||||
|
|
||||||
// read header
|
// read header
|
||||||
if (rx_header_buf_len_ < 3) {
|
if (rx_header_buf_len_ < 3) {
|
||||||
// no header information yet
|
// no header information yet
|
||||||
@@ -178,23 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
|||||||
// read body
|
// read body
|
||||||
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
||||||
|
|
||||||
if (state_ != State::DATA && msg_size > 128) {
|
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
|
||||||
// for handshake message only permit up to 128 bytes
|
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
|
||||||
|
if (msg_size > limit) {
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
|
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
|
||||||
return APIError::BAD_HANDSHAKE_PACKET_LEN;
|
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check against maximum message size to prevent OOM
|
// Reserve space for body
|
||||||
if (msg_size > MAX_MESSAGE_SIZE) {
|
if (this->rx_buf_.size() != msg_size) {
|
||||||
state_ = State::FAILED;
|
this->rx_buf_.resize(msg_size);
|
||||||
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, MAX_MESSAGE_SIZE);
|
|
||||||
return APIError::BAD_DATA_PACKET;
|
|
||||||
}
|
|
||||||
|
|
||||||
// reserve space for body
|
|
||||||
if (rx_buf_.size() != msg_size) {
|
|
||||||
rx_buf_.resize(msg_size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx_buf_len_ < msg_size) {
|
if (rx_buf_len_ < msg_size) {
|
||||||
@@ -212,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_PACKET_RECEIVED(rx_buf_);
|
LOG_PACKET_RECEIVED(this->rx_buf_);
|
||||||
*frame = std::move(rx_buf_);
|
|
||||||
// consume msg
|
// Clear state for next frame (rx_buf_ still contains data for caller)
|
||||||
rx_buf_ = {};
|
this->rx_buf_len_ = 0;
|
||||||
rx_buf_len_ = 0;
|
this->rx_header_buf_len_ = 0;
|
||||||
rx_header_buf_len_ = 0;
|
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,18 +223,17 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
}
|
}
|
||||||
if (state_ == State::CLIENT_HELLO) {
|
if (state_ == State::CLIENT_HELLO) {
|
||||||
// waiting for client hello
|
// waiting for client hello
|
||||||
std::vector<uint8_t> frame;
|
aerr = this->try_read_frame_();
|
||||||
aerr = try_read_frame_(&frame);
|
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
return handle_handshake_frame_error_(aerr);
|
return handle_handshake_frame_error_(aerr);
|
||||||
}
|
}
|
||||||
// ignore contents, may be used in future for flags
|
// ignore contents, may be used in future for flags
|
||||||
// Resize for: existing prologue + 2 size bytes + frame data
|
// Resize for: existing prologue + 2 size bytes + frame data
|
||||||
size_t old_size = prologue_.size();
|
size_t old_size = this->prologue_.size();
|
||||||
prologue_.resize(old_size + 2 + frame.size());
|
this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
|
||||||
prologue_[old_size] = (uint8_t) (frame.size() >> 8);
|
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
|
||||||
prologue_[old_size + 1] = (uint8_t) frame.size();
|
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
|
||||||
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
|
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
|
||||||
|
|
||||||
state_ = State::SERVER_HELLO;
|
state_ = State::SERVER_HELLO;
|
||||||
}
|
}
|
||||||
@@ -292,24 +275,23 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
int action = noise_handshakestate_get_action(handshake_);
|
int action = noise_handshakestate_get_action(handshake_);
|
||||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||||
// waiting for handshake msg
|
// waiting for handshake msg
|
||||||
std::vector<uint8_t> frame;
|
aerr = this->try_read_frame_();
|
||||||
aerr = try_read_frame_(&frame);
|
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
return handle_handshake_frame_error_(aerr);
|
return handle_handshake_frame_error_(aerr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame.empty()) {
|
if (this->rx_buf_.empty()) {
|
||||||
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||||
} else if (frame[0] != 0x00) {
|
} else if (this->rx_buf_[0] != 0x00) {
|
||||||
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
|
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
||||||
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
||||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
NoiseBuffer mbuf;
|
NoiseBuffer mbuf;
|
||||||
noise_buffer_init(mbuf);
|
noise_buffer_init(mbuf);
|
||||||
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
|
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||||
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
// Special handling for MAC failure
|
// Special handling for MAC failure
|
||||||
@@ -386,35 +368,33 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
|||||||
state_ = orig_state;
|
state_ = orig_state;
|
||||||
}
|
}
|
||||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||||
int err;
|
APIError aerr = this->state_action_();
|
||||||
APIError aerr;
|
|
||||||
aerr = state_action_();
|
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
return aerr;
|
return aerr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state_ != State::DATA) {
|
if (this->state_ != State::DATA) {
|
||||||
return APIError::WOULD_BLOCK;
|
return APIError::WOULD_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> frame;
|
aerr = this->try_read_frame_();
|
||||||
aerr = try_read_frame_(&frame);
|
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|
||||||
NoiseBuffer mbuf;
|
NoiseBuffer mbuf;
|
||||||
noise_buffer_init(mbuf);
|
noise_buffer_init(mbuf);
|
||||||
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
|
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
|
||||||
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
|
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
|
||||||
APIError decrypt_err =
|
APIError decrypt_err =
|
||||||
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
|
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
|
||||||
if (decrypt_err != APIError::OK)
|
if (decrypt_err != APIError::OK) {
|
||||||
return decrypt_err;
|
return decrypt_err;
|
||||||
|
}
|
||||||
|
|
||||||
uint16_t msg_size = mbuf.size;
|
uint16_t msg_size = mbuf.size;
|
||||||
uint8_t *msg_data = frame.data();
|
uint8_t *msg_data = this->rx_buf_.data();
|
||||||
if (msg_size < 4) {
|
if (msg_size < 4) {
|
||||||
state_ = State::FAILED;
|
this->state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad data packet: size %d too short", msg_size);
|
HELPER_LOG("Bad data packet: size %d too short", msg_size);
|
||||||
return APIError::BAD_DATA_PACKET;
|
return APIError::BAD_DATA_PACKET;
|
||||||
}
|
}
|
||||||
@@ -422,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
|
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
|
||||||
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
|
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
|
||||||
if (data_len > msg_size - 4) {
|
if (data_len > msg_size - 4) {
|
||||||
state_ = State::FAILED;
|
this->state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
|
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
|
||||||
return APIError::BAD_DATA_PACKET;
|
return APIError::BAD_DATA_PACKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer->container = std::move(frame);
|
buffer->container = std::move(this->rx_buf_);
|
||||||
buffer->data_offset = 4;
|
buffer->data_offset = 4;
|
||||||
buffer->data_len = data_len;
|
buffer->data_len = data_len;
|
||||||
buffer->type = type;
|
buffer->type = type;
|
||||||
|
@@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIError state_action_();
|
APIError state_action_();
|
||||||
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
APIError try_read_frame_();
|
||||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||||
APIError init_handshake_();
|
APIError init_handshake_();
|
||||||
APIError check_handshake_finished_();
|
APIError check_handshake_finished_();
|
||||||
|
@@ -47,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() {
|
|||||||
return APIFrameHelper::loop();
|
return APIFrameHelper::loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
/** Read a packet into the rx_buf_.
|
||||||
*
|
|
||||||
* @param frame: The struct to hold the frame information in.
|
|
||||||
* msg: store the parsed frame in that struct
|
|
||||||
*
|
*
|
||||||
* @return See APIError
|
* @return See APIError
|
||||||
*
|
*
|
||||||
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||||
*/
|
*/
|
||||||
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
APIError APIPlaintextFrameHelper::try_read_frame_() {
|
||||||
if (frame == nullptr) {
|
|
||||||
HELPER_LOG("Bad argument for try_read_frame_");
|
|
||||||
return APIError::BAD_ARG;
|
|
||||||
}
|
|
||||||
|
|
||||||
// read header
|
// read header
|
||||||
while (!rx_header_parsed_) {
|
while (!rx_header_parsed_) {
|
||||||
// Now that we know when the socket is ready, we can read up to 3 bytes
|
// Now that we know when the socket is ready, we can read up to 3 bytes
|
||||||
@@ -150,9 +142,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
|||||||
}
|
}
|
||||||
// header reading done
|
// header reading done
|
||||||
|
|
||||||
// reserve space for body
|
// Reserve space for body
|
||||||
if (rx_buf_.size() != rx_header_parsed_len_) {
|
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
|
||||||
rx_buf_.resize(rx_header_parsed_len_);
|
this->rx_buf_.resize(this->rx_header_parsed_len_);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||||
@@ -170,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_PACKET_RECEIVED(rx_buf_);
|
LOG_PACKET_RECEIVED(this->rx_buf_);
|
||||||
*frame = std::move(rx_buf_);
|
|
||||||
// consume msg
|
// Clear state for next frame (rx_buf_ still contains data for caller)
|
||||||
rx_buf_ = {};
|
this->rx_buf_len_ = 0;
|
||||||
rx_buf_len_ = 0;
|
this->rx_header_buf_pos_ = 0;
|
||||||
rx_header_buf_pos_ = 0;
|
this->rx_header_parsed_ = false;
|
||||||
rx_header_parsed_ = false;
|
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|
||||||
APIError aerr;
|
|
||||||
|
|
||||||
if (state_ != State::DATA) {
|
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||||
|
if (this->state_ != State::DATA) {
|
||||||
return APIError::WOULD_BLOCK;
|
return APIError::WOULD_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> frame;
|
APIError aerr = this->try_read_frame_();
|
||||||
aerr = try_read_frame_(&frame);
|
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
if (aerr == APIError::BAD_INDICATOR) {
|
if (aerr == APIError::BAD_INDICATOR) {
|
||||||
// Make sure to tell the remote that we don't
|
// Make sure to tell the remote that we don't
|
||||||
@@ -220,10 +210,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
return aerr;
|
return aerr;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer->container = std::move(frame);
|
buffer->container = std::move(this->rx_buf_);
|
||||||
buffer->data_offset = 0;
|
buffer->data_offset = 0;
|
||||||
buffer->data_len = rx_header_parsed_len_;
|
buffer->data_len = this->rx_header_parsed_len_;
|
||||||
buffer->type = rx_header_parsed_type_;
|
buffer->type = this->rx_header_parsed_type_;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||||
|
@@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
|||||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
APIError try_read_frame_();
|
||||||
|
|
||||||
// Group 2-byte aligned types
|
// Group 2-byte aligned types
|
||||||
uint16_t rx_header_parsed_type_ = 0;
|
uint16_t rx_header_parsed_type_ = 0;
|
||||||
|
@@ -884,6 +884,15 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_message(4, it, true);
|
buffer.encode_message(4, it, true);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(5, this->is_event);
|
buffer.encode_bool(5, this->is_event);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
buffer.encode_uint32(6, this->call_id);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
buffer.encode_bool(7, this->wants_response);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
buffer.encode_string(8, this->response_template);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
||||||
size.add_length(1, this->service_ref_.size());
|
size.add_length(1, this->service_ref_.size());
|
||||||
@@ -891,6 +900,48 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_repeated_message(1, this->data_template);
|
size.add_repeated_message(1, this->data_template);
|
||||||
size.add_repeated_message(1, this->variables);
|
size.add_repeated_message(1, this->variables);
|
||||||
size.add_bool(1, this->is_event);
|
size.add_bool(1, this->is_event);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
size.add_uint32(1, this->call_id);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
size.add_bool(1, this->wants_response);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
size.add_length(1, this->response_template.size());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1:
|
||||||
|
this->call_id = value.as_uint32();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this->success = value.as_bool();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 3:
|
||||||
|
this->error_message = value.as_string();
|
||||||
|
break;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
case 4: {
|
||||||
|
// Use raw data directly to avoid allocation
|
||||||
|
this->response_data = value.data();
|
||||||
|
this->response_data_len = value.size();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
|
@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
|
|||||||
class HomeassistantActionRequest final : public ProtoMessage {
|
class HomeassistantActionRequest final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 35;
|
static constexpr uint8_t MESSAGE_TYPE = 35;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 113;
|
static constexpr uint8_t ESTIMATED_SIZE = 128;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "homeassistant_action_request"; }
|
const char *message_name() const override { return "homeassistant_action_request"; }
|
||||||
#endif
|
#endif
|
||||||
@@ -1114,6 +1114,15 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
|||||||
std::vector<HomeassistantServiceMap> data_template{};
|
std::vector<HomeassistantServiceMap> data_template{};
|
||||||
std::vector<HomeassistantServiceMap> variables{};
|
std::vector<HomeassistantServiceMap> variables{};
|
||||||
bool is_event{false};
|
bool is_event{false};
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
uint32_t call_id{0};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
bool wants_response{false};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
std::string response_template{};
|
||||||
|
#endif
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1123,6 +1132,30 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
|||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t MESSAGE_TYPE = 130;
|
||||||
|
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
const char *message_name() const override { return "homeassistant_action_response"; }
|
||||||
|
#endif
|
||||||
|
uint32_t call_id{0};
|
||||||
|
bool success{false};
|
||||||
|
std::string error_message{};
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
const uint8_t *response_data{nullptr};
|
||||||
|
uint16_t response_data_len{0};
|
||||||
|
#endif
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void dump_to(std::string &out) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||||
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
|
@@ -1122,6 +1122,28 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
|
|||||||
out.append("\n");
|
out.append("\n");
|
||||||
}
|
}
|
||||||
dump_field(out, "is_event", this->is_event);
|
dump_field(out, "is_event", this->is_event);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
dump_field(out, "call_id", this->call_id);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
dump_field(out, "wants_response", this->wants_response);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
dump_field(out, "response_template", this->response_template);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void HomeassistantActionResponse::dump_to(std::string &out) const {
|
||||||
|
MessageDumpHelper helper(out, "HomeassistantActionResponse");
|
||||||
|
dump_field(out, "call_id", this->call_id);
|
||||||
|
dump_field(out, "success", this->success);
|
||||||
|
dump_field(out, "error_message", this->error_message);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
out.append(" response_data: ");
|
||||||
|
out.append(format_hex_pretty(this->response_data, this->response_data_len));
|
||||||
|
out.append("\n");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
|
@@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
this->on_z_wave_proxy_request(msg);
|
this->on_z_wave_proxy_request(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
case HomeassistantActionResponse::MESSAGE_TYPE: {
|
||||||
|
HomeassistantActionResponse msg;
|
||||||
|
msg.decode(msg_data, msg_size);
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
this->on_homeassistant_action_response(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
||||||
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
@@ -9,12 +9,16 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
#include "esphome/core/version.h"
|
#include "esphome/core/version.h"
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
|
#include "homeassistant_service.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
#include "esphome/components/logger/logger.h"
|
#include "esphome/components/logger/logger.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
@@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
|||||||
client->send_homeassistant_action(call);
|
client->send_homeassistant_action(call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
|
||||||
|
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
|
||||||
|
}
|
||||||
|
|
||||||
|
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
|
||||||
|
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
||||||
|
if (it->call_id == call_id) {
|
||||||
|
auto callback = std::move(it->callback);
|
||||||
|
this->action_response_callbacks_.erase(it);
|
||||||
|
ActionResponse response(success, error_message);
|
||||||
|
callback(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||||
|
const uint8_t *response_data, size_t response_data_len) {
|
||||||
|
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
||||||
|
if (it->call_id == call_id) {
|
||||||
|
auto callback = std::move(it->callback);
|
||||||
|
this->action_response_callbacks_.erase(it);
|
||||||
|
ActionResponse response(success, error_message, response_data, response_data_len);
|
||||||
|
callback(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
#include "user_services.h"
|
#include "user_services.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
@@ -111,7 +112,17 @@ class APIServer : public Component, public Controller {
|
|||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||||
|
|
||||||
#endif
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
// Action response handling
|
||||||
|
using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
|
||||||
|
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
|
||||||
|
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||||
|
const uint8_t *response_data, size_t response_data_len);
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
#endif
|
#endif
|
||||||
@@ -187,6 +198,13 @@ class APIServer : public Component, public Controller {
|
|||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
std::vector<UserServiceDescriptor *> user_services_;
|
std::vector<UserServiceDescriptor *> user_services_;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
struct PendingActionResponse {
|
||||||
|
uint32_t call_id;
|
||||||
|
ActionResponseCallback callback;
|
||||||
|
};
|
||||||
|
std::vector<PendingActionResponse> action_response_callbacks_;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Group smaller types together
|
// Group smaller types together
|
||||||
uint16_t port_{6053};
|
uint16_t port_{6053};
|
||||||
|
@@ -3,8 +3,13 @@
|
|||||||
#include "api_server.h"
|
#include "api_server.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
|
#include <functional>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
#include "esphome/components/json/json_util.h"
|
||||||
|
#endif
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
@@ -44,9 +49,47 @@ template<typename... Ts> class TemplatableKeyValuePair {
|
|||||||
TemplatableStringValue<Ts...> value;
|
TemplatableStringValue<Ts...> value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
// Represents the response data from a Home Assistant action
|
||||||
|
class ActionResponse {
|
||||||
|
public:
|
||||||
|
ActionResponse(bool success, std::string error_message = "")
|
||||||
|
: success_(success), error_message_(std::move(error_message)) {}
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
|
||||||
|
: success_(success), error_message_(std::move(error_message)) {
|
||||||
|
if (data == nullptr || data_len == 0)
|
||||||
|
return;
|
||||||
|
this->json_document_ = json::parse_json(data, data_len);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool is_success() const { return this->success_; }
|
||||||
|
const std::string &get_error_message() const { return this->error_message_; }
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
// Get data as parsed JSON object (const version returns read-only view)
|
||||||
|
JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool success_;
|
||||||
|
std::string error_message_;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
JsonDocument json_document_;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callback type for action responses
|
||||||
|
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
|
||||||
|
#endif
|
||||||
|
|
||||||
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
|
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
|
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
|
||||||
|
this->flags_.is_event = is_event;
|
||||||
|
}
|
||||||
|
|
||||||
template<typename T> void set_service(T service) { this->service_ = service; }
|
template<typename T> void set_service(T service) { this->service_ = service; }
|
||||||
|
|
||||||
@@ -61,11 +104,29 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
this->variables_.emplace_back(std::move(key), value);
|
this->variables_.emplace_back(std::move(key), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
template<typename T> void set_response_template(T response_template) {
|
||||||
|
this->response_template_ = response_template;
|
||||||
|
this->flags_.has_response_template = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_wants_status() { this->flags_.wants_status = true; }
|
||||||
|
void set_wants_response() { this->flags_.wants_response = true; }
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
|
||||||
|
return this->success_trigger_with_response_;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
|
||||||
|
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
|
||||||
void play(Ts... x) override {
|
void play(Ts... x) override {
|
||||||
HomeassistantActionRequest resp;
|
HomeassistantActionRequest resp;
|
||||||
std::string service_value = this->service_.value(x...);
|
std::string service_value = this->service_.value(x...);
|
||||||
resp.set_service(StringRef(service_value));
|
resp.set_service(StringRef(service_value));
|
||||||
resp.is_event = this->is_event_;
|
resp.is_event = this->flags_.is_event;
|
||||||
for (auto &it : this->data_) {
|
for (auto &it : this->data_) {
|
||||||
resp.data.emplace_back();
|
resp.data.emplace_back();
|
||||||
auto &kv = resp.data.back();
|
auto &kv = resp.data.back();
|
||||||
@@ -84,18 +145,74 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
kv.set_key(StringRef(it.key));
|
kv.set_key(StringRef(it.key));
|
||||||
kv.value = it.value.value(x...);
|
kv.value = it.value.value(x...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
if (this->flags_.wants_status) {
|
||||||
|
// Generate a unique call ID for this service call
|
||||||
|
static uint32_t call_id_counter = 1;
|
||||||
|
uint32_t call_id = call_id_counter++;
|
||||||
|
resp.call_id = call_id;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
if (this->flags_.wants_response) {
|
||||||
|
resp.wants_response = true;
|
||||||
|
// Set response template if provided
|
||||||
|
if (this->flags_.has_response_template) {
|
||||||
|
std::string response_template_value = this->response_template_.value(x...);
|
||||||
|
resp.response_template = response_template_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
auto captured_args = std::make_tuple(x...);
|
||||||
|
this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
|
||||||
|
std::apply(
|
||||||
|
[this, &response](auto &&...args) {
|
||||||
|
if (response.is_success()) {
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
if (this->flags_.wants_response) {
|
||||||
|
this->success_trigger_with_response_->trigger(response.get_json(), args...);
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
this->success_trigger_->trigger(args...);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this->error_trigger_->trigger(response.get_error_message(), args...);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
captured_args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
this->parent_->send_homeassistant_action(resp);
|
this->parent_->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIServer *parent_;
|
APIServer *parent_;
|
||||||
bool is_event_;
|
|
||||||
TemplatableStringValue<Ts...> service_{};
|
TemplatableStringValue<Ts...> service_{};
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
|
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
TemplatableStringValue<Ts...> response_template_{""};
|
||||||
|
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
|
||||||
|
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
|
||||||
|
struct Flags {
|
||||||
|
uint8_t is_event : 1;
|
||||||
|
uint8_t wants_status : 1;
|
||||||
|
uint8_t wants_response : 1;
|
||||||
|
uint8_t has_response_template : 1;
|
||||||
|
uint8_t reserved : 5;
|
||||||
|
} flags_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
@@ -35,7 +35,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
|||||||
msg.set_name(StringRef(this->name_));
|
msg.set_name(StringRef(this->name_));
|
||||||
msg.key = this->key_;
|
msg.key = this->key_;
|
||||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||||
for (int i = 0; i < sizeof...(Ts); i++) {
|
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||||
msg.args.emplace_back();
|
msg.args.emplace_back();
|
||||||
auto &arg = msg.args.back();
|
auto &arg = msg.args.back();
|
||||||
arg.type = arg_types[i];
|
arg.type = arg_types[i];
|
||||||
|
@@ -165,4 +165,4 @@ def final_validate_audio_schema(
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_library("esphome/esp-audio-libs", "1.1.4")
|
cg.add_library("esphome/esp-audio-libs", "2.0.1")
|
||||||
|
@@ -57,7 +57,7 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
|
|||||||
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
|
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
|
||||||
size_t samples_to_scale) {
|
size_t samples_to_scale) {
|
||||||
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
|
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
|
||||||
for (int i = 0; i < samples_to_scale; i++) {
|
for (size_t i = 0; i < samples_to_scale; i++) {
|
||||||
int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
|
int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
|
||||||
output_buffer[i] = (int16_t) (acc >> 15);
|
output_buffer[i] = (int16_t) (acc >> 15);
|
||||||
}
|
}
|
||||||
|
@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
|
|||||||
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
|
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
|
||||||
this->input_transfer_buffer_->available());
|
this->input_transfer_buffer_->available());
|
||||||
|
|
||||||
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
||||||
return FileDecoderState::POTENTIALLY_FAILED;
|
// Serrious error reading FLAC header, there is no recovery
|
||||||
}
|
|
||||||
|
|
||||||
if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
|
|
||||||
// Couldn't read FLAC header
|
|
||||||
return FileDecoderState::FAILED;
|
return FileDecoderState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
|
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
|
||||||
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
|
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
|
||||||
|
|
||||||
|
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
||||||
|
return FileDecoderState::MORE_TO_PROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
// Reallocate the output transfer buffer to the smallest necessary size
|
// Reallocate the output transfer buffer to the smallest necessary size
|
||||||
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
|
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
|
||||||
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
||||||
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t output_samples = 0;
|
uint32_t output_samples = 0;
|
||||||
auto result = this->flac_decoder_->decode_frame(
|
auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
|
||||||
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
|
this->input_transfer_buffer_->available(),
|
||||||
reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples);
|
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
|
||||||
|
|
||||||
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
|
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
|
||||||
// Not an issue, just needs more data that we'll get next time.
|
// Not an issue, just needs more data that we'll get next time.
|
||||||
|
@@ -97,10 +97,10 @@ void BL0906::handle_actions_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ActionCallbackFuncPtr ptr_func = nullptr;
|
ActionCallbackFuncPtr ptr_func = nullptr;
|
||||||
for (int i = 0; i < this->action_queue_.size(); i++) {
|
for (size_t i = 0; i < this->action_queue_.size(); i++) {
|
||||||
ptr_func = this->action_queue_[i];
|
ptr_func = this->action_queue_[i];
|
||||||
if (ptr_func) {
|
if (ptr_func) {
|
||||||
ESP_LOGI(TAG, "HandleActionCallback[%d]", i);
|
ESP_LOGI(TAG, "HandleActionCallback[%zu]", i);
|
||||||
(this->*ptr_func)();
|
(this->*ptr_func)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -51,7 +51,7 @@ void BL0942::loop() {
|
|||||||
if (!avail) {
|
if (!avail) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (avail < sizeof(buffer)) {
|
if (static_cast<size_t>(avail) < sizeof(buffer)) {
|
||||||
if (!this->rx_start_) {
|
if (!this->rx_start_) {
|
||||||
this->rx_start_ = millis();
|
this->rx_start_ = millis();
|
||||||
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
||||||
@@ -148,7 +148,7 @@ void BL0942::setup() {
|
|||||||
|
|
||||||
this->write_reg_(BL0942_REG_USR_WRPROT, 0);
|
this->write_reg_(BL0942_REG_USR_WRPROT, 0);
|
||||||
|
|
||||||
if (this->read_reg_(BL0942_REG_MODE) != mode)
|
if (static_cast<uint32_t>(this->read_reg_(BL0942_REG_MODE)) != mode)
|
||||||
this->status_set_warning(LOG_STR("BL0942 setup failed!"));
|
this->status_set_warning(LOG_STR("BL0942 setup failed!"));
|
||||||
|
|
||||||
this->flush();
|
this->flush();
|
||||||
|
@@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
)
|
)
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
|
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
|
||||||
esp32_ble_tracker.consume_connection_slots(1, "ble_client"),
|
esp32_ble.consume_connection_slots(1, "ble_client"),
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_BLE_CLIENT_ID = "ble_client_id"
|
CONF_BLE_CLIENT_ID = "ble_client_id"
|
||||||
|
@@ -42,9 +42,7 @@ def validate_connections(config):
|
|||||||
)
|
)
|
||||||
elif config[CONF_ACTIVE]:
|
elif config[CONF_ACTIVE]:
|
||||||
connection_slots: int = config[CONF_CONNECTION_SLOTS]
|
connection_slots: int = config[CONF_CONNECTION_SLOTS]
|
||||||
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
|
esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
|
||||||
config
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**config,
|
**config,
|
||||||
@@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
default=DEFAULT_CONNECTION_SLOTS,
|
default=DEFAULT_CONNECTION_SLOTS,
|
||||||
): cv.All(
|
): cv.All(
|
||||||
cv.positive_int,
|
cv.positive_int,
|
||||||
cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
|
cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_CONNECTIONS): cv.All(
|
cv.Optional(CONF_CONNECTIONS): cv.All(
|
||||||
cv.ensure_list(CONNECTION_SCHEMA),
|
cv.ensure_list(CONNECTION_SCHEMA),
|
||||||
cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
|
cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -11,14 +11,14 @@ namespace captive_portal {
|
|||||||
static const char *const TAG = "captive_portal";
|
static const char *const TAG = "captive_portal";
|
||||||
|
|
||||||
void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||||
AsyncResponseStream *stream = request->beginResponseStream(F("application/json"));
|
AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json"));
|
||||||
stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate"));
|
stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate"));
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
stream->print(F("{\"mac\":\""));
|
stream->print(ESPHOME_F("{\"mac\":\""));
|
||||||
stream->print(get_mac_address_pretty().c_str());
|
stream->print(get_mac_address_pretty().c_str());
|
||||||
stream->print(F("\",\"name\":\""));
|
stream->print(ESPHOME_F("\",\"name\":\""));
|
||||||
stream->print(App.get_name().c_str());
|
stream->print(App.get_name().c_str());
|
||||||
stream->print(F("\",\"aps\":[{}"));
|
stream->print(ESPHOME_F("\",\"aps\":[{}"));
|
||||||
#else
|
#else
|
||||||
stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str());
|
stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str());
|
||||||
#endif
|
#endif
|
||||||
@@ -29,19 +29,19 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
// Assumes no " in ssid, possible unicode isses?
|
// Assumes no " in ssid, possible unicode isses?
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
stream->print(F(",{\"ssid\":\""));
|
stream->print(ESPHOME_F(",{\"ssid\":\""));
|
||||||
stream->print(scan.get_ssid().c_str());
|
stream->print(scan.get_ssid().c_str());
|
||||||
stream->print(F("\",\"rssi\":"));
|
stream->print(ESPHOME_F("\",\"rssi\":"));
|
||||||
stream->print(scan.get_rssi());
|
stream->print(scan.get_rssi());
|
||||||
stream->print(F(",\"lock\":"));
|
stream->print(ESPHOME_F(",\"lock\":"));
|
||||||
stream->print(scan.get_with_auth());
|
stream->print(scan.get_with_auth());
|
||||||
stream->print(F("}"));
|
stream->print(ESPHOME_F("}"));
|
||||||
#else
|
#else
|
||||||
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
|
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
|
||||||
scan.get_with_auth());
|
scan.get_with_auth());
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
stream->print(F("]}"));
|
stream->print(ESPHOME_F("]}"));
|
||||||
request->send(stream);
|
request->send(stream);
|
||||||
}
|
}
|
||||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||||
@@ -52,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
|||||||
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
|
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
|
||||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
||||||
wifi::global_wifi_component->start_scanning();
|
wifi::global_wifi_component->start_scanning();
|
||||||
request->redirect(F("/?save"));
|
request->redirect(ESPHOME_F("/?save"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CaptivePortal::setup() {
|
void CaptivePortal::setup() {
|
||||||
@@ -75,7 +75,7 @@ void CaptivePortal::start() {
|
|||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
this->dns_server_ = make_unique<DNSServer>();
|
this->dns_server_ = make_unique<DNSServer>();
|
||||||
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
this->dns_server_->start(53, F("*"), ip);
|
this->dns_server_->start(53, ESPHOME_F("*"), ip);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this->initialized_ = true;
|
this->initialized_ = true;
|
||||||
@@ -88,10 +88,10 @@ void CaptivePortal::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
||||||
if (req->url() == F("/config.json")) {
|
if (req->url() == ESPHOME_F("/config.json")) {
|
||||||
this->handle_config(req);
|
this->handle_config(req);
|
||||||
return;
|
return;
|
||||||
} else if (req->url() == F("/wifisave")) {
|
} else if (req->url() == ESPHOME_F("/wifisave")) {
|
||||||
this->handle_wifisave(req);
|
this->handle_wifisave(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,11 +100,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
|||||||
// This includes OS captive portal detection endpoints which will trigger
|
// This includes OS captive portal detection endpoints which will trigger
|
||||||
// the captive portal when they don't receive their expected responses
|
// the captive portal when they don't receive their expected responses
|
||||||
#ifndef USE_ESP8266
|
#ifndef USE_ESP8266
|
||||||
auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
|
auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
|
||||||
#else
|
#else
|
||||||
auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
|
auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
|
||||||
#endif
|
#endif
|
||||||
response->addHeader(F("Content-Encoding"), F("gzip"));
|
response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
|
||||||
req->send(response);
|
req->send(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03,
|
|||||||
|
|
||||||
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
|
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
|
||||||
uint8_t crc = 0;
|
uint8_t crc = 0;
|
||||||
for (int i = 0; i < len - 1; i++) {
|
for (size_t i = 0; i < len - 1; i++) {
|
||||||
crc -= response[i];
|
crc -= response[i];
|
||||||
}
|
}
|
||||||
return crc;
|
return crc;
|
||||||
|
@@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() {
|
|||||||
uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};
|
uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};
|
||||||
|
|
||||||
// Calculate checksum
|
// Calculate checksum
|
||||||
for (int i = 0; i < sizeof(remote_header) - 1; i++) {
|
for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
|
||||||
remote_header[sizeof(remote_header) - 1] += remote_header[i];
|
remote_header[sizeof(remote_header) - 1] += remote_header[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() {
|
|||||||
remote_state[9] = fan_speed & 0xff;
|
remote_state[9] = fan_speed & 0xff;
|
||||||
|
|
||||||
// Calculate checksum
|
// Calculate checksum
|
||||||
for (int i = 0; i < sizeof(remote_header) - 1; i++) {
|
for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
|
||||||
remote_header[sizeof(remote_header) - 1] += remote_header[i];
|
remote_header[sizeof(remote_header) - 1] += remote_header[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
bool valid_daikin_frame = false;
|
bool valid_daikin_frame = false;
|
||||||
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
|
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
|
||||||
valid_daikin_frame = true;
|
valid_daikin_frame = true;
|
||||||
int bytes_count = data.size() / 2 / 8;
|
size_t bytes_count = data.size() / 2 / 8;
|
||||||
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
|
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
|
||||||
buf[0] = '\0';
|
buf[0] = '\0';
|
||||||
for (size_t i = 0; i < bytes_count; i++) {
|
for (size_t i = 0; i < bytes_count; i++) {
|
||||||
@@ -370,7 +370,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
if (!valid_daikin_frame) {
|
if (!valid_daikin_frame) {
|
||||||
char sbuf[16 * 10 + 1];
|
char sbuf[16 * 10 + 1];
|
||||||
sbuf[0] = '\0';
|
sbuf[0] = '\0';
|
||||||
for (size_t j = 0; j < data.size(); j++) {
|
for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
|
||||||
if ((j - 2) % 16 == 0) {
|
if ((j - 2) % 16 == 0) {
|
||||||
if (j > 0) {
|
if (j > 0) {
|
||||||
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
|
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
|
||||||
@@ -380,19 +380,26 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
char type_ch = ' ';
|
char type_ch = ' ';
|
||||||
// debug_tolerance = 25%
|
// debug_tolerance = 25%
|
||||||
|
|
||||||
if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))
|
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] &&
|
||||||
|
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)))
|
||||||
type_ch = 'P';
|
type_ch = 'P';
|
||||||
if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))
|
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] &&
|
||||||
|
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)))
|
||||||
type_ch = 'a';
|
type_ch = 'a';
|
||||||
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))
|
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] &&
|
||||||
|
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)))
|
||||||
type_ch = 'H';
|
type_ch = 'H';
|
||||||
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))
|
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] &&
|
||||||
|
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)))
|
||||||
type_ch = 'h';
|
type_ch = 'h';
|
||||||
if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))
|
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] &&
|
||||||
|
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)))
|
||||||
type_ch = 'B';
|
type_ch = 'B';
|
||||||
if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))
|
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] &&
|
||||||
|
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)))
|
||||||
type_ch = '1';
|
type_ch = '1';
|
||||||
if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))
|
if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] &&
|
||||||
|
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)))
|
||||||
type_ch = '0';
|
type_ch = '0';
|
||||||
|
|
||||||
if (abs(data[j]) > 100000) {
|
if (abs(data[j]) > 100000) {
|
||||||
@@ -400,7 +407,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
} else {
|
} else {
|
||||||
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
|
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
|
||||||
}
|
}
|
||||||
if (j == data.size() - 1) {
|
if (j + 1 == static_cast<size_t>(data.size())) {
|
||||||
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
|
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ namespace dashboard_import {
|
|||||||
|
|
||||||
static std::string g_package_import_url; // NOLINT
|
static std::string g_package_import_url; // NOLINT
|
||||||
|
|
||||||
std::string get_package_import_url() { return g_package_import_url; }
|
const std::string &get_package_import_url() { return g_package_import_url; }
|
||||||
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
|
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
|
||||||
|
|
||||||
} // namespace dashboard_import
|
} // namespace dashboard_import
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace dashboard_import {
|
namespace dashboard_import {
|
||||||
|
|
||||||
std::string get_package_import_url();
|
const std::string &get_package_import_url();
|
||||||
void set_package_import_url(std::string url);
|
void set_package_import_url(std::string url);
|
||||||
|
|
||||||
} // namespace dashboard_import
|
} // namespace dashboard_import
|
||||||
|
1
esphome/components/epaper_spi/__init__.py
Normal file
1
esphome/components/epaper_spi/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@esphome/core"]
|
80
esphome/components/epaper_spi/display.py
Normal file
80
esphome/components/epaper_spi/display.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from esphome import core, pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import display, spi
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BUSY_PIN,
|
||||||
|
CONF_DC_PIN,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_LAMBDA,
|
||||||
|
CONF_MODEL,
|
||||||
|
CONF_PAGES,
|
||||||
|
CONF_RESET_DURATION,
|
||||||
|
CONF_RESET_PIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTO_LOAD = ["split_buffer"]
|
||||||
|
DEPENDENCIES = ["spi"]
|
||||||
|
|
||||||
|
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
|
||||||
|
EPaperBase = epaper_spi_ns.class_(
|
||||||
|
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
|
||||||
|
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
|
||||||
|
|
||||||
|
MODELS = {
|
||||||
|
"7.3in-spectra-e6": EPaper7p3InSpectraE6,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
display.FULL_DISPLAY_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(EPaperBase),
|
||||||
|
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
|
||||||
|
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
|
||||||
|
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||||
|
cv.positive_time_period_milliseconds,
|
||||||
|
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(spi.spi_device_schema()),
|
||||||
|
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||||
|
)
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||||
|
"epaper_spi", require_miso=False, require_mosi=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
|
||||||
|
rhs = model.new()
|
||||||
|
var = cg.Pvariable(config[CONF_ID], rhs, model)
|
||||||
|
|
||||||
|
await display.register_display(var, config)
|
||||||
|
await spi.register_spi_device(var, config)
|
||||||
|
|
||||||
|
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||||
|
cg.add(var.set_dc_pin(dc))
|
||||||
|
|
||||||
|
if CONF_LAMBDA in config:
|
||||||
|
lambda_ = await cg.process_lambda(
|
||||||
|
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
|
||||||
|
)
|
||||||
|
cg.add(var.set_writer(lambda_))
|
||||||
|
if CONF_RESET_PIN in config:
|
||||||
|
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
|
||||||
|
cg.add(var.set_reset_pin(reset))
|
||||||
|
if CONF_BUSY_PIN in config:
|
||||||
|
busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
|
||||||
|
cg.add(var.set_busy_pin(busy))
|
||||||
|
if CONF_RESET_DURATION in config:
|
||||||
|
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
|
227
esphome/components/epaper_spi/epaper_spi.cpp
Normal file
227
esphome/components/epaper_spi/epaper_spi.cpp
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#include "epaper_spi.h"
|
||||||
|
#include <cinttypes>
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
static const char *const TAG = "epaper_spi";
|
||||||
|
|
||||||
|
static const LogString *epaper_state_to_string(EPaperState state) {
|
||||||
|
switch (state) {
|
||||||
|
case EPaperState::IDLE:
|
||||||
|
return LOG_STR("IDLE");
|
||||||
|
case EPaperState::UPDATE:
|
||||||
|
return LOG_STR("UPDATE");
|
||||||
|
case EPaperState::RESET:
|
||||||
|
return LOG_STR("RESET");
|
||||||
|
case EPaperState::INITIALISE:
|
||||||
|
return LOG_STR("INITIALISE");
|
||||||
|
case EPaperState::TRANSFER_DATA:
|
||||||
|
return LOG_STR("TRANSFER_DATA");
|
||||||
|
case EPaperState::POWER_ON:
|
||||||
|
return LOG_STR("POWER_ON");
|
||||||
|
case EPaperState::REFRESH_SCREEN:
|
||||||
|
return LOG_STR("REFRESH_SCREEN");
|
||||||
|
case EPaperState::POWER_OFF:
|
||||||
|
return LOG_STR("POWER_OFF");
|
||||||
|
case EPaperState::DEEP_SLEEP:
|
||||||
|
return LOG_STR("DEEP_SLEEP");
|
||||||
|
default:
|
||||||
|
return LOG_STR("UNKNOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::setup() {
|
||||||
|
if (!this->init_buffer_(this->get_buffer_length())) {
|
||||||
|
this->mark_failed("Failed to initialise buffer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->setup_pins_();
|
||||||
|
this->spi_setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EPaperBase::init_buffer_(size_t buffer_length) {
|
||||||
|
if (!this->buffer_.init(buffer_length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::setup_pins_() {
|
||||||
|
this->dc_pin_->setup(); // OUTPUT
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->reset_pin_->setup(); // OUTPUT
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->busy_pin_ != nullptr) {
|
||||||
|
this->busy_pin_->setup(); // INPUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
||||||
|
|
||||||
|
void EPaperBase::command(uint8_t value) {
|
||||||
|
this->start_command_();
|
||||||
|
this->write_byte(value);
|
||||||
|
this->end_command_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::data(uint8_t value) {
|
||||||
|
this->start_data_();
|
||||||
|
this->write_byte(value);
|
||||||
|
this->end_data_();
|
||||||
|
}
|
||||||
|
|
||||||
|
// write a command followed by zero or more bytes of data.
|
||||||
|
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
|
||||||
|
// [COMMAND, LENGTH, DATA...]
|
||||||
|
void EPaperBase::cmd_data(const uint8_t *data) {
|
||||||
|
const uint8_t command = data[0];
|
||||||
|
const uint8_t length = data[1];
|
||||||
|
const uint8_t *ptr = data + 2;
|
||||||
|
|
||||||
|
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
|
||||||
|
format_hex_pretty(ptr, length, '.', false).c_str());
|
||||||
|
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(command);
|
||||||
|
if (length > 0) {
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
this->write_array(ptr, length);
|
||||||
|
}
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EPaperBase::is_idle_() {
|
||||||
|
if (this->busy_pin_ == nullptr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !this->busy_pin_->digital_read();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::reset() {
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
this->disable_loop();
|
||||||
|
this->set_timeout(this->reset_duration_, [this] {
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
this->set_timeout(20, [this] { this->enable_loop(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::update() {
|
||||||
|
if (!this->state_queue_.empty()) {
|
||||||
|
ESP_LOGE(TAG, "Display update already in progress - %s",
|
||||||
|
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->state_queue_.push(EPaperState::UPDATE);
|
||||||
|
this->state_queue_.push(EPaperState::RESET);
|
||||||
|
this->state_queue_.push(EPaperState::INITIALISE);
|
||||||
|
this->state_queue_.push(EPaperState::TRANSFER_DATA);
|
||||||
|
this->state_queue_.push(EPaperState::POWER_ON);
|
||||||
|
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
|
||||||
|
this->state_queue_.push(EPaperState::POWER_OFF);
|
||||||
|
this->state_queue_.push(EPaperState::DEEP_SLEEP);
|
||||||
|
this->state_queue_.push(EPaperState::IDLE);
|
||||||
|
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::loop() {
|
||||||
|
if (this->waiting_for_idle_) {
|
||||||
|
if (this->is_idle_()) {
|
||||||
|
this->waiting_for_idle_ = false;
|
||||||
|
} else {
|
||||||
|
if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
|
||||||
|
ESP_LOGV(TAG, "Waiting for idle");
|
||||||
|
this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto state = this->state_queue_.front();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case EPaperState::IDLE:
|
||||||
|
this->disable_loop();
|
||||||
|
break;
|
||||||
|
case EPaperState::UPDATE:
|
||||||
|
this->do_update_(); // Calls ESPHome (current page) lambda
|
||||||
|
break;
|
||||||
|
case EPaperState::RESET:
|
||||||
|
this->reset();
|
||||||
|
break;
|
||||||
|
case EPaperState::INITIALISE:
|
||||||
|
this->initialise_();
|
||||||
|
break;
|
||||||
|
case EPaperState::TRANSFER_DATA:
|
||||||
|
if (!this->transfer_data()) {
|
||||||
|
return; // Not done yet, come back next loop
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EPaperState::POWER_ON:
|
||||||
|
this->power_on();
|
||||||
|
break;
|
||||||
|
case EPaperState::REFRESH_SCREEN:
|
||||||
|
this->refresh_screen();
|
||||||
|
break;
|
||||||
|
case EPaperState::POWER_OFF:
|
||||||
|
this->power_off();
|
||||||
|
break;
|
||||||
|
case EPaperState::DEEP_SLEEP:
|
||||||
|
this->deep_sleep();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->state_queue_.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::start_command_() {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::end_command_() { this->disable(); }
|
||||||
|
|
||||||
|
void EPaperBase::start_data_() {
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
this->enable();
|
||||||
|
}
|
||||||
|
void EPaperBase::end_data_() { this->disable(); }
|
||||||
|
|
||||||
|
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
|
||||||
|
|
||||||
|
void EPaperBase::initialise_() {
|
||||||
|
size_t index = 0;
|
||||||
|
const auto &sequence = this->init_sequence_;
|
||||||
|
const size_t sequence_size = this->init_sequence_length_;
|
||||||
|
while (index != sequence_size) {
|
||||||
|
if (sequence_size - index < 2) {
|
||||||
|
this->mark_failed("Malformed init sequence");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto *ptr = sequence + index;
|
||||||
|
const uint8_t length = ptr[1];
|
||||||
|
if (sequence_size - index < length + 2) {
|
||||||
|
this->mark_failed("Malformed init sequence");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->cmd_data(ptr);
|
||||||
|
index += length + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->power_on();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
93
esphome/components/epaper_spi/epaper_spi.h
Normal file
93
esphome/components/epaper_spi/epaper_spi.h
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/display/display_buffer.h"
|
||||||
|
#include "esphome/components/spi/spi.h"
|
||||||
|
#include "esphome/components/split_buffer/split_buffer.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
enum class EPaperState : uint8_t {
|
||||||
|
IDLE,
|
||||||
|
UPDATE,
|
||||||
|
RESET,
|
||||||
|
INITIALISE,
|
||||||
|
TRANSFER_DATA,
|
||||||
|
POWER_ON,
|
||||||
|
REFRESH_SCREEN,
|
||||||
|
POWER_OFF,
|
||||||
|
DEEP_SLEEP,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
|
||||||
|
|
||||||
|
class EPaperBase : public display::DisplayBuffer,
|
||||||
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||||
|
spi::DATA_RATE_2MHZ> {
|
||||||
|
public:
|
||||||
|
EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
|
||||||
|
: init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
|
||||||
|
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||||
|
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
|
||||||
|
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
|
||||||
|
|
||||||
|
void command(uint8_t value);
|
||||||
|
void data(uint8_t value);
|
||||||
|
void cmd_data(const uint8_t *data);
|
||||||
|
|
||||||
|
void update() override;
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
void on_safe_shutdown() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool is_idle_();
|
||||||
|
void setup_pins_();
|
||||||
|
virtual void reset();
|
||||||
|
void initialise_();
|
||||||
|
bool init_buffer_(size_t buffer_length);
|
||||||
|
|
||||||
|
virtual int get_width_controller() { return this->get_width_internal(); };
|
||||||
|
virtual void deep_sleep() = 0;
|
||||||
|
/**
|
||||||
|
* Send data to the device via SPI
|
||||||
|
* @return true if done, false if should be called next loop
|
||||||
|
*/
|
||||||
|
virtual bool transfer_data() = 0;
|
||||||
|
virtual void refresh_screen() = 0;
|
||||||
|
|
||||||
|
virtual void power_on() = 0;
|
||||||
|
virtual void power_off() = 0;
|
||||||
|
virtual uint32_t get_buffer_length() = 0;
|
||||||
|
|
||||||
|
void start_command_();
|
||||||
|
void end_command_();
|
||||||
|
void start_data_();
|
||||||
|
void end_data_();
|
||||||
|
|
||||||
|
const size_t init_sequence_length_{0};
|
||||||
|
|
||||||
|
size_t current_data_index_{0};
|
||||||
|
uint32_t reset_duration_{200};
|
||||||
|
uint32_t waiting_for_idle_last_print_{0};
|
||||||
|
|
||||||
|
GPIOPin *dc_pin_;
|
||||||
|
GPIOPin *busy_pin_{nullptr};
|
||||||
|
GPIOPin *reset_pin_{nullptr};
|
||||||
|
|
||||||
|
const uint8_t *init_sequence_{nullptr};
|
||||||
|
|
||||||
|
bool waiting_for_idle_{false};
|
||||||
|
|
||||||
|
split_buffer::SplitBuffer buffer_;
|
||||||
|
|
||||||
|
std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
@@ -0,0 +1,42 @@
|
|||||||
|
#include "epaper_spi_model_7p3in_spectra_e6.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::power_on() {
|
||||||
|
ESP_LOGI(TAG, "Power on");
|
||||||
|
this->command(0x04);
|
||||||
|
this->waiting_for_idle_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::power_off() {
|
||||||
|
ESP_LOGI(TAG, "Power off");
|
||||||
|
this->command(0x02);
|
||||||
|
this->data(0x00);
|
||||||
|
this->waiting_for_idle_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::refresh_screen() {
|
||||||
|
ESP_LOGI(TAG, "Refresh");
|
||||||
|
this->command(0x12);
|
||||||
|
this->data(0x00);
|
||||||
|
this->waiting_for_idle_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::deep_sleep() {
|
||||||
|
ESP_LOGI(TAG, "Deep sleep");
|
||||||
|
this->command(0x07);
|
||||||
|
this->data(0xA5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::dump_config() {
|
||||||
|
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||||
|
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
|
||||||
|
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||||
|
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||||
|
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||||
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "epaper_spi_spectra_e6.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
|
||||||
|
static constexpr const uint16_t WIDTH = 800;
|
||||||
|
static constexpr const uint16_t HEIGHT = 480;
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
// Command, data length, data
|
||||||
|
static constexpr uint8_t INIT_SEQUENCE[] = {
|
||||||
|
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
|
||||||
|
0x01, 1, 0x3F,
|
||||||
|
0x00, 2, 0x5F, 0x69,
|
||||||
|
0x03, 4, 0x00, 0x54, 0x00, 0x44,
|
||||||
|
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
|
||||||
|
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
|
||||||
|
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
|
||||||
|
0x30, 1, 0x03,
|
||||||
|
0x50, 1, 0x3F,
|
||||||
|
0x60, 2, 0x02, 0x00,
|
||||||
|
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
|
||||||
|
0x84, 1, 0x01,
|
||||||
|
0xE3, 1, 0x2F,
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
public:
|
||||||
|
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int get_width_internal() override { return WIDTH; };
|
||||||
|
int get_height_internal() override { return HEIGHT; };
|
||||||
|
|
||||||
|
void refresh_screen() override;
|
||||||
|
void power_on() override;
|
||||||
|
void power_off() override;
|
||||||
|
void deep_sleep() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
135
esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
Normal file
135
esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#include "epaper_spi_spectra_e6.h"
|
||||||
|
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
static constexpr const char *const TAG = "epaper_spi.6c";
|
||||||
|
|
||||||
|
static inline uint8_t color_to_hex(Color color) {
|
||||||
|
if (color.red > 127) {
|
||||||
|
if (color.green > 170) {
|
||||||
|
if (color.blue > 127) {
|
||||||
|
return 0x1; // White
|
||||||
|
} else {
|
||||||
|
return 0x2; // Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0x3; // Red (or Magenta)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (color.green > 127) {
|
||||||
|
if (color.blue > 127) {
|
||||||
|
return 0x5; // Cyan -> Blue
|
||||||
|
} else {
|
||||||
|
return 0x6; // Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (color.blue > 127) {
|
||||||
|
return 0x5; // Blue
|
||||||
|
} else {
|
||||||
|
return 0x0; // Black
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperSpectraE6::fill(Color color) {
|
||||||
|
uint8_t pixel_color;
|
||||||
|
if (color.is_on()) {
|
||||||
|
pixel_color = color_to_hex(color);
|
||||||
|
} else {
|
||||||
|
pixel_color = 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We store 8 bitset<3> in 3 bytes
|
||||||
|
// | byte 1 | byte 2 | byte 3 |
|
||||||
|
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||||
|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
|
||||||
|
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
|
||||||
|
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
|
||||||
|
|
||||||
|
const size_t buffer_length = this->get_buffer_length();
|
||||||
|
for (size_t i = 0; i < buffer_length; i += 3) {
|
||||||
|
this->buffer_[i + 0] = byte_1;
|
||||||
|
this->buffer_[i + 1] = byte_2;
|
||||||
|
this->buffer_[i + 2] = byte_3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t EPaperSpectraE6::get_buffer_length() {
|
||||||
|
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
|
||||||
|
return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||||
|
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint8_t pixel_bits = color_to_hex(color);
|
||||||
|
uint32_t pixel_position = x + y * this->get_width_controller();
|
||||||
|
uint32_t first_bit_position = pixel_position * 3;
|
||||||
|
uint32_t byte_position = first_bit_position / 8u;
|
||||||
|
uint32_t byte_subposition = first_bit_position % 8u;
|
||||||
|
|
||||||
|
if (byte_subposition <= 5) {
|
||||||
|
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
|
||||||
|
(pixel_bits << (5 - byte_subposition));
|
||||||
|
} else {
|
||||||
|
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
|
||||||
|
(pixel_bits >> (byte_subposition - 5));
|
||||||
|
|
||||||
|
this->buffer_[byte_position + 1] =
|
||||||
|
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
|
||||||
|
(pixel_bits << (13 - byte_subposition));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HOT EPaperSpectraE6::transfer_data() {
|
||||||
|
const uint32_t start_time = App.get_loop_component_start_time();
|
||||||
|
if (this->current_data_index_ == 0) {
|
||||||
|
ESP_LOGV(TAG, "Sending data");
|
||||||
|
this->command(0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t bytes_to_send[4]{0};
|
||||||
|
const size_t buffer_length = this->get_buffer_length();
|
||||||
|
for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
|
||||||
|
const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
|
||||||
|
// 8 pixels are stored in 3 bytes
|
||||||
|
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||||
|
// | byte 1 | byte 2 | byte 3 |
|
||||||
|
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
|
||||||
|
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
|
||||||
|
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
|
||||||
|
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
|
||||||
|
|
||||||
|
this->start_data_();
|
||||||
|
this->write_array(bytes_to_send, sizeof(bytes_to_send));
|
||||||
|
this->end_data_();
|
||||||
|
|
||||||
|
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||||
|
// Let the main loop run and come back next loop
|
||||||
|
this->current_data_index_ = i + 3;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Finished the entire dataset
|
||||||
|
this->current_data_index_ = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperSpectraE6::reset() {
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->disable_loop();
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
this->set_timeout(20, [this] {
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
delay(2);
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
this->set_timeout(20, [this] { this->enable_loop(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
23
esphome/components/epaper_spi/epaper_spi_spectra_e6.h
Normal file
23
esphome/components/epaper_spi/epaper_spi_spectra_e6.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "epaper_spi.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
class EPaperSpectraE6 : public EPaperBase {
|
||||||
|
public:
|
||||||
|
EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
|
||||||
|
: EPaperBase(init_sequence, init_sequence_length) {}
|
||||||
|
|
||||||
|
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||||
|
void fill(Color color) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||||
|
uint32_t get_buffer_length() override;
|
||||||
|
|
||||||
|
bool transfer_data() override;
|
||||||
|
void reset() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
@@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ES7210::configure_sample_rate_() {
|
bool ES7210::configure_sample_rate_() {
|
||||||
int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
|
uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
|
||||||
int coeff = -1;
|
int coeff = -1;
|
||||||
|
|
||||||
for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
|
for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
|
||||||
if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
|
if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
|
||||||
coeff = i;
|
coeff = static_cast<int>(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coeff >= 0) {
|
if (coeff >= 0) {
|
||||||
|
@@ -296,14 +296,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
|||||||
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
|
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
|
||||||
|
|
||||||
|
|
||||||
def _format_framework_espidf_version(
|
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||||
ver: cv.Version, release: str, for_platformio: bool
|
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
||||||
) -> str:
|
|
||||||
# format the given arduino (https://github.com/espressif/esp-idf/releases) version to
|
|
||||||
# a PIO platformio/framework-espidf value
|
# a PIO platformio/framework-espidf value
|
||||||
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
|
|
||||||
if for_platformio:
|
|
||||||
return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
|
||||||
if release:
|
if release:
|
||||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
|
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
|
||||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
|
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
|
||||||
@@ -317,157 +312,114 @@ def _format_framework_espidf_version(
|
|||||||
|
|
||||||
# The default/recommended arduino framework version
|
# The default/recommended arduino framework version
|
||||||
# - https://github.com/espressif/arduino-esp32/releases
|
# - https://github.com/espressif/arduino-esp32/releases
|
||||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
|
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
# The platform-espressif32 version to use for arduino frameworks
|
"recommended": cv.Version(3, 2, 1),
|
||||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
"latest": cv.Version(3, 3, 1),
|
||||||
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
|
"dev": cv.Version(3, 3, 1),
|
||||||
|
}
|
||||||
|
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||||
|
cv.Version(3, 3, 1): cv.Version(55, 3, 31),
|
||||||
|
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
|
||||||
|
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
|
||||||
|
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
|
||||||
|
cv.Version(3, 1, 3): cv.Version(53, 3, 13),
|
||||||
|
cv.Version(3, 1, 2): cv.Version(53, 3, 12),
|
||||||
|
cv.Version(3, 1, 1): cv.Version(53, 3, 11),
|
||||||
|
cv.Version(3, 1, 0): cv.Version(53, 3, 10),
|
||||||
|
}
|
||||||
|
|
||||||
# The default/recommended esp-idf framework version
|
# The default/recommended esp-idf framework version
|
||||||
# - https://github.com/espressif/esp-idf/releases
|
# - https://github.com/espressif/esp-idf/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
|
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
|
"recommended": cv.Version(5, 4, 2),
|
||||||
# The platformio/espressif32 version to use for esp-idf frameworks
|
"latest": cv.Version(5, 5, 1),
|
||||||
# - https://github.com/platformio/platform-espressif32/releases
|
"dev": cv.Version(5, 5, 1),
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
}
|
||||||
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
|
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||||
|
cv.Version(5, 5, 1): cv.Version(55, 3, 31),
|
||||||
|
cv.Version(5, 5, 0): cv.Version(55, 3, 31),
|
||||||
|
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
||||||
|
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
||||||
|
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
|
||||||
|
cv.Version(5, 3, 2): cv.Version(53, 3, 13),
|
||||||
|
cv.Version(5, 3, 1): cv.Version(53, 3, 13),
|
||||||
|
cv.Version(5, 3, 0): cv.Version(53, 3, 13),
|
||||||
|
cv.Version(5, 1, 6): cv.Version(51, 3, 7),
|
||||||
|
cv.Version(5, 1, 5): cv.Version(51, 3, 7),
|
||||||
|
}
|
||||||
|
|
||||||
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
|
# The platform-espressif32 version
|
||||||
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
|
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||||
cv.Version(5, 3, 1),
|
PLATFORM_VERSION_LOOKUP = {
|
||||||
cv.Version(5, 3, 0),
|
"recommended": cv.Version(54, 3, 21, "2"),
|
||||||
cv.Version(5, 2, 2),
|
"latest": cv.Version(55, 3, 31),
|
||||||
cv.Version(5, 2, 1),
|
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||||
cv.Version(5, 1, 2),
|
}
|
||||||
cv.Version(5, 1, 1),
|
|
||||||
cv.Version(5, 1, 0),
|
|
||||||
cv.Version(5, 0, 2),
|
|
||||||
cv.Version(5, 0, 1),
|
|
||||||
cv.Version(5, 0, 0),
|
|
||||||
]
|
|
||||||
|
|
||||||
# pioarduino versions that don't require a release number
|
|
||||||
# List based on https://github.com/pioarduino/esp-idf/releases
|
|
||||||
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
|
|
||||||
cv.Version(5, 5, 1),
|
|
||||||
cv.Version(5, 5, 0),
|
|
||||||
cv.Version(5, 4, 2),
|
|
||||||
cv.Version(5, 4, 1),
|
|
||||||
cv.Version(5, 4, 0),
|
|
||||||
cv.Version(5, 3, 3),
|
|
||||||
cv.Version(5, 3, 2),
|
|
||||||
cv.Version(5, 3, 1),
|
|
||||||
cv.Version(5, 3, 0),
|
|
||||||
cv.Version(5, 1, 5),
|
|
||||||
cv.Version(5, 1, 6),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _check_versions(value):
|
def _check_versions(value):
|
||||||
value = value.copy()
|
value = value.copy()
|
||||||
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
|
||||||
lookups = {
|
|
||||||
"dev": (
|
|
||||||
cv.Version(3, 2, 1),
|
|
||||||
"https://github.com/espressif/arduino-esp32.git",
|
|
||||||
),
|
|
||||||
"latest": (cv.Version(3, 2, 1), None),
|
|
||||||
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
|
|
||||||
}
|
|
||||||
|
|
||||||
if value[CONF_VERSION] in lookups:
|
if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
|
||||||
if CONF_SOURCE in value:
|
if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
|
||||||
raise cv.Invalid(
|
|
||||||
"Framework version needs to be explicitly specified when custom source is used."
|
|
||||||
)
|
|
||||||
|
|
||||||
version, source = lookups[value[CONF_VERSION]]
|
|
||||||
else:
|
|
||||||
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
|
||||||
source = value.get(CONF_SOURCE, None)
|
|
||||||
|
|
||||||
value[CONF_VERSION] = str(version)
|
|
||||||
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
|
|
||||||
|
|
||||||
value[CONF_PLATFORM_VERSION] = value.get(
|
|
||||||
CONF_PLATFORM_VERSION,
|
|
||||||
_parse_platform_version(str(ARDUINO_PLATFORM_VERSION)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if value[CONF_SOURCE].startswith("http"):
|
|
||||||
# prefix is necessary or platformio will complain with a cryptic error
|
|
||||||
value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
|
|
||||||
|
|
||||||
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"The selected Arduino framework version is not the recommended one. "
|
|
||||||
"If there are connectivity or build issues please remove the manual version."
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
lookups = {
|
|
||||||
"dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
|
|
||||||
"latest": (cv.Version(5, 2, 2), None),
|
|
||||||
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
|
|
||||||
}
|
|
||||||
|
|
||||||
if value[CONF_VERSION] in lookups:
|
|
||||||
if CONF_SOURCE in value:
|
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
"Framework version needs to be explicitly specified when custom source is used."
|
"Version needs to be explicitly set when a custom source or platform_version is used."
|
||||||
)
|
)
|
||||||
|
|
||||||
version, source = lookups[value[CONF_VERSION]]
|
platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
|
||||||
|
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
|
||||||
|
|
||||||
|
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||||
|
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
|
||||||
|
else:
|
||||||
|
version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
|
||||||
else:
|
else:
|
||||||
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
||||||
source = value.get(CONF_SOURCE, None)
|
|
||||||
|
|
||||||
if version < cv.Version(5, 0, 0):
|
|
||||||
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
|
|
||||||
|
|
||||||
# flag this for later *before* we set value[CONF_PLATFORM_VERSION] below
|
|
||||||
has_platform_ver = CONF_PLATFORM_VERSION in value
|
|
||||||
|
|
||||||
value[CONF_PLATFORM_VERSION] = value.get(
|
|
||||||
CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION))
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION])
|
|
||||||
) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"ESP-IDF {str(version)} not supported by platformio/espressif32"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
version in SUPPORTED_PLATFORMIO_ESP_IDF_5X
|
|
||||||
and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
|
|
||||||
) and not has_platform_ver:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
not is_platformio
|
|
||||||
and CONF_RELEASE not in value
|
|
||||||
and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
|
|
||||||
):
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
value[CONF_VERSION] = str(version)
|
value[CONF_VERSION] = str(version)
|
||||||
value[CONF_SOURCE] = source or _format_framework_espidf_version(
|
|
||||||
version, value.get(CONF_RELEASE, None), is_platformio
|
|
||||||
)
|
|
||||||
|
|
||||||
if value[CONF_SOURCE].startswith("http"):
|
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||||
# prefix is necessary or platformio will complain with a cryptic error
|
if version < cv.Version(3, 0, 0):
|
||||||
value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}"
|
raise cv.Invalid("Only Arduino 3.0+ is supported.")
|
||||||
|
recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
|
||||||
|
platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version)
|
||||||
|
value[CONF_SOURCE] = value.get(
|
||||||
|
CONF_SOURCE, _format_framework_arduino_version(version)
|
||||||
|
)
|
||||||
|
if value[CONF_SOURCE].startswith("http"):
|
||||||
|
value[CONF_SOURCE] = (
|
||||||
|
f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if version < cv.Version(5, 0, 0):
|
||||||
|
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
|
||||||
|
recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
|
||||||
|
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
|
||||||
|
value[CONF_SOURCE] = value.get(
|
||||||
|
CONF_SOURCE,
|
||||||
|
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
|
||||||
|
)
|
||||||
|
if value[CONF_SOURCE].startswith("http"):
|
||||||
|
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
|
||||||
|
|
||||||
if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION:
|
if CONF_PLATFORM_VERSION not in value:
|
||||||
|
if platform_lookup is None:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Framework version not recognized; please specify platform_version"
|
||||||
|
)
|
||||||
|
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
|
||||||
|
|
||||||
|
if version != recommended_version:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The selected ESP-IDF framework version is not the recommended one. "
|
"The selected framework version is not the recommended one. "
|
||||||
|
"If there are connectivity or build issues please remove the manual version."
|
||||||
|
)
|
||||||
|
|
||||||
|
if value[CONF_PLATFORM_VERSION] != _parse_platform_version(
|
||||||
|
str(PLATFORM_VERSION_LOOKUP["recommended"])
|
||||||
|
):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The selected platform version is not the recommended one. "
|
||||||
"If there are connectivity or build issues please remove the manual version."
|
"If there are connectivity or build issues please remove the manual version."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -477,26 +429,14 @@ def _check_versions(value):
|
|||||||
def _parse_platform_version(value):
|
def _parse_platform_version(value):
|
||||||
try:
|
try:
|
||||||
ver = cv.Version.parse(cv.version_number(value))
|
ver = cv.Version.parse(cv.version_number(value))
|
||||||
if ver.major >= 50: # a pioarduino version
|
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
|
||||||
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
|
if ver.extra:
|
||||||
if ver.extra:
|
release += f"-{ver.extra}"
|
||||||
release += f"-{ver.extra}"
|
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
|
||||||
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
|
|
||||||
# if platform version is a valid version constraint, prefix the default package
|
|
||||||
cv.platformio_version_constraint(value)
|
|
||||||
return f"platformio/espressif32@{value}"
|
|
||||||
except cv.Invalid:
|
except cv.Invalid:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _platform_is_platformio(value):
|
|
||||||
try:
|
|
||||||
ver = cv.Version.parse(cv.version_number(value))
|
|
||||||
return ver.major < 50
|
|
||||||
except cv.Invalid:
|
|
||||||
return "platformio" in value
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_variant(value):
|
def _detect_variant(value):
|
||||||
board = value.get(CONF_BOARD)
|
board = value.get(CONF_BOARD)
|
||||||
variant = value.get(CONF_VARIANT)
|
variant = value.get(CONF_VARIANT)
|
||||||
@@ -808,6 +748,8 @@ async def to_code(config):
|
|||||||
|
|
||||||
conf = config[CONF_FRAMEWORK]
|
conf = config[CONF_FRAMEWORK]
|
||||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||||
|
if CONF_SOURCE in conf:
|
||||||
|
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||||
|
|
||||||
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
|
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
|
||||||
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
|
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
|
||||||
@@ -850,8 +792,6 @@ async def to_code(config):
|
|||||||
|
|
||||||
cg.add_build_flag("-Wno-nonnull-compare")
|
cg.add_build_flag("-Wno-nonnull-compare")
|
||||||
|
|
||||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
|
||||||
|
|
||||||
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
|
from collections.abc import Callable, MutableMapping
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
@@ -9,16 +12,19 @@ from esphome.const import (
|
|||||||
CONF_ENABLE_ON_BOOT,
|
CONF_ENABLE_ON_BOOT,
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
|
CONF_MAX_CONNECTIONS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_NAME_ADD_MAC_SUFFIX,
|
CONF_NAME_ADD_MAC_SUFFIX,
|
||||||
)
|
)
|
||||||
from esphome.core import TimePeriod
|
from esphome.core import CORE, TimePeriod
|
||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
DEPENDENCIES = ["esp32"]
|
DEPENDENCIES = ["esp32"]
|
||||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
||||||
DOMAIN = "esp32_ble"
|
DOMAIN = "esp32_ble"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BTLoggers(Enum):
|
class BTLoggers(Enum):
|
||||||
"""Bluetooth logger categories available in ESP-IDF.
|
"""Bluetooth logger categories available in ESP-IDF.
|
||||||
@@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs"
|
|||||||
CONF_CONNECTION_TIMEOUT = "connection_timeout"
|
CONF_CONNECTION_TIMEOUT = "connection_timeout"
|
||||||
CONF_MAX_NOTIFICATIONS = "max_notifications"
|
CONF_MAX_NOTIFICATIONS = "max_notifications"
|
||||||
|
|
||||||
|
# BLE connection limits
|
||||||
|
# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4
|
||||||
|
# Total instances: 10 (ADV + SCAN + connections)
|
||||||
|
# - ADV only: up to 9 connections
|
||||||
|
# - SCAN only: up to 9 connections
|
||||||
|
# - ADV + SCAN: up to 8 connections
|
||||||
|
DEFAULT_MAX_CONNECTIONS = 3
|
||||||
|
IDF_MAX_CONNECTIONS = 9
|
||||||
|
|
||||||
|
# Connection slot tracking keys
|
||||||
|
KEY_ESP32_BLE = "esp32_ble"
|
||||||
|
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
|
||||||
|
|
||||||
|
# Export for use by other components (bluetooth_proxy, etc.)
|
||||||
|
__all__ = [
|
||||||
|
"DEFAULT_MAX_CONNECTIONS",
|
||||||
|
"IDF_MAX_CONNECTIONS",
|
||||||
|
"KEY_ESP32_BLE",
|
||||||
|
"KEY_USED_CONNECTION_SLOTS",
|
||||||
|
"consume_connection_slots",
|
||||||
|
]
|
||||||
|
|
||||||
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
|
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
|
||||||
|
|
||||||
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
|
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
|
||||||
@@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
cv.positive_int,
|
cv.positive_int,
|
||||||
cv.Range(min=1, max=64),
|
cv.Range(min=1, max=64),
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
|
||||||
|
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
@@ -230,6 +261,56 @@ def validate_variant(_):
|
|||||||
raise cv.Invalid(f"{variant} does not support Bluetooth")
|
raise cv.Invalid(f"{variant} does not support Bluetooth")
|
||||||
|
|
||||||
|
|
||||||
|
def consume_connection_slots(
|
||||||
|
value: int, consumer: str
|
||||||
|
) -> Callable[[MutableMapping], MutableMapping]:
|
||||||
|
"""Reserve BLE connection slots for a component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Number of connection slots to reserve
|
||||||
|
consumer: Name of the component consuming the slots
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A validator function that records the slot usage
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
|
||||||
|
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {})
|
||||||
|
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
|
||||||
|
slots.extend([consumer] * value)
|
||||||
|
return config
|
||||||
|
|
||||||
|
return _consume_connection_slots
|
||||||
|
|
||||||
|
|
||||||
|
def validate_connection_slots(max_connections: int) -> None:
|
||||||
|
"""Validate that BLE connection slots don't exceed the configured maximum."""
|
||||||
|
ble_data = CORE.data.get(KEY_ESP32_BLE, {})
|
||||||
|
used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
|
||||||
|
num_used = len(used_slots)
|
||||||
|
|
||||||
|
if num_used <= max_connections:
|
||||||
|
return
|
||||||
|
|
||||||
|
slot_users = ", ".join(used_slots)
|
||||||
|
|
||||||
|
if num_used > IDF_MAX_CONNECTIONS:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. "
|
||||||
|
f"Reduce the number of BLE clients. Components: {slot_users}"
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.warning(
|
||||||
|
"BLE components require %d connection slot(s) but only %d configured. "
|
||||||
|
"Please set 'max_connections: %d' in the 'esp32_ble' component. "
|
||||||
|
"Components: %s",
|
||||||
|
num_used,
|
||||||
|
max_connections,
|
||||||
|
num_used,
|
||||||
|
slot_users,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def final_validation(config):
|
def final_validation(config):
|
||||||
validate_variant(config)
|
validate_variant(config)
|
||||||
if (name := config.get(CONF_NAME)) is not None:
|
if (name := config.get(CONF_NAME)) is not None:
|
||||||
@@ -245,6 +326,10 @@ def final_validation(config):
|
|||||||
# Set GATT Client/Server sdkconfig options based on which components are loaded
|
# Set GATT Client/Server sdkconfig options based on which components are loaded
|
||||||
full_config = fv.full_config.get()
|
full_config = fv.full_config.get()
|
||||||
|
|
||||||
|
# Validate connection slots usage
|
||||||
|
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||||
|
validate_connection_slots(max_connections)
|
||||||
|
|
||||||
# Check if BLE Server is needed
|
# Check if BLE Server is needed
|
||||||
has_ble_server = "esp32_ble_server" in full_config
|
has_ble_server = "esp32_ble_server" in full_config
|
||||||
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
|
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
|
||||||
@@ -255,6 +340,26 @@ def final_validation(config):
|
|||||||
)
|
)
|
||||||
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
|
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
|
||||||
|
|
||||||
|
# Handle max_connections: check for deprecated location in esp32_ble_tracker
|
||||||
|
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||||
|
|
||||||
|
# Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat)
|
||||||
|
if "esp32_ble_tracker" in full_config:
|
||||||
|
tracker_config = full_config["esp32_ble_tracker"]
|
||||||
|
if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config:
|
||||||
|
max_connections = tracker_config["max_connections"]
|
||||||
|
|
||||||
|
# Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN
|
||||||
|
# This is the Bluedroid host stack total instance limit (range 1-9, default 4)
|
||||||
|
# Total instances = ADV/SCAN (1) + connection slots (max_connections)
|
||||||
|
# Shared between client (tracker/ble_client) and server
|
||||||
|
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1)
|
||||||
|
|
||||||
|
# Set controller-specific max connections for ESP32 (classic)
|
||||||
|
# CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN)
|
||||||
|
# For newer chips (C3/S3/etc), different configs are used automatically
|
||||||
|
add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
@@ -270,6 +375,10 @@ async def to_code(config):
|
|||||||
cg.add(var.set_name(name))
|
cg.add(var.set_name(name))
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
# Define max connections for use in C++ code (e.g., ble_server.h)
|
||||||
|
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||||
|
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
|
||||||
|
|
||||||
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
||||||
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
||||||
|
|
||||||
|
@@ -68,6 +68,10 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||||
|
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
||||||
this->advertising_init_();
|
this->advertising_init_();
|
||||||
this->advertising_->set_manufacturer_data(data);
|
this->advertising_->set_manufacturer_data(data);
|
||||||
this->advertising_start();
|
this->advertising_start();
|
||||||
@@ -213,15 +217,17 @@ bool ESP32BLE::ble_setup_() {
|
|||||||
if (this->name_.has_value()) {
|
if (this->name_.has_value()) {
|
||||||
name = this->name_.value();
|
name = this->name_.value();
|
||||||
if (App.is_name_add_mac_suffix_enabled()) {
|
if (App.is_name_add_mac_suffix_enabled()) {
|
||||||
name += "-" + get_mac_address().substr(6);
|
name += "-";
|
||||||
|
name += get_mac_address().substr(6);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
name = App.get_name();
|
name = App.get_name();
|
||||||
if (name.length() > 20) {
|
if (name.length() > 20) {
|
||||||
if (App.is_name_add_mac_suffix_enabled()) {
|
if (App.is_name_add_mac_suffix_enabled()) {
|
||||||
name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address
|
// Keep first 13 chars and last 7 chars (MAC suffix), remove middle
|
||||||
|
name.erase(13, name.length() - 20);
|
||||||
} else {
|
} else {
|
||||||
name = name.substr(0, 20);
|
name.resize(20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -118,6 +118,7 @@ class ESP32BLE : public Component {
|
|||||||
void advertising_start();
|
void advertising_start();
|
||||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||||
|
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||||
|
@@ -59,6 +59,10 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||||
|
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||||
delete[] this->advertising_data_.p_manufacturer_data;
|
delete[] this->advertising_data_.p_manufacturer_data;
|
||||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||||
this->advertising_data_.manufacturer_len = data.size();
|
this->advertising_data_.manufacturer_len = data.size();
|
||||||
@@ -152,7 +156,7 @@ void BLEAdvertising::loop() {
|
|||||||
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
|
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
|
||||||
this->stop();
|
this->stop();
|
||||||
this->current_adv_index_ += 1;
|
this->current_adv_index_ += 1;
|
||||||
if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) {
|
if (static_cast<size_t>(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) {
|
||||||
this->current_adv_index_ = -1;
|
this->current_adv_index_ = -1;
|
||||||
}
|
}
|
||||||
this->start();
|
this->start();
|
||||||
|
@@ -35,6 +35,7 @@ class BLEAdvertising {
|
|||||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||||
|
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||||
void set_service_data(const std::vector<uint8_t> &data);
|
void set_service_data(const std::vector<uint8_t> &data);
|
||||||
void set_service_data(std::span<const uint8_t> data);
|
void set_service_data(std::span<const uint8_t> data);
|
||||||
|
@@ -42,32 +42,18 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
|
|||||||
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
|
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
|
||||||
ESPBTUUID ret;
|
ESPBTUUID ret;
|
||||||
if (data.length() == 4) {
|
if (data.length() == 4) {
|
||||||
ret.uuid_.len = ESP_UUID_LEN_16;
|
// 16-bit UUID as 4-character hex string
|
||||||
ret.uuid_.uuid.uuid16 = 0;
|
auto parsed = parse_hex<uint16_t>(data);
|
||||||
for (uint i = 0; i < data.length(); i += 2) {
|
if (parsed.has_value()) {
|
||||||
uint8_t msb = data.c_str()[i];
|
ret.uuid_.len = ESP_UUID_LEN_16;
|
||||||
uint8_t lsb = data.c_str()[i + 1];
|
ret.uuid_.uuid.uuid16 = parsed.value();
|
||||||
uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0;
|
|
||||||
|
|
||||||
if (msb > '9')
|
|
||||||
msb -= 7;
|
|
||||||
if (lsb > '9')
|
|
||||||
lsb -= 7;
|
|
||||||
ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
|
|
||||||
}
|
}
|
||||||
} else if (data.length() == 8) {
|
} else if (data.length() == 8) {
|
||||||
ret.uuid_.len = ESP_UUID_LEN_32;
|
// 32-bit UUID as 8-character hex string
|
||||||
ret.uuid_.uuid.uuid32 = 0;
|
auto parsed = parse_hex<uint32_t>(data);
|
||||||
for (uint i = 0; i < data.length(); i += 2) {
|
if (parsed.has_value()) {
|
||||||
uint8_t msb = data.c_str()[i];
|
ret.uuid_.len = ESP_UUID_LEN_32;
|
||||||
uint8_t lsb = data.c_str()[i + 1];
|
ret.uuid_.uuid.uuid32 = parsed.value();
|
||||||
uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0;
|
|
||||||
|
|
||||||
if (msb > '9')
|
|
||||||
msb -= 7;
|
|
||||||
if (lsb > '9')
|
|
||||||
lsb -= 7;
|
|
||||||
ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
|
|
||||||
}
|
}
|
||||||
} else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
|
} else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
|
||||||
// investigated (lack of time)
|
// investigated (lack of time)
|
||||||
@@ -145,28 +131,16 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
|
|||||||
if (this->uuid_.len == uuid.uuid_.len) {
|
if (this->uuid_.len == uuid.uuid_.len) {
|
||||||
switch (this->uuid_.len) {
|
switch (this->uuid_.len) {
|
||||||
case ESP_UUID_LEN_16:
|
case ESP_UUID_LEN_16:
|
||||||
if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) {
|
return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ESP_UUID_LEN_32:
|
case ESP_UUID_LEN_32:
|
||||||
if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) {
|
return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ESP_UUID_LEN_128:
|
case ESP_UUID_LEN_128:
|
||||||
for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) {
|
return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0;
|
||||||
if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) {
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return this->as_128bit() == uuid.as_128bit();
|
|
||||||
}
|
}
|
||||||
return false;
|
return this->as_128bit() == uuid.as_128bit();
|
||||||
}
|
}
|
||||||
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
|
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
|
||||||
std::string ESPBTUUID::to_string() const {
|
std::string ESPBTUUID::to_string() const {
|
||||||
|
@@ -49,7 +49,11 @@ void BLECharacteristic::notify() {
|
|||||||
this->service_->get_server()->get_connected_client_count() == 0)
|
this->service_->get_server()->get_connected_client_count() == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (auto &client : this->service_->get_server()->get_clients()) {
|
const uint16_t *clients = this->service_->get_server()->get_clients();
|
||||||
|
uint8_t client_count = this->service_->get_server()->get_client_count();
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < client_count; i++) {
|
||||||
|
uint16_t client = clients[i];
|
||||||
size_t length = this->value_.size();
|
size_t length = this->value_.size();
|
||||||
// Find the client in the list of clients to notify
|
// Find the client in the list of clients to notify
|
||||||
auto *entry = this->find_client_in_notify_list_(client);
|
auto *entry = this->find_client_in_notify_list_(client);
|
||||||
@@ -121,69 +125,49 @@ bool BLECharacteristic::is_created() {
|
|||||||
if (this->state_ != CREATING_DEPENDENTS)
|
if (this->state_ != CREATING_DEPENDENTS)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
bool created = true;
|
|
||||||
for (auto *descriptor : this->descriptors_) {
|
for (auto *descriptor : this->descriptors_) {
|
||||||
created &= descriptor->is_created();
|
if (!descriptor->is_created())
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (created)
|
// All descriptors are created if we reach here
|
||||||
this->state_ = CREATED;
|
this->state_ = CREATED;
|
||||||
return this->state_ == CREATED;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BLECharacteristic::is_failed() {
|
bool BLECharacteristic::is_failed() {
|
||||||
if (this->state_ == FAILED)
|
if (this->state_ == FAILED)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
bool failed = false;
|
|
||||||
for (auto *descriptor : this->descriptors_) {
|
for (auto *descriptor : this->descriptors_) {
|
||||||
failed |= descriptor->is_failed();
|
if (descriptor->is_failed()) {
|
||||||
|
this->state_ = FAILED;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BLECharacteristic::set_property_bit_(esp_gatt_char_prop_t bit, bool value) {
|
||||||
|
if (value) {
|
||||||
|
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | bit);
|
||||||
|
} else {
|
||||||
|
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~bit);
|
||||||
}
|
}
|
||||||
if (failed)
|
|
||||||
this->state_ = FAILED;
|
|
||||||
return this->state_ == FAILED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BLECharacteristic::set_broadcast_property(bool value) {
|
void BLECharacteristic::set_broadcast_property(bool value) {
|
||||||
if (value) {
|
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, value);
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST);
|
|
||||||
} else {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void BLECharacteristic::set_indicate_property(bool value) {
|
void BLECharacteristic::set_indicate_property(bool value) {
|
||||||
if (value) {
|
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, value);
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE);
|
|
||||||
} else {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void BLECharacteristic::set_notify_property(bool value) {
|
void BLECharacteristic::set_notify_property(bool value) {
|
||||||
if (value) {
|
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, value);
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY);
|
|
||||||
} else {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void BLECharacteristic::set_read_property(bool value) {
|
|
||||||
if (value) {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ);
|
|
||||||
} else {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void BLECharacteristic::set_write_property(bool value) {
|
|
||||||
if (value) {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE);
|
|
||||||
} else {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
void BLECharacteristic::set_read_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_READ, value); }
|
||||||
|
void BLECharacteristic::set_write_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE, value); }
|
||||||
void BLECharacteristic::set_write_no_response_property(bool value) {
|
void BLECharacteristic::set_write_no_response_property(bool value) {
|
||||||
if (value) {
|
this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, value);
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
|
|
||||||
} else {
|
|
||||||
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||||
|
@@ -97,6 +97,8 @@ class BLECharacteristic {
|
|||||||
void remove_client_from_notify_list_(uint16_t conn_id);
|
void remove_client_from_notify_list_(uint16_t conn_id);
|
||||||
ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id);
|
ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id);
|
||||||
|
|
||||||
|
void set_property_bit_(esp_gatt_char_prop_t bit, bool value);
|
||||||
|
|
||||||
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
|
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
|
||||||
std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_;
|
std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_;
|
||||||
|
|
||||||
|
@@ -185,9 +185,38 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int8_t BLEServer::find_client_index_(uint16_t conn_id) const {
|
||||||
|
for (uint8_t i = 0; i < this->client_count_; i++) {
|
||||||
|
if (this->clients_[i] == conn_id)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BLEServer::add_client_(uint16_t conn_id) {
|
||||||
|
// Check if already in list
|
||||||
|
if (this->find_client_index_(conn_id) >= 0)
|
||||||
|
return;
|
||||||
|
// Add if there's space
|
||||||
|
if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) {
|
||||||
|
this->clients_[this->client_count_++] = conn_id;
|
||||||
|
} else {
|
||||||
|
// This should never happen since max clients is known at compile time
|
||||||
|
ESP_LOGE(TAG, "Client array full");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BLEServer::remove_client_(uint16_t conn_id) {
|
||||||
|
int8_t index = this->find_client_index_(conn_id);
|
||||||
|
if (index >= 0) {
|
||||||
|
// Replace with last element and decrement count (client order not preserved)
|
||||||
|
this->clients_[index] = this->clients_[--this->client_count_];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BLEServer::ble_before_disabled_event_handler() {
|
void BLEServer::ble_before_disabled_event_handler() {
|
||||||
// Delete all clients
|
// Delete all clients
|
||||||
this->clients_.clear();
|
this->client_count_ = 0;
|
||||||
// Delete all services
|
// Delete all services
|
||||||
for (auto &entry : this->services_) {
|
for (auto &entry : this->services_) {
|
||||||
entry.service->do_delete();
|
entry.service->do_delete();
|
||||||
|
@@ -12,7 +12,6 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
@@ -47,8 +46,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
|||||||
void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
|
void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
|
||||||
|
|
||||||
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
|
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
|
||||||
uint32_t get_connected_client_count() { return this->clients_.size(); }
|
uint32_t get_connected_client_count() { return this->client_count_; }
|
||||||
const std::unordered_set<uint16_t> &get_clients() { return this->clients_; }
|
const uint16_t *get_clients() const { return this->clients_; }
|
||||||
|
uint8_t get_client_count() const { return this->client_count_; }
|
||||||
|
|
||||||
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||||
esp_ble_gatts_cb_param_t *param) override;
|
esp_ble_gatts_cb_param_t *param) override;
|
||||||
@@ -82,8 +82,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
|||||||
|
|
||||||
void restart_advertising_();
|
void restart_advertising_();
|
||||||
|
|
||||||
void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
|
int8_t find_client_index_(uint16_t conn_id) const;
|
||||||
void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
|
void add_client_(uint16_t conn_id);
|
||||||
|
void remove_client_(uint16_t conn_id);
|
||||||
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
|
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
|
||||||
|
|
||||||
std::vector<CallbackEntry> callbacks_;
|
std::vector<CallbackEntry> callbacks_;
|
||||||
@@ -92,7 +93,8 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
|||||||
esp_gatt_if_t gatts_if_{0};
|
esp_gatt_if_t gatts_if_{0};
|
||||||
bool registered_{false};
|
bool registered_{false};
|
||||||
|
|
||||||
std::unordered_set<uint16_t> clients_;
|
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
|
||||||
|
uint8_t client_count_{0};
|
||||||
std::vector<ServiceEntry> services_{};
|
std::vector<ServiceEntry> services_{};
|
||||||
std::vector<BLEService *> services_to_start_{};
|
std::vector<BLEService *> services_to_start_{};
|
||||||
BLEService *device_information_service_{};
|
BLEService *device_information_service_{};
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable, MutableMapping
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble
|
from esphome.components import esp32_ble
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||||
from esphome.components.esp32_ble import (
|
from esphome.components.esp32_ble import (
|
||||||
|
IDF_MAX_CONNECTIONS,
|
||||||
BTLoggers,
|
BTLoggers,
|
||||||
bt_uuid,
|
bt_uuid,
|
||||||
bt_uuid16_format,
|
bt_uuid16_format,
|
||||||
@@ -24,6 +23,7 @@ from esphome.const import (
|
|||||||
CONF_INTERVAL,
|
CONF_INTERVAL,
|
||||||
CONF_MAC_ADDRESS,
|
CONF_MAC_ADDRESS,
|
||||||
CONF_MANUFACTURER_ID,
|
CONF_MANUFACTURER_ID,
|
||||||
|
CONF_MAX_CONNECTIONS,
|
||||||
CONF_ON_BLE_ADVERTISE,
|
CONF_ON_BLE_ADVERTISE,
|
||||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
||||||
@@ -38,19 +38,12 @@ AUTO_LOAD = ["esp32_ble"]
|
|||||||
DEPENDENCIES = ["esp32"]
|
DEPENDENCIES = ["esp32"]
|
||||||
CODEOWNERS = ["@bdraco"]
|
CODEOWNERS = ["@bdraco"]
|
||||||
|
|
||||||
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
|
|
||||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
|
|
||||||
|
|
||||||
CONF_MAX_CONNECTIONS = "max_connections"
|
|
||||||
CONF_ESP32_BLE_ID = "esp32_ble_id"
|
CONF_ESP32_BLE_ID = "esp32_ble_id"
|
||||||
CONF_SCAN_PARAMETERS = "scan_parameters"
|
CONF_SCAN_PARAMETERS = "scan_parameters"
|
||||||
CONF_WINDOW = "window"
|
CONF_WINDOW = "window"
|
||||||
CONF_ON_SCAN_END = "on_scan_end"
|
CONF_ON_SCAN_END = "on_scan_end"
|
||||||
CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
|
CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
|
||||||
|
|
||||||
DEFAULT_MAX_CONNECTIONS = 3
|
|
||||||
IDF_MAX_CONNECTIONS = 9
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -128,6 +121,15 @@ def validate_scan_parameters(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
|
||||||
|
if CONF_MAX_CONNECTIONS in config:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The 'max_connections' option in 'esp32_ble_tracker' is deprecated. "
|
||||||
|
"Please move it to the 'esp32_ble' component instead."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def as_hex(value):
|
def as_hex(value):
|
||||||
return cg.RawExpression(f"0x{value}ULL")
|
return cg.RawExpression(f"0x{value}ULL")
|
||||||
|
|
||||||
@@ -150,24 +152,12 @@ def as_reversed_hex_array(value):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def consume_connection_slots(
|
|
||||||
value: int, consumer: str
|
|
||||||
) -> Callable[[MutableMapping], MutableMapping]:
|
|
||||||
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
|
|
||||||
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {})
|
|
||||||
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
|
|
||||||
slots.extend([consumer] * value)
|
|
||||||
return config
|
|
||||||
|
|
||||||
return _consume_connection_slots
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(ESP32BLETracker),
|
cv.GenerateID(): cv.declare_id(ESP32BLETracker),
|
||||||
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
|
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
|
||||||
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
|
cv.Optional(CONF_MAX_CONNECTIONS): cv.All(
|
||||||
cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
|
cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
|
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
|
||||||
@@ -224,48 +214,11 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
|
cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
validate_max_connections_deprecated,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_remaining_connections(config):
|
FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
|
||||||
data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {})
|
|
||||||
slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, [])
|
|
||||||
used_slots = len(slots)
|
|
||||||
if used_slots <= config[CONF_MAX_CONNECTIONS]:
|
|
||||||
return config
|
|
||||||
slot_users = ", ".join(slots)
|
|
||||||
|
|
||||||
if used_slots < IDF_MAX_CONNECTIONS:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
|
|
||||||
"connection slot(s) out of available configured maximum %d connection "
|
|
||||||
"slot(s); The system automatically increased `%s` to %d to match the "
|
|
||||||
"number of used connection slot(s) by components: %s.",
|
|
||||||
CONF_MAX_CONNECTIONS,
|
|
||||||
used_slots,
|
|
||||||
config[CONF_MAX_CONNECTIONS],
|
|
||||||
CONF_MAX_CONNECTIONS,
|
|
||||||
used_slots,
|
|
||||||
slot_users,
|
|
||||||
)
|
|
||||||
config[CONF_MAX_CONNECTIONS] = used_slots
|
|
||||||
return config
|
|
||||||
|
|
||||||
msg = (
|
|
||||||
f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: "
|
|
||||||
f"components attempted to consume {used_slots} connection slot(s) "
|
|
||||||
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
|
|
||||||
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
|
|
||||||
)
|
|
||||||
if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
|
|
||||||
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
|
|
||||||
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
|
|
||||||
raise cv.Invalid(msg)
|
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
|
||||||
validate_remaining_connections, esp32_ble.validate_variant
|
|
||||||
)
|
|
||||||
|
|
||||||
ESP_BLE_DEVICE_SCHEMA = cv.Schema(
|
ESP_BLE_DEVICE_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -345,10 +298,8 @@ async def to_code(config):
|
|||||||
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
||||||
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
|
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
|
||||||
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
|
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
|
||||||
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
|
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
|
||||||
add_idf_sdkconfig_option(
|
# configured in esp32_ble component based on max_connections setting
|
||||||
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
|
|
||||||
)
|
|
||||||
|
|
||||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||||
cg.add_define("USE_ESP32_BLE_CLIENT")
|
cg.add_define("USE_ESP32_BLE_CLIENT")
|
||||||
|
@@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ESP32Can::setup_internal() {
|
bool ESP32Can::setup_internal() {
|
||||||
|
static int next_twai_ctrl_num = 0;
|
||||||
|
if (static_cast<unsigned>(next_twai_ctrl_num) >= SOC_TWAI_CONTROLLER_NUM) {
|
||||||
|
ESP_LOGW(TAG, "Maximum number of esp32_can components created already");
|
||||||
|
this->mark_failed();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
twai_general_config_t g_config =
|
twai_general_config_t g_config =
|
||||||
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
|
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
|
||||||
|
g_config.controller_id = next_twai_ctrl_num++;
|
||||||
if (this->tx_queue_len_.has_value()) {
|
if (this->tx_queue_len_.has_value()) {
|
||||||
g_config.tx_queue_len = this->tx_queue_len_.value();
|
g_config.tx_queue_len = this->tx_queue_len_.value();
|
||||||
}
|
}
|
||||||
@@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install TWAI driver
|
// Install TWAI driver
|
||||||
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
|
if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) {
|
||||||
// Failed to install driver
|
// Failed to install driver
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start TWAI driver
|
// Start TWAI driver
|
||||||
if (twai_start() != ESP_OK) {
|
if (twai_start_v2(this->twai_handle_) != ESP_OK) {
|
||||||
// Failed to start driver
|
// Failed to start driver
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
return false;
|
return false;
|
||||||
@@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
|
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
|
||||||
|
if (this->twai_handle_ == nullptr) {
|
||||||
|
// not setup yet or setup failed
|
||||||
|
return canbus::ERROR_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) {
|
if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) {
|
||||||
return canbus::ERROR_FAILTX;
|
return canbus::ERROR_FAILTX;
|
||||||
}
|
}
|
||||||
@@ -124,7 +137,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
|
|||||||
memcpy(message.data, frame->data, frame->can_data_length_code);
|
memcpy(message.data, frame->data, frame->can_data_length_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (twai_transmit(&message, this->tx_enqueue_timeout_ticks_) == ESP_OK) {
|
if (twai_transmit_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) {
|
||||||
return canbus::ERROR_OK;
|
return canbus::ERROR_OK;
|
||||||
} else {
|
} else {
|
||||||
return canbus::ERROR_ALLTXBUSY;
|
return canbus::ERROR_ALLTXBUSY;
|
||||||
@@ -132,9 +145,14 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) {
|
canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) {
|
||||||
|
if (this->twai_handle_ == nullptr) {
|
||||||
|
// not setup yet or setup failed
|
||||||
|
return canbus::ERROR_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
twai_message_t message;
|
twai_message_t message;
|
||||||
|
|
||||||
if (twai_receive(&message, 0) != ESP_OK) {
|
if (twai_receive_v2(this->twai_handle_, &message, 0) != ESP_OK) {
|
||||||
return canbus::ERROR_NOMSG;
|
return canbus::ERROR_NOMSG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,6 +5,8 @@
|
|||||||
#include "esphome/components/canbus/canbus.h"
|
#include "esphome/components/canbus/canbus.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
#include <driver/twai.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace esp32_can {
|
namespace esp32_can {
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus {
|
|||||||
TickType_t tx_enqueue_timeout_ticks_{};
|
TickType_t tx_enqueue_timeout_ticks_{};
|
||||||
optional<uint32_t> tx_queue_len_{};
|
optional<uint32_t> tx_queue_len_{};
|
||||||
optional<uint32_t> rx_queue_len_{};
|
optional<uint32_t> rx_queue_len_{};
|
||||||
|
twai_handle_t twai_handle_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp32_can
|
} // namespace esp32_can
|
||||||
|
@@ -35,7 +35,7 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
|
|||||||
if (symbols_free < RMT_SYMBOLS_PER_BYTE) {
|
if (symbols_free < RMT_SYMBOLS_PER_BYTE) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
|
for (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
|
||||||
if (bytes[index] & (1 << (7 - i))) {
|
if (bytes[index] & (1 << (7 - i))) {
|
||||||
symbols[i] = params->bit1;
|
symbols[i] = params->bit1;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -614,24 +614,67 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate nonce with appropriate hasher
|
// Generate nonce - hasher must be created and used in same stack frame
|
||||||
bool success = false;
|
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
|
||||||
|
// 1. Hash objects must NEVER be passed to another function (different stack frame)
|
||||||
|
// 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA
|
||||||
|
// 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created
|
||||||
|
// Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption.
|
||||||
|
//
|
||||||
|
// Buffer layout after AUTH_READ completes:
|
||||||
|
// [0]: auth_type (1 byte)
|
||||||
|
// [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
|
||||||
|
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
|
||||||
|
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
|
||||||
|
|
||||||
|
// Declare both hash objects in same stack frame, use pointer to select.
|
||||||
|
// NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
|
||||||
|
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
|
||||||
|
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
|
||||||
|
#ifdef USE_OTA_SHA256
|
||||||
|
sha256::SHA256 sha_hasher;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_OTA_MD5
|
||||||
|
md5::MD5Digest md5_hasher;
|
||||||
|
#endif
|
||||||
|
HashBase *hasher = nullptr;
|
||||||
|
|
||||||
#ifdef USE_OTA_SHA256
|
#ifdef USE_OTA_SHA256
|
||||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
|
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
|
||||||
sha256::SHA256 sha_hasher;
|
hasher = &sha_hasher;
|
||||||
success = this->prepare_auth_nonce_(&sha_hasher);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_OTA_MD5
|
#ifdef USE_OTA_MD5
|
||||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
|
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
|
||||||
md5::MD5Digest md5_hasher;
|
hasher = &md5_hasher;
|
||||||
success = this->prepare_auth_nonce_(&md5_hasher);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!success) {
|
const size_t hex_size = hasher->get_size() * 2;
|
||||||
|
const size_t nonce_len = hasher->get_size() / 4;
|
||||||
|
const size_t auth_buf_size = 1 + 3 * hex_size;
|
||||||
|
this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
|
||||||
|
this->auth_buf_pos_ = 0;
|
||||||
|
|
||||||
|
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
|
||||||
|
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
|
||||||
|
this->log_auth_warning_(LOG_STR("Random failed"));
|
||||||
|
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasher->init();
|
||||||
|
hasher->add(buf, nonce_len);
|
||||||
|
hasher->calculate();
|
||||||
|
this->auth_buf_[0] = this->auth_type_;
|
||||||
|
hasher->get_hex(buf);
|
||||||
|
|
||||||
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
|
||||||
|
memcpy(log_buf, buf, hex_size);
|
||||||
|
log_buf[hex_size] = '\0';
|
||||||
|
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to write auth_type + nonce
|
// Try to write auth_type + nonce
|
||||||
@@ -678,89 +721,41 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have all the data, verify it
|
// We have all the data, verify it
|
||||||
bool matches = false;
|
const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
|
||||||
|
const char *cnonce = nonce + hex_size;
|
||||||
|
const char *response = cnonce + hex_size;
|
||||||
|
|
||||||
|
// CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions).
|
||||||
|
// Declare both hash objects in same stack frame, use pointer to select.
|
||||||
|
// NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
|
||||||
|
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
|
||||||
|
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
|
||||||
|
#ifdef USE_OTA_SHA256
|
||||||
|
sha256::SHA256 sha_hasher;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_OTA_MD5
|
||||||
|
md5::MD5Digest md5_hasher;
|
||||||
|
#endif
|
||||||
|
HashBase *hasher = nullptr;
|
||||||
|
|
||||||
#ifdef USE_OTA_SHA256
|
#ifdef USE_OTA_SHA256
|
||||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
|
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
|
||||||
sha256::SHA256 sha_hasher;
|
hasher = &sha_hasher;
|
||||||
matches = this->verify_hash_auth_(&sha_hasher, hex_size);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_OTA_MD5
|
#ifdef USE_OTA_MD5
|
||||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
|
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
|
||||||
md5::MD5Digest md5_hasher;
|
hasher = &md5_hasher;
|
||||||
matches = this->verify_hash_auth_(&md5_hasher, hex_size);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
this->log_auth_warning_(LOG_STR("Password mismatch"));
|
|
||||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication successful - clean up auth state
|
|
||||||
this->cleanup_auth_();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) {
|
|
||||||
// Calculate required buffer size using the hasher
|
|
||||||
const size_t hex_size = hasher->get_size() * 2;
|
|
||||||
const size_t nonce_len = hasher->get_size() / 4;
|
|
||||||
|
|
||||||
// Buffer layout after AUTH_READ completes:
|
|
||||||
// [0]: auth_type (1 byte)
|
|
||||||
// [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
|
|
||||||
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
|
|
||||||
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
|
|
||||||
// Total: 1 + 3*hex_size
|
|
||||||
const size_t auth_buf_size = 1 + 3 * hex_size;
|
|
||||||
this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
|
|
||||||
this->auth_buf_pos_ = 0;
|
|
||||||
|
|
||||||
// Generate nonce
|
|
||||||
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
|
|
||||||
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
|
|
||||||
this->log_auth_warning_(LOG_STR("Random failed"));
|
|
||||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasher->init();
|
|
||||||
hasher->add(buf, nonce_len);
|
|
||||||
hasher->calculate();
|
|
||||||
|
|
||||||
// Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes)
|
|
||||||
this->auth_buf_[0] = this->auth_type_;
|
|
||||||
hasher->get_hex(buf);
|
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
|
||||||
char log_buf[hex_size + 1];
|
|
||||||
// Log nonce for debugging
|
|
||||||
memcpy(log_buf, buf, hex_size);
|
|
||||||
log_buf[hex_size] = '\0';
|
|
||||||
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) {
|
|
||||||
// Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout)
|
|
||||||
const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1); // Skip auth_type byte
|
|
||||||
const char *cnonce = nonce + hex_size; // CNonce immediately follows nonce
|
|
||||||
const char *response = cnonce + hex_size; // Response immediately follows cnonce
|
|
||||||
|
|
||||||
// Calculate expected hash: password + nonce + cnonce
|
|
||||||
hasher->init();
|
hasher->init();
|
||||||
hasher->add(this->password_.c_str(), this->password_.length());
|
hasher->add(this->password_.c_str(), this->password_.length());
|
||||||
hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
|
hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
|
||||||
hasher->calculate();
|
hasher->calculate();
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
char log_buf[hex_size + 1];
|
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
|
||||||
// Log CNonce
|
// Log CNonce
|
||||||
memcpy(log_buf, cnonce, hex_size);
|
memcpy(log_buf, cnonce, hex_size);
|
||||||
log_buf[hex_size] = '\0';
|
log_buf[hex_size] = '\0';
|
||||||
@@ -778,7 +773,18 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Compare response
|
// Compare response
|
||||||
return hasher->equals_hex(response);
|
bool matches = hasher->equals_hex(response);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
this->log_auth_warning_(LOG_STR("Password mismatch"));
|
||||||
|
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication successful - clean up auth state
|
||||||
|
this->cleanup_auth_();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ESPHomeOTAComponent::get_auth_hex_size_() const {
|
size_t ESPHomeOTAComponent::get_auth_hex_size_() const {
|
||||||
|
@@ -47,8 +47,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
|||||||
bool handle_auth_send_();
|
bool handle_auth_send_();
|
||||||
bool handle_auth_read_();
|
bool handle_auth_read_();
|
||||||
bool select_auth_type_();
|
bool select_auth_type_();
|
||||||
bool prepare_auth_nonce_(HashBase *hasher);
|
|
||||||
bool verify_hash_auth_(HashBase *hasher, size_t hex_size);
|
|
||||||
size_t get_auth_hex_size_() const;
|
size_t get_auth_hex_size_() const;
|
||||||
void cleanup_auth_();
|
void cleanup_auth_();
|
||||||
void log_auth_warning_(const LogString *msg);
|
void log_auth_warning_(const LogString *msg);
|
||||||
|
@@ -41,17 +41,20 @@ static const char *const TAG = "ethernet";
|
|||||||
|
|
||||||
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
|
||||||
|
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
|
||||||
|
this->mark_failed();
|
||||||
|
}
|
||||||
|
|
||||||
#define ESPHL_ERROR_CHECK(err, message) \
|
#define ESPHL_ERROR_CHECK(err, message) \
|
||||||
if ((err) != ESP_OK) { \
|
if ((err) != ESP_OK) { \
|
||||||
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
|
this->log_error_and_mark_failed_(err, message); \
|
||||||
this->mark_failed(); \
|
|
||||||
return; \
|
return; \
|
||||||
}
|
}
|
||||||
|
|
||||||
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
|
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
|
||||||
if ((err) != ESP_OK) { \
|
if ((err) != ESP_OK) { \
|
||||||
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
|
this->log_error_and_mark_failed_(err, message); \
|
||||||
this->mark_failed(); \
|
|
||||||
return ret; \
|
return ret; \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -106,6 +106,7 @@ class EthernetComponent : public Component {
|
|||||||
void start_connect_();
|
void start_connect_();
|
||||||
void finish_connect_();
|
void finish_connect_();
|
||||||
void dump_connect_params_();
|
void dump_connect_params_();
|
||||||
|
void log_error_and_mark_failed_(esp_err_t err, const char *message);
|
||||||
#ifdef USE_ETHERNET_KSZ8081
|
#ifdef USE_ETHERNET_KSZ8081
|
||||||
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
|
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
|
||||||
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
|
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
|
||||||
@@ -162,7 +163,7 @@ class EthernetComponent : public Component {
|
|||||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
extern EthernetComponent *global_eth_component;
|
extern EthernetComponent *global_eth_component;
|
||||||
|
|
||||||
#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
|
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
|
||||||
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
|
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@@ -80,7 +80,7 @@ void FingerprintGrowComponent::setup() {
|
|||||||
delay(20); // This delay guarantees the sensor will in fact be powered power.
|
delay(20); // This delay guarantees the sensor will in fact be powered power.
|
||||||
|
|
||||||
if (this->check_password_()) {
|
if (this->check_password_()) {
|
||||||
if (this->new_password_ != -1) {
|
if (this->new_password_ != std::numeric_limits<uint32_t>::max()) {
|
||||||
if (this->set_password_())
|
if (this->set_password_())
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -177,7 +178,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
|
|||||||
uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF};
|
uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
uint16_t capacity_ = 64;
|
uint16_t capacity_ = 64;
|
||||||
uint32_t password_ = 0x0;
|
uint32_t password_ = 0x0;
|
||||||
uint32_t new_password_ = -1;
|
uint32_t new_password_ = std::numeric_limits<uint32_t>::max();
|
||||||
GPIOPin *sensing_pin_{nullptr};
|
GPIOPin *sensing_pin_{nullptr};
|
||||||
GPIOPin *sensor_power_pin_{nullptr};
|
GPIOPin *sensor_power_pin_{nullptr};
|
||||||
uint8_t enrollment_image_ = 0;
|
uint8_t enrollment_image_ = 0;
|
||||||
|
@@ -179,7 +179,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
|
|||||||
if (b) {
|
if (b) {
|
||||||
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
|
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
|
||||||
auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
|
auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
|
||||||
if (y >= y_offset && y < y_offset + this->height_)
|
if (y >= y_offset && static_cast<uint32_t>(y) < y_offset + this->height_)
|
||||||
buff->draw_pixel_at(x, y, c);
|
buff->draw_pixel_at(x, y, c);
|
||||||
};
|
};
|
||||||
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
|
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
|
||||||
|
@@ -116,7 +116,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
|
|||||||
int number_items_fit_to_screen = 0;
|
int number_items_fit_to_screen = 0;
|
||||||
const int max_item_index = this->displayed_item_->items_size() - 1;
|
const int max_item_index = this->displayed_item_->items_size() - 1;
|
||||||
|
|
||||||
for (size_t i = 0; i <= max_item_index; i++) {
|
for (size_t i = 0; max_item_index >= 0 && i <= static_cast<size_t>(max_item_index); i++) {
|
||||||
const auto *item = this->displayed_item_->get_item(i);
|
const auto *item = this->displayed_item_->get_item(i);
|
||||||
const bool selected = i == this->cursor_index_;
|
const bool selected = i == this->cursor_index_;
|
||||||
const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
|
const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
|
||||||
@@ -174,7 +174,8 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
|
|||||||
|
|
||||||
display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_);
|
display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_);
|
||||||
auto y_offset = bounds->y;
|
auto y_offset = bounds->y;
|
||||||
for (size_t i = first_item_index; i <= last_item_index; i++) {
|
for (size_t i = static_cast<size_t>(first_item_index);
|
||||||
|
last_item_index >= 0 && i <= static_cast<size_t>(last_item_index); i++) {
|
||||||
const auto *item = this->displayed_item_->get_item(i);
|
const auto *item = this->displayed_item_->get_item(i);
|
||||||
const bool selected = i == this->cursor_index_;
|
const bool selected = i == this->cursor_index_;
|
||||||
display::Rect dimensions = menu_dimensions[i];
|
display::Rect dimensions = menu_dimensions[i];
|
||||||
|
@@ -213,7 +213,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
|
|||||||
this->real_control_packet_size_);
|
this->real_control_packet_size_);
|
||||||
this->status_message_callback_.call((const char *) data, data_size);
|
this->status_message_callback_.call((const char *) data, data_size);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_);
|
ESP_LOGW(TAG, "Status packet too small: %zu (should be >= %zu)", data_size, this->real_control_packet_size_);
|
||||||
}
|
}
|
||||||
switch (this->protocol_phase_) {
|
switch (this->protocol_phase_) {
|
||||||
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||||
@@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
|||||||
size_t expected_size =
|
size_t expected_size =
|
||||||
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
|
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
|
||||||
if (size < expected_size) {
|
if (size < expected_size) {
|
||||||
ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size);
|
ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size);
|
||||||
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||||
}
|
}
|
||||||
uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1];
|
uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1];
|
||||||
|
@@ -178,7 +178,7 @@ class HonClimate : public HaierClimateBase {
|
|||||||
int extra_control_packet_bytes_{0};
|
int extra_control_packet_bytes_{0};
|
||||||
int extra_sensors_packet_bytes_{4};
|
int extra_sensors_packet_bytes_{4};
|
||||||
int status_message_header_size_{0};
|
int status_message_header_size_{0};
|
||||||
int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)};
|
size_t real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)};
|
||||||
int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4};
|
int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4};
|
||||||
HonControlMethod control_method_;
|
HonControlMethod control_method_;
|
||||||
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
|
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
|
||||||
|
@@ -5,6 +5,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS
|
|||||||
from esphome.config_helpers import filter_source_files_from_platform
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_CAPTURE_RESPONSE,
|
||||||
CONF_ESP8266_DISABLE_SSL_SUPPORT,
|
CONF_ESP8266_DISABLE_SSL_SUPPORT,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_METHOD,
|
CONF_METHOD,
|
||||||
@@ -57,7 +58,6 @@ CONF_HEADERS = "headers"
|
|||||||
CONF_COLLECT_HEADERS = "collect_headers"
|
CONF_COLLECT_HEADERS = "collect_headers"
|
||||||
CONF_BODY = "body"
|
CONF_BODY = "body"
|
||||||
CONF_JSON = "json"
|
CONF_JSON = "json"
|
||||||
CONF_CAPTURE_RESPONSE = "capture_response"
|
|
||||||
|
|
||||||
|
|
||||||
def validate_url(value):
|
def validate_url(value):
|
||||||
|
@@ -377,7 +377,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
|
|||||||
this_speaker->current_stream_info_.get_bits_per_sample() <= 16) {
|
this_speaker->current_stream_info_.get_bits_per_sample() <= 16) {
|
||||||
size_t len = bytes_read / sizeof(int16_t);
|
size_t len = bytes_read / sizeof(int16_t);
|
||||||
int16_t *tmp_buf = (int16_t *) new_data;
|
int16_t *tmp_buf = (int16_t *) new_data;
|
||||||
for (int i = 0; i < len; i += 2) {
|
for (size_t i = 0; i < len; i += 2) {
|
||||||
int16_t tmp = tmp_buf[i];
|
int16_t tmp = tmp_buf[i];
|
||||||
tmp_buf[i] = tmp_buf[i + 1];
|
tmp_buf[i] = tmp_buf[i + 1];
|
||||||
tmp_buf[i + 1] = tmp;
|
tmp_buf[i + 1] = tmp;
|
||||||
|
@@ -325,7 +325,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons
|
|||||||
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
|
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
|
||||||
this->write_array(ptr, w * h * 2);
|
this->write_array(ptr, w * h * 2);
|
||||||
} else {
|
} else {
|
||||||
for (size_t y = 0; y != h; y++) {
|
for (size_t y = 0; y != static_cast<size_t>(h); y++) {
|
||||||
this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2);
|
this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,7 +349,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons
|
|||||||
App.feed_wdt();
|
App.feed_wdt();
|
||||||
}
|
}
|
||||||
// end of line? Skip to the next.
|
// end of line? Skip to the next.
|
||||||
if (++pixel == w) {
|
if (++pixel == static_cast<size_t>(w)) {
|
||||||
pixel = 0;
|
pixel = 0;
|
||||||
ptr += (x_pad + x_offset) * 2;
|
ptr += (x_pad + x_offset) * 2;
|
||||||
}
|
}
|
||||||
|
@@ -19,15 +19,19 @@ std::string build_json(const json_build_t &f) {
|
|||||||
|
|
||||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
JsonDocument doc = parse_json(data);
|
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
|
||||||
if (doc.overflowed() || doc.isNull())
|
if (doc.overflowed() || doc.isNull())
|
||||||
return false;
|
return false;
|
||||||
return f(doc.as<JsonObject>());
|
return f(doc.as<JsonObject>());
|
||||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonDocument parse_json(const std::string &data) {
|
JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
if (data == nullptr || len == 0) {
|
||||||
|
ESP_LOGE(TAG, "No data to parse");
|
||||||
|
return JsonObject(); // return unbound object
|
||||||
|
}
|
||||||
#ifdef USE_PSRAM
|
#ifdef USE_PSRAM
|
||||||
auto doc_allocator = SpiRamAllocator();
|
auto doc_allocator = SpiRamAllocator();
|
||||||
JsonDocument json_document(&doc_allocator);
|
JsonDocument json_document(&doc_allocator);
|
||||||
@@ -38,7 +42,7 @@ JsonDocument parse_json(const std::string &data) {
|
|||||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||||
return JsonObject(); // return unbound object
|
return JsonObject(); // return unbound object
|
||||||
}
|
}
|
||||||
DeserializationError err = deserializeJson(json_document, data);
|
DeserializationError err = deserializeJson(json_document, data, len);
|
||||||
|
|
||||||
if (err == DeserializationError::Ok) {
|
if (err == DeserializationError::Ok) {
|
||||||
return json_document;
|
return json_document;
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT
|
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT
|
||||||
@@ -49,8 +50,13 @@ std::string build_json(const json_build_t &f);
|
|||||||
|
|
||||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
/// Parse a JSON string and run the provided json parse function if it's valid.
|
||||||
bool parse_json(const std::string &data, const json_parse_t &f);
|
bool parse_json(const std::string &data, const json_parse_t &f);
|
||||||
|
|
||||||
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
|
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
|
||||||
JsonDocument parse_json(const std::string &data);
|
JsonDocument parse_json(const uint8_t *data, size_t len);
|
||||||
|
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
|
||||||
|
inline JsonDocument parse_json(const std::string &data) {
|
||||||
|
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
|
||||||
|
}
|
||||||
|
|
||||||
/// Builder class for creating JSON documents without lambdas
|
/// Builder class for creating JSON documents without lambdas
|
||||||
class JsonBuilder {
|
class JsonBuilder {
|
||||||
|
@@ -22,7 +22,7 @@ void KamstrupKMPComponent::dump_config() {
|
|||||||
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
|
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
|
||||||
LOG_SENSOR(" ", "Volume", this->volume_sensor_);
|
LOG_SENSOR(" ", "Volume", this->volume_sensor_);
|
||||||
|
|
||||||
for (int i = 0; i < this->custom_sensors_.size(); i++) {
|
for (size_t i = 0; i < this->custom_sensors_.size(); i++) {
|
||||||
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
|
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
|
||||||
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
|
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Custom sensors
|
// Custom sensors
|
||||||
for (int i = 0; i < this->custom_commands_.size(); i++) {
|
for (size_t i = 0; i < this->custom_commands_.size(); i++) {
|
||||||
if (command == this->custom_commands_[i]) {
|
if (command == this->custom_commands_[i]) {
|
||||||
this->custom_sensors_[i]->publish_state(value);
|
this->custom_sensors_[i]->publish_state(value);
|
||||||
}
|
}
|
||||||
|
@@ -13,8 +13,8 @@ class KeyCollector : public Component {
|
|||||||
void loop() override;
|
void loop() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void set_provider(key_provider::KeyProvider *provider);
|
void set_provider(key_provider::KeyProvider *provider);
|
||||||
void set_min_length(int min_length) { this->min_length_ = min_length; };
|
void set_min_length(uint32_t min_length) { this->min_length_ = min_length; };
|
||||||
void set_max_length(int max_length) { this->max_length_ = max_length; };
|
void set_max_length(uint32_t max_length) { this->max_length_ = max_length; };
|
||||||
void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); };
|
void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); };
|
||||||
void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); };
|
void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); };
|
||||||
void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; };
|
void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; };
|
||||||
@@ -33,8 +33,8 @@ class KeyCollector : public Component {
|
|||||||
protected:
|
protected:
|
||||||
void key_pressed_(uint8_t key);
|
void key_pressed_(uint8_t key);
|
||||||
|
|
||||||
int min_length_{0};
|
uint32_t min_length_{0};
|
||||||
int max_length_{0};
|
uint32_t max_length_{0};
|
||||||
std::string start_keys_;
|
std::string start_keys_;
|
||||||
std::string end_keys_;
|
std::string end_keys_;
|
||||||
bool end_key_required_{false};
|
bool end_key_required_{false};
|
||||||
|
@@ -10,11 +10,15 @@ namespace light {
|
|||||||
static const char *const TAG = "light";
|
static const char *const TAG = "light";
|
||||||
|
|
||||||
// Helper functions to reduce code size for logging
|
// Helper functions to reduce code size for logging
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
|
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
|
||||||
static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) {
|
float max = 1.0f) {
|
||||||
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max);
|
if (value < min || value > max) {
|
||||||
|
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
|
||||||
|
value = clamp(value, min, max);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
|
||||||
static void log_feature_not_supported(const char *name, const LogString *feature) {
|
static void log_feature_not_supported(const char *name, const LogString *feature) {
|
||||||
ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature));
|
ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature));
|
||||||
}
|
}
|
||||||
@@ -27,7 +31,6 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
|
|||||||
ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message));
|
ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message));
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
#define log_validation_warning(name, param_name, val, min, max)
|
|
||||||
#define log_feature_not_supported(name, feature)
|
#define log_feature_not_supported(name, feature)
|
||||||
#define log_color_mode_not_supported(name, feature)
|
#define log_color_mode_not_supported(name, feature)
|
||||||
#define log_invalid_parameter(name, message)
|
#define log_invalid_parameter(name, message)
|
||||||
@@ -44,7 +47,7 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
|
|||||||
} \
|
} \
|
||||||
LightCall &LightCall::set_##name(type name) { \
|
LightCall &LightCall::set_##name(type name) { \
|
||||||
this->name##_ = name; \
|
this->name##_ = name; \
|
||||||
this->set_flag_(flag, true); \
|
this->set_flag_(flag); \
|
||||||
return *this; \
|
return *this; \
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +184,16 @@ void LightCall::perform() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LightCall::log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log) {
|
||||||
|
auto *name = this->parent_->get_name().c_str();
|
||||||
|
if (use_color_mode_log) {
|
||||||
|
log_color_mode_not_supported(name, feature);
|
||||||
|
} else {
|
||||||
|
log_feature_not_supported(name, feature);
|
||||||
|
}
|
||||||
|
this->clear_flag_(flag);
|
||||||
|
}
|
||||||
|
|
||||||
LightColorValues LightCall::validate_() {
|
LightColorValues LightCall::validate_() {
|
||||||
auto *name = this->parent_->get_name().c_str();
|
auto *name = this->parent_->get_name().c_str();
|
||||||
auto traits = this->parent_->get_traits();
|
auto traits = this->parent_->get_traits();
|
||||||
@@ -188,141 +201,108 @@ LightColorValues LightCall::validate_() {
|
|||||||
// Color mode check
|
// Color mode check
|
||||||
if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
|
if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
|
||||||
ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
|
ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
|
||||||
this->set_flag_(FLAG_HAS_COLOR_MODE, false);
|
this->clear_flag_(FLAG_HAS_COLOR_MODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure there is always a color mode set
|
// Ensure there is always a color mode set
|
||||||
if (!this->has_color_mode()) {
|
if (!this->has_color_mode()) {
|
||||||
this->color_mode_ = this->compute_color_mode_();
|
this->color_mode_ = this->compute_color_mode_();
|
||||||
this->set_flag_(FLAG_HAS_COLOR_MODE, true);
|
this->set_flag_(FLAG_HAS_COLOR_MODE);
|
||||||
}
|
}
|
||||||
auto color_mode = this->color_mode_;
|
auto color_mode = this->color_mode_;
|
||||||
|
|
||||||
// Transform calls that use non-native parameters for the current mode.
|
// Transform calls that use non-native parameters for the current mode.
|
||||||
this->transform_parameters_();
|
this->transform_parameters_();
|
||||||
|
|
||||||
// Brightness exists check
|
// Business logic adjustments before validation
|
||||||
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
|
|
||||||
log_feature_not_supported(name, LOG_STR("brightness"));
|
|
||||||
this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transition length possible check
|
|
||||||
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
|
|
||||||
log_feature_not_supported(name, LOG_STR("transitions"));
|
|
||||||
this->set_flag_(FLAG_HAS_TRANSITION, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color brightness exists check
|
|
||||||
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
|
|
||||||
log_color_mode_not_supported(name, LOG_STR("RGB brightness"));
|
|
||||||
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// RGB exists check
|
|
||||||
if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
|
|
||||||
(this->has_blue() && this->blue_ > 0.0f)) {
|
|
||||||
if (!(color_mode & ColorCapability::RGB)) {
|
|
||||||
log_color_mode_not_supported(name, LOG_STR("RGB color"));
|
|
||||||
this->set_flag_(FLAG_HAS_RED, false);
|
|
||||||
this->set_flag_(FLAG_HAS_GREEN, false);
|
|
||||||
this->set_flag_(FLAG_HAS_BLUE, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// White value exists check
|
|
||||||
if (this->has_white() && this->white_ > 0.0f &&
|
|
||||||
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
|
|
||||||
log_color_mode_not_supported(name, LOG_STR("white value"));
|
|
||||||
this->set_flag_(FLAG_HAS_WHITE, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color temperature exists check
|
|
||||||
if (this->has_color_temperature() &&
|
|
||||||
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
|
|
||||||
log_color_mode_not_supported(name, LOG_STR("color temperature"));
|
|
||||||
this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cold/warm white value exists check
|
|
||||||
if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
|
|
||||||
if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
|
|
||||||
log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
|
|
||||||
this->set_flag_(FLAG_HAS_COLD_WHITE, false);
|
|
||||||
this->set_flag_(FLAG_HAS_WARM_WHITE, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#define VALIDATE_RANGE_(name_, upper_name, min, max) \
|
|
||||||
if (this->has_##name_()) { \
|
|
||||||
auto val = this->name_##_; \
|
|
||||||
if (val < (min) || val > (max)) { \
|
|
||||||
log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \
|
|
||||||
this->name_##_ = clamp(val, (min), (max)); \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
|
|
||||||
|
|
||||||
// Range checks
|
|
||||||
VALIDATE_RANGE(brightness, "Brightness")
|
|
||||||
VALIDATE_RANGE(color_brightness, "Color brightness")
|
|
||||||
VALIDATE_RANGE(red, "Red")
|
|
||||||
VALIDATE_RANGE(green, "Green")
|
|
||||||
VALIDATE_RANGE(blue, "Blue")
|
|
||||||
VALIDATE_RANGE(white, "White")
|
|
||||||
VALIDATE_RANGE(cold_white, "Cold white")
|
|
||||||
VALIDATE_RANGE(warm_white, "Warm white")
|
|
||||||
VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
|
|
||||||
|
|
||||||
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
|
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
|
||||||
bool explicit_turn_off_request = this->has_state() && !this->state_;
|
bool explicit_turn_off_request = this->has_state() && !this->state_;
|
||||||
|
|
||||||
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
|
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
|
||||||
if (this->has_brightness() && this->brightness_ == 0.0f) {
|
if (this->has_brightness() && this->brightness_ == 0.0f) {
|
||||||
this->state_ = false;
|
this->state_ = false;
|
||||||
this->set_flag_(FLAG_HAS_STATE, true);
|
this->set_flag_(FLAG_HAS_STATE);
|
||||||
this->brightness_ = 1.0f;
|
this->brightness_ = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set color brightness to 100% if currently zero and a color is set.
|
// Set color brightness to 100% if currently zero and a color is set.
|
||||||
if (this->has_red() || this->has_green() || this->has_blue()) {
|
if ((this->has_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() &&
|
||||||
if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) {
|
this->parent_->remote_values.get_color_brightness() == 0.0f) {
|
||||||
this->color_brightness_ = 1.0f;
|
this->color_brightness_ = 1.0f;
|
||||||
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true);
|
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create color values for the light with this call applied.
|
// Capability validation
|
||||||
|
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS))
|
||||||
|
this->log_and_clear_unsupported_(FLAG_HAS_BRIGHTNESS, LOG_STR("brightness"), false);
|
||||||
|
|
||||||
|
// Transition length possible check
|
||||||
|
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS))
|
||||||
|
this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
|
||||||
|
|
||||||
|
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB))
|
||||||
|
this->log_and_clear_unsupported_(FLAG_HAS_COLOR_BRIGHTNESS, LOG_STR("RGB brightness"), true);
|
||||||
|
|
||||||
|
// RGB exists check
|
||||||
|
if (((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
|
||||||
|
(this->has_blue() && this->blue_ > 0.0f)) &&
|
||||||
|
!(color_mode & ColorCapability::RGB)) {
|
||||||
|
log_color_mode_not_supported(name, LOG_STR("RGB color"));
|
||||||
|
this->clear_flag_(FLAG_HAS_RED);
|
||||||
|
this->clear_flag_(FLAG_HAS_GREEN);
|
||||||
|
this->clear_flag_(FLAG_HAS_BLUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// White value exists check
|
||||||
|
if (this->has_white() && this->white_ > 0.0f &&
|
||||||
|
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE))
|
||||||
|
this->log_and_clear_unsupported_(FLAG_HAS_WHITE, LOG_STR("white value"), true);
|
||||||
|
|
||||||
|
// Color temperature exists check
|
||||||
|
if (this->has_color_temperature() &&
|
||||||
|
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE))
|
||||||
|
this->log_and_clear_unsupported_(FLAG_HAS_COLOR_TEMPERATURE, LOG_STR("color temperature"), true);
|
||||||
|
|
||||||
|
// Cold/warm white value exists check
|
||||||
|
if (((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) &&
|
||||||
|
!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
|
||||||
|
log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
|
||||||
|
this->clear_flag_(FLAG_HAS_COLD_WHITE);
|
||||||
|
this->clear_flag_(FLAG_HAS_WARM_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create color values and validate+apply ranges in one step to eliminate duplicate checks
|
||||||
auto v = this->parent_->remote_values;
|
auto v = this->parent_->remote_values;
|
||||||
if (this->has_color_mode())
|
if (this->has_color_mode())
|
||||||
v.set_color_mode(this->color_mode_);
|
v.set_color_mode(this->color_mode_);
|
||||||
if (this->has_state())
|
if (this->has_state())
|
||||||
v.set_state(this->state_);
|
v.set_state(this->state_);
|
||||||
if (this->has_brightness())
|
|
||||||
v.set_brightness(this->brightness_);
|
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
|
||||||
if (this->has_color_brightness())
|
if (this->has_##field()) { \
|
||||||
v.set_color_brightness(this->color_brightness_);
|
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
|
||||||
if (this->has_red())
|
v.setter(this->field##_); \
|
||||||
v.set_red(this->red_);
|
}
|
||||||
if (this->has_green())
|
|
||||||
v.set_green(this->green_);
|
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
|
||||||
if (this->has_blue())
|
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
|
||||||
v.set_blue(this->blue_);
|
VALIDATE_AND_APPLY(red, set_red, "Red")
|
||||||
if (this->has_white())
|
VALIDATE_AND_APPLY(green, set_green, "Green")
|
||||||
v.set_white(this->white_);
|
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
|
||||||
if (this->has_color_temperature())
|
VALIDATE_AND_APPLY(white, set_white, "White")
|
||||||
v.set_color_temperature(this->color_temperature_);
|
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
|
||||||
if (this->has_cold_white())
|
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
|
||||||
v.set_cold_white(this->cold_white_);
|
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
|
||||||
if (this->has_warm_white())
|
traits.get_max_mireds())
|
||||||
v.set_warm_white(this->warm_white_);
|
|
||||||
|
#undef VALIDATE_AND_APPLY
|
||||||
|
|
||||||
v.normalize_color();
|
v.normalize_color();
|
||||||
|
|
||||||
// Flash length check
|
// Flash length check
|
||||||
if (this->has_flash_() && this->flash_length_ == 0) {
|
if (this->has_flash_() && this->flash_length_ == 0) {
|
||||||
log_invalid_parameter(name, LOG_STR("flash length must be greater than zero"));
|
log_invalid_parameter(name, LOG_STR("flash length must be >0"));
|
||||||
this->set_flag_(FLAG_HAS_FLASH, false);
|
this->clear_flag_(FLAG_HAS_FLASH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate transition length/flash length/effect not used at the same time
|
// validate transition length/flash length/effect not used at the same time
|
||||||
@@ -330,42 +310,40 @@ LightColorValues LightCall::validate_() {
|
|||||||
|
|
||||||
// If effect is already active, remove effect start
|
// If effect is already active, remove effect start
|
||||||
if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
|
if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
|
||||||
this->set_flag_(FLAG_HAS_EFFECT, false);
|
this->clear_flag_(FLAG_HAS_EFFECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate effect index
|
// validate effect index
|
||||||
if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
|
if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
|
||||||
ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
|
ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
|
||||||
this->set_flag_(FLAG_HAS_EFFECT, false);
|
this->clear_flag_(FLAG_HAS_EFFECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
|
if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
|
||||||
log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash"));
|
log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash"));
|
||||||
this->set_flag_(FLAG_HAS_TRANSITION, false);
|
this->clear_flag_(FLAG_HAS_TRANSITION);
|
||||||
this->set_flag_(FLAG_HAS_FLASH, false);
|
this->clear_flag_(FLAG_HAS_FLASH);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->has_flash_() && this->has_transition_()) {
|
if (this->has_flash_() && this->has_transition_()) {
|
||||||
log_invalid_parameter(name, LOG_STR("flash cannot be used with transition"));
|
log_invalid_parameter(name, LOG_STR("flash cannot be used with transition"));
|
||||||
this->set_flag_(FLAG_HAS_TRANSITION, false);
|
this->clear_flag_(FLAG_HAS_TRANSITION);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
|
if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
|
||||||
supports_transition) {
|
supports_transition) {
|
||||||
// nothing specified and light supports transitions, set default transition length
|
// nothing specified and light supports transitions, set default transition length
|
||||||
this->transition_length_ = this->parent_->default_transition_length_;
|
this->transition_length_ = this->parent_->default_transition_length_;
|
||||||
this->set_flag_(FLAG_HAS_TRANSITION, true);
|
this->set_flag_(FLAG_HAS_TRANSITION);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->has_transition_() && this->transition_length_ == 0) {
|
if (this->has_transition_() && this->transition_length_ == 0) {
|
||||||
// 0 transition is interpreted as no transition (instant change)
|
// 0 transition is interpreted as no transition (instant change)
|
||||||
this->set_flag_(FLAG_HAS_TRANSITION, false);
|
this->clear_flag_(FLAG_HAS_TRANSITION);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->has_transition_() && !supports_transition) {
|
if (this->has_transition_() && !supports_transition)
|
||||||
log_feature_not_supported(name, LOG_STR("transitions"));
|
this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
|
||||||
this->set_flag_(FLAG_HAS_TRANSITION, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not a flash and turning the light off, then disable the light
|
// If not a flash and turning the light off, then disable the light
|
||||||
// Do not use light color values directly, so that effects can set 0% brightness
|
// Do not use light color values directly, so that effects can set 0% brightness
|
||||||
@@ -374,17 +352,17 @@ LightColorValues LightCall::validate_() {
|
|||||||
if (!this->has_flash_() && !target_state) {
|
if (!this->has_flash_() && !target_state) {
|
||||||
if (this->has_effect_()) {
|
if (this->has_effect_()) {
|
||||||
log_invalid_parameter(name, LOG_STR("cannot start effect when turning off"));
|
log_invalid_parameter(name, LOG_STR("cannot start effect when turning off"));
|
||||||
this->set_flag_(FLAG_HAS_EFFECT, false);
|
this->clear_flag_(FLAG_HAS_EFFECT);
|
||||||
} else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
|
} else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
|
||||||
// Auto turn off effect
|
// Auto turn off effect
|
||||||
this->effect_ = 0;
|
this->effect_ = 0;
|
||||||
this->set_flag_(FLAG_HAS_EFFECT, true);
|
this->set_flag_(FLAG_HAS_EFFECT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable saving for flashes
|
// Disable saving for flashes
|
||||||
if (this->has_flash_())
|
if (this->has_flash_())
|
||||||
this->set_flag_(FLAG_SAVE, false);
|
this->clear_flag_(FLAG_SAVE);
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
@@ -418,12 +396,12 @@ void LightCall::transform_parameters_() {
|
|||||||
const float gamma = this->parent_->get_gamma_correct();
|
const float gamma = this->parent_->get_gamma_correct();
|
||||||
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
|
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
|
||||||
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
|
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
|
||||||
this->set_flag_(FLAG_HAS_COLD_WHITE, true);
|
this->set_flag_(FLAG_HAS_COLD_WHITE);
|
||||||
this->set_flag_(FLAG_HAS_WARM_WHITE, true);
|
this->set_flag_(FLAG_HAS_WARM_WHITE);
|
||||||
}
|
}
|
||||||
if (this->has_white()) {
|
if (this->has_white()) {
|
||||||
this->brightness_ = this->white_;
|
this->brightness_ = this->white_;
|
||||||
this->set_flag_(FLAG_HAS_BRIGHTNESS, true);
|
this->set_flag_(FLAG_HAS_BRIGHTNESS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,7 +608,7 @@ LightCall &LightCall::set_effect(optional<std::string> effect) {
|
|||||||
}
|
}
|
||||||
LightCall &LightCall::set_effect(uint32_t effect_number) {
|
LightCall &LightCall::set_effect(uint32_t effect_number) {
|
||||||
this->effect_ = effect_number;
|
this->effect_ = effect_number;
|
||||||
this->set_flag_(FLAG_HAS_EFFECT, true);
|
this->set_flag_(FLAG_HAS_EFFECT);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {
|
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {
|
||||||
|
@@ -4,6 +4,10 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
struct LogString;
|
||||||
|
|
||||||
namespace light {
|
namespace light {
|
||||||
|
|
||||||
class LightState;
|
class LightState;
|
||||||
@@ -207,14 +211,14 @@ class LightCall {
|
|||||||
FLAG_SAVE = 1 << 15,
|
FLAG_SAVE = 1 << 15,
|
||||||
};
|
};
|
||||||
|
|
||||||
bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
|
inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
|
||||||
bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
|
inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
|
||||||
bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
|
inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
|
||||||
bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
|
inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
|
||||||
bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
|
inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
|
||||||
|
|
||||||
// Helper to set flag
|
// Helper to set flag - defaults to true for common case
|
||||||
void set_flag_(FieldFlags flag, bool value) {
|
void set_flag_(FieldFlags flag, bool value = true) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this->flags_ |= flag;
|
this->flags_ |= flag;
|
||||||
} else {
|
} else {
|
||||||
@@ -222,6 +226,12 @@ class LightCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to clear flag - reduces code size for common case
|
||||||
|
void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
|
||||||
|
|
||||||
|
// Helper to log unsupported feature and clear flag - reduces code duplication
|
||||||
|
void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);
|
||||||
|
|
||||||
LightState *parent_;
|
LightState *parent_;
|
||||||
|
|
||||||
// Light state values - use flags_ to check if a value has been set.
|
// Light state values - use flags_ to check if a value has been set.
|
||||||
|
0
esphome/components/lm75b/__init__.py
Normal file
0
esphome/components/lm75b/__init__.py
Normal file
39
esphome/components/lm75b/lm75b.cpp
Normal file
39
esphome/components/lm75b/lm75b.cpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#include "lm75b.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lm75b {
|
||||||
|
|
||||||
|
static const char *const TAG = "lm75b";
|
||||||
|
|
||||||
|
void LM75BComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "LM75B:");
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
if (this->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, "Setting up LM75B failed!");
|
||||||
|
}
|
||||||
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
LOG_SENSOR(" ", "Temperature", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LM75BComponent::update() {
|
||||||
|
// Create a temporary buffer
|
||||||
|
uint8_t buff[2];
|
||||||
|
if (this->read_register(LM75B_REG_TEMPERATURE, buff, 2) != i2c::ERROR_OK) {
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Obtain combined 16-bit value
|
||||||
|
int16_t raw_temperature = (buff[0] << 8) | buff[1];
|
||||||
|
// Read the 11-bit raw temperature value
|
||||||
|
raw_temperature >>= 5;
|
||||||
|
// Publish the temperature in °C
|
||||||
|
this->publish_state(raw_temperature * 0.125);
|
||||||
|
if (this->status_has_warning()) {
|
||||||
|
this->status_clear_warning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lm75b
|
||||||
|
} // namespace esphome
|
19
esphome/components/lm75b/lm75b.h
Normal file
19
esphome/components/lm75b/lm75b.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lm75b {
|
||||||
|
|
||||||
|
static const uint8_t LM75B_REG_TEMPERATURE = 0x00;
|
||||||
|
|
||||||
|
class LM75BComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
|
||||||
|
public:
|
||||||
|
void dump_config() override;
|
||||||
|
void update() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lm75b
|
||||||
|
} // namespace esphome
|
34
esphome/components/lm75b/sensor.py
Normal file
34
esphome/components/lm75b/sensor.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c, sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@beormund"]
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
|
||||||
|
lm75b_ns = cg.esphome_ns.namespace("lm75b")
|
||||||
|
LM75BComponent = lm75b_ns.class_(
|
||||||
|
"LM75BComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
sensor.sensor_schema(
|
||||||
|
LM75BComponent,
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
accuracy_decimals=3,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(i2c.i2c_device_schema(0x48))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = await sensor.new_sensor(config)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await i2c.register_i2c_device(var, config)
|
@@ -95,6 +95,7 @@ DEFAULT = "DEFAULT"
|
|||||||
|
|
||||||
CONF_INITIAL_LEVEL = "initial_level"
|
CONF_INITIAL_LEVEL = "initial_level"
|
||||||
CONF_LOGGER_ID = "logger_id"
|
CONF_LOGGER_ID = "logger_id"
|
||||||
|
CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels"
|
||||||
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
|
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
|
||||||
|
|
||||||
UART_SELECTION_ESP32 = {
|
UART_SELECTION_ESP32 = {
|
||||||
@@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INITIAL_LEVEL): is_log_level,
|
cv.Optional(CONF_INITIAL_LEVEL): is_log_level,
|
||||||
|
cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
|
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
|
||||||
@@ -291,8 +293,12 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
cg.add(log.pre_setup())
|
cg.add(log.pre_setup())
|
||||||
|
|
||||||
for tag, log_level in config[CONF_LOGS].items():
|
# Enable runtime tag levels if logs are configured or explicitly enabled
|
||||||
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
|
logs_config = config[CONF_LOGS]
|
||||||
|
if logs_config or config[CONF_RUNTIME_TAG_LEVELS]:
|
||||||
|
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
|
||||||
|
for tag, log_level in logs_config.items():
|
||||||
|
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
|
||||||
|
|
||||||
cg.add_define("USE_LOGGER")
|
cg.add_define("USE_LOGGER")
|
||||||
this_severity = LOG_LEVEL_SEVERITY.index(level)
|
this_severity = LOG_LEVEL_SEVERITY.index(level)
|
||||||
@@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
|
|||||||
level = LOG_LEVELS[config[CONF_LEVEL]]
|
level = LOG_LEVELS[config[CONF_LEVEL]]
|
||||||
logger = await cg.get_variable(config[CONF_LOGGER_ID])
|
logger = await cg.get_variable(config[CONF_LOGGER_ID])
|
||||||
if tag := config.get(CONF_TAG):
|
if tag := config.get(CONF_TAG):
|
||||||
|
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
|
||||||
text = str(cg.statement(logger.set_log_level(tag, level)))
|
text = str(cg.statement(logger.set_log_level(tag, level)))
|
||||||
else:
|
else:
|
||||||
text = str(cg.statement(logger.set_log_level(level)))
|
text = str(cg.statement(logger.set_log_level(level)))
|
||||||
|
@@ -148,9 +148,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
|||||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||||
|
|
||||||
inline uint8_t Logger::level_for(const char *tag) {
|
inline uint8_t Logger::level_for(const char *tag) {
|
||||||
|
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||||
auto it = this->log_levels_.find(tag);
|
auto it = this->log_levels_.find(tag);
|
||||||
if (it != this->log_levels_.end())
|
if (it != this->log_levels_.end())
|
||||||
return it->second;
|
return it->second;
|
||||||
|
#endif
|
||||||
return this->current_level_;
|
return this->current_level_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +222,9 @@ void Logger::process_messages_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||||
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
|
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||||
|
void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
UARTSelection Logger::get_uart() const { return this->uart_; }
|
UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||||
@@ -271,9 +275,11 @@ void Logger::dump_config() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||||
for (auto &it : this->log_levels_) {
|
for (auto &it : this->log_levels_) {
|
||||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second]));
|
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Logger::set_log_level(uint8_t level) {
|
void Logger::set_log_level(uint8_t level) {
|
||||||
|
@@ -36,6 +36,13 @@ struct device;
|
|||||||
|
|
||||||
namespace esphome::logger {
|
namespace esphome::logger {
|
||||||
|
|
||||||
|
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||||
|
// Comparison function for const char* keys in log_levels_ map
|
||||||
|
struct CStrCompare {
|
||||||
|
bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; }
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||||
'\0', // NONE
|
'\0', // NONE
|
||||||
@@ -133,8 +140,10 @@ class Logger : public Component {
|
|||||||
|
|
||||||
/// Set the default log level for this logger.
|
/// Set the default log level for this logger.
|
||||||
void set_log_level(uint8_t level);
|
void set_log_level(uint8_t level);
|
||||||
|
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||||
/// Set the log level of the specified tag.
|
/// Set the log level of the specified tag.
|
||||||
void set_log_level(const std::string &tag, uint8_t log_level);
|
void set_log_level(const char *tag, uint8_t log_level);
|
||||||
|
#endif
|
||||||
uint8_t get_log_level() { return this->current_level_; }
|
uint8_t get_log_level() { return this->current_level_; }
|
||||||
|
|
||||||
// ========== INTERNAL METHODS ==========
|
// ========== INTERNAL METHODS ==========
|
||||||
@@ -242,7 +251,9 @@ class Logger : public Component {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Large objects (internally aligned)
|
// Large objects (internally aligned)
|
||||||
std::map<std::string, uint8_t> log_levels_{};
|
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||||
|
std::map<const char *, uint8_t, CStrCompare> log_levels_{};
|
||||||
|
#endif
|
||||||
CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
|
CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
|
||||||
CallbackManager<void(uint8_t)> level_callback_{};
|
CallbackManager<void(uint8_t)> level_callback_{};
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
@@ -355,6 +366,12 @@ class Logger : public Component {
|
|||||||
buffer[pos++] = '[';
|
buffer[pos++] = '[';
|
||||||
copy_string(buffer, pos, tag);
|
copy_string(buffer, pos, tag);
|
||||||
buffer[pos++] = ':';
|
buffer[pos++] = ':';
|
||||||
|
// Format line number without modulo operations (passed by value, safe to mutate)
|
||||||
|
if (line > 999) [[unlikely]] {
|
||||||
|
int thousands = line / 1000;
|
||||||
|
buffer[pos++] = '0' + thousands;
|
||||||
|
line -= thousands * 1000;
|
||||||
|
}
|
||||||
int hundreds = line / 100;
|
int hundreds = line / 100;
|
||||||
int remainder = line - hundreds * 100;
|
int remainder = line - hundreds * 100;
|
||||||
int tens = remainder / 10;
|
int tens = remainder / 10;
|
||||||
|
@@ -3,11 +3,10 @@
|
|||||||
namespace esphome::logger {
|
namespace esphome::logger {
|
||||||
|
|
||||||
void LoggerLevelSelect::publish_state(int level) {
|
void LoggerLevelSelect::publish_state(int level) {
|
||||||
auto value = this->at(level);
|
const auto &option = this->at(level_to_index(level));
|
||||||
if (!value) {
|
if (!option)
|
||||||
return;
|
return;
|
||||||
}
|
Select::publish_state(option.value());
|
||||||
Select::publish_state(value.value());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoggerLevelSelect::setup() {
|
void LoggerLevelSelect::setup() {
|
||||||
@@ -16,10 +15,10 @@ void LoggerLevelSelect::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LoggerLevelSelect::control(const std::string &value) {
|
void LoggerLevelSelect::control(const std::string &value) {
|
||||||
auto level = this->index_of(value);
|
const auto index = this->index_of(value);
|
||||||
if (!level)
|
if (!index)
|
||||||
return;
|
return;
|
||||||
this->parent_->set_log_level(level.value());
|
this->parent_->set_log_level(index_to_level(index.value()));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::logger
|
} // namespace esphome::logger
|
||||||
|
@@ -3,11 +3,18 @@
|
|||||||
#include "esphome/components/select/select.h"
|
#include "esphome/components/select/select.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/logger/logger.h"
|
#include "esphome/components/logger/logger.h"
|
||||||
|
|
||||||
namespace esphome::logger {
|
namespace esphome::logger {
|
||||||
class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> {
|
class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> {
|
||||||
public:
|
public:
|
||||||
void publish_state(int level);
|
void publish_state(int level);
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void control(const std::string &value) override;
|
void control(const std::string &value) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Convert log level to option index (skip CONFIG at level 4)
|
||||||
|
static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; }
|
||||||
|
// Convert option index to log level (skip CONFIG at level 4)
|
||||||
|
static uint8_t index_to_level(uint8_t index) { return (index >= ESPHOME_LOG_LEVEL_CONFIG) ? index + 1 : index; }
|
||||||
};
|
};
|
||||||
} // namespace esphome::logger
|
} // namespace esphome::logger
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
using esphome::i2c::ErrorCode;
|
using esphome::i2c::ErrorCode;
|
||||||
|
|
||||||
@@ -28,30 +29,30 @@ bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) {
|
|||||||
|
|
||||||
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
|
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
size_t idx = -1;
|
size_t idx = std::numeric_limits<size_t>::max();
|
||||||
while (idx == -1 && i < size) {
|
while (idx == std::numeric_limits<size_t>::max() && i < size) {
|
||||||
if (array[i] == val) {
|
if (array[i] == val) {
|
||||||
idx = i;
|
idx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
if (idx == -1 || i + 1 >= size)
|
if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
|
||||||
return val;
|
return val;
|
||||||
return array[i + 1];
|
return array[i + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
|
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
|
||||||
size_t i = size - 1;
|
size_t i = size - 1;
|
||||||
size_t idx = -1;
|
size_t idx = std::numeric_limits<size_t>::max();
|
||||||
while (idx == -1 && i > 0) {
|
while (idx == std::numeric_limits<size_t>::max() && i > 0) {
|
||||||
if (array[i] == val) {
|
if (array[i] == val) {
|
||||||
idx = i;
|
idx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
if (idx == -1 || i == 0)
|
if (idx == std::numeric_limits<size_t>::max() || i == 0)
|
||||||
return val;
|
return val;
|
||||||
return array[i - 1];
|
return array[i - 1];
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
using esphome::i2c::ErrorCode;
|
using esphome::i2c::ErrorCode;
|
||||||
|
|
||||||
@@ -14,30 +15,30 @@ static const uint8_t MAX_TRIES = 5;
|
|||||||
|
|
||||||
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
|
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
size_t idx = -1;
|
size_t idx = std::numeric_limits<size_t>::max();
|
||||||
while (idx == -1 && i < size) {
|
while (idx == std::numeric_limits<size_t>::max() && i < size) {
|
||||||
if (array[i] == val) {
|
if (array[i] == val) {
|
||||||
idx = i;
|
idx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
if (idx == -1 || i + 1 >= size)
|
if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
|
||||||
return val;
|
return val;
|
||||||
return array[i + 1];
|
return array[i + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
|
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
|
||||||
size_t i = size - 1;
|
size_t i = size - 1;
|
||||||
size_t idx = -1;
|
size_t idx = std::numeric_limits<size_t>::max();
|
||||||
while (idx == -1 && i > 0) {
|
while (idx == std::numeric_limits<size_t>::max() && i > 0) {
|
||||||
if (array[i] == val) {
|
if (array[i] == val) {
|
||||||
idx = i;
|
idx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
if (idx == -1 || i == 0)
|
if (idx == std::numeric_limits<size_t>::max() || i == 0)
|
||||||
return val;
|
return val;
|
||||||
return array[i - 1];
|
return array[i - 1];
|
||||||
}
|
}
|
||||||
|
@@ -29,9 +29,9 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
|
|||||||
void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); };
|
void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); };
|
||||||
void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); };
|
void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); };
|
||||||
void set_keys(std::string keys) { keys_ = std::move(keys); };
|
void set_keys(std::string keys) { keys_ = std::move(keys); };
|
||||||
void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; };
|
void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; };
|
||||||
void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; };
|
void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; };
|
||||||
void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; };
|
void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; };
|
||||||
|
|
||||||
void register_listener(MatrixKeypadListener *listener);
|
void register_listener(MatrixKeypadListener *listener);
|
||||||
void register_key_trigger(MatrixKeyTrigger *trig);
|
void register_key_trigger(MatrixKeyTrigger *trig);
|
||||||
@@ -40,7 +40,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
|
|||||||
std::vector<GPIOPin *> rows_;
|
std::vector<GPIOPin *> rows_;
|
||||||
std::vector<GPIOPin *> columns_;
|
std::vector<GPIOPin *> columns_;
|
||||||
std::string keys_;
|
std::string keys_;
|
||||||
int debounce_time_ = 0;
|
uint32_t debounce_time_ = 0;
|
||||||
bool has_diodes_{false};
|
bool has_diodes_{false};
|
||||||
bool has_pulldowns_{false};
|
bool has_pulldowns_{false};
|
||||||
int pressed_key_ = -1;
|
int pressed_key_ = -1;
|
||||||
|
@@ -90,7 +90,7 @@ void MAX7219Component::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this->scroll_mode_ == ScrollMode::STOP) {
|
if (this->scroll_mode_ == ScrollMode::STOP) {
|
||||||
if (this->stepsleft_ + get_width_internal() == first_line_size + 1) {
|
if (static_cast<size_t>(this->stepsleft_ + get_width_internal()) == first_line_size + 1) {
|
||||||
if (millis_since_last_scroll < this->scroll_dwell_) {
|
if (millis_since_last_scroll < this->scroll_dwell_) {
|
||||||
ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.",
|
ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.",
|
||||||
this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_);
|
this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_);
|
||||||
|
@@ -20,6 +20,23 @@ bool MCP2515::setup_internal() {
|
|||||||
return false;
|
return false;
|
||||||
if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK)
|
if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// setup hardware filter RXF0 accepting all standard CAN IDs
|
||||||
|
if (this->set_filter_(RXF::RXF0, false, 0) != canbus::ERROR_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this->set_filter_mask_(MASK::MASK0, false, 0) != canbus::ERROR_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup hardware filter RXF1 accepting all extended CAN IDs
|
||||||
|
if (this->set_filter_(RXF::RXF1, true, 0) != canbus::ERROR_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this->set_filter_mask_(MASK::MASK1, true, 0) != canbus::ERROR_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK)
|
if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK)
|
||||||
return false;
|
return false;
|
||||||
uint8_t err_flags = this->get_error_flags_();
|
uint8_t err_flags = this->get_error_flags_();
|
||||||
|
@@ -58,21 +58,13 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def mdns_txt_record(key: str, value: str):
|
|
||||||
return cg.StructInitializer(
|
|
||||||
MDNSTXTRecord,
|
|
||||||
("key", key),
|
|
||||||
("value", value),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mdns_service(
|
def mdns_service(
|
||||||
service: str, proto: str, port: int, txt_records: list[dict[str, str]]
|
service: str, proto: str, port: int, txt_records: list[dict[str, str]]
|
||||||
):
|
):
|
||||||
return cg.StructInitializer(
|
return cg.StructInitializer(
|
||||||
MDNSService,
|
MDNSService,
|
||||||
("service_type", service),
|
("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")),
|
||||||
("proto", proto),
|
("proto", cg.RawExpression(f"MDNS_STR({cg.safe_exp(proto)})")),
|
||||||
("port", port),
|
("port", port),
|
||||||
("txt_records", txt_records),
|
("txt_records", txt_records),
|
||||||
)
|
)
|
||||||
@@ -107,23 +99,53 @@ async def to_code(config):
|
|||||||
# Ensure at least 1 service (fallback service)
|
# Ensure at least 1 service (fallback service)
|
||||||
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
|
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
|
||||||
|
|
||||||
|
# Calculate compile-time dynamic TXT value count
|
||||||
|
# Dynamic values are those that cannot be stored in flash at compile time
|
||||||
|
dynamic_txt_count = 0
|
||||||
|
if "api" in CORE.config:
|
||||||
|
# Always: get_mac_address()
|
||||||
|
dynamic_txt_count += 1
|
||||||
|
# User-provided templatable TXT values (only lambdas, not static strings)
|
||||||
|
dynamic_txt_count += sum(
|
||||||
|
1
|
||||||
|
for service in config[CONF_SERVICES]
|
||||||
|
for txt_value in service[CONF_TXT].values()
|
||||||
|
if cg.is_template(txt_value)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure at least 1 to avoid zero-size array
|
||||||
|
cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count))
|
||||||
|
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
for service in config[CONF_SERVICES]:
|
for service in config[CONF_SERVICES]:
|
||||||
txt = [
|
# Build the txt records list for the service
|
||||||
cg.StructInitializer(
|
txt_records = []
|
||||||
MDNSTXTRecord,
|
for txt_key, txt_value in service[CONF_TXT].items():
|
||||||
("key", txt_key),
|
if cg.is_template(txt_value):
|
||||||
("value", await cg.templatable(txt_value, [], cg.std_string)),
|
# It's a lambda - evaluate and store using helper
|
||||||
)
|
templated_value = await cg.templatable(txt_value, [], cg.std_string)
|
||||||
for txt_key, txt_value in service[CONF_TXT].items()
|
safe_key = cg.safe_exp(txt_key)
|
||||||
]
|
dynamic_call = f"{var}->add_dynamic_txt_value(({templated_value})())"
|
||||||
|
txt_records.append(
|
||||||
|
cg.RawExpression(
|
||||||
|
f"{{MDNS_STR({safe_key}), MDNS_STR({dynamic_call})}}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# It's a static string - use directly in flash, no need to store in vector
|
||||||
|
txt_records.append(
|
||||||
|
cg.RawExpression(
|
||||||
|
f"{{MDNS_STR({cg.safe_exp(txt_key)}), MDNS_STR({cg.safe_exp(txt_value)})}}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
exp = mdns_service(
|
exp = mdns_service(
|
||||||
service[CONF_SERVICE],
|
service[CONF_SERVICE],
|
||||||
service[CONF_PROTOCOL],
|
service[CONF_PROTOCOL],
|
||||||
await cg.templatable(service[CONF_PORT], [], cg.uint16),
|
await cg.templatable(service[CONF_PORT], [], cg.uint16),
|
||||||
txt,
|
txt_records,
|
||||||
)
|
)
|
||||||
|
|
||||||
cg.add(var.add_extra_service(exp))
|
cg.add(var.add_extra_service(exp))
|
||||||
|
@@ -9,24 +9,9 @@
|
|||||||
#include <pgmspace.h>
|
#include <pgmspace.h>
|
||||||
// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms
|
// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms
|
||||||
#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value
|
#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value
|
||||||
// Helper to get string from PROGMEM - returns a temporary std::string
|
|
||||||
// Only define this function if we have services that will use it
|
|
||||||
#if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES)
|
|
||||||
static std::string mdns_string_p(const char *src) {
|
|
||||||
char buf[64];
|
|
||||||
strncpy_P(buf, src, sizeof(buf) - 1);
|
|
||||||
buf[sizeof(buf) - 1] = '\0';
|
|
||||||
return std::string(buf);
|
|
||||||
}
|
|
||||||
#define MDNS_STR(name) mdns_string_p(name)
|
|
||||||
#else
|
|
||||||
// If no services are configured, we still need the fallback service but it uses string literals
|
|
||||||
#define MDNS_STR(name) std::string(name)
|
|
||||||
#endif
|
|
||||||
#else
|
#else
|
||||||
// On non-ESP8266 platforms, use regular const char*
|
// On non-ESP8266 platforms, use regular const char*
|
||||||
#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char *name = value
|
#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value
|
||||||
#define MDNS_STR(name) name
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
@@ -46,30 +31,10 @@ static const char *const TAG = "mdns";
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Define all constant strings using the macro
|
// Define all constant strings using the macro
|
||||||
MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
|
|
||||||
MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
|
MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
|
||||||
MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
|
|
||||||
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
|
|
||||||
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name");
|
// Wrap build-time defines into flash storage
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
|
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
|
|
||||||
MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
|
|
||||||
|
|
||||||
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
|
|
||||||
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
|
|
||||||
MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040");
|
|
||||||
|
|
||||||
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
|
|
||||||
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
|
|
||||||
MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
|
|
||||||
|
|
||||||
void MDNSComponent::compile_records_() {
|
void MDNSComponent::compile_records_() {
|
||||||
this->hostname_ = App.get_name();
|
this->hostname_ = App.get_name();
|
||||||
@@ -78,8 +43,17 @@ void MDNSComponent::compile_records_() {
|
|||||||
// in mdns/__init__.py. If you add a new service here, update both locations.
|
// in mdns/__init__.py. If you add a new service here, update both locations.
|
||||||
|
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
|
MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
|
||||||
|
MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD);
|
||||||
|
|
||||||
if (api::global_api_server != nullptr) {
|
if (api::global_api_server != nullptr) {
|
||||||
auto &service = this->services_[this->services_.count()++];
|
auto &service = this->services_.emplace_next();
|
||||||
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
|
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
|
||||||
service.proto = MDNS_STR(SERVICE_TCP);
|
service.proto = MDNS_STR(SERVICE_TCP);
|
||||||
service.port = api::global_api_server->get_port();
|
service.port = api::global_api_server->get_port();
|
||||||
@@ -112,73 +86,92 @@ void MDNSComponent::compile_records_() {
|
|||||||
txt_records.reserve(txt_count);
|
txt_records.reserve(txt_count);
|
||||||
|
|
||||||
if (!friendly_name_empty) {
|
if (!friendly_name_empty) {
|
||||||
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name});
|
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
|
||||||
}
|
}
|
||||||
txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION});
|
txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
|
||||||
txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()});
|
txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))});
|
||||||
|
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
|
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
|
||||||
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
|
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
|
||||||
#elif defined(USE_ESP32)
|
#elif defined(USE_ESP32)
|
||||||
|
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
|
||||||
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
|
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
|
||||||
#elif defined(USE_RP2040)
|
#elif defined(USE_RP2040)
|
||||||
|
MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040");
|
||||||
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
|
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
|
||||||
#elif defined(USE_LIBRETINY)
|
#elif defined(USE_LIBRETINY)
|
||||||
txt_records.emplace_back(MDNSTXTRecord{"platform", lt_cpu_get_model_name()});
|
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())});
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD});
|
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
|
||||||
|
|
||||||
#if defined(USE_WIFI)
|
#if defined(USE_WIFI)
|
||||||
|
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
|
||||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
|
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
|
||||||
#elif defined(USE_ETHERNET)
|
#elif defined(USE_ETHERNET)
|
||||||
|
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
|
||||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
|
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
|
||||||
#elif defined(USE_OPENTHREAD)
|
#elif defined(USE_OPENTHREAD)
|
||||||
|
MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
|
||||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
|
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
|
||||||
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
|
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
|
||||||
if (api::global_api_server->get_noise_ctx()->has_psk()) {
|
bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
|
||||||
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR(NOISE_ENCRYPTION)});
|
const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
|
||||||
} else {
|
txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
|
||||||
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR(NOISE_ENCRYPTION)});
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ESPHOME_PROJECT_NAME
|
#ifdef ESPHOME_PROJECT_NAME
|
||||||
txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME});
|
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
|
||||||
txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION});
|
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
|
||||||
|
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME);
|
||||||
|
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION);
|
||||||
|
txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)});
|
||||||
|
txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)});
|
||||||
#endif // ESPHOME_PROJECT_NAME
|
#endif // ESPHOME_PROJECT_NAME
|
||||||
|
|
||||||
#ifdef USE_DASHBOARD_IMPORT
|
#ifdef USE_DASHBOARD_IMPORT
|
||||||
txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()});
|
MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
|
||||||
|
txt_records.push_back(
|
||||||
|
{MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif // USE_API
|
#endif // USE_API
|
||||||
|
|
||||||
#ifdef USE_PROMETHEUS
|
#ifdef USE_PROMETHEUS
|
||||||
auto &prom_service = this->services_[this->services_.count()++];
|
MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
|
||||||
|
|
||||||
|
auto &prom_service = this->services_.emplace_next();
|
||||||
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
|
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
|
||||||
prom_service.proto = MDNS_STR(SERVICE_TCP);
|
prom_service.proto = MDNS_STR(SERVICE_TCP);
|
||||||
prom_service.port = USE_WEBSERVER_PORT;
|
prom_service.port = USE_WEBSERVER_PORT;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER
|
#ifdef USE_WEBSERVER
|
||||||
auto &web_service = this->services_[this->services_.count()++];
|
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
|
||||||
|
|
||||||
|
auto &web_service = this->services_.emplace_next();
|
||||||
web_service.service_type = MDNS_STR(SERVICE_HTTP);
|
web_service.service_type = MDNS_STR(SERVICE_HTTP);
|
||||||
web_service.proto = MDNS_STR(SERVICE_TCP);
|
web_service.proto = MDNS_STR(SERVICE_TCP);
|
||||||
web_service.port = USE_WEBSERVER_PORT;
|
web_service.port = USE_WEBSERVER_PORT;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES)
|
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES)
|
||||||
|
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
|
||||||
|
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
|
||||||
|
|
||||||
// Publish "http" service if not using native API or any other services
|
// Publish "http" service if not using native API or any other services
|
||||||
// This is just to have *some* mDNS service so that .local resolution works
|
// This is just to have *some* mDNS service so that .local resolution works
|
||||||
auto &fallback_service = this->services_[this->services_.count()++];
|
auto &fallback_service = this->services_.emplace_next();
|
||||||
fallback_service.service_type = "_http";
|
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
|
||||||
fallback_service.proto = "_tcp";
|
fallback_service.proto = MDNS_STR(SERVICE_TCP);
|
||||||
fallback_service.port = USE_WEBSERVER_PORT;
|
fallback_service.port = USE_WEBSERVER_PORT;
|
||||||
fallback_service.txt_records.emplace_back(MDNSTXTRecord{"version", ESPHOME_VERSION});
|
fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,11 +183,10 @@ void MDNSComponent::dump_config() {
|
|||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
ESP_LOGV(TAG, " Services:");
|
ESP_LOGV(TAG, " Services:");
|
||||||
for (const auto &service : this->services_) {
|
for (const auto &service : this->services_) {
|
||||||
ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(),
|
ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
|
||||||
const_cast<TemplatableValue<uint16_t> &>(service.port).value());
|
const_cast<TemplatableValue<uint16_t> &>(service.port).value());
|
||||||
for (const auto &record : service.txt_records) {
|
for (const auto &record : service.txt_records) {
|
||||||
ESP_LOGV(TAG, " TXT: %s = %s", record.key.c_str(),
|
ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
|
||||||
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@@ -9,21 +9,34 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mdns {
|
namespace mdns {
|
||||||
|
|
||||||
|
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
|
||||||
|
struct MDNSString;
|
||||||
|
|
||||||
|
// Macro to cast string literals to MDNSString* (works on all platforms)
|
||||||
|
#define MDNS_STR(name) (reinterpret_cast<const esphome::mdns::MDNSString *>(name))
|
||||||
|
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
#include <pgmspace.h>
|
||||||
|
#define MDNS_STR_ARG(s) ((PGM_P) (s))
|
||||||
|
#else
|
||||||
|
#define MDNS_STR_ARG(s) (reinterpret_cast<const char *>(s))
|
||||||
|
#endif
|
||||||
|
|
||||||
// Service count is calculated at compile time by Python codegen
|
// Service count is calculated at compile time by Python codegen
|
||||||
// MDNS_SERVICE_COUNT will always be defined
|
// MDNS_SERVICE_COUNT will always be defined
|
||||||
|
|
||||||
struct MDNSTXTRecord {
|
struct MDNSTXTRecord {
|
||||||
std::string key;
|
const MDNSString *key;
|
||||||
TemplatableValue<std::string> value;
|
const MDNSString *value;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MDNSService {
|
struct MDNSService {
|
||||||
// service name _including_ underscore character prefix
|
// service name _including_ underscore character prefix
|
||||||
// as defined in RFC6763 Section 7
|
// as defined in RFC6763 Section 7
|
||||||
std::string service_type;
|
const MDNSString *service_type;
|
||||||
// second label indicating protocol _including_ underscore character prefix
|
// second label indicating protocol _including_ underscore character prefix
|
||||||
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
|
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
|
||||||
std::string proto;
|
const MDNSString *proto;
|
||||||
TemplatableValue<uint16_t> port;
|
TemplatableValue<uint16_t> port;
|
||||||
std::vector<MDNSTXTRecord> txt_records;
|
std::vector<MDNSTXTRecord> txt_records;
|
||||||
};
|
};
|
||||||
@@ -39,13 +52,24 @@ class MDNSComponent : public Component {
|
|||||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||||
|
|
||||||
#ifdef USE_MDNS_EXTRA_SERVICES
|
#ifdef USE_MDNS_EXTRA_SERVICES
|
||||||
void add_extra_service(MDNSService service) { this->services_[this->services_.count()++] = std::move(service); }
|
void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }
|
const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }
|
||||||
|
|
||||||
void on_shutdown() override;
|
void on_shutdown() override;
|
||||||
|
|
||||||
|
/// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord
|
||||||
|
const char *add_dynamic_txt_value(const std::string &value) {
|
||||||
|
this->dynamic_txt_values_.push_back(value);
|
||||||
|
return this->dynamic_txt_values_[this->dynamic_txt_values_.size() - 1].c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Storage for runtime-generated TXT values (MAC address, user lambdas)
|
||||||
|
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
|
||||||
|
/// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this.
|
||||||
|
StaticVector<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
|
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
|
||||||
std::string hostname_;
|
std::string hostname_;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user