mirror of
https://github.com/esphome/esphome.git
synced 2025-09-28 16:12:24 +01:00
Merge remote-tracking branch 'upstream/dev' into sha256_ota
This commit is contained in:
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
|||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.2.4
|
uses: actions/cache/restore@v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
|
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.4
|
uses: actions/cache@v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
@@ -162,7 +162,7 @@ jobs:
|
|||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
- name: Save Python virtual environment cache
|
- name: Save Python virtual environment cache
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache/save@v4.2.4
|
uses: actions/cache/save@v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
@@ -223,7 +223,7 @@ jobs:
|
|||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.4
|
uses: actions/cache@v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
@@ -301,14 +301,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache@v4.2.4
|
uses: actions/cache@v4.3.0
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref != 'refs/heads/dev'
|
if: github.ref != 'refs/heads/dev'
|
||||||
uses: actions/cache/restore@v4.2.4
|
uses: actions/cache/restore@v4.3.0
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
@@ -731,6 +731,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
return clean_mqtt(config, args)
|
return clean_mqtt(config, args)
|
||||||
|
|
||||||
|
|
||||||
|
def command_clean_platform(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
|
try:
|
||||||
|
writer.clean_platform()
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Error deleting platform files: %s", err)
|
||||||
|
return 1
|
||||||
|
_LOGGER.info("Done!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
from esphome import mqtt
|
from esphome import mqtt
|
||||||
|
|
||||||
@@ -929,9 +939,10 @@ POST_CONFIG_ACTIONS = {
|
|||||||
"upload": command_upload,
|
"upload": command_upload,
|
||||||
"logs": command_logs,
|
"logs": command_logs,
|
||||||
"run": command_run,
|
"run": command_run,
|
||||||
"clean-mqtt": command_clean_mqtt,
|
|
||||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
|
||||||
"clean": command_clean,
|
"clean": command_clean,
|
||||||
|
"clean-mqtt": command_clean_mqtt,
|
||||||
|
"clean-platform": command_clean_platform,
|
||||||
|
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||||
"idedata": command_idedata,
|
"idedata": command_idedata,
|
||||||
"rename": command_rename,
|
"rename": command_rename,
|
||||||
"discover": command_discover,
|
"discover": command_discover,
|
||||||
@@ -940,6 +951,7 @@ POST_CONFIG_ACTIONS = {
|
|||||||
SIMPLE_CONFIG_ACTIONS = [
|
SIMPLE_CONFIG_ACTIONS = [
|
||||||
"clean",
|
"clean",
|
||||||
"clean-mqtt",
|
"clean-mqtt",
|
||||||
|
"clean-platform",
|
||||||
"config",
|
"config",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1144,6 +1156,13 @@ def parse_args(argv):
|
|||||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser_clean = subparsers.add_parser(
|
||||||
|
"clean-platform", help="Delete all platform files."
|
||||||
|
)
|
||||||
|
parser_clean.add_argument(
|
||||||
|
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||||
|
)
|
||||||
|
|
||||||
parser_dashboard = subparsers.add_parser(
|
parser_dashboard = subparsers.add_parser(
|
||||||
"dashboard", help="Create a simple web server for a dashboard."
|
"dashboard", help="Create a simple web server for a dashboard."
|
||||||
)
|
)
|
||||||
|
@@ -15,7 +15,10 @@ from esphome.const import (
|
|||||||
CONF_TYPE_ID,
|
CONF_TYPE_ID,
|
||||||
CONF_UPDATE_INTERVAL,
|
CONF_UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
|
from esphome.core import ID
|
||||||
|
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||||
|
from esphome.types import ConfigType
|
||||||
from esphome.util import Registry
|
from esphome.util import Registry
|
||||||
|
|
||||||
|
|
||||||
@@ -49,11 +52,11 @@ def maybe_conf(conf, *validators):
|
|||||||
return validate
|
return validate
|
||||||
|
|
||||||
|
|
||||||
def register_action(name, action_type, schema):
|
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
|
||||||
return ACTION_REGISTRY.register(name, action_type, schema)
|
return ACTION_REGISTRY.register(name, action_type, schema)
|
||||||
|
|
||||||
|
|
||||||
def register_condition(name, condition_type, schema):
|
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
|
||||||
return CONDITION_REGISTRY.register(name, condition_type, schema)
|
return CONDITION_REGISTRY.register(name, condition_type, schema)
|
||||||
|
|
||||||
|
|
||||||
@@ -164,43 +167,78 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
|
|||||||
|
|
||||||
|
|
||||||
@register_condition("and", AndCondition, validate_condition_list)
|
@register_condition("and", AndCondition, validate_condition_list)
|
||||||
async def and_condition_to_code(config, condition_id, template_arg, args):
|
async def and_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
conditions = await build_condition_list(config, template_arg, args)
|
conditions = await build_condition_list(config, template_arg, args)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||||
|
|
||||||
|
|
||||||
@register_condition("or", OrCondition, validate_condition_list)
|
@register_condition("or", OrCondition, validate_condition_list)
|
||||||
async def or_condition_to_code(config, condition_id, template_arg, args):
|
async def or_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
conditions = await build_condition_list(config, template_arg, args)
|
conditions = await build_condition_list(config, template_arg, args)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||||
|
|
||||||
|
|
||||||
@register_condition("all", AndCondition, validate_condition_list)
|
@register_condition("all", AndCondition, validate_condition_list)
|
||||||
async def all_condition_to_code(config, condition_id, template_arg, args):
|
async def all_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
conditions = await build_condition_list(config, template_arg, args)
|
conditions = await build_condition_list(config, template_arg, args)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||||
|
|
||||||
|
|
||||||
@register_condition("any", OrCondition, validate_condition_list)
|
@register_condition("any", OrCondition, validate_condition_list)
|
||||||
async def any_condition_to_code(config, condition_id, template_arg, args):
|
async def any_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
conditions = await build_condition_list(config, template_arg, args)
|
conditions = await build_condition_list(config, template_arg, args)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||||
|
|
||||||
|
|
||||||
@register_condition("not", NotCondition, validate_potentially_and_condition)
|
@register_condition("not", NotCondition, validate_potentially_and_condition)
|
||||||
async def not_condition_to_code(config, condition_id, template_arg, args):
|
async def not_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
condition = await build_condition(config, template_arg, args)
|
condition = await build_condition(config, template_arg, args)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, condition)
|
return cg.new_Pvariable(condition_id, template_arg, condition)
|
||||||
|
|
||||||
|
|
||||||
@register_condition("xor", XorCondition, validate_condition_list)
|
@register_condition("xor", XorCondition, validate_condition_list)
|
||||||
async def xor_condition_to_code(config, condition_id, template_arg, args):
|
async def xor_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
conditions = await build_condition_list(config, template_arg, args)
|
conditions = await build_condition_list(config, template_arg, args)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||||
|
|
||||||
|
|
||||||
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
|
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
|
||||||
async def lambda_condition_to_code(config, condition_id, template_arg, args):
|
async def lambda_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
lambda_ = await cg.process_lambda(config, args, return_type=bool)
|
lambda_ = await cg.process_lambda(config, args, return_type=bool)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, lambda_)
|
return cg.new_Pvariable(condition_id, template_arg, lambda_)
|
||||||
|
|
||||||
@@ -217,7 +255,12 @@ async def lambda_condition_to_code(config, condition_id, template_arg, args):
|
|||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
)
|
)
|
||||||
async def for_condition_to_code(config, condition_id, template_arg, args):
|
async def for_condition_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
condition_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
condition = await build_condition(
|
condition = await build_condition(
|
||||||
config[CONF_CONDITION], cg.TemplateArguments(), []
|
config[CONF_CONDITION], cg.TemplateArguments(), []
|
||||||
)
|
)
|
||||||
@@ -231,7 +274,12 @@ async def for_condition_to_code(config, condition_id, template_arg, args):
|
|||||||
@register_action(
|
@register_action(
|
||||||
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
|
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
|
||||||
)
|
)
|
||||||
async def delay_action_to_code(config, action_id, template_arg, args):
|
async def delay_action_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
var = cg.new_Pvariable(action_id, template_arg)
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
await cg.register_component(var, {})
|
await cg.register_component(var, {})
|
||||||
template_ = await cg.templatable(config, args, cg.uint32)
|
template_ = await cg.templatable(config, args, cg.uint32)
|
||||||
@@ -256,10 +304,15 @@ async def delay_action_to_code(config, action_id, template_arg, args):
|
|||||||
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
|
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def if_action_to_code(config, action_id, template_arg, args):
|
async def if_action_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
|
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
|
||||||
conditions = await build_condition(config[cond_conf], template_arg, args)
|
condition = await build_condition(config[cond_conf], template_arg, args)
|
||||||
var = cg.new_Pvariable(action_id, template_arg, conditions)
|
var = cg.new_Pvariable(action_id, template_arg, condition)
|
||||||
if CONF_THEN in config:
|
if CONF_THEN in config:
|
||||||
actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
||||||
cg.add(var.add_then(actions))
|
cg.add(var.add_then(actions))
|
||||||
@@ -279,9 +332,14 @@ async def if_action_to_code(config, action_id, template_arg, args):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def while_action_to_code(config, action_id, template_arg, args):
|
async def while_action_to_code(
|
||||||
conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
|
config: ConfigType,
|
||||||
var = cg.new_Pvariable(action_id, template_arg, conditions)
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
|
condition = await build_condition(config[CONF_CONDITION], template_arg, args)
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, condition)
|
||||||
actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
||||||
cg.add(var.add_then(actions))
|
cg.add(var.add_then(actions))
|
||||||
return var
|
return var
|
||||||
@@ -297,7 +355,12 @@ async def while_action_to_code(config, action_id, template_arg, args):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def repeat_action_to_code(config, action_id, template_arg, args):
|
async def repeat_action_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
var = cg.new_Pvariable(action_id, template_arg)
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32)
|
count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32)
|
||||||
cg.add(var.set_count(count_template))
|
cg.add(var.set_count(count_template))
|
||||||
@@ -320,9 +383,14 @@ _validate_wait_until = cv.maybe_simple_value(
|
|||||||
|
|
||||||
|
|
||||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
|
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
|
||||||
async def wait_until_action_to_code(config, action_id, template_arg, args):
|
async def wait_until_action_to_code(
|
||||||
conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
|
config: ConfigType,
|
||||||
var = cg.new_Pvariable(action_id, template_arg, conditions)
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
|
condition = await build_condition(config[CONF_CONDITION], template_arg, args)
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, condition)
|
||||||
if CONF_TIMEOUT in config:
|
if CONF_TIMEOUT in config:
|
||||||
template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32)
|
template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32)
|
||||||
cg.add(var.set_timeout_value(template_))
|
cg.add(var.set_timeout_value(template_))
|
||||||
@@ -331,7 +399,12 @@ async def wait_until_action_to_code(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
|
|
||||||
@register_action("lambda", LambdaAction, cv.lambda_)
|
@register_action("lambda", LambdaAction, cv.lambda_)
|
||||||
async def lambda_action_to_code(config, action_id, template_arg, args):
|
async def lambda_action_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
|
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
|
||||||
return cg.new_Pvariable(action_id, template_arg, lambda_)
|
return cg.new_Pvariable(action_id, template_arg, lambda_)
|
||||||
|
|
||||||
@@ -345,7 +418,12 @@ async def lambda_action_to_code(config, action_id, template_arg, args):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def component_update_action_to_code(config, action_id, template_arg, args):
|
async def component_update_action_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
comp = await cg.get_variable(config[CONF_ID])
|
comp = await cg.get_variable(config[CONF_ID])
|
||||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
return cg.new_Pvariable(action_id, template_arg, comp)
|
||||||
|
|
||||||
@@ -359,7 +437,12 @@ async def component_update_action_to_code(config, action_id, template_arg, args)
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def component_suspend_action_to_code(config, action_id, template_arg, args):
|
async def component_suspend_action_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
comp = await cg.get_variable(config[CONF_ID])
|
comp = await cg.get_variable(config[CONF_ID])
|
||||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
return cg.new_Pvariable(action_id, template_arg, comp)
|
||||||
|
|
||||||
@@ -376,7 +459,12 @@ async def component_suspend_action_to_code(config, action_id, template_arg, args
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def component_resume_action_to_code(config, action_id, template_arg, args):
|
async def component_resume_action_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
comp = await cg.get_variable(config[CONF_ID])
|
comp = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, comp)
|
var = cg.new_Pvariable(action_id, template_arg, comp)
|
||||||
if CONF_UPDATE_INTERVAL in config:
|
if CONF_UPDATE_INTERVAL in config:
|
||||||
@@ -385,7 +473,9 @@ async def component_resume_action_to_code(config, action_id, template_arg, args)
|
|||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
async def build_action(full_config, template_arg, args):
|
async def build_action(
|
||||||
|
full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType
|
||||||
|
) -> MockObj:
|
||||||
registry_entry, config = cg.extract_registry_entry_config(
|
registry_entry, config = cg.extract_registry_entry_config(
|
||||||
ACTION_REGISTRY, full_config
|
ACTION_REGISTRY, full_config
|
||||||
)
|
)
|
||||||
@@ -394,15 +484,19 @@ async def build_action(full_config, template_arg, args):
|
|||||||
return await builder(config, action_id, template_arg, args)
|
return await builder(config, action_id, template_arg, args)
|
||||||
|
|
||||||
|
|
||||||
async def build_action_list(config, templ, arg_type):
|
async def build_action_list(
|
||||||
actions = []
|
config: list[ConfigType], templ: cg.TemplateArguments, arg_type: TemplateArgsType
|
||||||
|
) -> list[MockObj]:
|
||||||
|
actions: list[MockObj] = []
|
||||||
for conf in config:
|
for conf in config:
|
||||||
action = await build_action(conf, templ, arg_type)
|
action = await build_action(conf, templ, arg_type)
|
||||||
actions.append(action)
|
actions.append(action)
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
async def build_condition(full_config, template_arg, args):
|
async def build_condition(
|
||||||
|
full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType
|
||||||
|
) -> MockObj:
|
||||||
registry_entry, config = cg.extract_registry_entry_config(
|
registry_entry, config = cg.extract_registry_entry_config(
|
||||||
CONDITION_REGISTRY, full_config
|
CONDITION_REGISTRY, full_config
|
||||||
)
|
)
|
||||||
@@ -411,15 +505,19 @@ async def build_condition(full_config, template_arg, args):
|
|||||||
return await builder(config, action_id, template_arg, args)
|
return await builder(config, action_id, template_arg, args)
|
||||||
|
|
||||||
|
|
||||||
async def build_condition_list(config, templ, args):
|
async def build_condition_list(
|
||||||
conditions = []
|
config: ConfigType, templ: cg.TemplateArguments, args: TemplateArgsType
|
||||||
|
) -> list[MockObj]:
|
||||||
|
conditions: list[MockObj] = []
|
||||||
for conf in config:
|
for conf in config:
|
||||||
condition = await build_condition(conf, templ, args)
|
condition = await build_condition(conf, templ, args)
|
||||||
conditions.append(condition)
|
conditions.append(condition)
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
async def build_automation(trigger, args, config):
|
async def build_automation(
|
||||||
|
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
||||||
|
) -> MockObj:
|
||||||
arg_types = [arg[0] for arg in args]
|
arg_types = [arg[0] for arg in args]
|
||||||
templ = cg.TemplateArguments(*arg_types)
|
templ = cg.TemplateArguments(*arg_types)
|
||||||
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
|
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import logging
|
||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.automation import Condition
|
from esphome.automation import Condition
|
||||||
@@ -25,6 +26,9 @@ from esphome.const import (
|
|||||||
CONF_VARIABLES,
|
CONF_VARIABLES,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "api"
|
DOMAIN = "api"
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
@@ -101,6 +105,32 @@ def _encryption_schema(config):
|
|||||||
return ENCRYPTION_SCHEMA(config)
|
return ENCRYPTION_SCHEMA(config)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_api_config(config: ConfigType) -> ConfigType:
|
||||||
|
"""Validate API configuration with mutual exclusivity check and deprecation warning."""
|
||||||
|
# Check if both password and encryption are configured
|
||||||
|
has_password = CONF_PASSWORD in config and config[CONF_PASSWORD]
|
||||||
|
has_encryption = CONF_ENCRYPTION in config
|
||||||
|
|
||||||
|
if has_password and has_encryption:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"The 'password' and 'encryption' options are mutually exclusive. "
|
||||||
|
"The API client only supports one authentication method at a time. "
|
||||||
|
"Please remove one of them. "
|
||||||
|
"Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. "
|
||||||
|
"We strongly recommend using 'encryption' instead for better security."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Warn about password deprecation
|
||||||
|
if has_password:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
|
||||||
|
"Please migrate to the 'encryption' configuration. "
|
||||||
|
"See https://esphome.io/components/api.html#configuration-variables"
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -131,6 +161,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||||
|
_validate_api_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -102,7 +102,7 @@ message HelloRequest {
|
|||||||
// For example "Home Assistant"
|
// For example "Home Assistant"
|
||||||
// Not strictly necessary to send but nice for debugging
|
// Not strictly necessary to send but nice for debugging
|
||||||
// purposes.
|
// purposes.
|
||||||
string client_info = 1;
|
string client_info = 1 [(pointer_to_buffer) = true];
|
||||||
uint32 api_version_major = 2;
|
uint32 api_version_major = 2;
|
||||||
uint32 api_version_minor = 3;
|
uint32 api_version_minor = 3;
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ message AuthenticationRequest {
|
|||||||
option (ifdef) = "USE_API_PASSWORD";
|
option (ifdef) = "USE_API_PASSWORD";
|
||||||
|
|
||||||
// The password to log in with
|
// The password to log in with
|
||||||
string password = 1;
|
string password = 1 [(pointer_to_buffer) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirmation of successful connection. After this the connection is available for all traffic.
|
// Confirmation of successful connection. After this the connection is available for all traffic.
|
||||||
@@ -769,7 +769,7 @@ message HomeassistantServiceMap {
|
|||||||
string value = 2 [(no_zero_copy) = true];
|
string value = 2 [(no_zero_copy) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomeassistantServiceResponse {
|
message HomeassistantActionRequest {
|
||||||
option (id) = 35;
|
option (id) = 35;
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
@@ -824,7 +824,7 @@ message GetTimeResponse {
|
|||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
|
||||||
fixed32 epoch_seconds = 1;
|
fixed32 epoch_seconds = 1;
|
||||||
string timezone = 2;
|
string timezone = 2 [(pointer_to_buffer) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== USER-DEFINES SERVICES ====================
|
// ==================== USER-DEFINES SERVICES ====================
|
||||||
@@ -1465,7 +1465,7 @@ message BluetoothDeviceRequest {
|
|||||||
|
|
||||||
uint64 address = 1;
|
uint64 address = 1;
|
||||||
BluetoothDeviceRequestType request_type = 2;
|
BluetoothDeviceRequestType request_type = 2;
|
||||||
bool has_address_type = 3;
|
bool has_address_type = 3; // Deprecated, should be removed in 2027.8 - https://github.com/esphome/esphome/pull/10318
|
||||||
uint32 address_type = 4;
|
uint32 address_type = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1571,7 +1571,7 @@ message BluetoothGATTWriteRequest {
|
|||||||
uint32 handle = 2;
|
uint32 handle = 2;
|
||||||
bool response = 3;
|
bool response = 3;
|
||||||
|
|
||||||
bytes data = 4;
|
bytes data = 4 [(pointer_to_buffer) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
message BluetoothGATTReadDescriptorRequest {
|
message BluetoothGATTReadDescriptorRequest {
|
||||||
@@ -1591,7 +1591,7 @@ message BluetoothGATTWriteDescriptorRequest {
|
|||||||
uint64 address = 1;
|
uint64 address = 1;
|
||||||
uint32 handle = 2;
|
uint32 handle = 2;
|
||||||
|
|
||||||
bytes data = 3;
|
bytes data = 3 [(pointer_to_buffer) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
message BluetoothGATTNotifyRequest {
|
message BluetoothGATTNotifyRequest {
|
||||||
@@ -1865,10 +1865,22 @@ message VoiceAssistantWakeWord {
|
|||||||
repeated string trained_languages = 3;
|
repeated string trained_languages = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message VoiceAssistantExternalWakeWord {
|
||||||
|
string id = 1;
|
||||||
|
string wake_word = 2;
|
||||||
|
repeated string trained_languages = 3;
|
||||||
|
string model_type = 4;
|
||||||
|
uint32 model_size = 5;
|
||||||
|
string model_hash = 6;
|
||||||
|
string url = 7;
|
||||||
|
}
|
||||||
|
|
||||||
message VoiceAssistantConfigurationRequest {
|
message VoiceAssistantConfigurationRequest {
|
||||||
option (id) = 121;
|
option (id) = 121;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_VOICE_ASSISTANT";
|
option (ifdef) = "USE_VOICE_ASSISTANT";
|
||||||
|
|
||||||
|
repeated VoiceAssistantExternalWakeWord external_wake_words = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VoiceAssistantConfigurationResponse {
|
message VoiceAssistantConfigurationResponse {
|
||||||
@@ -2292,7 +2304,7 @@ message ZWaveProxyFrame {
|
|||||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
|
||||||
bytes data = 1 [(fixed_array_size) = 257];
|
bytes data = 1 [(pointer_to_buffer) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ZWaveProxyRequestType {
|
enum ZWaveProxyRequestType {
|
||||||
|
@@ -1078,8 +1078,14 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
|||||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||||
#ifdef USE_TIME_TIMEZONE
|
#ifdef USE_TIME_TIMEZONE
|
||||||
if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) {
|
if (value.timezone_len > 0) {
|
||||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone);
|
const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone();
|
||||||
|
// Compare without allocating a string
|
||||||
|
if (current_tz.length() != value.timezone_len ||
|
||||||
|
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
|
||||||
|
homeassistant::global_homeassistant_time->set_timezone(
|
||||||
|
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -1196,6 +1202,23 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
|
|||||||
resp_wake_word.trained_languages.push_back(lang);
|
resp_wake_word.trained_languages.push_back(lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter external wake words
|
||||||
|
for (auto &wake_word : msg.external_wake_words) {
|
||||||
|
if (wake_word.model_type != "micro") {
|
||||||
|
// microWakeWord only
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.available_wake_words.emplace_back();
|
||||||
|
auto &resp_wake_word = resp.available_wake_words.back();
|
||||||
|
resp_wake_word.set_id(StringRef(wake_word.id));
|
||||||
|
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
|
||||||
|
for (const auto &lang : wake_word.trained_languages) {
|
||||||
|
resp_wake_word.trained_languages.push_back(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp.active_wake_words = &config.active_wake_words;
|
resp.active_wake_words = &config.active_wake_words;
|
||||||
resp.max_active_wake_words = config.max_active_wake_words;
|
resp.max_active_wake_words = config.max_active_wake_words;
|
||||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||||
@@ -1374,7 +1397,7 @@ void APIConnection::complete_authentication_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||||
this->client_info_.name = msg.client_info;
|
this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
|
||||||
this->client_info_.peername = this->helper_->getpeername();
|
this->client_info_.peername = this->helper_->getpeername();
|
||||||
this->client_api_version_major_ = msg.api_version_major;
|
this->client_api_version_major_ = msg.api_version_major;
|
||||||
this->client_api_version_minor_ = msg.api_version_minor;
|
this->client_api_version_minor_ = msg.api_version_minor;
|
||||||
@@ -1402,7 +1425,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
|||||||
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
|
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
|
||||||
AuthenticationResponse resp;
|
AuthenticationResponse resp;
|
||||||
// bool invalid_password = 1;
|
// bool invalid_password = 1;
|
||||||
resp.invalid_password = !this->parent_->check_password(msg.password);
|
resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len);
|
||||||
if (!resp.invalid_password) {
|
if (!resp.invalid_password) {
|
||||||
this->complete_authentication_();
|
this->complete_authentication_();
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,8 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
@@ -132,10 +132,10 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#endif
|
#endif
|
||||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
void send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||||
if (!this->flags_.service_call_subscription)
|
if (!this->flags_.service_call_subscription)
|
||||||
return;
|
return;
|
||||||
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
|
@@ -32,6 +32,13 @@ extend google.protobuf.FieldOptions {
|
|||||||
optional string fixed_array_size_define = 50010;
|
optional string fixed_array_size_define = 50010;
|
||||||
optional string fixed_array_with_length_define = 50011;
|
optional string fixed_array_with_length_define = 50011;
|
||||||
|
|
||||||
|
// pointer_to_buffer: Use pointer instead of array for fixed-size byte fields
|
||||||
|
// When set, the field will be declared as a pointer (const uint8_t *data)
|
||||||
|
// instead of an array (uint8_t data[N]). This allows zero-copy on decode
|
||||||
|
// by pointing directly to the protobuf buffer. The buffer must remain valid
|
||||||
|
// until the message is processed (which is guaranteed for stack-allocated messages).
|
||||||
|
optional bool pointer_to_buffer = 50012 [default=false];
|
||||||
|
|
||||||
// container_pointer: Zero-copy optimization for repeated fields.
|
// container_pointer: Zero-copy optimization for repeated fields.
|
||||||
//
|
//
|
||||||
// When container_pointer is set on a repeated field, the generated message will
|
// When container_pointer is set on a repeated field, the generated message will
|
||||||
|
@@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
}
|
}
|
||||||
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 1:
|
case 1: {
|
||||||
this->client_info = value.as_string();
|
// Use raw data directly to avoid allocation
|
||||||
|
this->client_info = value.data();
|
||||||
|
this->client_info_len = value.size();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -45,9 +48,12 @@ void HelloResponse::calculate_size(ProtoSize &size) const {
|
|||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 1:
|
case 1: {
|
||||||
this->password = value.as_string();
|
// Use raw data directly to avoid allocation
|
||||||
|
this->password = value.data();
|
||||||
|
this->password_len = value.size();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -866,7 +872,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_length(1, this->key_ref_.size());
|
size.add_length(1, this->key_ref_.size());
|
||||||
size.add_length(1, this->value.size());
|
size.add_length(1, this->value.size());
|
||||||
}
|
}
|
||||||
void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_string(1, this->service_ref_);
|
buffer.encode_string(1, this->service_ref_);
|
||||||
for (auto &it : this->data) {
|
for (auto &it : this->data) {
|
||||||
buffer.encode_message(2, it, true);
|
buffer.encode_message(2, it, true);
|
||||||
@@ -879,7 +885,7 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
}
|
}
|
||||||
buffer.encode_bool(5, this->is_event);
|
buffer.encode_bool(5, this->is_event);
|
||||||
}
|
}
|
||||||
void HomeassistantServiceResponse::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());
|
||||||
size.add_repeated_message(1, this->data);
|
size.add_repeated_message(1, this->data);
|
||||||
size.add_repeated_message(1, this->data_template);
|
size.add_repeated_message(1, this->data_template);
|
||||||
@@ -917,9 +923,12 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
|
|||||||
#endif
|
#endif
|
||||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 2:
|
case 2: {
|
||||||
this->timezone = value.as_string();
|
// Use raw data directly to avoid allocation
|
||||||
|
this->timezone = value.data();
|
||||||
|
this->timezone_len = value.size();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2028,9 +2037,12 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val
|
|||||||
}
|
}
|
||||||
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 4:
|
case 4: {
|
||||||
this->data = value.as_string();
|
// Use raw data directly to avoid allocation
|
||||||
|
this->data = value.data();
|
||||||
|
this->data_len = value.size();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2064,9 +2076,12 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto
|
|||||||
}
|
}
|
||||||
bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 3:
|
case 3: {
|
||||||
this->data = value.as_string();
|
// Use raw data directly to avoid allocation
|
||||||
|
this->data = value.data();
|
||||||
|
this->data_len = value.size();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2382,6 +2397,52 @@ void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 5:
|
||||||
|
this->model_size = value.as_uint32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1:
|
||||||
|
this->id = value.as_string();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this->wake_word = value.as_string();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
this->trained_languages.push_back(value.as_string());
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
this->model_type = value.as_string();
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
this->model_hash = value.as_string();
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
this->url = value.as_string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1:
|
||||||
|
this->external_wake_words.emplace_back();
|
||||||
|
value.decode_to_message(this->external_wake_words.back());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
|
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
for (auto &it : this->available_wake_words) {
|
for (auto &it : this->available_wake_words) {
|
||||||
buffer.encode_message(1, it, true);
|
buffer.encode_message(1, it, true);
|
||||||
@@ -3029,12 +3090,9 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 1: {
|
case 1: {
|
||||||
const std::string &data_str = value.as_string();
|
// Use raw data directly to avoid allocation
|
||||||
this->data_len = data_str.size();
|
this->data = value.data();
|
||||||
if (this->data_len > 257) {
|
this->data_len = value.size();
|
||||||
this->data_len = 257;
|
|
||||||
}
|
|
||||||
memcpy(this->data, data_str.data(), this->data_len);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@@ -330,11 +330,12 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
|||||||
class HelloRequest final : public ProtoDecodableMessage {
|
class HelloRequest final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 1;
|
static constexpr uint8_t MESSAGE_TYPE = 1;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "hello_request"; }
|
const char *message_name() const override { return "hello_request"; }
|
||||||
#endif
|
#endif
|
||||||
std::string client_info{};
|
const uint8_t *client_info{nullptr};
|
||||||
|
uint16_t client_info_len{0};
|
||||||
uint32_t api_version_major{0};
|
uint32_t api_version_major{0};
|
||||||
uint32_t api_version_minor{0};
|
uint32_t api_version_minor{0};
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -370,11 +371,12 @@ class HelloResponse final : public ProtoMessage {
|
|||||||
class AuthenticationRequest final : public ProtoDecodableMessage {
|
class AuthenticationRequest final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 3;
|
static constexpr uint8_t MESSAGE_TYPE = 3;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 9;
|
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "authentication_request"; }
|
const char *message_name() const override { return "authentication_request"; }
|
||||||
#endif
|
#endif
|
||||||
std::string password{};
|
const uint8_t *password{nullptr};
|
||||||
|
uint16_t password_len{0};
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -1098,12 +1100,12 @@ class HomeassistantServiceMap final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
class HomeassistantServiceResponse 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 = 113;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "homeassistant_service_response"; }
|
const char *message_name() const override { return "homeassistant_action_request"; }
|
||||||
#endif
|
#endif
|
||||||
StringRef service_ref_{};
|
StringRef service_ref_{};
|
||||||
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
||||||
@@ -1188,12 +1190,13 @@ class GetTimeRequest final : public ProtoMessage {
|
|||||||
class GetTimeResponse final : public ProtoDecodableMessage {
|
class GetTimeResponse final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 37;
|
static constexpr uint8_t MESSAGE_TYPE = 37;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 14;
|
static constexpr uint8_t ESTIMATED_SIZE = 24;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "get_time_response"; }
|
const char *message_name() const override { return "get_time_response"; }
|
||||||
#endif
|
#endif
|
||||||
uint32_t epoch_seconds{0};
|
uint32_t epoch_seconds{0};
|
||||||
std::string timezone{};
|
const uint8_t *timezone{nullptr};
|
||||||
|
uint16_t timezone_len{0};
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -1985,14 +1988,15 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
|
|||||||
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
|
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 75;
|
static constexpr uint8_t MESSAGE_TYPE = 75;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
static constexpr uint8_t ESTIMATED_SIZE = 29;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "bluetooth_gatt_write_request"; }
|
const char *message_name() const override { return "bluetooth_gatt_write_request"; }
|
||||||
#endif
|
#endif
|
||||||
uint64_t address{0};
|
uint64_t address{0};
|
||||||
uint32_t handle{0};
|
uint32_t handle{0};
|
||||||
bool response{false};
|
bool response{false};
|
||||||
std::string data{};
|
const uint8_t *data{nullptr};
|
||||||
|
uint16_t data_len{0};
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -2020,13 +2024,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
|
|||||||
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
|
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 77;
|
static constexpr uint8_t MESSAGE_TYPE = 77;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
|
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
|
||||||
#endif
|
#endif
|
||||||
uint64_t address{0};
|
uint64_t address{0};
|
||||||
uint32_t handle{0};
|
uint32_t handle{0};
|
||||||
std::string data{};
|
const uint8_t *data{nullptr};
|
||||||
|
uint16_t data_len{0};
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -2451,18 +2456,37 @@ class VoiceAssistantWakeWord final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
class VoiceAssistantConfigurationRequest final : public ProtoMessage {
|
class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 121;
|
std::string id{};
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
std::string wake_word{};
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
std::vector<std::string> trained_languages{};
|
||||||
const char *message_name() const override { return "voice_assistant_configuration_request"; }
|
std::string model_type{};
|
||||||
#endif
|
uint32_t model_size{0};
|
||||||
|
std::string model_hash{};
|
||||||
|
std::string url{};
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||||
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
|
};
|
||||||
|
class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t MESSAGE_TYPE = 121;
|
||||||
|
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
const char *message_name() const override { return "voice_assistant_configuration_request"; }
|
||||||
|
#endif
|
||||||
|
std::vector<VoiceAssistantExternalWakeWord> external_wake_words{};
|
||||||
|
#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;
|
||||||
};
|
};
|
||||||
class VoiceAssistantConfigurationResponse final : public ProtoMessage {
|
class VoiceAssistantConfigurationResponse final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
@@ -2929,11 +2953,11 @@ class UpdateCommandRequest final : public CommandProtoMessage {
|
|||||||
class ZWaveProxyFrame final : public ProtoDecodableMessage {
|
class ZWaveProxyFrame final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 128;
|
static constexpr uint8_t MESSAGE_TYPE = 128;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 33;
|
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "z_wave_proxy_frame"; }
|
const char *message_name() const override { return "z_wave_proxy_frame"; }
|
||||||
#endif
|
#endif
|
||||||
uint8_t data[257]{};
|
const uint8_t *data{nullptr};
|
||||||
uint16_t data_len{0};
|
uint16_t data_len{0};
|
||||||
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;
|
||||||
|
@@ -670,7 +670,9 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
|
|||||||
|
|
||||||
void HelloRequest::dump_to(std::string &out) const {
|
void HelloRequest::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "HelloRequest");
|
MessageDumpHelper helper(out, "HelloRequest");
|
||||||
dump_field(out, "client_info", this->client_info);
|
out.append(" client_info: ");
|
||||||
|
out.append(format_hex_pretty(this->client_info, this->client_info_len));
|
||||||
|
out.append("\n");
|
||||||
dump_field(out, "api_version_major", this->api_version_major);
|
dump_field(out, "api_version_major", this->api_version_major);
|
||||||
dump_field(out, "api_version_minor", this->api_version_minor);
|
dump_field(out, "api_version_minor", this->api_version_minor);
|
||||||
}
|
}
|
||||||
@@ -682,7 +684,12 @@ void HelloResponse::dump_to(std::string &out) const {
|
|||||||
dump_field(out, "name", this->name_ref_);
|
dump_field(out, "name", this->name_ref_);
|
||||||
}
|
}
|
||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
void AuthenticationRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); }
|
void AuthenticationRequest::dump_to(std::string &out) const {
|
||||||
|
MessageDumpHelper helper(out, "AuthenticationRequest");
|
||||||
|
out.append(" password: ");
|
||||||
|
out.append(format_hex_pretty(this->password, this->password_len));
|
||||||
|
out.append("\n");
|
||||||
|
}
|
||||||
void AuthenticationResponse::dump_to(std::string &out) const {
|
void AuthenticationResponse::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "AuthenticationResponse");
|
MessageDumpHelper helper(out, "AuthenticationResponse");
|
||||||
dump_field(out, "invalid_password", this->invalid_password);
|
dump_field(out, "invalid_password", this->invalid_password);
|
||||||
@@ -1094,8 +1101,8 @@ void HomeassistantServiceMap::dump_to(std::string &out) const {
|
|||||||
dump_field(out, "key", this->key_ref_);
|
dump_field(out, "key", this->key_ref_);
|
||||||
dump_field(out, "value", this->value);
|
dump_field(out, "value", this->value);
|
||||||
}
|
}
|
||||||
void HomeassistantServiceResponse::dump_to(std::string &out) const {
|
void HomeassistantActionRequest::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "HomeassistantServiceResponse");
|
MessageDumpHelper helper(out, "HomeassistantActionRequest");
|
||||||
dump_field(out, "service", this->service_ref_);
|
dump_field(out, "service", this->service_ref_);
|
||||||
for (const auto &it : this->data) {
|
for (const auto &it : this->data) {
|
||||||
out.append(" data: ");
|
out.append(" data: ");
|
||||||
@@ -1136,7 +1143,9 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques
|
|||||||
void GetTimeResponse::dump_to(std::string &out) const {
|
void GetTimeResponse::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "GetTimeResponse");
|
MessageDumpHelper helper(out, "GetTimeResponse");
|
||||||
dump_field(out, "epoch_seconds", this->epoch_seconds);
|
dump_field(out, "epoch_seconds", this->epoch_seconds);
|
||||||
dump_field(out, "timezone", this->timezone);
|
out.append(" timezone: ");
|
||||||
|
out.append(format_hex_pretty(this->timezone, this->timezone_len));
|
||||||
|
out.append("\n");
|
||||||
}
|
}
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
|
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
|
||||||
@@ -1649,7 +1658,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
|
|||||||
dump_field(out, "handle", this->handle);
|
dump_field(out, "handle", this->handle);
|
||||||
dump_field(out, "response", this->response);
|
dump_field(out, "response", this->response);
|
||||||
out.append(" data: ");
|
out.append(" data: ");
|
||||||
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
|
out.append(format_hex_pretty(this->data, this->data_len));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
}
|
}
|
||||||
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
|
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
|
||||||
@@ -1662,7 +1671,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
|
|||||||
dump_field(out, "address", this->address);
|
dump_field(out, "address", this->address);
|
||||||
dump_field(out, "handle", this->handle);
|
dump_field(out, "handle", this->handle);
|
||||||
out.append(" data: ");
|
out.append(" data: ");
|
||||||
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
|
out.append(format_hex_pretty(this->data, this->data_len));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
}
|
}
|
||||||
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
|
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
|
||||||
@@ -1815,8 +1824,25 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const {
|
|||||||
dump_field(out, "trained_languages", it, 4);
|
dump_field(out, "trained_languages", it, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void VoiceAssistantExternalWakeWord::dump_to(std::string &out) const {
|
||||||
|
MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord");
|
||||||
|
dump_field(out, "id", this->id);
|
||||||
|
dump_field(out, "wake_word", this->wake_word);
|
||||||
|
for (const auto &it : this->trained_languages) {
|
||||||
|
dump_field(out, "trained_languages", it, 4);
|
||||||
|
}
|
||||||
|
dump_field(out, "model_type", this->model_type);
|
||||||
|
dump_field(out, "model_size", this->model_size);
|
||||||
|
dump_field(out, "model_hash", this->model_hash);
|
||||||
|
dump_field(out, "url", this->url);
|
||||||
|
}
|
||||||
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
|
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
|
||||||
out.append("VoiceAssistantConfigurationRequest {}");
|
MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest");
|
||||||
|
for (const auto &it : this->external_wake_words) {
|
||||||
|
out.append(" external_wake_words: ");
|
||||||
|
it.dump_to(out);
|
||||||
|
out.append("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
|
void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse");
|
MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse");
|
||||||
|
@@ -548,7 +548,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
|
case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
|
||||||
VoiceAssistantConfigurationRequest msg;
|
VoiceAssistantConfigurationRequest msg;
|
||||||
// Empty message: no decode needed
|
msg.decode(msg_data, msg_size);
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
|
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
|
||||||
#endif
|
#endif
|
||||||
@@ -639,241 +639,139 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
|
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
|
||||||
if (this->check_connection_setup_() && !this->send_device_info_response(msg)) {
|
if (!this->send_device_info_response(msg)) {
|
||||||
this->on_fatal_error();
|
this->on_fatal_error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
|
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->list_entities(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
|
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->subscribe_states(msg);
|
this->subscribe_states(msg);
|
||||||
}
|
}
|
||||||
}
|
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); }
|
||||||
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->subscribe_logs(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void APIServerConnection::on_subscribe_homeassistant_services_request(
|
void APIServerConnection::on_subscribe_homeassistant_services_request(
|
||||||
const SubscribeHomeassistantServicesRequest &msg) {
|
const SubscribeHomeassistantServicesRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->subscribe_homeassistant_services(msg);
|
this->subscribe_homeassistant_services(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
|
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->subscribe_home_assistant_states(msg);
|
this->subscribe_home_assistant_states(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
|
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->execute_service(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||||
if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) {
|
if (!this->send_noise_encryption_set_key_response(msg)) {
|
||||||
this->on_fatal_error();
|
this->on_fatal_error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BUTTON
|
#ifdef USE_BUTTON
|
||||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
|
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->button_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
|
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->camera_image(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
|
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->climate_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->cover_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_DATE
|
#ifdef USE_DATETIME_DATE
|
||||||
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) {
|
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->date_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_DATETIME
|
#ifdef USE_DATETIME_DATETIME
|
||||||
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->datetime_command(msg);
|
this->datetime_command(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
|
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->fan_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
|
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->light_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LOCK
|
#ifdef USE_LOCK
|
||||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
|
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->lock_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->media_player_command(msg);
|
this->media_player_command(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
|
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->number_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->select_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SIREN
|
#ifdef USE_SIREN
|
||||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
|
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->siren_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SWITCH
|
#ifdef USE_SWITCH
|
||||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->switch_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_TEXT
|
#ifdef USE_TEXT
|
||||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
|
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->text_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_TIME
|
#ifdef USE_DATETIME_TIME
|
||||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
|
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->time_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
|
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->update_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VALVE
|
#ifdef USE_VALVE
|
||||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
|
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->valve_command(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
|
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->subscribe_bluetooth_le_advertisements(msg);
|
this->subscribe_bluetooth_le_advertisements(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_device_request(msg);
|
this->bluetooth_device_request(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
|
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_gatt_get_services(msg);
|
this->bluetooth_gatt_get_services(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
|
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_gatt_read(msg);
|
this->bluetooth_gatt_read(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
|
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_gatt_write(msg);
|
this->bluetooth_gatt_write(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
|
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_gatt_read_descriptor(msg);
|
this->bluetooth_gatt_read_descriptor(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
|
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_gatt_write_descriptor(msg);
|
this->bluetooth_gatt_write_descriptor(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
|
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_gatt_notify(msg);
|
this->bluetooth_gatt_notify(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
|
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
|
||||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||||
if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) {
|
if (!this->send_subscribe_bluetooth_connections_free_response(msg)) {
|
||||||
this->on_fatal_error();
|
this->on_fatal_error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -881,59 +779,68 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
|
|||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
|
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
|
||||||
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->unsubscribe_bluetooth_le_advertisements(msg);
|
this->unsubscribe_bluetooth_le_advertisements(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->bluetooth_scanner_set_mode(msg);
|
this->bluetooth_scanner_set_mode(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->subscribe_voice_assistant(msg);
|
this->subscribe_voice_assistant(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||||
if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) {
|
if (!this->send_voice_assistant_get_configuration_response(msg)) {
|
||||||
this->on_fatal_error();
|
this->on_fatal_error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->voice_assistant_set_configuration(msg);
|
this->voice_assistant_set_configuration(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->alarm_control_panel_command(msg);
|
this->alarm_control_panel_command(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ZWAVE_PROXY
|
#ifdef USE_ZWAVE_PROXY
|
||||||
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
|
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->zwave_proxy_frame(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ZWAVE_PROXY
|
#ifdef USE_ZWAVE_PROXY
|
||||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
|
||||||
if (this->check_authenticated_()) {
|
|
||||||
this->zwave_proxy_request(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||||
|
// Check authentication/connection requirements for messages
|
||||||
|
switch (msg_type) {
|
||||||
|
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||||
|
#ifdef USE_API_PASSWORD
|
||||||
|
case AuthenticationRequest::MESSAGE_TYPE: // No setup required
|
||||||
|
#endif
|
||||||
|
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||||
|
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||||
|
break; // Skip all checks for these messages
|
||||||
|
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
|
||||||
|
if (!this->check_connection_setup_()) {
|
||||||
|
return; // Connection not setup
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// All other messages require authentication (which includes connection check)
|
||||||
|
if (!this->check_authenticated_()) {
|
||||||
|
return; // Authentication failed
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call base implementation to process the message
|
||||||
|
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
@@ -477,6 +477,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
#ifdef USE_ZWAVE_PROXY
|
#ifdef USE_ZWAVE_PROXY
|
||||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
@@ -217,12 +217,12 @@ void APIServer::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
bool APIServer::check_password(const std::string &password) const {
|
bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const {
|
||||||
// depend only on input password length
|
// depend only on input password length
|
||||||
const char *a = this->password_.c_str();
|
const char *a = this->password_.c_str();
|
||||||
uint32_t len_a = this->password_.length();
|
uint32_t len_a = this->password_.length();
|
||||||
const char *b = password.c_str();
|
const char *b = reinterpret_cast<const char *>(password_data);
|
||||||
uint32_t len_b = password.length();
|
uint32_t len_b = password_len;
|
||||||
|
|
||||||
// disable optimization with volatile
|
// disable optimization with volatile
|
||||||
volatile uint32_t length = len_b;
|
volatile uint32_t length = len_b;
|
||||||
@@ -245,6 +245,7 @@ bool APIServer::check_password(const std::string &password) const {
|
|||||||
|
|
||||||
return result == 0;
|
return result == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void APIServer::handle_disconnect(APIConnection *conn) {}
|
void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||||
@@ -370,9 +371,9 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
|
|||||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||||
for (auto &client : this->clients_) {
|
for (auto &client : this->clients_) {
|
||||||
client->send_homeassistant_service_call(call);
|
client->send_homeassistant_action(call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@@ -37,7 +37,7 @@ class APIServer : public Component, public Controller {
|
|||||||
void on_shutdown() override;
|
void on_shutdown() override;
|
||||||
bool teardown() override;
|
bool teardown() override;
|
||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
bool check_password(const std::string &password) const;
|
bool check_password(const uint8_t *password_data, size_t password_len) const;
|
||||||
void set_password(const std::string &password);
|
void set_password(const std::string &password);
|
||||||
#endif
|
#endif
|
||||||
void set_port(uint16_t port);
|
void set_port(uint16_t port);
|
||||||
@@ -107,7 +107,8 @@ class APIServer : public Component, public Controller {
|
|||||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#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); }
|
||||||
|
@@ -179,9 +179,9 @@ class CustomAPIDevice {
|
|||||||
* @param service_name The service to call.
|
* @param service_name The service to call.
|
||||||
*/
|
*/
|
||||||
void call_homeassistant_service(const std::string &service_name) {
|
void call_homeassistant_service(const std::string &service_name) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantActionRequest resp;
|
||||||
resp.set_service(StringRef(service_name));
|
resp.set_service(StringRef(service_name));
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Call a Home Assistant service from ESPHome.
|
/** Call a Home Assistant service from ESPHome.
|
||||||
@@ -199,7 +199,7 @@ class CustomAPIDevice {
|
|||||||
* @param data The data for the service call, mapping from string to string.
|
* @param data The data for the service call, mapping from string to string.
|
||||||
*/
|
*/
|
||||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantActionRequest resp;
|
||||||
resp.set_service(StringRef(service_name));
|
resp.set_service(StringRef(service_name));
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
resp.data.emplace_back();
|
resp.data.emplace_back();
|
||||||
@@ -207,7 +207,7 @@ class CustomAPIDevice {
|
|||||||
kv.set_key(StringRef(it.first));
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
}
|
}
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fire an ESPHome event in Home Assistant.
|
/** Fire an ESPHome event in Home Assistant.
|
||||||
@@ -221,10 +221,10 @@ class CustomAPIDevice {
|
|||||||
* @param event_name The event to fire.
|
* @param event_name The event to fire.
|
||||||
*/
|
*/
|
||||||
void fire_homeassistant_event(const std::string &event_name) {
|
void fire_homeassistant_event(const std::string &event_name) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantActionRequest resp;
|
||||||
resp.set_service(StringRef(event_name));
|
resp.set_service(StringRef(event_name));
|
||||||
resp.is_event = true;
|
resp.is_event = true;
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fire an ESPHome event in Home Assistant.
|
/** Fire an ESPHome event in Home Assistant.
|
||||||
@@ -241,7 +241,7 @@ class CustomAPIDevice {
|
|||||||
* @param data The data for the event, mapping from string to string.
|
* @param data The data for the event, mapping from string to string.
|
||||||
*/
|
*/
|
||||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantActionRequest resp;
|
||||||
resp.set_service(StringRef(service_name));
|
resp.set_service(StringRef(service_name));
|
||||||
resp.is_event = true;
|
resp.is_event = true;
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
@@ -250,7 +250,7 @@ class CustomAPIDevice {
|
|||||||
kv.set_key(StringRef(it.first));
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
}
|
}
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
|
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
|
||||||
|
@@ -3,10 +3,10 @@
|
|||||||
#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 <vector>
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
}
|
}
|
||||||
|
|
||||||
void play(Ts... x) override {
|
void play(Ts... x) override {
|
||||||
HomeassistantServiceResponse 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->is_event_;
|
||||||
@@ -84,7 +84,7 @@ 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...);
|
||||||
}
|
}
|
||||||
this->parent_->send_homeassistant_service_call(resp);
|
this->parent_->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@@ -182,6 +182,10 @@ class ProtoLengthDelimited {
|
|||||||
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
||||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
||||||
|
|
||||||
|
// Direct access to raw data without string allocation
|
||||||
|
const uint8_t *data() const { return this->value_; }
|
||||||
|
size_t size() const { return this->length_; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
|
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
|
||||||
*
|
*
|
||||||
@@ -827,7 +831,7 @@ class ProtoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authentication helper methods
|
// Authentication helper methods
|
||||||
bool check_connection_setup_() {
|
inline bool check_connection_setup_() {
|
||||||
if (!this->is_connection_setup()) {
|
if (!this->is_connection_setup()) {
|
||||||
this->on_no_setup_connection();
|
this->on_no_setup_connection();
|
||||||
return false;
|
return false;
|
||||||
@@ -835,7 +839,7 @@ class ProtoService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool check_authenticated_() {
|
inline bool check_authenticated_() {
|
||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
if (!this->check_connection_setup_()) {
|
if (!this->check_connection_setup_()) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -6,8 +6,6 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
|
|||||||
from esphome.components.esp32_ble import BTLoggers
|
from esphome.components.esp32_ble import BTLoggers
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ACTIVE, CONF_ID
|
from esphome.const import CONF_ACTIVE, CONF_ID
|
||||||
from esphome.core import CORE
|
|
||||||
from esphome.log import AnsiFore, color
|
|
||||||
|
|
||||||
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
|
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
|
||||||
DEPENDENCIES = ["api", "esp32"]
|
DEPENDENCIES = ["api", "esp32"]
|
||||||
@@ -48,26 +46,6 @@ def validate_connections(config):
|
|||||||
config
|
config
|
||||||
)
|
)
|
||||||
|
|
||||||
# Warn about connection slot waste when using Arduino framework
|
|
||||||
if CORE.using_arduino and connection_slots:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
|
|
||||||
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
|
|
||||||
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
|
|
||||||
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
|
|
||||||
"\n"
|
|
||||||
"To switch to ESP-IDF, add this to your YAML:\n"
|
|
||||||
" esp32:\n"
|
|
||||||
" framework:\n"
|
|
||||||
" type: esp-idf\n"
|
|
||||||
"\n"
|
|
||||||
"For detailed migration instructions, see:\n"
|
|
||||||
"%s",
|
|
||||||
color(
|
|
||||||
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**config,
|
**config,
|
||||||
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
||||||
@@ -81,19 +59,17 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(BluetoothProxy),
|
cv.GenerateID(): cv.declare_id(BluetoothProxy),
|
||||||
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
|
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
|
||||||
cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
|
cv.Optional(CONF_CACHE_SERVICES, default=True): cv.boolean,
|
||||||
cv.only_with_esp_idf, cv.boolean
|
|
||||||
),
|
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_CONNECTION_SLOTS,
|
CONF_CONNECTION_SLOTS,
|
||||||
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.max_connections()),
|
cv.Range(min=1, max=esp32_ble_tracker.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.max_connections()),
|
cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -514,7 +514,8 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
|||||||
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
|
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length,
|
||||||
|
bool response) {
|
||||||
if (!this->connected()) {
|
if (!this->connected()) {
|
||||||
this->log_gatt_not_connected_("write", "characteristic");
|
this->log_gatt_not_connected_("write", "characteristic");
|
||||||
return ESP_GATT_NOT_CONNECTED;
|
return ESP_GATT_NOT_CONNECTED;
|
||||||
@@ -522,8 +523,11 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
|
|||||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||||
handle);
|
handle);
|
||||||
|
|
||||||
|
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||||
|
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||||
|
// const_cast is safe here and was previously hidden by a C-style cast
|
||||||
esp_err_t err =
|
esp_err_t err =
|
||||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
|
||||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||||
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
|
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
|
||||||
}
|
}
|
||||||
@@ -540,7 +544,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
|
|||||||
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
|
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) {
|
||||||
if (!this->connected()) {
|
if (!this->connected()) {
|
||||||
this->log_gatt_not_connected_("write", "descriptor");
|
this->log_gatt_not_connected_("write", "descriptor");
|
||||||
return ESP_GATT_NOT_CONNECTED;
|
return ESP_GATT_NOT_CONNECTED;
|
||||||
@@ -548,8 +552,11 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
|
|||||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||||
handle);
|
handle);
|
||||||
|
|
||||||
|
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||||
|
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||||
|
// const_cast is safe here and was previously hidden by a C-style cast
|
||||||
esp_err_t err = esp_ble_gattc_write_char_descr(
|
esp_err_t err = esp_ble_gattc_write_char_descr(
|
||||||
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
|
||||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||||
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
|
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
|
||||||
}
|
}
|
||||||
|
@@ -18,9 +18,9 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
|
|||||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||||
|
|
||||||
esp_err_t read_characteristic(uint16_t handle);
|
esp_err_t read_characteristic(uint16_t handle);
|
||||||
esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response);
|
esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
|
||||||
esp_err_t read_descriptor(uint16_t handle);
|
esp_err_t read_descriptor(uint16_t handle);
|
||||||
esp_err_t write_descriptor(uint16_t handle, const std::string &data, bool response);
|
esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response);
|
||||||
|
|
||||||
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
||||||
|
|
||||||
|
@@ -305,7 +305,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto err = connection->write_characteristic(msg.handle, msg.data, msg.response);
|
auto err = connection->write_characteristic(msg.handle, msg.data, msg.data_len, msg.response);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
this->send_gatt_error(msg.address, msg.handle, err);
|
this->send_gatt_error(msg.address, msg.handle, err);
|
||||||
}
|
}
|
||||||
@@ -331,7 +331,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto err = connection->write_descriptor(msg.handle, msg.data, true);
|
auto err = connection->write_descriptor(msg.handle, msg.data, msg.data_len, true);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
this->send_gatt_error(msg.address, msg.handle, err);
|
this->send_gatt_error(msg.address, msg.handle, err);
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ import esphome.codegen as cg
|
|||||||
from esphome.components.esp32 import add_idf_component
|
from esphome.components.esp32 import add_idf_component
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE
|
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE
|
||||||
from esphome.core import CORE
|
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CODEOWNERS = ["@DT-art1"]
|
CODEOWNERS = ["@DT-art1"]
|
||||||
@@ -51,9 +50,8 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
|
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
|
||||||
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||||
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
||||||
if CORE.using_esp_idf:
|
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.0")
|
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
|
||||||
cg.add_build_flag("-DUSE_ESP32_CAMERA_JPEG_ENCODER")
|
|
||||||
var = cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
config[CONF_QUALITY],
|
config[CONF_QUALITY],
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
|
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
|
||||||
|
|
||||||
#include "esp32_camera_jpeg_encoder.h"
|
#include "esp32_camera_jpeg_encoder.h"
|
||||||
@@ -15,7 +17,7 @@ camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSp
|
|||||||
this->bytes_written_ = 0;
|
this->bytes_written_ = 0;
|
||||||
this->out_of_output_memory_ = false;
|
this->out_of_output_memory_ = false;
|
||||||
bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height,
|
bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height,
|
||||||
to_internal_(spec->format), this->quality_, callback_, this);
|
to_internal_(spec->format), this->quality_, callback, this);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
return camera::ENCODER_ERROR_CONFIGURATION;
|
return camera::ENCODER_ERROR_CONFIGURATION;
|
||||||
@@ -49,7 +51,7 @@ void ESP32CameraJPEGEncoder::dump_config() {
|
|||||||
this->output_->get_max_size(), this->quality_, this->buffer_expand_size_);
|
this->output_->get_max_size(), this->quality_, this->buffer_expand_size_);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ESP32CameraJPEGEncoder::callback_(void *arg, size_t index, const void *data, size_t len) {
|
size_t ESP32CameraJPEGEncoder::callback(void *arg, size_t index, const void *data, size_t len) {
|
||||||
ESP32CameraJPEGEncoder *that = reinterpret_cast<ESP32CameraJPEGEncoder *>(arg);
|
ESP32CameraJPEGEncoder *that = reinterpret_cast<ESP32CameraJPEGEncoder *>(arg);
|
||||||
uint8_t *buffer = that->output_->get_data();
|
uint8_t *buffer = that->output_->get_data();
|
||||||
size_t buffer_length = that->output_->get_max_size();
|
size_t buffer_length = that->output_->get_max_size();
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
|
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
|
||||||
|
|
||||||
#include <esp_camera.h>
|
#include <esp_camera.h>
|
||||||
@@ -24,7 +26,7 @@ class ESP32CameraJPEGEncoder : public camera::Encoder {
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
// -------------------------
|
// -------------------------
|
||||||
protected:
|
protected:
|
||||||
static size_t callback_(void *arg, size_t index, const void *data, size_t len);
|
static size_t callback(void *arg, size_t index, const void *data, size_t len);
|
||||||
pixformat_t to_internal_(camera::PixelFormat format);
|
pixformat_t to_internal_(camera::PixelFormat format);
|
||||||
|
|
||||||
camera::EncoderBuffer *output_{};
|
camera::EncoderBuffer *output_{};
|
||||||
|
@@ -174,16 +174,12 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_ADVERTISING_CYCLE_TIME, default="10s"
|
CONF_ADVERTISING_CYCLE_TIME, default="10s"
|
||||||
): cv.positive_time_period_milliseconds,
|
): cv.positive_time_period_milliseconds,
|
||||||
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
|
cv.Optional(CONF_DISABLE_BT_LOGS, default=True): cv.boolean,
|
||||||
cv.only_with_esp_idf, cv.boolean
|
cv.Optional(CONF_CONNECTION_TIMEOUT, default="20s"): cv.All(
|
||||||
),
|
|
||||||
cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
|
|
||||||
cv.only_with_esp_idf,
|
|
||||||
cv.positive_time_period_seconds,
|
cv.positive_time_period_seconds,
|
||||||
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
|
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
|
||||||
),
|
),
|
||||||
cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
|
cv.Optional(CONF_MAX_NOTIFICATIONS, default=12): cv.All(
|
||||||
cv.only_with_esp_idf,
|
|
||||||
cv.positive_int,
|
cv.positive_int,
|
||||||
cv.Range(min=1, max=64),
|
cv.Range(min=1, max=64),
|
||||||
),
|
),
|
||||||
|
@@ -150,10 +150,6 @@ def as_reversed_hex_array(value):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def max_connections() -> int:
|
|
||||||
return IDF_MAX_CONNECTIONS if CORE.using_esp_idf else DEFAULT_MAX_CONNECTIONS
|
|
||||||
|
|
||||||
|
|
||||||
def consume_connection_slots(
|
def consume_connection_slots(
|
||||||
value: int, consumer: str
|
value: int, consumer: str
|
||||||
) -> Callable[[MutableMapping], MutableMapping]:
|
) -> Callable[[MutableMapping], MutableMapping]:
|
||||||
@@ -172,7 +168,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
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, default=DEFAULT_MAX_CONNECTIONS): cv.All(
|
||||||
cv.positive_int, cv.Range(min=0, max=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(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
@@ -238,9 +234,8 @@ def validate_remaining_connections(config):
|
|||||||
if used_slots <= config[CONF_MAX_CONNECTIONS]:
|
if used_slots <= config[CONF_MAX_CONNECTIONS]:
|
||||||
return config
|
return config
|
||||||
slot_users = ", ".join(slots)
|
slot_users = ", ".join(slots)
|
||||||
hard_limit = max_connections()
|
|
||||||
|
|
||||||
if used_slots < hard_limit:
|
if used_slots < IDF_MAX_CONNECTIONS:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
|
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
|
||||||
"connection slot(s) out of available configured maximum %d connection "
|
"connection slot(s) out of available configured maximum %d connection "
|
||||||
@@ -262,9 +257,9 @@ def validate_remaining_connections(config):
|
|||||||
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
|
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
|
||||||
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
|
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
|
||||||
)
|
)
|
||||||
if config[CONF_MAX_CONNECTIONS] < hard_limit:
|
if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
|
||||||
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
|
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
|
||||||
msg += f" to stay under the {hard_limit} connection slot(s) limit."
|
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
|
||||||
raise cv.Invalid(msg)
|
raise cv.Invalid(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -128,4 +128,4 @@ async def to_code(config):
|
|||||||
|
|
||||||
cg.add_library("tonia/HeatpumpIR", "1.0.37")
|
cg.add_library("tonia/HeatpumpIR", "1.0.37")
|
||||||
if CORE.is_libretiny or CORE.is_esp32:
|
if CORE.is_libretiny or CORE.is_esp32:
|
||||||
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
|
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])
|
||||||
|
@@ -87,7 +87,7 @@ void HomeassistantNumber::control(float value) {
|
|||||||
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
|
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
|
||||||
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
|
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
|
||||||
|
|
||||||
api::HomeassistantServiceResponse resp;
|
api::HomeassistantActionRequest resp;
|
||||||
resp.set_service(SERVICE_NAME);
|
resp.set_service(SERVICE_NAME);
|
||||||
|
|
||||||
resp.data.emplace_back();
|
resp.data.emplace_back();
|
||||||
@@ -100,7 +100,7 @@ void HomeassistantNumber::control(float value) {
|
|||||||
entity_value.set_key(VALUE_KEY);
|
entity_value.set_key(VALUE_KEY);
|
||||||
entity_value.value = to_string(value);
|
entity_value.value = to_string(value);
|
||||||
|
|
||||||
api::global_api_server->send_homeassistant_service_call(resp);
|
api::global_api_server->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace homeassistant
|
} // namespace homeassistant
|
||||||
|
@@ -44,7 +44,7 @@ void HomeassistantSwitch::write_state(bool state) {
|
|||||||
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
|
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
|
||||||
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
|
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
|
||||||
|
|
||||||
api::HomeassistantServiceResponse resp;
|
api::HomeassistantActionRequest resp;
|
||||||
if (state) {
|
if (state) {
|
||||||
resp.set_service(SERVICE_ON);
|
resp.set_service(SERVICE_ON);
|
||||||
} else {
|
} else {
|
||||||
@@ -56,7 +56,7 @@ void HomeassistantSwitch::write_state(bool state) {
|
|||||||
entity_id_kv.set_key(ENTITY_ID_KEY);
|
entity_id_kv.set_key(ENTITY_ID_KEY);
|
||||||
entity_id_kv.value = this->entity_id_;
|
entity_id_kv.value = this->entity_id_;
|
||||||
|
|
||||||
api::global_api_server->send_homeassistant_service_call(resp);
|
api::global_api_server->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace homeassistant
|
} // namespace homeassistant
|
||||||
|
@@ -7,6 +7,7 @@ wave_4_3 = DriverChip(
|
|||||||
"ESP32-S3-TOUCH-LCD-4.3",
|
"ESP32-S3-TOUCH-LCD-4.3",
|
||||||
swap_xy=UNDEFINED,
|
swap_xy=UNDEFINED,
|
||||||
initsequence=(),
|
initsequence=(),
|
||||||
|
color_order="RGB",
|
||||||
width=800,
|
width=800,
|
||||||
height=480,
|
height=480,
|
||||||
pclk_frequency="16MHz",
|
pclk_frequency="16MHz",
|
||||||
|
@@ -128,21 +128,21 @@ void MMC5603Component::update() {
|
|||||||
raw_x |= buffer[1] << 4;
|
raw_x |= buffer[1] << 4;
|
||||||
raw_x |= buffer[2] << 0;
|
raw_x |= buffer[2] << 0;
|
||||||
|
|
||||||
const float x = 0.0625 * (raw_x - 524288);
|
const float x = 0.00625 * (raw_x - 524288);
|
||||||
|
|
||||||
int32_t raw_y = 0;
|
int32_t raw_y = 0;
|
||||||
raw_y |= buffer[3] << 12;
|
raw_y |= buffer[3] << 12;
|
||||||
raw_y |= buffer[4] << 4;
|
raw_y |= buffer[4] << 4;
|
||||||
raw_y |= buffer[5] << 0;
|
raw_y |= buffer[5] << 0;
|
||||||
|
|
||||||
const float y = 0.0625 * (raw_y - 524288);
|
const float y = 0.00625 * (raw_y - 524288);
|
||||||
|
|
||||||
int32_t raw_z = 0;
|
int32_t raw_z = 0;
|
||||||
raw_z |= buffer[6] << 12;
|
raw_z |= buffer[6] << 12;
|
||||||
raw_z |= buffer[7] << 4;
|
raw_z |= buffer[7] << 4;
|
||||||
raw_z |= buffer[8] << 0;
|
raw_z |= buffer[8] << 0;
|
||||||
|
|
||||||
const float z = 0.0625 * (raw_z - 524288);
|
const float z = 0.00625 * (raw_z - 524288);
|
||||||
|
|
||||||
const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
|
const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
|
||||||
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading);
|
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading);
|
||||||
|
@@ -5,7 +5,10 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "usb/usb_host.h"
|
#include "usb/usb_host.h"
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
#include "esphome/core/lock_free_queue.h"
|
||||||
|
#include "esphome/core/event_pool.h"
|
||||||
#include <list>
|
#include <list>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -13,6 +16,10 @@ namespace usb_host {
|
|||||||
|
|
||||||
static const char *const TAG = "usb_host";
|
static const char *const TAG = "usb_host";
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
struct TransferRequest;
|
||||||
|
class USBClient;
|
||||||
|
|
||||||
// constants for setup packet type
|
// constants for setup packet type
|
||||||
static const uint8_t USB_RECIP_DEVICE = 0;
|
static const uint8_t USB_RECIP_DEVICE = 0;
|
||||||
static const uint8_t USB_RECIP_INTERFACE = 1;
|
static const uint8_t USB_RECIP_INTERFACE = 1;
|
||||||
@@ -26,6 +33,9 @@ static const uint8_t USB_DIR_OUT = 0;
|
|||||||
static const size_t SETUP_PACKET_SIZE = 8;
|
static const size_t SETUP_PACKET_SIZE = 8;
|
||||||
|
|
||||||
static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
|
static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
|
||||||
|
static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop
|
||||||
|
static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples)
|
||||||
|
static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5)
|
||||||
|
|
||||||
// used to report a transfer status
|
// used to report a transfer status
|
||||||
struct TransferStatus {
|
struct TransferStatus {
|
||||||
@@ -49,6 +59,31 @@ struct TransferRequest {
|
|||||||
USBClient *client;
|
USBClient *client;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum EventType : uint8_t {
|
||||||
|
EVENT_DEVICE_NEW,
|
||||||
|
EVENT_DEVICE_GONE,
|
||||||
|
EVENT_TRANSFER_COMPLETE,
|
||||||
|
EVENT_CONTROL_COMPLETE,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UsbEvent {
|
||||||
|
EventType type;
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
uint8_t address;
|
||||||
|
} device_new;
|
||||||
|
struct {
|
||||||
|
usb_device_handle_t handle;
|
||||||
|
} device_gone;
|
||||||
|
struct {
|
||||||
|
TransferRequest *trq;
|
||||||
|
} transfer;
|
||||||
|
} data;
|
||||||
|
|
||||||
|
// Required for EventPool - no cleanup needed for POD types
|
||||||
|
void release() {}
|
||||||
|
};
|
||||||
|
|
||||||
// callback function type.
|
// callback function type.
|
||||||
|
|
||||||
enum ClientState {
|
enum ClientState {
|
||||||
@@ -84,6 +119,11 @@ class USBClient : public Component {
|
|||||||
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
|
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
|
||||||
const std::vector<uint8_t> &data = {});
|
const std::vector<uint8_t> &data = {});
|
||||||
|
|
||||||
|
// Lock-free event queue and pool for USB task to main loop communication
|
||||||
|
// Must be public for access from static callbacks
|
||||||
|
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
|
||||||
|
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool register_();
|
bool register_();
|
||||||
TransferRequest *get_trq_();
|
TransferRequest *get_trq_();
|
||||||
@@ -91,6 +131,12 @@ class USBClient : public Component {
|
|||||||
virtual void on_connected() {}
|
virtual void on_connected() {}
|
||||||
virtual void on_disconnected() { this->init_pool(); }
|
virtual void on_disconnected() { this->init_pool(); }
|
||||||
|
|
||||||
|
// USB task management
|
||||||
|
static void usb_task_fn(void *arg);
|
||||||
|
void usb_task_loop();
|
||||||
|
|
||||||
|
TaskHandle_t usb_task_handle_{nullptr};
|
||||||
|
|
||||||
usb_host_client_handle_t handle_{};
|
usb_host_client_handle_t handle_{};
|
||||||
usb_device_handle_t device_handle_{};
|
usb_device_handle_t device_handle_{};
|
||||||
int device_addr_{-1};
|
int device_addr_{-1};
|
||||||
|
@@ -139,24 +139,40 @@ static std::string get_descriptor_string(const usb_str_desc_t *desc) {
|
|||||||
return {buffer};
|
return {buffer};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||||
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
|
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
|
||||||
auto *client = static_cast<USBClient *>(ptr);
|
auto *client = static_cast<USBClient *>(ptr);
|
||||||
|
|
||||||
|
// Allocate event from pool
|
||||||
|
UsbEvent *event = client->event_pool.allocate();
|
||||||
|
if (event == nullptr) {
|
||||||
|
// No events available - increment counter for periodic logging
|
||||||
|
client->event_queue.increment_dropped_count();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue events to be processed in main loop
|
||||||
switch (event_msg->event) {
|
switch (event_msg->event) {
|
||||||
case USB_HOST_CLIENT_EVENT_NEW_DEV: {
|
case USB_HOST_CLIENT_EVENT_NEW_DEV: {
|
||||||
auto addr = event_msg->new_dev.address;
|
|
||||||
ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address);
|
ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address);
|
||||||
client->on_opened(addr);
|
event->type = EVENT_DEVICE_NEW;
|
||||||
|
event->data.device_new.address = event_msg->new_dev.address;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case USB_HOST_CLIENT_EVENT_DEV_GONE: {
|
case USB_HOST_CLIENT_EVENT_DEV_GONE: {
|
||||||
client->on_removed(event_msg->dev_gone.dev_hdl);
|
ESP_LOGD(TAG, "Device gone");
|
||||||
ESP_LOGD(TAG, "Device gone %d", event_msg->new_dev.address);
|
event->type = EVENT_DEVICE_GONE;
|
||||||
|
event->data.device_gone.handle = event_msg->dev_gone.dev_hdl;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
ESP_LOGD(TAG, "Unknown event %d", event_msg->event);
|
ESP_LOGD(TAG, "Unknown event %d", event_msg->event);
|
||||||
break;
|
client->event_pool.release(event);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push to lock-free queue (always succeeds since pool size == queue size)
|
||||||
|
client->event_queue.push(event);
|
||||||
}
|
}
|
||||||
void USBClient::setup() {
|
void USBClient::setup() {
|
||||||
usb_host_client_config_t config{.is_synchronous = false,
|
usb_host_client_config_t config{.is_synchronous = false,
|
||||||
@@ -173,9 +189,59 @@ void USBClient::setup() {
|
|||||||
usb_host_transfer_alloc(64, 0, &trq->transfer);
|
usb_host_transfer_alloc(64, 0, &trq->transfer);
|
||||||
trq->client = this;
|
trq->client = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create and start USB task
|
||||||
|
xTaskCreate(usb_task_fn, "usb_task",
|
||||||
|
USB_TASK_STACK_SIZE, // Stack size
|
||||||
|
this, // Task parameter
|
||||||
|
USB_TASK_PRIORITY, // Priority (higher than main loop)
|
||||||
|
&this->usb_task_handle_);
|
||||||
|
|
||||||
|
if (this->usb_task_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create USB task");
|
||||||
|
this->mark_failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBClient::usb_task_fn(void *arg) {
|
||||||
|
auto *client = static_cast<USBClient *>(arg);
|
||||||
|
client->usb_task_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBClient::usb_task_loop() {
|
||||||
|
while (true) {
|
||||||
|
usb_host_client_handle_events(this->handle_, portMAX_DELAY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void USBClient::loop() {
|
void USBClient::loop() {
|
||||||
|
// Process any events from the USB task
|
||||||
|
UsbEvent *event;
|
||||||
|
while ((event = this->event_queue.pop()) != nullptr) {
|
||||||
|
switch (event->type) {
|
||||||
|
case EVENT_DEVICE_NEW:
|
||||||
|
this->on_opened(event->data.device_new.address);
|
||||||
|
break;
|
||||||
|
case EVENT_DEVICE_GONE:
|
||||||
|
this->on_removed(event->data.device_gone.handle);
|
||||||
|
break;
|
||||||
|
case EVENT_TRANSFER_COMPLETE:
|
||||||
|
case EVENT_CONTROL_COMPLETE: {
|
||||||
|
auto *trq = event->data.transfer.trq;
|
||||||
|
this->release_trq(trq);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return event to pool for reuse
|
||||||
|
this->event_pool.release(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log dropped events periodically
|
||||||
|
uint16_t dropped = this->event_queue.get_and_reset_dropped_count();
|
||||||
|
if (dropped > 0) {
|
||||||
|
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
|
||||||
|
}
|
||||||
|
|
||||||
switch (this->state_) {
|
switch (this->state_) {
|
||||||
case USB_CLIENT_OPEN: {
|
case USB_CLIENT_OPEN: {
|
||||||
int err;
|
int err;
|
||||||
@@ -228,7 +294,6 @@ void USBClient::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
usb_host_client_handle_events(this->handle_, 0);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +310,26 @@ void USBClient::on_removed(usb_device_handle_t handle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to queue transfer cleanup to main loop
|
||||||
|
static void queue_transfer_cleanup(TransferRequest *trq, EventType type) {
|
||||||
|
auto *client = trq->client;
|
||||||
|
|
||||||
|
// Allocate event from pool
|
||||||
|
UsbEvent *event = client->event_pool.allocate();
|
||||||
|
if (event == nullptr) {
|
||||||
|
// No events available - increment counter for periodic logging
|
||||||
|
client->event_queue.increment_dropped_count();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event->type = type;
|
||||||
|
event->data.transfer.trq = trq;
|
||||||
|
|
||||||
|
// Push to lock-free queue (always succeeds since pool size == queue size)
|
||||||
|
client->event_queue.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||||
static void control_callback(const usb_transfer_t *xfer) {
|
static void control_callback(const usb_transfer_t *xfer) {
|
||||||
auto *trq = static_cast<TransferRequest *>(xfer->context);
|
auto *trq = static_cast<TransferRequest *>(xfer->context);
|
||||||
trq->status.error_code = xfer->status;
|
trq->status.error_code = xfer->status;
|
||||||
@@ -252,9 +337,14 @@ static void control_callback(const usb_transfer_t *xfer) {
|
|||||||
trq->status.endpoint = xfer->bEndpointAddress;
|
trq->status.endpoint = xfer->bEndpointAddress;
|
||||||
trq->status.data = xfer->data_buffer;
|
trq->status.data = xfer->data_buffer;
|
||||||
trq->status.data_len = xfer->actual_num_bytes;
|
trq->status.data_len = xfer->actual_num_bytes;
|
||||||
if (trq->callback != nullptr)
|
|
||||||
|
// Execute callback in USB task context
|
||||||
|
if (trq->callback != nullptr) {
|
||||||
trq->callback(trq->status);
|
trq->callback(trq->status);
|
||||||
trq->client->release_trq(trq);
|
}
|
||||||
|
|
||||||
|
// Queue cleanup to main loop
|
||||||
|
queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE);
|
||||||
}
|
}
|
||||||
|
|
||||||
TransferRequest *USBClient::get_trq_() {
|
TransferRequest *USBClient::get_trq_() {
|
||||||
@@ -315,6 +405,7 @@ bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||||
static void transfer_callback(usb_transfer_t *xfer) {
|
static void transfer_callback(usb_transfer_t *xfer) {
|
||||||
auto *trq = static_cast<TransferRequest *>(xfer->context);
|
auto *trq = static_cast<TransferRequest *>(xfer->context);
|
||||||
trq->status.error_code = xfer->status;
|
trq->status.error_code = xfer->status;
|
||||||
@@ -322,9 +413,15 @@ static void transfer_callback(usb_transfer_t *xfer) {
|
|||||||
trq->status.endpoint = xfer->bEndpointAddress;
|
trq->status.endpoint = xfer->bEndpointAddress;
|
||||||
trq->status.data = xfer->data_buffer;
|
trq->status.data = xfer->data_buffer;
|
||||||
trq->status.data_len = xfer->actual_num_bytes;
|
trq->status.data_len = xfer->actual_num_bytes;
|
||||||
if (trq->callback != nullptr)
|
|
||||||
|
// Always execute callback in USB task context
|
||||||
|
// Callbacks should be fast and non-blocking (e.g., copy data to queue)
|
||||||
|
if (trq->callback != nullptr) {
|
||||||
trq->callback(trq->status);
|
trq->callback(trq->status);
|
||||||
trq->client->release_trq(trq);
|
}
|
||||||
|
|
||||||
|
// Queue cleanup to main loop
|
||||||
|
queue_transfer_cleanup(trq, EVENT_TRANSFER_COMPLETE);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Performs a transfer input operation.
|
* Performs a transfer input operation.
|
||||||
|
@@ -16,12 +16,12 @@ using namespace bytebuffer;
|
|||||||
void USBUartTypeCH34X::enable_channels() {
|
void USBUartTypeCH34X::enable_channels() {
|
||||||
// enable the channels
|
// enable the channels
|
||||||
for (auto channel : this->channels_) {
|
for (auto channel : this->channels_) {
|
||||||
if (!channel->initialised_)
|
if (!channel->initialised_.load())
|
||||||
continue;
|
continue;
|
||||||
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
|
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
|
||||||
if (!status.success) {
|
if (!status.success) {
|
||||||
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
|
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
|
||||||
channel->initialised_ = false;
|
channel->initialised_.store(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ void USBUartTypeCH34X::enable_channels() {
|
|||||||
auto factor = static_cast<uint8_t>(clk / baud_rate);
|
auto factor = static_cast<uint8_t>(clk / baud_rate);
|
||||||
if (factor == 0 || factor == 0xFF) {
|
if (factor == 0 || factor == 0xFF) {
|
||||||
ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate);
|
ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate);
|
||||||
channel->initialised_ = false;
|
channel->initialised_.store(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1)))
|
if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1)))
|
||||||
|
@@ -100,12 +100,12 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
|
|||||||
void USBUartTypeCP210X::enable_channels() {
|
void USBUartTypeCP210X::enable_channels() {
|
||||||
// enable the channels
|
// enable the channels
|
||||||
for (auto channel : this->channels_) {
|
for (auto channel : this->channels_) {
|
||||||
if (!channel->initialised_)
|
if (!channel->initialised_.load())
|
||||||
continue;
|
continue;
|
||||||
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
|
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
|
||||||
if (!status.success) {
|
if (!status.success) {
|
||||||
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
|
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
|
||||||
channel->initialised_ = false;
|
channel->initialised_.store(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback);
|
this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback);
|
||||||
|
@@ -130,7 +130,7 @@ size_t RingBuffer::pop(uint8_t *data, size_t len) {
|
|||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
void USBUartChannel::write_array(const uint8_t *data, size_t len) {
|
void USBUartChannel::write_array(const uint8_t *data, size_t len) {
|
||||||
if (!this->initialised_) {
|
if (!this->initialised_.load()) {
|
||||||
ESP_LOGV(TAG, "Channel not initialised - write ignored");
|
ESP_LOGV(TAG, "Channel not initialised - write ignored");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ bool USBUartChannel::peek_byte(uint8_t *data) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool USBUartChannel::read_array(uint8_t *data, size_t len) {
|
bool USBUartChannel::read_array(uint8_t *data, size_t len) {
|
||||||
if (!this->initialised_) {
|
if (!this->initialised_.load()) {
|
||||||
ESP_LOGV(TAG, "Channel not initialised - read ignored");
|
ESP_LOGV(TAG, "Channel not initialised - read ignored");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,34 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
void USBUartComponent::setup() { USBClient::setup(); }
|
void USBUartComponent::setup() { USBClient::setup(); }
|
||||||
void USBUartComponent::loop() { USBClient::loop(); }
|
void USBUartComponent::loop() {
|
||||||
|
USBClient::loop();
|
||||||
|
|
||||||
|
// Process USB data from the lock-free queue
|
||||||
|
UsbDataChunk *chunk;
|
||||||
|
while ((chunk = this->usb_data_queue_.pop()) != nullptr) {
|
||||||
|
auto *channel = chunk->channel;
|
||||||
|
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
if (channel->debug_) {
|
||||||
|
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector<uint8_t>(chunk->data, chunk->data + chunk->length),
|
||||||
|
','); // NOLINT()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Push data to ring buffer (now safe in main loop)
|
||||||
|
channel->input_buffer_.push(chunk->data, chunk->length);
|
||||||
|
|
||||||
|
// Return chunk to pool for reuse
|
||||||
|
this->chunk_pool_.release(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log dropped USB data periodically
|
||||||
|
uint16_t dropped = this->usb_data_queue_.get_and_reset_dropped_count();
|
||||||
|
if (dropped > 0) {
|
||||||
|
ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped);
|
||||||
|
}
|
||||||
|
}
|
||||||
void USBUartComponent::dump_config() {
|
void USBUartComponent::dump_config() {
|
||||||
USBClient::dump_config();
|
USBClient::dump_config();
|
||||||
for (auto &channel : this->channels_) {
|
for (auto &channel : this->channels_) {
|
||||||
@@ -187,49 +214,70 @@ void USBUartComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
void USBUartComponent::start_input(USBUartChannel *channel) {
|
void USBUartComponent::start_input(USBUartChannel *channel) {
|
||||||
if (!channel->initialised_ || channel->input_started_ ||
|
if (!channel->initialised_.load() || channel->input_started_.load())
|
||||||
channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize)
|
|
||||||
return;
|
return;
|
||||||
|
// Note: This function is called from both USB task and main loop, so we cannot
|
||||||
|
// directly check ring buffer space here. Backpressure is handled by the chunk pool:
|
||||||
|
// when exhausted, USB input stops until chunks are freed by the main loop
|
||||||
const auto *ep = channel->cdc_dev_.in_ep;
|
const auto *ep = channel->cdc_dev_.in_ep;
|
||||||
|
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
|
||||||
auto callback = [this, channel](const usb_host::TransferStatus &status) {
|
auto callback = [this, channel](const usb_host::TransferStatus &status) {
|
||||||
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
|
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
|
||||||
if (!status.success) {
|
if (!status.success) {
|
||||||
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
|
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
|
||||||
|
// On failure, don't restart - let next read_array() trigger it
|
||||||
|
channel->input_started_.store(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#ifdef USE_UART_DEBUGGER
|
|
||||||
if (channel->debug_) {
|
if (!channel->dummy_receiver_ && status.data_len > 0) {
|
||||||
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
|
// Allocate a chunk from the pool
|
||||||
std::vector<uint8_t>(status.data, status.data + status.data_len), ','); // NOLINT()
|
UsbDataChunk *chunk = this->chunk_pool_.allocate();
|
||||||
|
if (chunk == nullptr) {
|
||||||
|
// No chunks available - queue is full or we're out of memory
|
||||||
|
this->usb_data_queue_.increment_dropped_count();
|
||||||
|
// Mark input as not started so we can retry
|
||||||
|
channel->input_started_.store(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
channel->input_started_ = false;
|
// Copy data to chunk (this is fast, happens in USB task)
|
||||||
if (!channel->dummy_receiver_) {
|
memcpy(chunk->data, status.data, status.data_len);
|
||||||
for (size_t i = 0; i != status.data_len; i++) {
|
chunk->length = status.data_len;
|
||||||
channel->input_buffer_.push(status.data[i]);
|
chunk->channel = channel;
|
||||||
}
|
|
||||||
}
|
// Push to lock-free queue for main loop processing
|
||||||
if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
|
// Push always succeeds because pool size == queue size
|
||||||
this->defer([this, channel] { this->start_input(channel); });
|
this->usb_data_queue_.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On success, restart input immediately from USB task for performance
|
||||||
|
// The lock-free queue will handle backpressure
|
||||||
|
channel->input_started_.store(false);
|
||||||
|
this->start_input(channel);
|
||||||
};
|
};
|
||||||
channel->input_started_ = true;
|
channel->input_started_.store(true);
|
||||||
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
|
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
void USBUartComponent::start_output(USBUartChannel *channel) {
|
void USBUartComponent::start_output(USBUartChannel *channel) {
|
||||||
if (channel->output_started_)
|
// IMPORTANT: This function must only be called from the main loop!
|
||||||
|
// The output_buffer_ is not thread-safe and can only be accessed from main loop.
|
||||||
|
// USB callbacks use defer() to ensure this function runs in the correct context.
|
||||||
|
if (channel->output_started_.load())
|
||||||
return;
|
return;
|
||||||
if (channel->output_buffer_.is_empty()) {
|
if (channel->output_buffer_.is_empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto *ep = channel->cdc_dev_.out_ep;
|
const auto *ep = channel->cdc_dev_.out_ep;
|
||||||
|
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
|
||||||
auto callback = [this, channel](const usb_host::TransferStatus &status) {
|
auto callback = [this, channel](const usb_host::TransferStatus &status) {
|
||||||
ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
|
ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
|
||||||
channel->output_started_ = false;
|
channel->output_started_.store(false);
|
||||||
|
// Defer restart to main loop (defer is thread-safe)
|
||||||
this->defer([this, channel] { this->start_output(channel); });
|
this->defer([this, channel] { this->start_output(channel); });
|
||||||
};
|
};
|
||||||
channel->output_started_ = true;
|
channel->output_started_.store(true);
|
||||||
uint8_t data[ep->wMaxPacketSize];
|
uint8_t data[ep->wMaxPacketSize];
|
||||||
auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize);
|
auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize);
|
||||||
this->transfer_out(ep->bEndpointAddress, callback, data, len);
|
this->transfer_out(ep->bEndpointAddress, callback, data, len);
|
||||||
@@ -272,7 +320,7 @@ void USBUartTypeCdcAcm::on_connected() {
|
|||||||
channel->cdc_dev_ = cdc_devs[i++];
|
channel->cdc_dev_ = cdc_devs[i++];
|
||||||
fix_mps(channel->cdc_dev_.in_ep);
|
fix_mps(channel->cdc_dev_.in_ep);
|
||||||
fix_mps(channel->cdc_dev_.out_ep);
|
fix_mps(channel->cdc_dev_.out_ep);
|
||||||
channel->initialised_ = true;
|
channel->initialised_.store(true);
|
||||||
auto err =
|
auto err =
|
||||||
usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0);
|
usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -301,9 +349,9 @@ void USBUartTypeCdcAcm::on_disconnected() {
|
|||||||
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
|
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
|
||||||
}
|
}
|
||||||
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
|
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
|
||||||
channel->initialised_ = false;
|
channel->initialised_.store(false);
|
||||||
channel->input_started_ = false;
|
channel->input_started_.store(false);
|
||||||
channel->output_started_ = false;
|
channel->output_started_.store(false);
|
||||||
channel->input_buffer_.clear();
|
channel->input_buffer_.clear();
|
||||||
channel->output_buffer_.clear();
|
channel->output_buffer_.clear();
|
||||||
}
|
}
|
||||||
@@ -312,10 +360,10 @@ void USBUartTypeCdcAcm::on_disconnected() {
|
|||||||
|
|
||||||
void USBUartTypeCdcAcm::enable_channels() {
|
void USBUartTypeCdcAcm::enable_channels() {
|
||||||
for (auto *channel : this->channels_) {
|
for (auto *channel : this->channels_) {
|
||||||
if (!channel->initialised_)
|
if (!channel->initialised_.load())
|
||||||
continue;
|
continue;
|
||||||
channel->input_started_ = false;
|
channel->input_started_.store(false);
|
||||||
channel->output_started_ = false;
|
channel->output_started_.store(false);
|
||||||
this->start_input(channel);
|
this->start_input(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,11 +5,15 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/uart/uart_component.h"
|
#include "esphome/components/uart/uart_component.h"
|
||||||
#include "esphome/components/usb_host/usb_host.h"
|
#include "esphome/components/usb_host/usb_host.h"
|
||||||
|
#include "esphome/core/lock_free_queue.h"
|
||||||
|
#include "esphome/core/event_pool.h"
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace usb_uart {
|
namespace usb_uart {
|
||||||
class USBUartTypeCdcAcm;
|
class USBUartTypeCdcAcm;
|
||||||
class USBUartComponent;
|
class USBUartComponent;
|
||||||
|
class USBUartChannel;
|
||||||
|
|
||||||
static const char *const TAG = "usb_uart";
|
static const char *const TAG = "usb_uart";
|
||||||
|
|
||||||
@@ -68,6 +72,17 @@ class RingBuffer {
|
|||||||
uint8_t *buffer_;
|
uint8_t *buffer_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Structure for queuing received USB data chunks
|
||||||
|
struct UsbDataChunk {
|
||||||
|
static constexpr size_t MAX_CHUNK_SIZE = 64; // USB packet size
|
||||||
|
uint8_t data[MAX_CHUNK_SIZE];
|
||||||
|
uint8_t length; // Max 64 bytes, so uint8_t is sufficient
|
||||||
|
USBUartChannel *channel;
|
||||||
|
|
||||||
|
// Required for EventPool - no cleanup needed for POD types
|
||||||
|
void release() {}
|
||||||
|
};
|
||||||
|
|
||||||
class USBUartChannel : public uart::UARTComponent, public Parented<USBUartComponent> {
|
class USBUartChannel : public uart::UARTComponent, public Parented<USBUartComponent> {
|
||||||
friend class USBUartComponent;
|
friend class USBUartComponent;
|
||||||
friend class USBUartTypeCdcAcm;
|
friend class USBUartTypeCdcAcm;
|
||||||
@@ -90,16 +105,20 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
|
|||||||
void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; }
|
void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
const uint8_t index_;
|
// Larger structures first for better alignment
|
||||||
RingBuffer input_buffer_;
|
RingBuffer input_buffer_;
|
||||||
RingBuffer output_buffer_;
|
RingBuffer output_buffer_;
|
||||||
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
|
|
||||||
bool input_started_{true};
|
|
||||||
bool output_started_{true};
|
|
||||||
CdcEps cdc_dev_{};
|
CdcEps cdc_dev_{};
|
||||||
|
// Enum (likely 4 bytes)
|
||||||
|
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
|
||||||
|
// Group atomics together (each 1 byte)
|
||||||
|
std::atomic<bool> input_started_{true};
|
||||||
|
std::atomic<bool> output_started_{true};
|
||||||
|
std::atomic<bool> initialised_{false};
|
||||||
|
// Group regular bytes together to minimize padding
|
||||||
|
const uint8_t index_;
|
||||||
bool debug_{};
|
bool debug_{};
|
||||||
bool dummy_receiver_{};
|
bool dummy_receiver_{};
|
||||||
bool initialised_{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class USBUartComponent : public usb_host::USBClient {
|
class USBUartComponent : public usb_host::USBClient {
|
||||||
@@ -115,6 +134,11 @@ class USBUartComponent : public usb_host::USBClient {
|
|||||||
void start_input(USBUartChannel *channel);
|
void start_input(USBUartChannel *channel);
|
||||||
void start_output(USBUartChannel *channel);
|
void start_output(USBUartChannel *channel);
|
||||||
|
|
||||||
|
// Lock-free data transfer from USB task to main loop
|
||||||
|
static constexpr int USB_DATA_QUEUE_SIZE = 32;
|
||||||
|
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
|
||||||
|
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<USBUartChannel *> channels_{};
|
std::vector<USBUartChannel *> channels_{};
|
||||||
};
|
};
|
||||||
|
@@ -40,5 +40,7 @@ async def to_code(config):
|
|||||||
cg.add_library("Update", None)
|
cg.add_library("Update", None)
|
||||||
if CORE.is_esp8266:
|
if CORE.is_esp8266:
|
||||||
cg.add_library("ESP8266WiFi", None)
|
cg.add_library("ESP8266WiFi", None)
|
||||||
|
if CORE.is_libretiny:
|
||||||
|
CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"])
|
||||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")
|
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")
|
||||||
|
@@ -125,8 +125,8 @@ EAP_AUTH_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_USERNAME): cv.string_strict,
|
cv.Optional(CONF_USERNAME): cv.string_strict,
|
||||||
cv.Optional(CONF_PASSWORD): cv.string_strict,
|
cv.Optional(CONF_PASSWORD): cv.string_strict,
|
||||||
cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate,
|
cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate,
|
||||||
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32_idf="mschapv2"): cv.All(
|
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32="mschapv2"): cv.All(
|
||||||
cv.enum(TTLS_PHASE_2), cv.only_with_esp_idf
|
cv.enum(TTLS_PHASE_2), cv.only_on_esp32
|
||||||
),
|
),
|
||||||
cv.Inclusive(
|
cv.Inclusive(
|
||||||
CONF_CERTIFICATE, "certificate_and_key"
|
CONF_CERTIFICATE, "certificate_and_key"
|
||||||
@@ -280,11 +280,11 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
||||||
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
||||||
),
|
),
|
||||||
cv.SplitDefault(CONF_ENABLE_BTM, esp32_idf=False): cv.All(
|
cv.SplitDefault(CONF_ENABLE_BTM, esp32=False): cv.All(
|
||||||
cv.boolean, cv.only_with_esp_idf
|
cv.boolean, cv.only_on_esp32
|
||||||
),
|
),
|
||||||
cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All(
|
cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All(
|
||||||
cv.boolean, cv.only_with_esp_idf
|
cv.boolean, cv.only_on_esp32
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
|
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
|
||||||
cv.Optional("enable_mdns"): cv.invalid(
|
cv.Optional("enable_mdns"): cv.invalid(
|
||||||
@@ -416,10 +416,10 @@ async def to_code(config):
|
|||||||
|
|
||||||
if CORE.is_esp8266:
|
if CORE.is_esp8266:
|
||||||
cg.add_library("ESP8266WiFi", None)
|
cg.add_library("ESP8266WiFi", None)
|
||||||
elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040:
|
elif CORE.is_rp2040:
|
||||||
cg.add_library("WiFi", None)
|
cg.add_library("WiFi", None)
|
||||||
|
|
||||||
if CORE.is_esp32 and CORE.using_esp_idf:
|
if CORE.is_esp32:
|
||||||
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:
|
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:
|
||||||
add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True)
|
add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True)
|
||||||
cg.add_define("USE_WIFI_11KV_SUPPORT")
|
cg.add_define("USE_WIFI_11KV_SUPPORT")
|
||||||
@@ -506,8 +506,10 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||||
{
|
{
|
||||||
"wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
|
"wifi_component_esp_idf.cpp": {
|
||||||
"wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
|
PlatformFramework.ESP32_IDF,
|
||||||
|
PlatformFramework.ESP32_ARDUINO,
|
||||||
|
},
|
||||||
"wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
|
"wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
|
||||||
"wifi_component_libretiny.cpp": {
|
"wifi_component_libretiny.cpp": {
|
||||||
PlatformFramework.BK72XX_ARDUINO,
|
PlatformFramework.BK72XX_ARDUINO,
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP32
|
||||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||||
#include <esp_eap_client.h>
|
#include <esp_eap_client.h>
|
||||||
#else
|
#else
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_ESP_IDF)
|
#if defined(USE_ESP32)
|
||||||
#include <esp_wifi.h>
|
#include <esp_wifi.h>
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
@@ -344,7 +344,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
|||||||
ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
|
ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
|
||||||
ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
|
ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
|
||||||
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
|
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
|
||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP32
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
|
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
|
||||||
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"},
|
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"},
|
||||||
|
@@ -20,7 +20,7 @@
|
|||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP_IDF) && defined(USE_WIFI_WPA2_EAP)
|
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP)
|
||||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||||
#include <esp_eap_client.h>
|
#include <esp_eap_client.h>
|
||||||
#else
|
#else
|
||||||
@@ -113,7 +113,7 @@ struct EAPAuth {
|
|||||||
const char *client_cert;
|
const char *client_cert;
|
||||||
const char *client_key;
|
const char *client_key;
|
||||||
// used for EAP-TTLS
|
// used for EAP-TTLS
|
||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP32
|
||||||
esp_eap_ttls_phase2_types ttls_phase_2;
|
esp_eap_ttls_phase2_types ttls_phase_2;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
@@ -199,7 +199,7 @@ enum WiFiPowerSaveMode : uint8_t {
|
|||||||
WIFI_POWER_SAVE_HIGH,
|
WIFI_POWER_SAVE_HIGH,
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP32
|
||||||
struct IDFWiFiEvent;
|
struct IDFWiFiEvent;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -368,7 +368,7 @@ class WiFiComponent : public Component {
|
|||||||
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info);
|
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info);
|
||||||
void wifi_scan_done_callback_();
|
void wifi_scan_done_callback_();
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP32
|
||||||
void wifi_process_event_(IDFWiFiEvent *data);
|
void wifi_process_event_(IDFWiFiEvent *data);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@@ -1,860 +0,0 @@
|
|||||||
#include "wifi_component.h"
|
|
||||||
|
|
||||||
#ifdef USE_WIFI
|
|
||||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
|
||||||
|
|
||||||
#include <esp_netif.h>
|
|
||||||
#include <esp_wifi.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <utility>
|
|
||||||
#ifdef USE_WIFI_WPA2_EAP
|
|
||||||
#include <esp_eap_client.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
|
||||||
#include "dhcpserver/dhcpserver.h"
|
|
||||||
#endif // USE_WIFI_AP
|
|
||||||
|
|
||||||
#include "lwip/apps/sntp.h"
|
|
||||||
#include "lwip/dns.h"
|
|
||||||
#include "lwip/err.h"
|
|
||||||
|
|
||||||
#include "esphome/core/application.h"
|
|
||||||
#include "esphome/core/hal.h"
|
|
||||||
#include "esphome/core/helpers.h"
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#include "esphome/core/util.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace wifi {
|
|
||||||
|
|
||||||
static const char *const TAG = "wifi_esp32";
|
|
||||||
|
|
||||||
static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
#ifdef USE_WIFI_AP
|
|
||||||
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
#endif // USE_WIFI_AP
|
|
||||||
|
|
||||||
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
|
|
||||||
void WiFiComponent::wifi_pre_setup_() {
|
|
||||||
uint8_t mac[6];
|
|
||||||
if (has_custom_mac_address()) {
|
|
||||||
get_mac_address_raw(mac);
|
|
||||||
set_mac_address(mac);
|
|
||||||
}
|
|
||||||
auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
|
|
||||||
WiFi.onEvent(f);
|
|
||||||
WiFi.persistent(false);
|
|
||||||
// Make sure WiFi is in clean state before anything starts
|
|
||||||
this->wifi_mode_(false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
|
|
||||||
wifi_mode_t current_mode = WiFiClass::getMode();
|
|
||||||
bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA;
|
|
||||||
bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA;
|
|
||||||
|
|
||||||
bool set_sta = sta.value_or(current_sta);
|
|
||||||
bool set_ap = ap.value_or(current_ap);
|
|
||||||
|
|
||||||
wifi_mode_t set_mode;
|
|
||||||
if (set_sta && set_ap) {
|
|
||||||
set_mode = WIFI_MODE_APSTA;
|
|
||||||
} else if (set_sta && !set_ap) {
|
|
||||||
set_mode = WIFI_MODE_STA;
|
|
||||||
} else if (!set_sta && set_ap) {
|
|
||||||
set_mode = WIFI_MODE_AP;
|
|
||||||
} else {
|
|
||||||
set_mode = WIFI_MODE_NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_mode == set_mode)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (set_sta && !current_sta) {
|
|
||||||
ESP_LOGV(TAG, "Enabling STA");
|
|
||||||
} else if (!set_sta && current_sta) {
|
|
||||||
ESP_LOGV(TAG, "Disabling STA");
|
|
||||||
}
|
|
||||||
if (set_ap && !current_ap) {
|
|
||||||
ESP_LOGV(TAG, "Enabling AP");
|
|
||||||
} else if (!set_ap && current_ap) {
|
|
||||||
ESP_LOGV(TAG, "Disabling AP");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ret = WiFiClass::mode(set_mode);
|
|
||||||
|
|
||||||
if (!ret) {
|
|
||||||
ESP_LOGW(TAG, "Setting mode failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and
|
|
||||||
// esp_netif_create_default_wifi_ap(), which creates the interfaces.
|
|
||||||
// s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event
|
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
|
||||||
if (set_ap)
|
|
||||||
s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_sta_pre_setup_() {
|
|
||||||
if (!this->wifi_mode_(true, {}))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
WiFi.setAutoReconnect(false);
|
|
||||||
delay(10);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_apply_output_power_(float output_power) {
|
|
||||||
int8_t val = static_cast<int8_t>(output_power * 4);
|
|
||||||
return esp_wifi_set_max_tx_power(val) == ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_apply_power_save_() {
|
|
||||||
wifi_ps_type_t power_save;
|
|
||||||
switch (this->power_save_) {
|
|
||||||
case WIFI_POWER_SAVE_LIGHT:
|
|
||||||
power_save = WIFI_PS_MIN_MODEM;
|
|
||||||
break;
|
|
||||||
case WIFI_POWER_SAVE_HIGH:
|
|
||||||
power_save = WIFI_PS_MAX_MODEM;
|
|
||||||
break;
|
|
||||||
case WIFI_POWER_SAVE_NONE:
|
|
||||||
default:
|
|
||||||
power_save = WIFI_PS_NONE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return esp_wifi_set_ps(power_save) == ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|
||||||
// enable STA
|
|
||||||
if (!this->wifi_mode_(true, {}))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
|
|
||||||
wifi_config_t conf;
|
|
||||||
memset(&conf, 0, sizeof(conf));
|
|
||||||
if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
|
|
||||||
ESP_LOGE(TAG, "SSID too long");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (ap.get_password().size() > sizeof(conf.sta.password)) {
|
|
||||||
ESP_LOGE(TAG, "Password too long");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
|
||||||
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
|
|
||||||
|
|
||||||
// The weakest authmode to accept in the fast scan mode
|
|
||||||
if (ap.get_password().empty()) {
|
|
||||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
|
||||||
} else {
|
|
||||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef USE_WIFI_WPA2_EAP
|
|
||||||
if (ap.get_eap().has_value()) {
|
|
||||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (ap.get_bssid().has_value()) {
|
|
||||||
conf.sta.bssid_set = true;
|
|
||||||
memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6);
|
|
||||||
} else {
|
|
||||||
conf.sta.bssid_set = false;
|
|
||||||
}
|
|
||||||
if (ap.get_channel().has_value()) {
|
|
||||||
conf.sta.channel = *ap.get_channel();
|
|
||||||
conf.sta.scan_method = WIFI_FAST_SCAN;
|
|
||||||
} else {
|
|
||||||
conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
|
||||||
}
|
|
||||||
// Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set.
|
|
||||||
// Units: AP beacon intervals. Defaults to 3 if set to 0.
|
|
||||||
conf.sta.listen_interval = 0;
|
|
||||||
|
|
||||||
// Protected Management Frame
|
|
||||||
// Device will prefer to connect in PMF mode if other device also advertises PMF capability.
|
|
||||||
conf.sta.pmf_cfg.capable = true;
|
|
||||||
conf.sta.pmf_cfg.required = false;
|
|
||||||
|
|
||||||
// note, we do our own filtering
|
|
||||||
// The minimum rssi to accept in the fast scan mode
|
|
||||||
conf.sta.threshold.rssi = -127;
|
|
||||||
|
|
||||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
|
||||||
|
|
||||||
wifi_config_t current_conf;
|
|
||||||
esp_err_t err;
|
|
||||||
err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf);
|
|
||||||
if (err != ERR_OK) {
|
|
||||||
ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err));
|
|
||||||
// can continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT
|
|
||||||
err = esp_wifi_disconnect();
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = esp_wifi_set_config(WIFI_IF_STA, &conf);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup enterprise authentication if required
|
|
||||||
#ifdef USE_WIFI_WPA2_EAP
|
|
||||||
if (ap.get_eap().has_value()) {
|
|
||||||
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
|
|
||||||
EAPAuth eap = ap.get_eap().value();
|
|
||||||
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err);
|
|
||||||
}
|
|
||||||
int ca_cert_len = strlen(eap.ca_cert);
|
|
||||||
int client_cert_len = strlen(eap.client_cert);
|
|
||||||
int client_key_len = strlen(eap.client_key);
|
|
||||||
if (ca_cert_len) {
|
|
||||||
err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// workout what type of EAP this is
|
|
||||||
// validation is not required as the config tool has already validated it
|
|
||||||
if (client_cert_len && client_key_len) {
|
|
||||||
// if we have certs, this must be EAP-TLS
|
|
||||||
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
|
|
||||||
(uint8_t *) eap.client_key, client_key_len + 1,
|
|
||||||
(uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// in the absence of certs, assume this is username/password based
|
|
||||||
err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err);
|
|
||||||
}
|
|
||||||
err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = esp_wifi_sta_enterprise_enable();
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif // USE_WIFI_WPA2_EAP
|
|
||||||
|
|
||||||
this->wifi_apply_hostname_();
|
|
||||||
|
|
||||||
s_sta_connecting = true;
|
|
||||||
|
|
||||||
err = esp_wifi_connect();
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
|
||||||
// enable STA
|
|
||||||
if (!this->wifi_mode_(true, {}))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if the STA interface is initialized before using it
|
|
||||||
if (s_sta_netif == nullptr) {
|
|
||||||
ESP_LOGW(TAG, "STA interface not initialized");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_netif_dhcp_status_t dhcp_status;
|
|
||||||
esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manual_ip.has_value()) {
|
|
||||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
|
||||||
// https://github.com/esphome/issues/issues/6591
|
|
||||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
|
||||||
{
|
|
||||||
LwIPLock lock;
|
|
||||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
|
||||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
|
||||||
// https://github.com/esphome/issues/issues/2299
|
|
||||||
sntp_servermode_dhcp(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No manual IP is set; use DHCP client
|
|
||||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
|
||||||
err = esp_netif_dhcpc_start(s_sta_netif);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "Starting DHCP client failed: %d", err);
|
|
||||||
}
|
|
||||||
return err == ESP_OK;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw
|
|
||||||
info.ip = manual_ip->static_ip;
|
|
||||||
info.gw = manual_ip->gateway;
|
|
||||||
info.netmask = manual_ip->subnet;
|
|
||||||
err = esp_netif_dhcpc_stop(s_sta_netif);
|
|
||||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
|
||||||
ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
err = esp_netif_set_ip_info(s_sta_netif, &info);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_netif_dns_info_t dns;
|
|
||||||
if (manual_ip->dns1.is_set()) {
|
|
||||||
dns.ip = manual_ip->dns1;
|
|
||||||
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns);
|
|
||||||
}
|
|
||||||
if (manual_ip->dns2.is_set()) {
|
|
||||||
dns.ip = manual_ip->dns2;
|
|
||||||
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
|
|
||||||
if (!this->has_sta())
|
|
||||||
return {};
|
|
||||||
network::IPAddresses addresses;
|
|
||||||
esp_netif_ip_info_t ip;
|
|
||||||
esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
|
|
||||||
// TODO: do something smarter
|
|
||||||
// return false;
|
|
||||||
} else {
|
|
||||||
addresses[0] = network::IPAddress(&ip.ip);
|
|
||||||
}
|
|
||||||
#if USE_NETWORK_IPV6
|
|
||||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
|
||||||
uint8_t count = 0;
|
|
||||||
count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s);
|
|
||||||
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
|
|
||||||
}
|
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
|
||||||
return addresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_apply_hostname_() {
|
|
||||||
// setting is done in SYSTEM_EVENT_STA_START callback
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const char *get_auth_mode_str(uint8_t mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case WIFI_AUTH_OPEN:
|
|
||||||
return "OPEN";
|
|
||||||
case WIFI_AUTH_WEP:
|
|
||||||
return "WEP";
|
|
||||||
case WIFI_AUTH_WPA_PSK:
|
|
||||||
return "WPA PSK";
|
|
||||||
case WIFI_AUTH_WPA2_PSK:
|
|
||||||
return "WPA2 PSK";
|
|
||||||
case WIFI_AUTH_WPA_WPA2_PSK:
|
|
||||||
return "WPA/WPA2 PSK";
|
|
||||||
case WIFI_AUTH_WPA2_ENTERPRISE:
|
|
||||||
return "WPA2 Enterprise";
|
|
||||||
case WIFI_AUTH_WPA3_PSK:
|
|
||||||
return "WPA3 PSK";
|
|
||||||
case WIFI_AUTH_WPA2_WPA3_PSK:
|
|
||||||
return "WPA2/WPA3 PSK";
|
|
||||||
case WIFI_AUTH_WAPI_PSK:
|
|
||||||
return "WAPI PSK";
|
|
||||||
default:
|
|
||||||
return "UNKNOWN";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using esphome_ip4_addr_t = esp_ip4_addr_t;
|
|
||||||
|
|
||||||
std::string format_ip4_addr(const esphome_ip4_addr_t &ip) {
|
|
||||||
char buf[20];
|
|
||||||
sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
|
|
||||||
uint8_t(ip.addr >> 24));
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
const char *get_op_mode_str(uint8_t mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case WIFI_OFF:
|
|
||||||
return "OFF";
|
|
||||||
case WIFI_STA:
|
|
||||||
return "STA";
|
|
||||||
case WIFI_AP:
|
|
||||||
return "AP";
|
|
||||||
case WIFI_AP_STA:
|
|
||||||
return "AP+STA";
|
|
||||||
default:
|
|
||||||
return "UNKNOWN";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const char *get_disconnect_reason_str(uint8_t reason) {
|
|
||||||
switch (reason) {
|
|
||||||
case WIFI_REASON_AUTH_EXPIRE:
|
|
||||||
return "Auth Expired";
|
|
||||||
case WIFI_REASON_AUTH_LEAVE:
|
|
||||||
return "Auth Leave";
|
|
||||||
case WIFI_REASON_ASSOC_EXPIRE:
|
|
||||||
return "Association Expired";
|
|
||||||
case WIFI_REASON_ASSOC_TOOMANY:
|
|
||||||
return "Too Many Associations";
|
|
||||||
case WIFI_REASON_NOT_AUTHED:
|
|
||||||
return "Not Authenticated";
|
|
||||||
case WIFI_REASON_NOT_ASSOCED:
|
|
||||||
return "Not Associated";
|
|
||||||
case WIFI_REASON_ASSOC_LEAVE:
|
|
||||||
return "Association Leave";
|
|
||||||
case WIFI_REASON_ASSOC_NOT_AUTHED:
|
|
||||||
return "Association not Authenticated";
|
|
||||||
case WIFI_REASON_DISASSOC_PWRCAP_BAD:
|
|
||||||
return "Disassociate Power Cap Bad";
|
|
||||||
case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
|
|
||||||
return "Disassociate Supported Channel Bad";
|
|
||||||
case WIFI_REASON_IE_INVALID:
|
|
||||||
return "IE Invalid";
|
|
||||||
case WIFI_REASON_MIC_FAILURE:
|
|
||||||
return "Mic Failure";
|
|
||||||
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
|
|
||||||
return "4-Way Handshake Timeout";
|
|
||||||
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
|
|
||||||
return "Group Key Update Timeout";
|
|
||||||
case WIFI_REASON_IE_IN_4WAY_DIFFERS:
|
|
||||||
return "IE In 4-Way Handshake Differs";
|
|
||||||
case WIFI_REASON_GROUP_CIPHER_INVALID:
|
|
||||||
return "Group Cipher Invalid";
|
|
||||||
case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
|
|
||||||
return "Pairwise Cipher Invalid";
|
|
||||||
case WIFI_REASON_AKMP_INVALID:
|
|
||||||
return "AKMP Invalid";
|
|
||||||
case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
|
|
||||||
return "Unsupported RSN IE version";
|
|
||||||
case WIFI_REASON_INVALID_RSN_IE_CAP:
|
|
||||||
return "Invalid RSN IE Cap";
|
|
||||||
case WIFI_REASON_802_1X_AUTH_FAILED:
|
|
||||||
return "802.1x Authentication Failed";
|
|
||||||
case WIFI_REASON_CIPHER_SUITE_REJECTED:
|
|
||||||
return "Cipher Suite Rejected";
|
|
||||||
case WIFI_REASON_BEACON_TIMEOUT:
|
|
||||||
return "Beacon Timeout";
|
|
||||||
case WIFI_REASON_NO_AP_FOUND:
|
|
||||||
return "AP Not Found";
|
|
||||||
case WIFI_REASON_AUTH_FAIL:
|
|
||||||
return "Authentication Failed";
|
|
||||||
case WIFI_REASON_ASSOC_FAIL:
|
|
||||||
return "Association Failed";
|
|
||||||
case WIFI_REASON_HANDSHAKE_TIMEOUT:
|
|
||||||
return "Handshake Failed";
|
|
||||||
case WIFI_REASON_CONNECTION_FAIL:
|
|
||||||
return "Connection Failed";
|
|
||||||
case WIFI_REASON_AP_TSF_RESET:
|
|
||||||
return "AP TSF reset";
|
|
||||||
case WIFI_REASON_ROAMING:
|
|
||||||
return "Station Roaming";
|
|
||||||
case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG:
|
|
||||||
return "Association comeback time too long";
|
|
||||||
case WIFI_REASON_SA_QUERY_TIMEOUT:
|
|
||||||
return "SA query timeout";
|
|
||||||
case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
|
|
||||||
return "No AP found with compatible security";
|
|
||||||
case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
|
|
||||||
return "No AP found in auth mode threshold";
|
|
||||||
case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD:
|
|
||||||
return "No AP found in RSSI threshold";
|
|
||||||
case WIFI_REASON_UNSPECIFIED:
|
|
||||||
default:
|
|
||||||
return "Unspecified";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WiFiComponent::wifi_loop_() {}
|
|
||||||
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED
|
|
||||||
#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6
|
|
||||||
using esphome_wifi_event_id_t = arduino_event_id_t;
|
|
||||||
using esphome_wifi_event_info_t = arduino_event_info_t;
|
|
||||||
|
|
||||||
void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
|
|
||||||
switch (event) {
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_READY: {
|
|
||||||
ESP_LOGV(TAG, "Ready");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
|
|
||||||
auto it = info.wifi_scan_done;
|
|
||||||
ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
|
|
||||||
|
|
||||||
this->wifi_scan_done_callback_();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_START: {
|
|
||||||
ESP_LOGV(TAG, "STA start");
|
|
||||||
// apply hostname
|
|
||||||
s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
|
||||||
esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
|
|
||||||
if (err != ERR_OK) {
|
|
||||||
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
|
|
||||||
ESP_LOGV(TAG, "STA stop");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
|
|
||||||
auto it = info.wifi_sta_connected;
|
|
||||||
char buf[33];
|
|
||||||
memcpy(buf, it.ssid, it.ssid_len);
|
|
||||||
buf[it.ssid_len] = '\0';
|
|
||||||
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
|
|
||||||
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
|
|
||||||
#if USE_NETWORK_IPV6
|
|
||||||
this->set_timeout(100, [] { WiFi.enableIPv6(); });
|
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
|
|
||||||
auto it = info.wifi_sta_disconnected;
|
|
||||||
char buf[33];
|
|
||||||
memcpy(buf, it.ssid, it.ssid_len);
|
|
||||||
buf[it.ssid_len] = '\0';
|
|
||||||
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
|
|
||||||
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
|
||||||
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t reason = it.reason;
|
|
||||||
if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT ||
|
|
||||||
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
|
|
||||||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
|
|
||||||
err_t err = esp_wifi_disconnect();
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err));
|
|
||||||
}
|
|
||||||
this->error_from_callback_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_sta_connecting = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
|
|
||||||
auto it = info.wifi_sta_authmode_change;
|
|
||||||
ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
|
|
||||||
// Mitigate CVE-2020-12638
|
|
||||||
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
|
|
||||||
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
|
|
||||||
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
|
|
||||||
// we can't call retry_connect() from this context, so disconnect immediately
|
|
||||||
// and notify main thread with error_from_callback_
|
|
||||||
err_t err = esp_wifi_disconnect();
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err));
|
|
||||||
}
|
|
||||||
this->error_from_callback_ = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
|
|
||||||
auto it = info.got_ip.ip_info;
|
|
||||||
ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str());
|
|
||||||
this->got_ipv4_address_ = true;
|
|
||||||
#if USE_NETWORK_IPV6
|
|
||||||
s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT;
|
|
||||||
#else
|
|
||||||
s_sta_connecting = false;
|
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#if USE_NETWORK_IPV6
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
|
|
||||||
auto it = info.got_ip6.ip6_info;
|
|
||||||
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip));
|
|
||||||
this->num_ipv6_addresses_++;
|
|
||||||
s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
|
|
||||||
ESP_LOGV(TAG, "Lost IP");
|
|
||||||
this->got_ipv4_address_ = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_AP_START: {
|
|
||||||
ESP_LOGV(TAG, "AP start");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
|
|
||||||
ESP_LOGV(TAG, "AP stop");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
|
|
||||||
auto it = info.wifi_sta_connected;
|
|
||||||
auto &mac = it.bssid;
|
|
||||||
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
|
|
||||||
auto it = info.wifi_sta_disconnected;
|
|
||||||
auto &mac = it.bssid;
|
|
||||||
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
|
|
||||||
ESP_LOGV(TAG, "AP client assigned IP");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
|
|
||||||
auto it = info.wifi_ap_probereqrecved;
|
|
||||||
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
|
||||||
const auto status = WiFi.status();
|
|
||||||
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
|
|
||||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
|
||||||
}
|
|
||||||
if (status == WL_NO_SSID_AVAIL) {
|
|
||||||
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
|
|
||||||
}
|
|
||||||
if (s_sta_connecting) {
|
|
||||||
return WiFiSTAConnectStatus::CONNECTING;
|
|
||||||
}
|
|
||||||
if (status == WL_CONNECTED) {
|
|
||||||
return WiFiSTAConnectStatus::CONNECTED;
|
|
||||||
}
|
|
||||||
return WiFiSTAConnectStatus::IDLE;
|
|
||||||
}
|
|
||||||
bool WiFiComponent::wifi_scan_start_(bool passive) {
|
|
||||||
// enable STA
|
|
||||||
if (!this->wifi_mode_(true, {}))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// need to use WiFi because of WiFiScanClass allocations :(
|
|
||||||
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
|
|
||||||
if (err != WIFI_SCAN_RUNNING) {
|
|
||||||
ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
void WiFiComponent::wifi_scan_done_callback_() {
|
|
||||||
this->scan_result_.clear();
|
|
||||||
|
|
||||||
int16_t num = WiFi.scanComplete();
|
|
||||||
if (num < 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this->scan_result_.reserve(static_cast<unsigned int>(num));
|
|
||||||
for (int i = 0; i < num; i++) {
|
|
||||||
String ssid = WiFi.SSID(i);
|
|
||||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
|
||||||
int32_t rssi = WiFi.RSSI(i);
|
|
||||||
uint8_t *bssid = WiFi.BSSID(i);
|
|
||||||
int32_t channel = WiFi.channel(i);
|
|
||||||
|
|
||||||
WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()),
|
|
||||||
channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0);
|
|
||||||
this->scan_result_.push_back(scan);
|
|
||||||
}
|
|
||||||
WiFi.scanDelete();
|
|
||||||
this->scan_done_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
|
||||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
|
||||||
esp_err_t err;
|
|
||||||
|
|
||||||
// enable AP
|
|
||||||
if (!this->wifi_mode_({}, true))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if the AP interface is initialized before using it
|
|
||||||
if (s_ap_netif == nullptr) {
|
|
||||||
ESP_LOGW(TAG, "AP interface not initialized");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_netif_ip_info_t info;
|
|
||||||
if (manual_ip.has_value()) {
|
|
||||||
info.ip = manual_ip->static_ip;
|
|
||||||
info.gw = manual_ip->gateway;
|
|
||||||
info.netmask = manual_ip->subnet;
|
|
||||||
} else {
|
|
||||||
info.ip = network::IPAddress(192, 168, 4, 1);
|
|
||||||
info.gw = network::IPAddress(192, 168, 4, 1);
|
|
||||||
info.netmask = network::IPAddress(255, 255, 255, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
err = esp_netif_dhcps_stop(s_ap_netif);
|
|
||||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
|
||||||
ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = esp_netif_set_ip_info(s_ap_netif, &info);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
dhcps_lease_t lease;
|
|
||||||
lease.enable = true;
|
|
||||||
network::IPAddress start_address = network::IPAddress(&info.ip);
|
|
||||||
start_address += 99;
|
|
||||||
lease.start_ip = start_address;
|
|
||||||
ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str());
|
|
||||||
start_address += 10;
|
|
||||||
lease.end_ip = start_address;
|
|
||||||
ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str());
|
|
||||||
err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
|
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = esp_netif_dhcps_start(s_ap_netif);
|
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|
||||||
// enable AP
|
|
||||||
if (!this->wifi_mode_({}, true))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
wifi_config_t conf;
|
|
||||||
memset(&conf, 0, sizeof(conf));
|
|
||||||
if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
|
|
||||||
ESP_LOGE(TAG, "AP SSID too long");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
|
||||||
conf.ap.channel = ap.get_channel().value_or(1);
|
|
||||||
conf.ap.ssid_hidden = ap.get_ssid().size();
|
|
||||||
conf.ap.max_connection = 5;
|
|
||||||
conf.ap.beacon_interval = 100;
|
|
||||||
|
|
||||||
if (ap.get_password().empty()) {
|
|
||||||
conf.ap.authmode = WIFI_AUTH_OPEN;
|
|
||||||
*conf.ap.password = 0;
|
|
||||||
} else {
|
|
||||||
conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
|
|
||||||
if (ap.get_password().size() > sizeof(conf.ap.password)) {
|
|
||||||
ESP_LOGE(TAG, "AP password too long");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// pairwise cipher of SoftAP, group cipher will be derived using this.
|
|
||||||
conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP;
|
|
||||||
|
|
||||||
esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield();
|
|
||||||
|
|
||||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
|
||||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
|
|
||||||
esp_netif_ip_info_t ip;
|
|
||||||
esp_netif_get_ip_info(s_ap_netif, &ip);
|
|
||||||
return network::IPAddress(&ip.ip);
|
|
||||||
}
|
|
||||||
#endif // USE_WIFI_AP
|
|
||||||
|
|
||||||
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
|
|
||||||
|
|
||||||
bssid_t WiFiComponent::wifi_bssid() {
|
|
||||||
bssid_t bssid{};
|
|
||||||
uint8_t *raw_bssid = WiFi.BSSID();
|
|
||||||
if (raw_bssid != nullptr) {
|
|
||||||
for (size_t i = 0; i < bssid.size(); i++)
|
|
||||||
bssid[i] = raw_bssid[i];
|
|
||||||
}
|
|
||||||
return bssid;
|
|
||||||
}
|
|
||||||
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
|
|
||||||
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
|
|
||||||
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
|
|
||||||
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return network::IPAddress(WiFi.subnetMask()); }
|
|
||||||
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(WiFi.gatewayIP()); }
|
|
||||||
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(WiFi.dnsIP(num)); }
|
|
||||||
|
|
||||||
} // namespace wifi
|
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
|
||||||
#endif
|
|
@@ -1,7 +1,7 @@
|
|||||||
#include "wifi_component.h"
|
#include "wifi_component.h"
|
||||||
|
|
||||||
#ifdef USE_WIFI
|
#ifdef USE_WIFI
|
||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
#include <esp_event.h>
|
#include <esp_event.h>
|
||||||
#include <esp_netif.h>
|
#include <esp_netif.h>
|
||||||
@@ -1050,5 +1050,5 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
|
|||||||
} // namespace wifi
|
} // namespace wifi
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ESP_IDF
|
#endif // USE_ESP32
|
||||||
#endif
|
#endif
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
#include "zwave_proxy.h"
|
#include "zwave_proxy.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 "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
@@ -12,6 +13,7 @@ static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
|
|||||||
// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
|
// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
|
||||||
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
|
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
|
||||||
static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
|
static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
|
||||||
|
static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
|
||||||
|
|
||||||
static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
|
static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
|
||||||
// Calculate Z-Wave frame checksum
|
// Calculate Z-Wave frame checksum
|
||||||
@@ -26,7 +28,44 @@ static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
|
|||||||
|
|
||||||
ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; }
|
ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; }
|
||||||
|
|
||||||
void ZWaveProxy::setup() { this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); }
|
void ZWaveProxy::setup() {
|
||||||
|
this->setup_time_ = App.get_loop_component_start_time();
|
||||||
|
this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
float ZWaveProxy::get_setup_priority() const {
|
||||||
|
// Set up before API so home ID is ready when API starts
|
||||||
|
return setup_priority::BEFORE_CONNECTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ZWaveProxy::can_proceed() {
|
||||||
|
// If we already have the home ID, we can proceed
|
||||||
|
if (this->home_id_ready_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle any pending responses
|
||||||
|
if (this->response_handler_()) {
|
||||||
|
ESP_LOGV(TAG, "Handled response during setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process UART data to check for home ID
|
||||||
|
this->process_uart_();
|
||||||
|
|
||||||
|
// Check if we got the home ID after processing
|
||||||
|
if (this->home_id_ready_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait up to HOME_ID_TIMEOUT_MS for home ID response
|
||||||
|
const uint32_t now = App.get_loop_component_start_time();
|
||||||
|
if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
|
||||||
|
ESP_LOGW(TAG, "Timeout reading Home ID during setup");
|
||||||
|
return true; // Proceed anyway after timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Keep waiting
|
||||||
|
}
|
||||||
|
|
||||||
void ZWaveProxy::loop() {
|
void ZWaveProxy::loop() {
|
||||||
if (this->response_handler_()) {
|
if (this->response_handler_()) {
|
||||||
@@ -37,6 +76,11 @@ void ZWaveProxy::loop() {
|
|||||||
this->api_connection_ = nullptr; // Unsubscribe if disconnected
|
this->api_connection_ = nullptr; // Unsubscribe if disconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->process_uart_();
|
||||||
|
this->status_clear_warning();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZWaveProxy::process_uart_() {
|
||||||
while (this->available()) {
|
while (this->available()) {
|
||||||
uint8_t byte;
|
uint8_t byte;
|
||||||
if (!this->read_byte(&byte)) {
|
if (!this->read_byte(&byte)) {
|
||||||
@@ -56,24 +100,24 @@ void ZWaveProxy::loop() {
|
|||||||
// Extract the 4-byte Home ID starting at offset 4
|
// Extract the 4-byte Home ID starting at offset 4
|
||||||
// The frame parser has already validated the checksum and ensured all bytes are present
|
// The frame parser has already validated the checksum and ensured all bytes are present
|
||||||
std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size());
|
std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size());
|
||||||
|
this->home_id_ready_ = true;
|
||||||
ESP_LOGI(TAG, "Home ID: %s",
|
ESP_LOGI(TAG, "Home ID: %s",
|
||||||
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
|
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
|
||||||
}
|
}
|
||||||
ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
|
ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
|
||||||
if (this->api_connection_ != nullptr) {
|
if (this->api_connection_ != nullptr) {
|
||||||
// minimize copying to reduce CPU overhead
|
// Zero-copy: point directly to our buffer
|
||||||
|
this->outgoing_proto_msg_.data = this->buffer_.data();
|
||||||
if (this->in_bootloader_) {
|
if (this->in_bootloader_) {
|
||||||
this->outgoing_proto_msg_.data_len = this->buffer_index_;
|
this->outgoing_proto_msg_.data_len = this->buffer_index_;
|
||||||
} else {
|
} else {
|
||||||
// If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
|
// If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
|
||||||
this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
|
this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
|
||||||
}
|
}
|
||||||
std::memcpy(this->outgoing_proto_msg_.data, this->buffer_.data(), this->outgoing_proto_msg_.data_len);
|
|
||||||
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->status_clear_warning();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); }
|
void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); }
|
||||||
@@ -228,7 +272,9 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
|
|||||||
}
|
}
|
||||||
// Forward response (ACK/NAK/CAN) back to client for processing
|
// Forward response (ACK/NAK/CAN) back to client for processing
|
||||||
if (this->api_connection_ != nullptr) {
|
if (this->api_connection_ != nullptr) {
|
||||||
this->outgoing_proto_msg_.data[0] = byte;
|
// Store single byte in buffer and point to it
|
||||||
|
this->buffer_[0] = byte;
|
||||||
|
this->outgoing_proto_msg_.data = this->buffer_.data();
|
||||||
this->outgoing_proto_msg_.data_len = 1;
|
this->outgoing_proto_msg_.data_len = 1;
|
||||||
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,8 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace zwave_proxy {
|
namespace zwave_proxy {
|
||||||
|
|
||||||
|
static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size
|
||||||
|
|
||||||
enum ZWaveResponseTypes : uint8_t {
|
enum ZWaveResponseTypes : uint8_t {
|
||||||
ZWAVE_FRAME_TYPE_ACK = 0x06,
|
ZWAVE_FRAME_TYPE_ACK = 0x06,
|
||||||
ZWAVE_FRAME_TYPE_CAN = 0x18,
|
ZWAVE_FRAME_TYPE_CAN = 0x18,
|
||||||
@@ -44,6 +46,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
|||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
bool can_proceed() override;
|
||||||
|
|
||||||
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type);
|
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type);
|
||||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||||
@@ -60,19 +64,24 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
|||||||
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
|
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
|
||||||
void parse_start_(uint8_t byte);
|
void parse_start_(uint8_t byte);
|
||||||
bool response_handler_();
|
bool response_handler_();
|
||||||
|
void process_uart_(); // Process all available UART data
|
||||||
|
|
||||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
// Pre-allocated message - always ready to send
|
||||||
|
api::ZWaveProxyFrame outgoing_proto_msg_;
|
||||||
|
std::array<uint8_t, MAX_ZWAVE_FRAME_SIZE> buffer_; // Fixed buffer for incoming data
|
||||||
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
|
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
|
||||||
std::array<uint8_t, sizeof(api::ZWaveProxyFrame::data)> buffer_; // Fixed buffer for incoming data
|
|
||||||
|
// Pointers and 32-bit values (aligned together)
|
||||||
|
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||||
|
uint32_t setup_time_{0}; // Time when setup() was called
|
||||||
|
|
||||||
|
// 8-bit values (grouped together to minimize padding)
|
||||||
uint8_t buffer_index_{0}; // Index for populating the data buffer
|
uint8_t buffer_index_{0}; // Index for populating the data buffer
|
||||||
uint8_t end_frame_after_{0}; // Payload reception ends after this index
|
uint8_t end_frame_after_{0}; // Payload reception ends after this index
|
||||||
uint8_t last_response_{0}; // Last response type sent
|
uint8_t last_response_{0}; // Last response type sent
|
||||||
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
||||||
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
|
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
|
||||||
|
bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module
|
||||||
// Pre-allocated message - always ready to send
|
|
||||||
api::ZWaveProxyFrame outgoing_proto_msg_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
@@ -396,7 +396,7 @@ async def add_includes(includes: list[str]) -> None:
|
|||||||
async def _add_platformio_options(pio_options):
|
async def _add_platformio_options(pio_options):
|
||||||
# Add includes at the very end, so that they override everything
|
# Add includes at the very end, so that they override everything
|
||||||
for key, val in pio_options.items():
|
for key, val in pio_options.items():
|
||||||
if key == "build_flags" and not isinstance(val, list):
|
if key in ["build_flags", "lib_ignore"] and not isinstance(val, list):
|
||||||
val = [val]
|
val = [val]
|
||||||
cg.add_platformio_option(key, val)
|
cg.add_platformio_option(key, val)
|
||||||
|
|
||||||
|
@@ -158,6 +158,7 @@
|
|||||||
#define USE_ESP32_BLE_SERVER
|
#define USE_ESP32_BLE_SERVER
|
||||||
#define USE_ESP32_BLE_UUID
|
#define USE_ESP32_BLE_UUID
|
||||||
#define USE_ESP32_BLE_ADVERTISING
|
#define USE_ESP32_BLE_ADVERTISING
|
||||||
|
#define USE_ESP32_CAMERA_JPEG_ENCODER
|
||||||
#define USE_I2C
|
#define USE_I2C
|
||||||
#define USE_IMPROV
|
#define USE_IMPROV
|
||||||
#define USE_MICROPHONE
|
#define USE_MICROPHONE
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import abc
|
import abc
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable
|
||||||
import inspect
|
import inspect
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
@@ -13,7 +13,6 @@ from esphome.core import (
|
|||||||
HexInt,
|
HexInt,
|
||||||
Lambda,
|
Lambda,
|
||||||
Library,
|
Library,
|
||||||
TimePeriod,
|
|
||||||
TimePeriodMicroseconds,
|
TimePeriodMicroseconds,
|
||||||
TimePeriodMilliseconds,
|
TimePeriodMilliseconds,
|
||||||
TimePeriodMinutes,
|
TimePeriodMinutes,
|
||||||
@@ -21,35 +20,11 @@ from esphome.core import (
|
|||||||
TimePeriodSeconds,
|
TimePeriodSeconds,
|
||||||
)
|
)
|
||||||
from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
|
from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
|
||||||
|
from esphome.types import Expression, SafeExpType, TemplateArgsType
|
||||||
from esphome.util import OrderedDict
|
from esphome.util import OrderedDict
|
||||||
from esphome.yaml_util import ESPHomeDataBase
|
from esphome.yaml_util import ESPHomeDataBase
|
||||||
|
|
||||||
|
|
||||||
class Expression(abc.ABC):
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __str__(self):
|
|
||||||
"""
|
|
||||||
Convert expression into C++ code
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
SafeExpType = (
|
|
||||||
Expression
|
|
||||||
| bool
|
|
||||||
| str
|
|
||||||
| str
|
|
||||||
| int
|
|
||||||
| float
|
|
||||||
| TimePeriod
|
|
||||||
| type[bool]
|
|
||||||
| type[int]
|
|
||||||
| type[float]
|
|
||||||
| Sequence[Any]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RawExpression(Expression):
|
class RawExpression(Expression):
|
||||||
__slots__ = ("text",)
|
__slots__ = ("text",)
|
||||||
|
|
||||||
@@ -575,7 +550,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable:
|
def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
|
||||||
"""Declare a new pointer variable in the code generation by calling it's constructor
|
"""Declare a new pointer variable in the code generation by calling it's constructor
|
||||||
with the given arguments.
|
with the given arguments.
|
||||||
|
|
||||||
@@ -681,7 +656,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
|
|||||||
|
|
||||||
async def process_lambda(
|
async def process_lambda(
|
||||||
value: Lambda,
|
value: Lambda,
|
||||||
parameters: list[tuple[SafeExpType, str]],
|
parameters: TemplateArgsType,
|
||||||
capture: str = "=",
|
capture: str = "=",
|
||||||
return_type: SafeExpType = None,
|
return_type: SafeExpType = None,
|
||||||
) -> LambdaExpression | None:
|
) -> LambdaExpression | None:
|
||||||
|
@@ -479,6 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
|
|||||||
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
|
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
|
||||||
|
|
||||||
|
|
||||||
|
class EsphomeCleanPlatformHandler(EsphomeCommandWebSocket):
|
||||||
|
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||||
|
config_file = settings.rel_path(json_message["configuration"])
|
||||||
|
return [*DASHBOARD_COMMAND, "clean-platform", config_file]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeCleanHandler(EsphomeCommandWebSocket):
|
class EsphomeCleanHandler(EsphomeCommandWebSocket):
|
||||||
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||||
config_file = settings.rel_path(json_message["configuration"])
|
config_file = settings.rel_path(json_message["configuration"])
|
||||||
@@ -1313,6 +1319,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
|
|||||||
(f"{rel}compile", EsphomeCompileHandler),
|
(f"{rel}compile", EsphomeCompileHandler),
|
||||||
(f"{rel}validate", EsphomeValidateHandler),
|
(f"{rel}validate", EsphomeValidateHandler),
|
||||||
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
|
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
|
||||||
|
(f"{rel}clean-platform", EsphomeCleanPlatformHandler),
|
||||||
(f"{rel}clean", EsphomeCleanHandler),
|
(f"{rel}clean", EsphomeCleanHandler),
|
||||||
(f"{rel}vscode", EsphomeVscodeHandler),
|
(f"{rel}vscode", EsphomeVscodeHandler),
|
||||||
(f"{rel}ace", EsphomeAceEditorHandler),
|
(f"{rel}ace", EsphomeAceEditorHandler),
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
"""This helper module tracks commonly used types in the esphome python codebase."""
|
"""This helper module tracks commonly used types in the esphome python codebase."""
|
||||||
|
|
||||||
from typing import TypedDict
|
import abc
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
from esphome.core import ID, EsphomeCore, Lambda
|
from esphome.core import ID, EsphomeCore, Lambda, TimePeriod
|
||||||
|
|
||||||
ConfigFragmentType = (
|
ConfigFragmentType = (
|
||||||
str
|
str
|
||||||
@@ -20,6 +22,32 @@ CoreType = EsphomeCore
|
|||||||
ConfigPathType = str | int
|
ConfigPathType = str | int
|
||||||
|
|
||||||
|
|
||||||
|
class Expression(abc.ABC):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __str__(self):
|
||||||
|
"""
|
||||||
|
Convert expression into C++ code
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
SafeExpType = (
|
||||||
|
Expression
|
||||||
|
| bool
|
||||||
|
| str
|
||||||
|
| int
|
||||||
|
| float
|
||||||
|
| TimePeriod
|
||||||
|
| type[bool]
|
||||||
|
| type[int]
|
||||||
|
| type[float]
|
||||||
|
| Sequence[Any]
|
||||||
|
)
|
||||||
|
|
||||||
|
TemplateArgsType = list[tuple[SafeExpType, str]]
|
||||||
|
|
||||||
|
|
||||||
class EntityMetadata(TypedDict):
|
class EntityMetadata(TypedDict):
|
||||||
"""Metadata stored for each entity to help with duplicate detection."""
|
"""Metadata stored for each entity to help with duplicate detection."""
|
||||||
|
|
||||||
|
@@ -1,19 +1,30 @@
|
|||||||
import collections
|
import collections
|
||||||
|
from collections.abc import Callable
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from esphome import const
|
from esphome import const
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from esphome.config_validation import Schema
|
||||||
|
from esphome.cpp_generator import MockObjClass
|
||||||
|
|
||||||
|
|
||||||
class RegistryEntry:
|
class RegistryEntry:
|
||||||
def __init__(self, name, fun, type_id, schema):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
fun: Callable[..., Any],
|
||||||
|
type_id: "MockObjClass",
|
||||||
|
schema: "Schema",
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.fun = fun
|
self.fun = fun
|
||||||
self.type_id = type_id
|
self.type_id = type_id
|
||||||
@@ -38,8 +49,8 @@ class Registry(dict[str, RegistryEntry]):
|
|||||||
self.base_schema = base_schema or {}
|
self.base_schema = base_schema or {}
|
||||||
self.type_id_key = type_id_key
|
self.type_id_key = type_id_key
|
||||||
|
|
||||||
def register(self, name, type_id, schema):
|
def register(self, name: str, type_id: "MockObjClass", schema: "Schema"):
|
||||||
def decorator(fun):
|
def decorator(fun: Callable[..., Any]):
|
||||||
self[name] = RegistryEntry(name, fun, type_id, schema)
|
self[name] = RegistryEntry(name, fun, type_id, schema)
|
||||||
return fun
|
return fun
|
||||||
|
|
||||||
@@ -47,8 +58,8 @@ class Registry(dict[str, RegistryEntry]):
|
|||||||
|
|
||||||
|
|
||||||
class SimpleRegistry(dict):
|
class SimpleRegistry(dict):
|
||||||
def register(self, name, data):
|
def register(self, name: str, data: Any):
|
||||||
def decorator(fun):
|
def decorator(fun: Callable[..., Any]):
|
||||||
self[name] = (fun, data)
|
self[name] = (fun, data)
|
||||||
return fun
|
return fun
|
||||||
|
|
||||||
|
@@ -323,19 +323,41 @@ def clean_build():
|
|||||||
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
||||||
# This helps when toolchain paths change or get corrupted
|
# This helps when toolchain paths change or get corrupted
|
||||||
try:
|
try:
|
||||||
from platformio.project.helpers import get_project_cache_dir
|
from platformio.project.config import ProjectConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# PlatformIO is not available, skip cache cleaning
|
# PlatformIO is not available, skip cache cleaning
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
cache_dir = get_project_cache_dir()
|
config = ProjectConfig.get_instance()
|
||||||
if cache_dir and cache_dir.strip():
|
cache_dir = Path(config.get("platformio", "cache_dir"))
|
||||||
cache_path = Path(cache_dir)
|
if cache_dir.is_dir():
|
||||||
if cache_path.is_dir():
|
|
||||||
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
|
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
|
||||||
shutil.rmtree(cache_dir)
|
shutil.rmtree(cache_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_platform():
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Clean entire build dir
|
||||||
|
if CORE.build_path.is_dir():
|
||||||
|
_LOGGER.info("Deleting %s", CORE.build_path)
|
||||||
|
shutil.rmtree(CORE.build_path)
|
||||||
|
|
||||||
|
# Clean PlatformIO project files
|
||||||
|
try:
|
||||||
|
from platformio.project.config import ProjectConfig
|
||||||
|
except ImportError:
|
||||||
|
# PlatformIO is not available, skip cleaning
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
config = ProjectConfig.get_instance()
|
||||||
|
for pio_dir in ["cache_dir", "packages_dir", "platforms_dir", "core_dir"]:
|
||||||
|
path = Path(config.get("platformio", pio_dir))
|
||||||
|
if path.is_dir():
|
||||||
|
_LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path)
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
|
|
||||||
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
|
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
|
||||||
# This is an example and may include too much for your use-case.
|
# This is an example and may include too much for your use-case.
|
||||||
# You can modify this file to suit your needs.
|
# You can modify this file to suit your needs.
|
||||||
|
@@ -12,10 +12,11 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
|||||||
esptool==5.1.0
|
esptool==5.1.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20250904.0
|
esphome-dashboard==20250904.0
|
||||||
aioesphomeapi==41.4.0
|
aioesphomeapi==41.10.0
|
||||||
zeroconf==0.147.2
|
zeroconf==0.147.2
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.18.15 # dashboard_import
|
ruamel.yaml==0.18.15 # dashboard_import
|
||||||
|
ruamel.yaml.clib==0.2.12 # dashboard_import
|
||||||
esphome-glyphsets==0.2.0
|
esphome-glyphsets==0.2.0
|
||||||
pillow==10.4.0
|
pillow==10.4.0
|
||||||
cairosvg==2.8.2
|
cairosvg==2.8.2
|
||||||
|
@@ -353,13 +353,34 @@ def create_field_type_info(
|
|||||||
return FixedArrayRepeatedType(field, size_define)
|
return FixedArrayRepeatedType(field, size_define)
|
||||||
return RepeatedTypeInfo(field)
|
return RepeatedTypeInfo(field)
|
||||||
|
|
||||||
# Check for fixed_array_size option on bytes fields
|
# Check for mutually exclusive options on bytes fields
|
||||||
if (
|
if field.type == 12:
|
||||||
field.type == 12
|
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
|
||||||
and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None
|
fixed_size = get_field_opt(field, pb.fixed_array_size, None)
|
||||||
):
|
|
||||||
|
if has_pointer_to_buffer and fixed_size is not None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Field '{field.name}' has both pointer_to_buffer and fixed_array_size. "
|
||||||
|
"These options are mutually exclusive. Use pointer_to_buffer for zero-copy "
|
||||||
|
"or fixed_array_size for traditional array storage."
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_pointer_to_buffer:
|
||||||
|
# Zero-copy pointer approach - no size needed, will use size_t for length
|
||||||
|
return PointerToBytesBufferType(field, None)
|
||||||
|
|
||||||
|
if fixed_size is not None:
|
||||||
|
# Traditional fixed array approach with copy
|
||||||
return FixedArrayBytesType(field, fixed_size)
|
return FixedArrayBytesType(field, fixed_size)
|
||||||
|
|
||||||
|
# Check for pointer_to_buffer option on string fields
|
||||||
|
if field.type == 9:
|
||||||
|
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
|
||||||
|
|
||||||
|
if has_pointer_to_buffer:
|
||||||
|
# Zero-copy pointer approach for strings
|
||||||
|
return PointerToBytesBufferType(field, None)
|
||||||
|
|
||||||
# Special handling for bytes fields
|
# Special handling for bytes fields
|
||||||
if field.type == 12:
|
if field.type == 12:
|
||||||
return BytesType(field, needs_decode, needs_encode)
|
return BytesType(field, needs_decode, needs_encode)
|
||||||
@@ -818,6 +839,91 @@ class BytesType(TypeInfo):
|
|||||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
||||||
|
|
||||||
|
|
||||||
|
class PointerToBytesBufferType(TypeInfo):
|
||||||
|
"""Type for bytes fields that use pointer_to_buffer option for zero-copy."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_use_dump_field(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, field: descriptor.FieldDescriptorProto, size: int | None = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(field)
|
||||||
|
# Size is not used for pointer_to_buffer - we always use size_t for length
|
||||||
|
self.array_size = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cpp_type(self) -> str:
|
||||||
|
return "const uint8_t*"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_value(self) -> str:
|
||||||
|
return "nullptr"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reference_type(self) -> str:
|
||||||
|
return "const uint8_t*"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def const_reference_type(self) -> str:
|
||||||
|
return "const uint8_t*"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_content(self) -> list[str]:
|
||||||
|
# Use uint16_t for length - max packet size is well below 65535
|
||||||
|
# Add pointer and length fields
|
||||||
|
return [
|
||||||
|
f"const uint8_t* {self.field_name}{{nullptr}};",
|
||||||
|
f"uint16_t {self.field_name}_len{{0}};",
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def encode_content(self) -> str:
|
||||||
|
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_length_content(self) -> str | None:
|
||||||
|
# Decode directly stores the pointer to avoid allocation
|
||||||
|
return f"""case {self.number}: {{
|
||||||
|
// Use raw data directly to avoid allocation
|
||||||
|
this->{self.field_name} = value.data();
|
||||||
|
this->{self.field_name}_len = value.size();
|
||||||
|
break;
|
||||||
|
}}"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_length(self) -> str | None:
|
||||||
|
# This is handled in decode_length_content
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wire_type(self) -> WireType:
|
||||||
|
"""Get the wire type for this bytes field."""
|
||||||
|
return WireType.LENGTH_DELIMITED # Uses wire type 2
|
||||||
|
|
||||||
|
def dump(self, name: str) -> str:
|
||||||
|
return (
|
||||||
|
f"format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len)"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dump_content(self) -> str:
|
||||||
|
# Custom dump that doesn't use dump_field template
|
||||||
|
return (
|
||||||
|
f'out.append(" {self.name}: ");\n'
|
||||||
|
+ f"out.append({self.dump(self.field_name)});\n"
|
||||||
|
+ 'out.append("\\n");'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||||
|
return f"size.add_length({self.number}, this->{self.field_name}_len);"
|
||||||
|
|
||||||
|
def get_estimated_size(self) -> int:
|
||||||
|
# field ID + length varint + typical data (assume small for pointer fields)
|
||||||
|
return self.calculate_field_id_size() + 2 + 16
|
||||||
|
|
||||||
|
|
||||||
class FixedArrayBytesType(TypeInfo):
|
class FixedArrayBytesType(TypeInfo):
|
||||||
"""Special type for fixed-size byte arrays."""
|
"""Special type for fixed-size byte arrays."""
|
||||||
|
|
||||||
@@ -2615,6 +2721,10 @@ static const char *const TAG = "api.service";
|
|||||||
hpp_protected = ""
|
hpp_protected = ""
|
||||||
cpp += "\n"
|
cpp += "\n"
|
||||||
|
|
||||||
|
# Build a mapping of message input types to their authentication requirements
|
||||||
|
message_auth_map: dict[str, bool] = {}
|
||||||
|
message_conn_map: dict[str, bool] = {}
|
||||||
|
|
||||||
m = serv.method[0]
|
m = serv.method[0]
|
||||||
for m in serv.method:
|
for m in serv.method:
|
||||||
func = m.name
|
func = m.name
|
||||||
@@ -2626,6 +2736,10 @@ static const char *const TAG = "api.service";
|
|||||||
needs_conn = get_opt(m, pb.needs_setup_connection, True)
|
needs_conn = get_opt(m, pb.needs_setup_connection, True)
|
||||||
needs_auth = get_opt(m, pb.needs_authentication, True)
|
needs_auth = get_opt(m, pb.needs_authentication, True)
|
||||||
|
|
||||||
|
# Store authentication requirements for message types
|
||||||
|
message_auth_map[inp] = needs_auth
|
||||||
|
message_conn_map[inp] = needs_conn
|
||||||
|
|
||||||
ifdef = message_ifdef_map.get(inp, ifdefs.get(inp))
|
ifdef = message_ifdef_map.get(inp, ifdefs.get(inp))
|
||||||
|
|
||||||
if ifdef is not None:
|
if ifdef is not None:
|
||||||
@@ -2643,26 +2757,7 @@ static const char *const TAG = "api.service";
|
|||||||
|
|
||||||
cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n"
|
cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n"
|
||||||
|
|
||||||
# Start with authentication/connection check if needed
|
# No authentication check here - it's done in read_message
|
||||||
if needs_auth or needs_conn:
|
|
||||||
# Determine which check to use
|
|
||||||
if needs_auth:
|
|
||||||
check_func = "this->check_authenticated_()"
|
|
||||||
else:
|
|
||||||
check_func = "this->check_connection_setup_()"
|
|
||||||
|
|
||||||
if is_void:
|
|
||||||
# For void methods, just wrap with auth check
|
|
||||||
body = f"if ({check_func}) {{\n"
|
|
||||||
body += f" this->{func}(msg);\n"
|
|
||||||
body += "}\n"
|
|
||||||
else:
|
|
||||||
# For non-void methods, combine auth check and send response check
|
|
||||||
body = f"if ({check_func} && !this->send_{func}_response(msg)) {{\n"
|
|
||||||
body += " this->on_fatal_error();\n"
|
|
||||||
body += "}\n"
|
|
||||||
else:
|
|
||||||
# No auth check needed, just call the handler
|
|
||||||
body = ""
|
body = ""
|
||||||
if is_void:
|
if is_void:
|
||||||
body += f"this->{func}(msg);\n"
|
body += f"this->{func}(msg);\n"
|
||||||
@@ -2678,6 +2773,65 @@ static const char *const TAG = "api.service";
|
|||||||
hpp_protected += "#endif\n"
|
hpp_protected += "#endif\n"
|
||||||
cpp += "#endif\n"
|
cpp += "#endif\n"
|
||||||
|
|
||||||
|
# Generate optimized read_message with authentication checking
|
||||||
|
# Categorize messages by their authentication requirements
|
||||||
|
no_conn_ids: set[int] = set()
|
||||||
|
conn_only_ids: set[int] = set()
|
||||||
|
|
||||||
|
for id_, (_, _, case_msg_name) in cases:
|
||||||
|
if case_msg_name in message_auth_map:
|
||||||
|
needs_auth = message_auth_map[case_msg_name]
|
||||||
|
needs_conn = message_conn_map[case_msg_name]
|
||||||
|
|
||||||
|
if not needs_conn:
|
||||||
|
no_conn_ids.add(id_)
|
||||||
|
elif not needs_auth:
|
||||||
|
conn_only_ids.add(id_)
|
||||||
|
|
||||||
|
# Generate override if we have messages that skip checks
|
||||||
|
if no_conn_ids or conn_only_ids:
|
||||||
|
# Helper to generate case statements with ifdefs
|
||||||
|
def generate_cases(ids: set[int], comment: str) -> str:
|
||||||
|
result = ""
|
||||||
|
for id_ in sorted(ids):
|
||||||
|
_, ifdef, msg_name = RECEIVE_CASES[id_]
|
||||||
|
if ifdef:
|
||||||
|
result += f"#ifdef {ifdef}\n"
|
||||||
|
result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n"
|
||||||
|
if ifdef:
|
||||||
|
result += "#endif\n"
|
||||||
|
return result
|
||||||
|
|
||||||
|
hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n"
|
||||||
|
|
||||||
|
cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n"
|
||||||
|
cpp += " // Check authentication/connection requirements for messages\n"
|
||||||
|
cpp += " switch (msg_type) {\n"
|
||||||
|
|
||||||
|
# Messages that don't need any checks
|
||||||
|
if no_conn_ids:
|
||||||
|
cpp += generate_cases(no_conn_ids, "// No setup required")
|
||||||
|
cpp += " break; // Skip all checks for these messages\n"
|
||||||
|
|
||||||
|
# Messages that only need connection setup
|
||||||
|
if conn_only_ids:
|
||||||
|
cpp += generate_cases(conn_only_ids, "// Connection setup only")
|
||||||
|
cpp += " if (!this->check_connection_setup_()) {\n"
|
||||||
|
cpp += " return; // Connection not setup\n"
|
||||||
|
cpp += " }\n"
|
||||||
|
cpp += " break;\n"
|
||||||
|
|
||||||
|
cpp += " default:\n"
|
||||||
|
cpp += " // All other messages require authentication (which includes connection check)\n"
|
||||||
|
cpp += " if (!this->check_authenticated_()) {\n"
|
||||||
|
cpp += " return; // Authentication failed\n"
|
||||||
|
cpp += " }\n"
|
||||||
|
cpp += " break;\n"
|
||||||
|
cpp += " }\n\n"
|
||||||
|
cpp += " // Call base implementation to process the message\n"
|
||||||
|
cpp += f" {class_name}Base::read_message(msg_size, msg_type, msg_data);\n"
|
||||||
|
cpp += "}\n"
|
||||||
|
|
||||||
hpp += " protected:\n"
|
hpp += " protected:\n"
|
||||||
hpp += hpp_protected
|
hpp += hpp_protected
|
||||||
hpp += "};\n"
|
hpp += "};\n"
|
||||||
|
@@ -13,7 +13,6 @@ esphome:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
port: 8000
|
port: 8000
|
||||||
password: pwd
|
|
||||||
reboot_timeout: 0min
|
reboot_timeout: 0min
|
||||||
encryption:
|
encryption:
|
||||||
key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=
|
key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=
|
||||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -16,6 +17,7 @@ from esphome import platformio_api
|
|||||||
from esphome.__main__ import (
|
from esphome.__main__ import (
|
||||||
Purpose,
|
Purpose,
|
||||||
choose_upload_log_host,
|
choose_upload_log_host,
|
||||||
|
command_clean_platform,
|
||||||
command_rename,
|
command_rename,
|
||||||
command_update_all,
|
command_update_all,
|
||||||
command_wizard,
|
command_wizard,
|
||||||
@@ -1853,3 +1855,101 @@ esp32:
|
|||||||
# Should not have any Python error messages
|
# Should not have any Python error messages
|
||||||
assert "TypeError" not in clean_output
|
assert "TypeError" not in clean_output
|
||||||
assert "can only concatenate str" not in clean_output
|
assert "can only concatenate str" not in clean_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_success(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_clean_platform when writer.clean_platform() succeeds."""
|
||||||
|
args = MockArgs()
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Set logger level to capture INFO messages
|
||||||
|
with (
|
||||||
|
caplog.at_level(logging.INFO),
|
||||||
|
patch("esphome.writer.clean_platform") as mock_clean_platform,
|
||||||
|
):
|
||||||
|
result = command_clean_platform(args, config)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
mock_clean_platform.assert_called_once()
|
||||||
|
|
||||||
|
# Check that success message was logged
|
||||||
|
assert "Done!" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_oserror(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_clean_platform when writer.clean_platform() raises OSError."""
|
||||||
|
args = MockArgs()
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Create a mock OSError with a specific message
|
||||||
|
mock_error = OSError("Permission denied: cannot delete directory")
|
||||||
|
|
||||||
|
# Set logger level to capture ERROR and INFO messages
|
||||||
|
with (
|
||||||
|
caplog.at_level(logging.INFO),
|
||||||
|
patch(
|
||||||
|
"esphome.writer.clean_platform", side_effect=mock_error
|
||||||
|
) as mock_clean_platform,
|
||||||
|
):
|
||||||
|
result = command_clean_platform(args, config)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
mock_clean_platform.assert_called_once()
|
||||||
|
|
||||||
|
# Check that error message was logged
|
||||||
|
assert (
|
||||||
|
"Error deleting platform files: Permission denied: cannot delete directory"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
# Should not have success message
|
||||||
|
assert "Done!" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_oserror_no_message(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_clean_platform when writer.clean_platform() raises OSError without message."""
|
||||||
|
args = MockArgs()
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Create a mock OSError without a message
|
||||||
|
mock_error = OSError()
|
||||||
|
|
||||||
|
# Set logger level to capture ERROR and INFO messages
|
||||||
|
with (
|
||||||
|
caplog.at_level(logging.INFO),
|
||||||
|
patch(
|
||||||
|
"esphome.writer.clean_platform", side_effect=mock_error
|
||||||
|
) as mock_clean_platform,
|
||||||
|
):
|
||||||
|
result = command_clean_platform(args, config)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
mock_clean_platform.assert_called_once()
|
||||||
|
|
||||||
|
# Check that error message was logged (should show empty string for OSError without message)
|
||||||
|
assert "Error deleting platform files:" in caplog.text
|
||||||
|
# Should not have success message
|
||||||
|
assert "Done!" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_args_and_config_ignored() -> None:
|
||||||
|
"""Test that command_clean_platform ignores args and config parameters."""
|
||||||
|
# Test with various args and config to ensure they don't affect the function
|
||||||
|
args1 = MockArgs(name="test1", file="test.bin")
|
||||||
|
config1 = {"wifi": {"ssid": "test"}}
|
||||||
|
|
||||||
|
args2 = MockArgs(name="test2", dashboard=True)
|
||||||
|
config2 = {"api": {}, "ota": {}}
|
||||||
|
|
||||||
|
with patch("esphome.writer.clean_platform") as mock_clean_platform:
|
||||||
|
result1 = command_clean_platform(args1, config1)
|
||||||
|
result2 = command_clean_platform(args2, config2)
|
||||||
|
|
||||||
|
assert result1 == 0
|
||||||
|
assert result2 == 0
|
||||||
|
assert mock_clean_platform.call_count == 2
|
||||||
|
@@ -362,11 +362,17 @@ def test_clean_build(
|
|||||||
assert dependencies_lock.exists()
|
assert dependencies_lock.exists()
|
||||||
assert platformio_cache_dir.exists()
|
assert platformio_cache_dir.exists()
|
||||||
|
|
||||||
# Mock PlatformIO's get_project_cache_dir
|
# Mock PlatformIO's ProjectConfig cache_dir
|
||||||
with patch(
|
with patch(
|
||||||
"platformio.project.helpers.get_project_cache_dir"
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
) as mock_get_cache_dir:
|
) as mock_get_instance:
|
||||||
mock_get_cache_dir.return_value = str(platformio_cache_dir)
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
mock_config.get.side_effect = (
|
||||||
|
lambda section, option: str(platformio_cache_dir)
|
||||||
|
if (section, option) == ("platformio", "cache_dir")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
# Call the function
|
# Call the function
|
||||||
with caplog.at_level("INFO"):
|
with caplog.at_level("INFO"):
|
||||||
@@ -486,7 +492,7 @@ def test_clean_build_platformio_not_available(
|
|||||||
|
|
||||||
# Mock import error for platformio
|
# Mock import error for platformio
|
||||||
with (
|
with (
|
||||||
patch.dict("sys.modules", {"platformio.project.helpers": None}),
|
patch.dict("sys.modules", {"platformio.project.config": None}),
|
||||||
caplog.at_level("INFO"),
|
caplog.at_level("INFO"),
|
||||||
):
|
):
|
||||||
# Call the function
|
# Call the function
|
||||||
@@ -520,11 +526,17 @@ def test_clean_build_empty_cache_dir(
|
|||||||
# Verify pioenvs exists before
|
# Verify pioenvs exists before
|
||||||
assert pioenvs_dir.exists()
|
assert pioenvs_dir.exists()
|
||||||
|
|
||||||
# Mock PlatformIO's get_project_cache_dir to return whitespace
|
# Mock PlatformIO's ProjectConfig cache_dir to return whitespace
|
||||||
with patch(
|
with patch(
|
||||||
"platformio.project.helpers.get_project_cache_dir"
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
) as mock_get_cache_dir:
|
) as mock_get_instance:
|
||||||
mock_get_cache_dir.return_value = " " # Whitespace only
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
mock_config.get.side_effect = (
|
||||||
|
lambda section, option: " " # Whitespace only
|
||||||
|
if (section, option) == ("platformio", "cache_dir")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
# Call the function
|
# Call the function
|
||||||
with caplog.at_level("INFO"):
|
with caplog.at_level("INFO"):
|
||||||
@@ -723,3 +735,126 @@ def test_write_cpp_with_duplicate_markers(
|
|||||||
# Call should raise an error
|
# Call should raise an error
|
||||||
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
|
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
|
||||||
write_cpp("// New code")
|
write_cpp("// New code")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("esphome.writer.CORE")
|
||||||
|
def test_clean_platform(
|
||||||
|
mock_core: MagicMock,
|
||||||
|
tmp_path: Path,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test clean_platform removes build and PlatformIO dirs."""
|
||||||
|
# Create build directory
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
(build_dir / "dummy.txt").write_text("x")
|
||||||
|
|
||||||
|
# Create PlatformIO directories
|
||||||
|
pio_cache = tmp_path / "pio_cache"
|
||||||
|
pio_packages = tmp_path / "pio_packages"
|
||||||
|
pio_platforms = tmp_path / "pio_platforms"
|
||||||
|
pio_core = tmp_path / "pio_core"
|
||||||
|
for d in (pio_cache, pio_packages, pio_platforms, pio_core):
|
||||||
|
d.mkdir()
|
||||||
|
(d / "keep").write_text("x")
|
||||||
|
|
||||||
|
# Setup CORE
|
||||||
|
mock_core.build_path = build_dir
|
||||||
|
|
||||||
|
# Mock ProjectConfig
|
||||||
|
with patch(
|
||||||
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
|
) as mock_get_instance:
|
||||||
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
|
||||||
|
def cfg_get(section: str, option: str) -> str:
|
||||||
|
mapping = {
|
||||||
|
("platformio", "cache_dir"): str(pio_cache),
|
||||||
|
("platformio", "packages_dir"): str(pio_packages),
|
||||||
|
("platformio", "platforms_dir"): str(pio_platforms),
|
||||||
|
("platformio", "core_dir"): str(pio_core),
|
||||||
|
}
|
||||||
|
return mapping.get((section, option), "")
|
||||||
|
|
||||||
|
mock_config.get.side_effect = cfg_get
|
||||||
|
|
||||||
|
# Call
|
||||||
|
from esphome.writer import clean_platform
|
||||||
|
|
||||||
|
with caplog.at_level("INFO"):
|
||||||
|
clean_platform()
|
||||||
|
|
||||||
|
# Verify deletions
|
||||||
|
assert not build_dir.exists()
|
||||||
|
assert not pio_cache.exists()
|
||||||
|
assert not pio_packages.exists()
|
||||||
|
assert not pio_platforms.exists()
|
||||||
|
assert not pio_core.exists()
|
||||||
|
|
||||||
|
# Verify logging mentions each
|
||||||
|
assert "Deleting" in caplog.text
|
||||||
|
assert str(build_dir) in caplog.text
|
||||||
|
assert "PlatformIO cache" in caplog.text
|
||||||
|
assert "PlatformIO packages" in caplog.text
|
||||||
|
assert "PlatformIO platforms" in caplog.text
|
||||||
|
assert "PlatformIO core" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@patch("esphome.writer.CORE")
|
||||||
|
def test_clean_platform_platformio_not_available(
|
||||||
|
mock_core: MagicMock,
|
||||||
|
tmp_path: Path,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test clean_platform when PlatformIO is not available."""
|
||||||
|
# Build dir
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
mock_core.build_path = build_dir
|
||||||
|
|
||||||
|
# PlatformIO dirs that should remain untouched
|
||||||
|
pio_cache = tmp_path / "pio_cache"
|
||||||
|
pio_cache.mkdir()
|
||||||
|
|
||||||
|
from esphome.writer import clean_platform
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict("sys.modules", {"platformio.project.config": None}),
|
||||||
|
caplog.at_level("INFO"),
|
||||||
|
):
|
||||||
|
clean_platform()
|
||||||
|
|
||||||
|
# Build dir removed, PlatformIO dirs remain
|
||||||
|
assert not build_dir.exists()
|
||||||
|
assert pio_cache.exists()
|
||||||
|
|
||||||
|
# No PlatformIO-specific logs
|
||||||
|
assert "PlatformIO" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@patch("esphome.writer.CORE")
|
||||||
|
def test_clean_platform_partial_exists(
|
||||||
|
mock_core: MagicMock,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Test clean_platform when only build dir exists."""
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
mock_core.build_path = build_dir
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
|
) as mock_get_instance:
|
||||||
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
# Return non-existent dirs
|
||||||
|
mock_config.get.side_effect = lambda *_args, **_kw: str(
|
||||||
|
tmp_path / "does_not_exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
from esphome.writer import clean_platform
|
||||||
|
|
||||||
|
clean_platform()
|
||||||
|
|
||||||
|
assert not build_dir.exists()
|
||||||
|
Reference in New Issue
Block a user