Compare commits

..
Author SHA1 Message Date
J. Nick Koston f228b21746 Merge remote-tracking branch 'origin/yaml-edit-apply' into cli-rotate-key 2026-10-26 04:40:28 -10:00
J. Nick Koston 970885b79b Merge remote-tracking branch 'origin/dev' into yaml-edit-apply
# Conflicts:
#	esphome/yaml_util.py
2026-10-26 04:40:20 -10:00
J. Nick Koston 73564dbebd A quoted include path keeps its hash in the shared-configuration scan 2026-10-17 10:37:14 -10:00
J. Nick Koston 4165dbb534 A comment needs whitespace before its hash in a secret name, old_key takes its secret from the ota block it lands on 2026-10-13 20:16:38 -10:00
J. Nick Koston 3bc9f74f46 The snapshot now carries the mode 2026-10-13 09:55:05 -10:00
J. Nick Koston 0dd27c5d08 Show the report an interrupted restore carries 2026-10-13 09:55:05 -10:00
J. Nick Koston 84b0f35b91 Keep the edit from the point the device accepted the session 2026-10-13 09:55:05 -10:00
J. Nick Koston 2aa3677bbf Advise by what the probe found: unreachable, another key, no encryption, a device error 2026-10-13 09:55:05 -10:00
J. Nick Koston c2fbcf0f24 Refuse a key that is not on a line of its own, name the sharers of an include that gets old_key 2026-10-13 09:55:05 -10:00
J. Nick Koston afbd2ab7b1 [cli] Add rotate-key to change the OTA encryption key over the air
Locates the current key on its yaml or secrets.yaml line, writes the new
one with the editor from #20602, keeps the replaced key as old_key, checks
the device with the probe from #20601, builds, uploads with the current key
and confirms with the new one; anything short of a reached device puts the
files back.
2026-10-13 09:55:04 -10:00
J. Nick Koston 3c65c55df0 Place a rebuilt mapping only when the key is on its value's line 2026-10-13 09:54:56 -10:00
J. Nick Koston 7704ad9384 Restore the mode from before the edit, finish the restore after any exception and raise an exit again with the report 2026-10-13 09:39:35 -10:00
J. Nick Koston 50fdc8e85b Reload a symlinked file by the path the loader read it by 2026-10-13 09:30:56 -10:00
J. Nick Koston 3e6324445b Match the restore report as text; a Windows path is no regex 2026-10-13 07:28:38 -10:00
J. Nick Koston e0e5cbdc27 Register a value written to a secrets file, keep the registry through own_documents, finish the restore after any failure and raise an interrupt again with the report 2026-10-13 07:28:38 -10:00
J. Nick Koston 870b80e463 Keep the secret registry on the reload, keep the order of insertions under one line, refuse an incomplete document set 2026-10-13 07:28:38 -10:00
J. Nick Koston 899e88be72 [core] Let the yaml editor insert lines, write with a rollback and restore
apply_line_edits rewrites the located lines in place, reloads every touched
file and drops the validated caches, undoing everything on any failure;
restore_files puts the originals back only over what was written. A key in
a mapping merge_config rebuilt is placed by its value's line.
2026-10-13 07:28:38 -10:00
316 changed files with 6773 additions and 4666 deletions

No files matched your search

+38
View File
@@ -2233,6 +2233,12 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
return 0
def command_rotate_key(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome.cli.rotate_key import command_rotate_key as run
return run(args, config)
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome.cli.rename import command_rename as run
@@ -2259,6 +2265,7 @@ POST_CONFIG_ACTIONS = {
"clean-mqtt": command_clean_mqtt,
"idedata": command_idedata,
"rename": command_rename,
"rotate-key": command_rotate_key,
"discover": command_discover,
"analyze-memory": command_analyze_memory,
"bundle": command_bundle,
@@ -2656,6 +2663,37 @@ def parse_args(argv):
)
parser_rename.add_argument("name", help="The new name for the device.", type=str)
parser_rotate_key = subparsers.add_parser(
"rotate-key",
help=(
"Change the OTA encryption key over the air: write a new key to the "
"yaml, build with it, upload with the current key, confirm."
),
parents=[mqtt_options],
)
parser_rotate_key.add_argument(
"configuration", help="Your YAML configuration file.", nargs=1
)
parser_rotate_key.add_argument(
"--device",
action="append",
help=ARGUMENT_HELP_DEVICE,
)
parser_rotate_key.add_argument(
"--prompt-new-key",
action="store_true",
help=(
"Ask for the new key instead of generating one; read without echo, "
"or as one line from stdin when that is not a terminal."
),
)
parser_rotate_key.add_argument(
"-y",
"--yes",
action="store_true",
help="Do not ask for confirmation when the api encryption key changes too.",
)
parser_analyze_memory = subparsers.add_parser(
"analyze-memory",
help="Analyze memory usage by component.",
+1 -30
View File
@@ -1,18 +1,15 @@
from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass, field
import logging
import math
import string
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ABOVE,
CONF_ALL,
CONF_ANY,
CONF_AUTOMATION_ID,
CONF_BELOW,
CONF_CONDITION,
CONF_COUNT,
CONF_ELSE,
@@ -366,25 +363,6 @@ class ApplyField:
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
def _in_range_low(config: ConfigType, value: float) -> str:
return "-INFINITY" if math.isnan(value) else str(cg.safe_exp(value))
def _in_range_high(config: ConfigType, value: float) -> str:
# With no lower bound, an unset upper bound stays NaN so the check is false, as before.
if math.isnan(value):
return "NAN" if math.isnan(config[CONF_ABOVE]) else "INFINITY"
return str(cg.safe_exp(value))
# ``above <= state <= below`` on the parent's float ``state``; register with ``IN_RANGE_DEFAULTS``.
IN_RANGE_CHECK = ApplyCall(
"state >= {} && {parent}->state <= {}",
((CONF_ABOVE, cg.float_, _in_range_low), (CONF_BELOW, cg.float_, _in_range_high)),
)
IN_RANGE_DEFAULTS = {CONF_ABOVE: math.nan, CONF_BELOW: math.nan}
@dataclass(frozen=True)
class ApplyBytes:
"""A byte payload key forwarded as ``target``, whose ``{}`` stands for ``data, len``.
@@ -713,11 +691,7 @@ async def _render_check(
def register_apply_condition(
name: str,
schema: cv.Schema,
check: str | ApplyCall,
id_key: str = CONF_ID,
defaults: ConfigType | None = None,
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
) -> None:
"""Register a condition that is one expression on its parent, with no C++ class.
@@ -726,7 +700,6 @@ def register_apply_condition(
against config values, all of which must be present. Write ``== false`` to negate.
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=``
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
``defaults`` fills absent keys at code generation only, so the validated config is unchanged.
Generates one static predicate and an ``ApplyCondition`` templated on it.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
@@ -741,8 +714,6 @@ def register_apply_condition(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
if defaults:
config = {**defaults, **config}
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
values = _apply_values(config, members)
+285
View File
@@ -0,0 +1,285 @@
"""``esphome rotate-key``."""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
from esphome import espota2
from esphome.components.noise import (
generate_encryption_key,
static_encryption_key,
validate_encryption_key,
)
from esphome.config_validation import Invalid
from esphome.const import CONF_API, CONF_PASSWORD, CONF_PORT
from esphome.core import CORE, EsphomeError
from esphome.espota2 import ProbeResult
from esphome.log import AnsiFore, color
from esphome.ota_key_edit import locate_key_edits, old_key_edit, with_sharers
from esphome.types import ConfigType
from esphome.upload_targets import PortType, get_port_type
from esphome.util import (
ESPHOME_COMMAND,
read_secret_line,
run_external_process,
safe_input,
safe_print,
)
from esphome.yaml_edit import RestoreError, Snapshot, apply_line_edits, restore_files
PRECHECK_TIMEOUT = 15.0
# What to do when the device does not accept the key in the yaml before anything is written
PRECHECK_ADVICE = {
ProbeResult.UNREACHABLE: (
"The device could not be reached; check that it is online, then try again."
),
ProbeResult.REJECTED: (
"The device runs a different key than the yaml. Install the current "
"configuration first, presenting its key with --prompt-ota-key, then rotate."
),
ProbeResult.NOT_OFFERED: (
"The device does not offer encryption: it runs a firmware older than "
"ESPHome 2026.9.0 or without a key. Install the current configuration "
"first, then rotate."
),
ProbeResult.DEVICE_ERROR: (
"The device answered with an error (see the warning above); fix that first."
),
}
def command_rotate_key(args: argparse.Namespace, config: ConfigType) -> int | None:
"""Write a new key, build with it, upload with the current one, confirm."""
from esphome.__main__ import (
Purpose,
_esphome_ota_conf,
_resolve_network_devices,
choose_upload_log_host,
)
def fail(message: str) -> int:
safe_print(color(AnsiFore.BOLD_RED, message))
return 1
ota_conf = _esphome_ota_conf(config)
if ota_conf is None:
return fail("rotate-key needs the esphome OTA platform in the configuration")
old_key = static_encryption_key(ota_conf)
if old_key is None:
return fail(
"rotate-key needs 'encryption:' under the esphome OTA platform with a "
"key in the yaml; a key provisioned at runtime is rotated from Home "
"Assistant"
)
if CORE.is_host:
return fail("rotate-key is for devices updated over the air")
old_key = str(old_key)
remote_port = int(ota_conf[CONF_PORT])
devices = choose_upload_log_host(
default=args.device, check_default=None, purpose=Purpose.UPLOADING
)
if get_port_type(devices[0]) in (PortType.SERIAL, PortType.BOOTSEL):
return fail(
"rotate-key works over the air; on a serial connection change the "
"key in the yaml and run 'esphome run' instead"
)
network_devices = _resolve_network_devices(devices, config, args)
if args.prompt_new_key:
new_key = read_secret_line("New OTA encryption key: ")
else:
new_key = generate_encryption_key()
try:
validate_encryption_key(new_key)
except Invalid as err:
return fail(f"Invalid new key: {err}")
if new_key == old_key:
return fail("The new key is the same as the current one")
try:
edits = locate_key_edits(old_key, new_key)
# The previous key stays in the config as old_key so an install still
# reaches the device if it ends up running either key
edits += old_key_edit(old_key)
unchecked = with_sharers(edits)
except EsphomeError as err:
return fail(str(err))
warnings = []
# With the recommended shape the ota inherits the api key, so rotating
# it changes what Home Assistant connects with
if str(static_encryption_key(config.get(CONF_API) or {})) == old_key:
warnings.append(
"The api encryption key changes too; Home Assistant will ask for "
"the new key after the install."
)
shared: dict[str, set[str]] = {}
for edit in edits:
what = f"secret '{edit.secret}'" if edit.secret else edit.path.name
for path in edit.shared_with:
shared.setdefault(
str(path.relative_to(CORE.config_dir.resolve())), set()
).add(what)
if unchecked:
warnings.append(
"Could not check every configuration for shared use: "
+ "; ".join(unchecked)
)
if shared:
warnings.append(
"Other configurations share what is rewritten: "
+ "; ".join(
f"{p} ({', '.join(sorted(w))})" for p, w in sorted(shared.items())
)
+ ". Their devices keep the previous key until their next install; "
"make sure those configurations carry it as old_key."
)
if warnings:
for warning in warnings:
safe_print(color(AnsiFore.BOLD_YELLOW, warning))
if not args.yes:
if not sys.stdin.isatty():
return fail("Confirm with --yes when there is no terminal")
if safe_input("Continue? [y/N] ").strip().lower() not in ("y", "yes"):
return 1
safe_print("Checking that the device accepts the current key...")
found = espota2.probe_ota_key(
network_devices,
remote_port,
old_key,
timeout=PRECHECK_TIMEOUT,
retry_rejected=False,
)
if found is not ProbeResult.ACCEPTED:
return fail(PRECHECK_ADVICE[found])
# Both keys are shown only when a file could not be put back
keys = f"Previous key: {old_key}\nNew key: {new_key}"
try:
originals = apply_line_edits(edits)
except RestoreError as err:
safe_print(keys)
return fail(str(err))
except EsphomeError as err:
return fail(str(err))
except KeyboardInterrupt as err:
if notes := getattr(err, "__notes__", None): # a rollback that failed too
safe_print(keys)
return fail("Interrupted; " + "; ".join(notes))
return fail("Interrupted before the upload")
# From here every exit restores or reports, so nothing sits outside the try
uploaded = False
try:
safe_print(
"Wrote the new key to "
+ ", ".join(color(AnsiFore.CYAN, str(p)) for p in originals)
)
# Global options go before the subcommand; the compile parser is strict
cli_args = ["--dashboard"] if CORE.dashboard else []
if args.toolchain:
cli_args += ["--toolchain", str(args.toolchain)]
for key, value in args.substitution or []:
cli_args += ["-s", key, value]
cli_args += ["compile", str(CORE.config_path)]
if run_external_process(*ESPHOME_COMMAND, *cli_args) != 0:
return fail("Compiling with the new key failed")
def image_sent() -> None:
# The device accepted the session and may commit from here on;
# the edited config reaches it on either key
nonlocal uploaded
uploaded = True
rc, _ = espota2.run_ota(
network_devices,
remote_port,
ota_conf.get(CONF_PASSWORD),
CORE.firmware_bin,
noise_psk=old_key,
plaintext_fallback=False,
on_upload_start=image_sent,
)
if rc != 0:
return fail(
"Uploading with the current key failed. The configuration keeps "
"the new key with the previous one as old_key, so the next "
"install reaches the device whichever key it runs."
if uploaded
else "The upload did not reach the device."
)
safe_print("Waiting for the device to come back with the new key...")
found = espota2.probe_ota_key(network_devices, remote_port, new_key)
if found is not ProbeResult.ACCEPTED:
return fail(
(
"The device still answers with the previous key."
if found is ProbeResult.REJECTED
else "The device did not come back to confirm the new key."
)
+ " The configuration keeps the new key, with the previous one as "
"old_key, so the next install reaches the device either way."
)
except KeyboardInterrupt:
return fail(
"Interrupted; the device's key was not confirmed"
if uploaded
else "Interrupted before the upload"
)
finally:
# Before the upload nothing changed on the device; after it the device
# most likely runs the new key, and old_key covers the other case
if uploaded:
safe_print(f"New OTA encryption key: {color(AnsiFore.CYAN, new_key)}")
else:
_restore(originals, keys)
safe_print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
return 0
def _restore(originals: dict[Path, Snapshot], keys: str) -> None:
"""Put the previous key back, and drop the images a build made with the
new key; both keys are shown when a step fails."""
files = ", ".join(str(p) for p in originals)
try:
restore_files(originals)
except (EsphomeError, KeyboardInterrupt) as err:
# An interrupted restore carries its report as a note
why = str(err) or "; ".join(getattr(err, "__notes__", ()))
safe_print(color(AnsiFore.BOLD_RED, why or f"Interrupted restoring {files}"))
safe_print(keys)
else:
safe_print(color(AnsiFore.BOLD_YELLOW, f"Restored the previous key in {files}"))
build_dir = CORE.firmware_bin.parent
failed: list[str] = []
images: list[Path] = []
try:
images = [p for s in (".bin", ".uf2") for p in build_dir.glob(f"*{s}")]
for image in images: # every one is tried, whatever the others did
try:
image.unlink()
except OSError as err:
failed.append(f"{image}: {err}")
except (OSError, KeyboardInterrupt) as err:
failed.append(f"{build_dir}: {str(err) or 'interrupted'}")
if failed:
safe_print(
color(
AnsiFore.BOLD_RED,
"Could not remove images that may hold the new key: "
+ "; ".join(failed),
)
)
safe_print(keys)
elif images:
safe_print(
color(
AnsiFore.BOLD_YELLOW,
f"Removed the images in {build_dir}, which may hold the new key; "
"compile again before the next upload.",
)
)
+27 -1
View File
@@ -33,9 +33,35 @@ _CACHE_VERSION = 1
_LAMBDA_KEY = "__esphome_lambda__"
_CACHE_SUFFIX = ".validated.json"
def compiled_config_path(config_filename: str) -> Path:
"""Path to the cached validated config alongside the storage sidecar."""
return CORE.data_dir / "storage" / f"{config_filename}.validated.json"
return CORE.data_dir / "storage" / f"{config_filename}{_CACHE_SUFFIX}"
def invalidate_compiled_config() -> None:
"""Drop every cache in the storage dir after a change to a file the
mtime check cannot see, such as secrets.yaml or an include; other
configurations may share that file."""
storage = compiled_config_path(CORE.config_filename).parent
try:
caches = [p for p in storage.iterdir() if p.name.endswith(_CACHE_SUFFIX)]
except FileNotFoundError:
return
except OSError as err:
raise EsphomeError(
f"Could not list the validated config caches in {storage}: {err}"
) from err
for path in caches:
try:
path.unlink(missing_ok=True)
except OSError as err:
# A stale cache would present the old key on the next upload
raise EsphomeError(
f"Could not remove the validated config cache {path}: {err}"
) from err
def save_compiled_config(config: ConfigType) -> None:
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_CODE,
@@ -9,11 +10,11 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_STATE,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -43,12 +44,12 @@ AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
_ALARM_CONTROL_PANEL_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTAlarmControlPanelComponent")
mqtt.MQTTAlarmControlPanelComponent
),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}),
@@ -165,7 +166,11 @@ _CALLBACK_AUTOMATIONS = (
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
await entity_helpers.setup_entity_integrations(var, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_alarm_control_panel(var, config):
+13 -5
View File
@@ -6,11 +6,19 @@ namespace esphome::as3935_i2c {
ESPHOME_LOG_TAG(TAG, "as3935_i2c");
void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) {
const uint8_t value = bits << start_pos;
// Bits of value outside mask were ORed in before, so they stay part of the update
// Also called at runtime, so a transient bus error only warns, like read_register()
if (!this->update_register(reg, mask | value, value)) {
ESP_LOGW(TAG, "Failed to update register 0x%02X", reg);
uint8_t write_reg;
if (!this->read_byte(reg, &write_reg)) {
this->mark_failed();
ESP_LOGW(TAG, "read_byte failed - increase log level for more details!");
return;
}
write_reg &= (~mask);
write_reg |= (bits << start_pos);
if (!this->write_byte(reg, write_reg)) {
ESP_LOGW(TAG, "write_byte failed - increase log level for more details!");
return;
}
}
+8 -2
View File
@@ -240,11 +240,17 @@ bool AS7341Component::write_register_bit(uint8_t address, bool value, uint8_t bi
}
bool AS7341Component::set_register_bit(uint8_t address, uint8_t bit_position) {
return this->update_register(address, 1 << bit_position, 1 << bit_position);
uint8_t data;
this->read_byte(address, &data);
data |= (1 << bit_position);
return this->write_byte(address, data);
}
bool AS7341Component::clear_register_bit(uint8_t address, uint8_t bit_position) {
return this->update_register(address, 1 << bit_position, 0);
uint8_t data;
this->read_byte(address, &data);
data &= ~(1 << bit_position);
return this->write_byte(address, data);
}
} // namespace esphome::as7341
+6 -1
View File
@@ -264,7 +264,12 @@ bool AS734XComponent::select_bank_(uint8_t reg) {
}
bool AS734XComponent::update_bits_(uint8_t reg, uint8_t mask, bool value) {
return this->select_bank_(reg) && this->update_register(reg, mask, value ? mask : 0);
uint8_t data = 0;
if (!this->select_bank_(reg) || !this->read_byte(reg, &data)) {
return false;
}
data = value ? (data | mask) : (data & ~mask);
return this->write_byte(reg, data);
}
// The exposure is applied before every measurement, so a gain, ATIME or ASTEP changed at runtime
+2 -1
View File
@@ -108,7 +108,8 @@ CONFIG_SCHEMA = cv.Schema(
def _final_validate(config: ConfigType) -> ConfigType:
hub = fv.get_declaring_config(config[CONF_AS734X_ID])
full = fv.full_config.get()
hub = full.get_config_for_path(full.get_path_for_id(config[CONF_AS734X_ID])[:-1])
if hub[CONF_TYPE] != "AS7341":
return config
for key in _BAND_KEYS:
+7 -6
View File
@@ -4,13 +4,14 @@ import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI
from esphome.types import ConfigType
from .. import CONF_AT581X_ID, AT581XComponent
from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns
DEPENDENCIES = ["at581x"]
RFSwitch = at581x_ns.class_("RFSwitch", switch.Switch)
CONFIG_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
RFSwitch,
device_class=DEVICE_CLASS_SWITCH,
icon=ICON_WIFI,
).extend(
@@ -23,7 +24,7 @@ CONFIG_SCHEMA = switch.switch_schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_AT581X_ID])
await switch.sub_switches(config, parent=hub).create(
config, hub.set_rf_power_switch, apply="set_rf_mode(state)", publish="before"
)
at581x_component = await cg.get_variable(config[CONF_AT581X_ID])
s = await switch.new_switch(config)
await cg.register_parented(s, config[CONF_AT581X_ID])
cg.add(at581x_component.set_rf_power_switch(s))
@@ -0,0 +1,10 @@
#include "rf_switch.h"
namespace esphome::at581x {
void RFSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_rf_mode(state);
}
} // namespace esphome::at581x
@@ -0,0 +1,13 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../at581x.h"
namespace esphome::at581x {
class RFSwitch final : public switch_::Switch, public Parented<AT581XComponent> {
protected:
void write_state(bool state) override;
};
} // namespace esphome::at581x
+4 -2
View File
@@ -63,7 +63,6 @@ from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
lazy_load_setup,
lazy_load_validator,
mqtt_component_class,
queue_entity_register,
@@ -622,7 +621,10 @@ async def setup_binary_sensor_core_(var, config):
await entity_helpers.setup_entity_integrations(var, config)
await lazy_load_setup("zigbee", "setup_binary_sensor", var, config)
if "zigbee" in CORE.loaded_integrations:
from esphome.components import zigbee
await zigbee.setup_binary_sensor(var, config)
async def register_binary_sensor(var, config):
-1
View File
@@ -29,7 +29,6 @@ from esphome.components.libretiny.const import (
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401
from esphome.types import ConfigType
from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS
+3 -6
View File
@@ -1,5 +1,4 @@
#include "bl0939.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -21,7 +20,7 @@ static const uint8_t BL0939_REG_SOFT_RESET = 0x19;
static const uint8_t BL0939_REG_USR_WRPROT = 0x1A;
static const uint8_t BL0939_REG_TPS_CTRL = 0x1B;
static constexpr uint8_t BL0939_INIT[6][6] PROGMEM = {
const uint8_t BL0939_INIT[6][6] = {
// Reset to default
{BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33},
// Enable User Operation Write
@@ -67,10 +66,8 @@ void BL0939::update() {
}
void BL0939::setup() {
uint8_t cmd[sizeof(BL0939_INIT[0])];
for (const auto *i : BL0939_INIT) {
progmem_memcpy(cmd, i, sizeof(cmd));
this->write_array(cmd, sizeof(cmd));
for (auto *i : BL0939_INIT) {
this->write_array(i, 6);
delay(1);
}
this->flush();
+30 -5
View File
@@ -139,24 +139,49 @@ bool BME680Component::setup_registers_() {
this->calibration_.ambient_temperature = 25; // prime ambient temperature
// Config register
if (!this->update_register(BME680_REGISTER_CONFIG, 0b00011100, (this->iir_filter_ & 0b111) << 2)) {
uint8_t config_register;
if (!this->read_byte(BME680_REGISTER_CONFIG, &config_register)) {
return false;
}
config_register &= ~0b00011100;
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME680_REGISTER_CONFIG, config_register)) {
return false;
}
// Humidity control register
if (!this->update_register(BME680_REGISTER_CONTROL_HUMIDITY, 0b00000111, this->humidity_oversampling_ & 0b111)) {
uint8_t hum_control;
if (!this->read_byte(BME680_REGISTER_CONTROL_HUMIDITY, &hum_control)) {
return false;
}
hum_control &= ~0b00000111;
hum_control |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME680_REGISTER_CONTROL_HUMIDITY, hum_control)) {
return false;
}
// Gas 1 control register: run gas, heater profile 0
if (!this->update_register(BME680_REGISTER_CONTROL_GAS1, 0b00011111, 1 << 4)) {
// Gas 1 control register
uint8_t gas1_control;
if (!this->read_byte(BME680_REGISTER_CONTROL_GAS1, &gas1_control)) {
return false;
}
gas1_control &= ~0b00011111;
gas1_control |= 1 << 4;
gas1_control |= 0; // profile 0
if (!this->write_byte(BME680_REGISTER_CONTROL_GAS1, gas1_control)) {
return false;
}
const bool heat_off = this->heater_temperature_ == 0 || this->heater_duration_ == 0;
// Gas 0 control register
if (!this->update_register(BME680_REGISTER_CONTROL_GAS0, 0b00001000, heat_off << 3)) {
uint8_t gas0_control;
if (!this->read_byte(BME680_REGISTER_CONTROL_GAS0, &gas0_control)) {
return false;
}
gas0_control &= ~0b00001000;
gas0_control |= heat_off << 3;
if (!this->write_byte(BME680_REGISTER_CONTROL_GAS0, gas0_control)) {
return false;
}
+3 -1
View File
@@ -66,7 +66,9 @@ CONFIG_SCHEMA = cv.typed_schema(
def _final_validate(config: ConfigType) -> None:
if config[CONF_TYPE] not in _MAGNETIC_FIELDS:
return
bmi270_config = fv.get_declaring_config(config[CONF_BMI270_ID])
full_config = fv.full_config.get()
bmi270_path = full_config.get_path_for_id(config[CONF_BMI270_ID])[:-1]
bmi270_config = full_config.get_config_for_path(bmi270_path)
if bmi270_config.get(CONF_AUX_DEVICE) != "BMM150":
raise cv.Invalid(
"This sensor requires the parent bmi270's 'aux_device' to be set to 'BMM150'",
+2 -2
View File
@@ -7,7 +7,6 @@ from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, CONF_WATCHDOG
from esphome.core import CORE, ID, EsphomeError, Lambda, TimePeriod
import esphome.final_validate as fv
from esphome.types import ConfigType, TemplateArgsType
CODEOWNERS = ["@Rapsssito"]
@@ -360,7 +359,8 @@ async def bq25186_configure_to_code(
parent = await cg.get_variable(config[CONF_ID])
# Options the action leaves out fall back to the component's configuration
parent_config = fv.get_declaring_config(config[CONF_ID], CORE.config)
path = CORE.config.get_path_for_id(config[CONF_ID])[:-1]
parent_config = CORE.config.get_config_for_path(path)
merged = {**parent_config, **config}
try:
_check_watchdog(merged)
+7 -4
View File
@@ -28,10 +28,13 @@ bool BQ25186Component::write_register_(uint8_t reg, uint8_t value) {
}
bool BQ25186Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value) {
if (this->update_register(reg, mask, value))
return true;
ESP_LOGW(TAG, "Failed to update register 0x%02X", reg);
return false;
uint8_t reg_value;
if (!this->read_byte(reg, &reg_value)) {
ESP_LOGW(TAG, "Failed to read register 0x%02X", reg);
return false;
}
reg_value = (reg_value & ~mask) | (value & mask);
return this->write_register_(reg, reg_value);
}
uint8_t BQ25186Component::expected_register_(size_t index) const {
+1 -1
View File
@@ -71,12 +71,12 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
protected:
bool write_register_(uint8_t reg, uint8_t value);
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
uint8_t expected_register_(size_t index) const;
bool config_matches_(const BQ25186StatusRegisters &regs) const;
#ifdef USE_BQ25186_PG_GPO
void check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending);
#endif
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
#ifdef BQ25186_BINARY_SENSOR_COUNT
StaticVector<BQ25186BinarySensor *, BQ25186_BINARY_SENSOR_COUNT> binary_sensors_;
@@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BUTTON
#include "bq25186.h"
#include "esphome/components/button/button.h"
namespace esphome::bq25186 {
/// Writes one SHIP_RST field when pressed
class BQ25186Button final : public button::Button, public Parented<BQ25186Component> {
public:
BQ25186Button(uint8_t mask, uint8_t value) : mask_(mask), value_(value) {}
protected:
void press_action() override { this->parent_->write_ship_rst(this->mask_, this->value_); }
const uint8_t mask_;
const uint8_t value_;
};
} // namespace esphome::bq25186
#endif // USE_BUTTON
+7 -4
View File
@@ -1,3 +1,4 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
@@ -9,10 +10,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
DEPENDENCIES = ["bq25186"]
BQ25186Button = bq25186_ns.class_("BQ25186Button", button.Button)
# SHIP_RST: bit 7 is the software reset, bits 6:5 the shutdown, ship and hardware reset codes
SOFTWARE_RESET_MASK = 0x80
@@ -31,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): button.button_schema(
button.ApplyButton,
BQ25186Button,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
device_class=device_class,
@@ -44,6 +46,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_BQ25186_ID])
for key, mask, value, _, _ in _BUTTONS:
await buttons(key, None, apply=f"write_ship_rst({mask:#04x}, {value:#04x})")
if (conf := config.get(key)) is not None:
var = await button.new_button(conf, mask, value)
await cg.register_parented(var, config[CONF_BQ25186_ID])
+11 -7
View File
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -9,6 +10,7 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_PRESS,
CONF_WEB_SERVER,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_IDENTIFY,
DEVICE_CLASS_RESTART,
@@ -19,12 +21,11 @@ from esphome.const import (
ICON_RESTART,
ICON_RESTART_ALERT,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
ApplyEntity,
SubEntities,
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -52,13 +53,11 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_BUTTON_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTButtonComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): validate_device_class,
@@ -136,7 +135,12 @@ async def setup_button_core_(var: MockObj, config: ConfigType) -> None:
setup_device_class(config)
await entity_helpers.setup_entity_integrations(var, config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_button(var: MockObj, config: ConfigType) -> None:
+4 -2
View File
@@ -42,8 +42,10 @@ void CAP1188Component::finish_setup_() {
}
// Set sensitivity
// The threshold replaces the upper nibble and is ORed into the lower one, as before
this->update_register(CAP1188_SENSITVITY, 0xF0 | this->touch_threshold_, this->touch_threshold_);
uint8_t sensitivity = 0;
this->read_byte(CAP1188_SENSITVITY, &sensitivity);
sensitivity = sensitivity & 0x0f;
this->write_byte(CAP1188_SENSITVITY, sensitivity | this->touch_threshold_);
// Allow multiple touches
this->write_byte(CAP1188_MULTI_TOUCH, this->allow_multiple_touches_);
+12 -8
View File
@@ -3,6 +3,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION_STATE_TOPIC,
@@ -48,15 +49,15 @@ from esphome.const import (
CONF_TEMPERATURE_STEP,
CONF_UNIT_OF_MEASUREMENT,
CONF_VISUAL,
CONF_WEB_SERVER,
UNIT_KELVIN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
TemperatureUnitResolver,
convert_visual_temperatures,
declare_fixed_temperature_unit,
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
setup_entity_temperature_unit,
@@ -176,13 +177,11 @@ _visual_temperatures_in_entity_unit = partial(
_CLIMATE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTClimateComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
@@ -354,8 +353,10 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
cg.add(var.set_visual_max_humidity_override(max_humidity))
setup_entity_temperature_unit(var, config)
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (action_state_topic := config.get(CONF_ACTION_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_action_state_topic(action_state_topic))
if (away_command_topic := config.get(CONF_AWAY_COMMAND_TOPIC)) is not None:
@@ -485,6 +486,9 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
def set_custom_fan_modes(var: MockObj, modes: list[str]) -> None:
"""Point the climate at a shared flash table of custom fan modes."""
+12 -8
View File
@@ -3,6 +3,7 @@ import logging
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -22,6 +23,7 @@ from esphome.const import (
CONF_TILT_COMMAND_TOPIC,
CONF_TILT_STATE_TOPIC,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_AWNING,
DEVICE_CLASS_BLIND,
DEVICE_CLASS_CURTAIN,
@@ -34,10 +36,9 @@ from esphome.const import (
DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -111,13 +112,11 @@ TRIGGERS = {
_COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTCoverComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
cv.requires_component("mqtt"), cv.boolean
),
@@ -203,8 +202,10 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (position_state_topic := config.get(CONF_POSITION_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_position_state_topic(position_state_topic))
if (
@@ -219,6 +220,9 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
cg.add(mqtt_.set_custom_tilt_command_topic(tilt_command_topic))
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_cover(var: MockObj, config: ConfigType) -> None:
if not CORE.has_id(config[CONF_ID]):
+22 -15
View File
@@ -28,23 +28,30 @@ void CSE7766Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
this->read_available<CSE7766_RAW_DATA_SIZE>(
[this](uint8_t byte) {
this->raw_data_[this->raw_data_index_] = byte;
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
return;
}
uint8_t buf[CSE7766_RAW_DATA_SIZE];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
for (size_t i = 0; i < to_read; i++) {
this->raw_data_[this->raw_data_index_] = buf[i];
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
continue;
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
},
avail);
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
}
}
}
bool CSE7766Component::check_byte_() {
+14 -17
View File
@@ -2,6 +2,7 @@ from collections.abc import Callable
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, time, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DATE,
@@ -19,12 +20,12 @@ from esphome.const import (
CONF_TIME_ID,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_WEB_SERVER,
CONF_YEAR,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -37,10 +38,6 @@ DOMAIN = "datetime"
IS_PLATFORM_COMPONENT = True
datetime_ns = cg.esphome_ns.namespace("datetime")
# Matches time.RealTimeClock by name without importing the time component
_RealTimeClock = cg.esphome_ns.namespace("time").class_(
"RealTimeClock", cg.PollingComponent
)
DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase)
DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
@@ -67,7 +64,7 @@ DATETIME_MODES = [
def _validate_time_present(config: ConfigType) -> ConfigType:
config = config.copy()
if CONF_ON_TIME in config and CONF_TIME_ID not in config:
time_id = cv.use_id(_RealTimeClock)(None)
time_id = cv.use_id(time.RealTimeClock)(None)
config[CONF_TIME_ID] = time_id
return config
@@ -76,10 +73,10 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.Schema(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
cv.Optional(CONF_TIME_ID): cv.use_id(_RealTimeClock),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
).add_extra(_validate_time_present)
@@ -90,9 +87,7 @@ def date_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTDateComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent),
cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
}
)
@@ -103,9 +98,7 @@ def time_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTTimeComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent),
cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True),
cv.Optional(CONF_ON_TIME): automation.validate_automation(
{
@@ -122,7 +115,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTDateTimeComponent")
mqtt.MQTTDateTimeComponent
),
cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of(
"DATETIME", upper=True
@@ -139,7 +132,11 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
@setup_entity("datetime")
async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None:
await entity_helpers.setup_entity_integrations(var, config)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
# The state callback carries nothing; the automation gets the value as an ESPTime.
for conf in config.get(CONF_ON_VALUE, []):
await automation.build_callback_automation(
+146 -133
View File
@@ -135,140 +135,153 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
}
void DFPlayer::loop() {
this->read_available([this](uint8_t byte) {
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
return;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
return;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
return;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
return;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
return;
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
});
avail -= to_read;
for (size_t bi = 0; bi < to_read; bi++) {
uint8_t byte = buf[bi];
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
continue;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG,
"Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
continue;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
}
}
}
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
@@ -1,7 +1,10 @@
#include "dfrobot_sen0465_uart.h"
#include <algorithm>
namespace esphome::dfrobot_sen0465_uart {
using dfrobot_sen0465_base::FRAME_HEAD;
using dfrobot_sen0465_base::FRAME_SIZE;
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
@@ -15,21 +18,30 @@ void DFRobotSEN0465UARTComponent::setup() {
}
void DFRobotSEN0465UARTComponent::loop() {
// The tail of a reading the probe sent on its own may precede the answer; the reader skips to the frame head.
const uint8_t *frame = nullptr;
this->read_available<FRAME_SIZE>([&](uint8_t c) { return (frame = this->reader_.feed(c)) == nullptr; });
if (frame == nullptr)
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
uint8_t byte;
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
this->read_byte(&byte);
const size_t available = this->available();
if (available == 0)
return;
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
return;
this->rx_count_ += len;
if (this->rx_count_ < FRAME_SIZE)
return;
this->rx_count_ = 0;
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
this->disable_loop();
this->handle_response_(frame);
this->handle_response_(this->rx_buffer_);
}
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
this->discard_available();
this->reader_.reset();
this->rx_count_ = 0;
this->write_array(frame, FRAME_SIZE);
return true;
}
@@ -38,7 +50,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
this->enable_loop();
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
this->disable_loop();
this->reader_.reset();
this->rx_count_ = 0;
this->handle_no_response_();
});
}
@@ -2,7 +2,6 @@
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/frame_reader.h"
namespace esphome::dfrobot_sen0465_uart {
@@ -18,7 +17,8 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
bool write_frame(const uint8_t *frame) override;
void read_response() override;
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
uint8_t rx_count_{0};
};
} // namespace esphome::dfrobot_sen0465_uart
@@ -3,7 +3,8 @@ from collections.abc import Callable
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_MODE, CONF_MODEL
from esphome.const import CONF_ID, CONF_MODE, CONF_MODEL
from esphome.core import ID
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -58,11 +59,16 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
def get_hub_config(root: ConfigType, hub_id: ID) -> ConfigType:
"""Return the configuration of the hub with the given id."""
return next(conf for conf in root[DOMAIN] if conf[CONF_ID] == hub_id)
def final_validate_mode(keys: dict[str, str]) -> Callable[[ConfigType], None]:
"""Reject entity keys that need a different hub mode; `keys` maps each key to the mode it needs."""
def validator(config: ConfigType) -> None:
hub = fv.get_declaring_config(config[CONF_DFROBOT_SEN0609_ID])
hub = get_hub_config(fv.full_config.get(), config[CONF_DFROBOT_SEN0609_ID])
for key, mode in keys.items():
if key in config and hub[CONF_MODE] != mode:
raise cv.Invalid(
@@ -2,19 +2,44 @@ import esphome.codegen as cg
from esphome.components import button
from esphome.components.const import CONF_QUERY_PARAMS
import esphome.config_validation as cv
from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_RESTART
from esphome.const import (
CONF_FACTORY_RESET,
CONF_ID,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_DATABASE,
ICON_RESTART,
ICON_RESTART_ALERT,
)
from esphome.types import ConfigType
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
DFRobotSEN0609Button = dfrobot_sen0609_ns.class_("DFRobotSEN0609Button", button.Button)
ButtonAction = dfrobot_sen0609_ns.enum("ButtonAction")
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
button.ApplyButton
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
DFRobotSEN0609Button,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_QUERY_PARAMS): button.button_schema(
DFRobotSEN0609Button,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
cv.Optional(CONF_RESTART): button.button_schema(
DFRobotSEN0609Button,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
}
@@ -22,7 +47,11 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
CONF_FACTORY_RESET,
hub.set_factory_reset_button,
ButtonAction.BUTTON_FACTORY_RESET,
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="query_params()")
await buttons(CONF_RESTART, hub.set_restart_button, apply="restart()")
await buttons(
CONF_QUERY_PARAMS, hub.set_query_button, ButtonAction.BUTTON_QUERY_PARAMS
)
await buttons(CONF_RESTART, hub.set_restart_button, ButtonAction.BUTTON_RESTART)
@@ -0,0 +1,30 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Button final : public button::Button, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Button(ButtonAction action) : action_(action) {}
protected:
void press_action() override {
switch (this->action_) {
case BUTTON_RESTART:
this->parent_->restart();
break;
case BUTTON_FACTORY_RESET:
this->parent_->factory_reset();
break;
case BUTTON_QUERY_PARAMS:
this->parent_->query_params();
break;
}
}
ButtonAction action_;
};
} // namespace esphome::dfrobot_sen0609
@@ -5,6 +5,7 @@
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -131,9 +132,9 @@ void DFRobotSEN0609Component::dump_config() {
LOG_BINARY_SENSOR(" ", "HasTarget", this->has_target_binary_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR_WITH_DEDUP(" ", "TargetDistance", this->target_distance_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "TargetSpeed", this->target_speed_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "TargetEnergy", this->target_energy_sensor_);
LOG_SENSOR(" ", "TargetDistance", this->target_distance_sensor_);
LOG_SENSOR(" ", "TargetSpeed", this->target_speed_sensor_);
LOG_SENSOR(" ", "TargetEnergy", this->target_energy_sensor_);
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
@@ -174,11 +175,21 @@ void DFRobotSEN0609Component::setup() {
}
void DFRobotSEN0609Component::loop() {
if (this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); })) {
uint8_t buf[MAX_LINE_LENGTH];
size_t avail = this->available();
while (avail > 0) {
const size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
if (!this->read_failed_) {
ESP_LOGW(TAG, "UART read failed");
}
this->read_failed_ = true;
break;
}
this->read_failed_ = false;
} else if (!this->read_failed_) {
ESP_LOGW(TAG, "UART read failed");
this->read_failed_ = true;
avail -= to_read;
for (size_t i = 0; i < to_read; i++)
this->readline_(buf[i]);
}
this->run_sequencer_();
}
@@ -313,18 +324,17 @@ void DFRobotSEN0609Component::log_bad_frame_(const LogString *problem, const cha
}
#ifdef USE_SENSOR
static void publish_deduplicated(sensor::SensorWithDedup<float> &sens, float value) {
if (std::isnan(value)) {
sens.publish_state_unknown();
} else {
sens.publish_state_if_not_dup(value);
}
static void publish_deduplicated(sensor::Sensor *sens, Deduplicator<float> &dedup, float value) {
if (sens == nullptr)
return;
if (std::isnan(value) ? dedup.next_unknown() : dedup.next(value))
sens->publish_state(value);
}
void DFRobotSEN0609Component::publish_target_(float distance, float speed, float energy) {
publish_deduplicated(this->target_distance_sensor_, distance);
publish_deduplicated(this->target_speed_sensor_, speed);
publish_deduplicated(this->target_energy_sensor_, energy);
publish_deduplicated(this->target_distance_sensor_, this->distance_dedup_, distance);
publish_deduplicated(this->target_speed_sensor_, this->speed_dedup_, speed);
publish_deduplicated(this->target_energy_sensor_, this->energy_dedup_, energy);
}
#endif
@@ -443,15 +453,6 @@ void DFRobotSEN0609Component::control_number(NumberSetting setting, float value)
this->request_write_(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[setting])));
}
void DFRobotSEN0609Component::control_number(number::Number *number, float value) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (this->numbers_[i] == number) {
this->control_number(static_cast<NumberSetting>(i), value);
return;
}
}
}
void DFRobotSEN0609Component::forget_number_changes_(Param param, bool unknown) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (progmem_read_byte(&NUMBER_PARAMS[i]) != param)
@@ -7,7 +7,7 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor_with_dedup.h"
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
@@ -62,6 +62,12 @@ enum SwitchSetting : uint8_t {
SWITCH_COUNT,
};
enum ButtonAction : uint8_t {
BUTTON_RESTART,
BUTTON_FACTORY_RESET,
BUTTON_QUERY_PARAMS,
};
/// Module parameters; a write or read of each is one command. Pending work runs in this order.
enum Param : uint8_t {
PARAM_RUN_APP,
@@ -97,9 +103,9 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
SUB_BINARY_SENSOR(has_target)
#endif
#ifdef USE_SENSOR
SUB_SENSOR_WITH_DEDUP(target_distance, float)
SUB_SENSOR_WITH_DEDUP(target_speed, float)
SUB_SENSOR_WITH_DEDUP(target_energy, float)
SUB_SENSOR(target_distance)
SUB_SENSOR(target_speed)
SUB_SENSOR(target_energy)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
@@ -124,8 +130,6 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
#ifdef USE_NUMBER
void set_number(NumberSetting setting, number::Number *number) { this->numbers_[setting] = number; }
void control_number(NumberSetting setting, float value);
// Finds the setting by pointer so every number shares one Apply function
void control_number(number::Number *number, float value);
#endif
#ifdef USE_SWITCH
void set_switch(SwitchSetting setting, switch_::Switch *sw) { this->switches_[setting] = sw; }
@@ -171,6 +175,9 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
#endif
#ifdef USE_SENSOR
void publish_target_(float distance, float speed, float energy);
Deduplicator<float> distance_dedup_;
Deduplicator<float> speed_dedup_;
Deduplicator<float> energy_dedup_;
#endif
#ifdef USE_NUMBER
@@ -18,7 +18,6 @@ from esphome.const import (
UNIT_SECOND,
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
from .. import (
@@ -29,8 +28,10 @@ from .. import (
DFRobotSEN0609Component,
dfrobot_sen0609_ns,
final_validate_mode,
get_hub_config,
)
DFRobotSEN0609Number = dfrobot_sen0609_ns.class_("DFRobotSEN0609Number", number.Number)
NumberSetting = dfrobot_sen0609_ns.enum("NumberSetting")
CONF_HOLD_SENSITIVITY = "hold_sensitivity"
@@ -54,7 +55,7 @@ class Setting(NamedTuple):
def _distance(icon: str = ICON_RULER) -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
@@ -64,7 +65,7 @@ def _distance(icon: str = ICON_RULER) -> cv.Schema:
def _duration() -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
@@ -74,7 +75,7 @@ def _duration() -> cv.Schema:
def _plain() -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
)
@@ -180,14 +181,14 @@ FINAL_VALIDATE_SCHEMA = final_validate_mode(
async def to_code(config: ConfigType) -> None:
hub_id = config[CONF_DFROBOT_SEN0609_ID]
hub = await cg.get_variable(hub_id)
max_range = MAX_RANGES[fv.get_declaring_config(hub_id, CORE.config)[CONF_MODEL]]
max_range = MAX_RANGES[get_hub_config(CORE.config, hub_id)[CONF_MODEL]]
numbers = number.sub_numbers(config, parent=hub)
for s in SETTINGS:
await numbers(
s.key,
lambda n, setting=s.setting: hub.set_number(setting, n),
s.setting,
min_value=s.min_value,
max_value=max_range if s.max_value is None else s.max_value,
step=s.step,
apply="control_number(entity, value)",
)
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Number final : public number::Number, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Number(NumberSetting setting) : setting_(setting) {}
protected:
void control(float value) override { this->parent_->control_number(this->setting_, value); }
NumberSetting setting_;
};
} // namespace esphome::dfrobot_sen0609
+4 -2
View File
@@ -28,7 +28,7 @@ from esphome.const import (
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.final_validate import full_config, get_declaring_config
from esphome.final_validate import full_config
from esphome.types import ConfigType
DOMAIN = "display"
@@ -225,7 +225,9 @@ def get_display_metadata(display_id: ID) -> DisplayMetaData:
return meta
# No metadata found, display driver may not yet support it.
# Read the raw config to populate the returned data
disp_config = get_declaring_config(display_id)
global_config = full_config.get()
path = global_config.get_path_for_id(display_id)[:-1]
disp_config = global_config.get_config_for_path(path)
dimensions = disp_config.get(CONF_DIMENSIONS, (0, 0))
if isinstance(dimensions, dict):
dimensions = (dimensions.get(CONF_WIDTH, 0), dimensions.get(CONF_HEIGHT, 0))
+36 -15
View File
@@ -1,5 +1,7 @@
#include "ds1603l.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -8,28 +10,35 @@ namespace esphome::ds1603l {
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
void DS1603L::loop() {
this->read_available([this](uint8_t byte) {
const uint8_t *frame = this->reader_.feed(byte);
if (frame == nullptr) {
if (this->reader_.length() == 0) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
}
return;
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
uint8_t byte;
while (this->available() > 0 && this->read_byte(&byte)) {
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
continue;
}
if (!this->parse_data_(frame)) {
this->rx_buffer_[this->rx_count_++] = byte;
if (this->rx_count_ < FRAME_SIZE) {
continue;
}
if (this->parse_data_()) {
this->rx_count_ = 0;
} else {
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
this->reader_.resync();
this->resync_();
}
});
}
}
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
bool DS1603L::parse_data_(const uint8_t *frame) {
uint8_t header = frame[0];
uint8_t data_h = frame[1];
uint8_t data_l = frame[2];
uint8_t checksum = frame[3];
bool DS1603L::parse_data_() {
uint8_t header = this->rx_buffer_[0];
uint8_t data_h = this->rx_buffer_[1];
uint8_t data_l = this->rx_buffer_[2];
uint8_t checksum = this->rx_buffer_[3];
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
@@ -44,4 +53,16 @@ bool DS1603L::parse_data_(const uint8_t *frame) {
return true;
}
void DS1603L::resync_() {
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
size_t start = 1;
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
start++;
}
this->rx_count_ -= start;
if (this->rx_count_ > 0) {
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
}
}
} // namespace esphome::ds1603l
+6 -4
View File
@@ -6,7 +6,6 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/frame_reader.h"
namespace esphome::ds1603l {
@@ -19,10 +18,13 @@ class DS1603L final : public sensor::Sensor, public Component, public uart::UART
static constexpr uint8_t HEADER_BYTE = 0xFF;
static constexpr size_t FRAME_SIZE = 4;
// Validates the checksum of the frame and publishes it. Returns false if the frame is invalid.
bool parse_data_(const uint8_t *frame);
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
bool parse_data_();
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
void resync_();
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
};
} // namespace esphome::ds1603l
+3 -1
View File
@@ -32,7 +32,9 @@ CONFIG_SCHEMA = cv.Schema(
def _final_validate(config: ConfigType) -> None:
"""Validate that the channel is within the parent's channel count."""
parent_config = fv.get_declaring_config(config[CONF_DS248X_ID])
fconf = fv.full_config.get()
path = fconf.get_path_for_id(config[CONF_DS248X_ID])[:-1]
parent_config = fconf.get_config_for_path(path)
channel_count = get_channel_count(parent_config)
channel = config[CONF_CHANNEL]
+25 -18
View File
@@ -50,7 +50,7 @@ void ES7210::setup() {
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF1_REG21, 0x2a));
// Secondary I2S mode settings
ES7210_ERROR_FAILED(this->update_register(ES7210_MODE_CONFIG_REG08, 0x01, 0x00));
ES7210_ERROR_FAILED(this->es7210_update_reg_bit_(ES7210_MODE_CONFIG_REG08, 0x01, 0x00));
// Configure analog power
ES7210_ERROR_FAILED(this->write_byte(ES7210_ANALOG_REG40, 0xC3));
@@ -141,39 +141,39 @@ bool ES7210::configure_sample_rate_() {
bool ES7210::configure_mic_gain_() {
for (uint8_t i = 0; i < 4; ++i) {
ES7210_ERROR_CHECK(this->update_register(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00));
}
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0xff));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0xff));
// Configure mic 1
ES7210_ERROR_CHECK(this->update_register(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
ES7210_ERROR_CHECK(this->update_register(ES7210_MIC1_GAIN_REG43, 0x10, 0x10));
ES7210_ERROR_CHECK(
this->update_register(ES7210_MIC1_GAIN_REG43, 0x0f, this->es7210_gain_reg_value_(this->channel_gains_[0])));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f,
this->es7210_gain_reg_value_(this->channel_gains_[0])));
// Configure mic 2
ES7210_ERROR_CHECK(this->update_register(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
ES7210_ERROR_CHECK(this->update_register(ES7210_MIC2_GAIN_REG44, 0x10, 0x10));
ES7210_ERROR_CHECK(
this->update_register(ES7210_MIC2_GAIN_REG44, 0x0f, this->es7210_gain_reg_value_(this->channel_gains_[1])));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f,
this->es7210_gain_reg_value_(this->channel_gains_[1])));
// Configure mic 3
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
ES7210_ERROR_CHECK(this->update_register(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->update_register(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(
this->update_register(ES7210_MIC3_GAIN_REG45, 0x0f, this->es7210_gain_reg_value_(this->channel_gains_[2])));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f,
this->es7210_gain_reg_value_(this->channel_gains_[2])));
// Configure mic 4
ES7210_ERROR_CHECK(this->update_register(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->update_register(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(
this->update_register(ES7210_MIC4_GAIN_REG46, 0x0f, this->es7210_gain_reg_value_(this->channel_gains_[3])));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f,
this->es7210_gain_reg_value_(this->channel_gains_[3])));
return true;
}
@@ -227,4 +227,11 @@ bool ES7210::configure_i2s_format_() {
return true;
}
bool ES7210::es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data) {
uint8_t regv;
ES7210_ERROR_CHECK(this->read_byte(reg_addr, &regv));
regv = (regv & (~update_bits)) | (update_bits & data);
return this->write_byte(reg_addr, regv);
}
} // namespace esphome::es7210
+7
View File
@@ -40,6 +40,13 @@ class ES7210 final : public audio_adc::AudioAdc, public Component, public i2c::I
float mic_gain() override { return this->channel_gains_[0]; };
protected:
/// @brief Updates an I2C registry address by modifying the current state
/// @param reg_addr I2C register address
/// @param update_bits Mask of allowed bits to be modified
/// @param data Bit values to be written
/// @return True if successful, false otherwise
bool es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data);
/// @brief Convert floating point mic gain value to register value
/// @param mic_gain Gain value to convert
/// @return Corresponding register value for specified gain
+5 -3
View File
@@ -1,9 +1,9 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.audio_dac import AudioDac
from esphome.components.speaker import DOMAIN as SPEAKER_DOMAIN, get_audio_dac_ids
from esphome.components.speaker import DOMAIN as SPEAKER_DOMAIN
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID
from esphome.const import CONF_AUDIO_DAC, CONF_BITS_PER_SAMPLE, CONF_ID
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -30,8 +30,10 @@ def _final_validate(config: ConfigType) -> None:
# Check all speaker configurations for ones that reference this es8156
speaker_configs = full_config.get(SPEAKER_DOMAIN, [])
for speaker_config in speaker_configs:
audio_dac_id = speaker_config.get(CONF_AUDIO_DAC)
if (
config[CONF_ID] in get_audio_dac_ids(speaker_config)
audio_dac_id is not None
and audio_dac_id == config[CONF_ID]
and (bits_per_sample := speaker_config.get(CONF_BITS_PER_SAMPLE))
is not None
and bits_per_sample > 24
+13 -2
View File
@@ -91,9 +91,20 @@ float ES8156::volume() {
}
bool ES8156::set_mute_state_(bool mute_state) {
uint8_t reg13;
this->is_muted_ = mute_state;
const uint8_t mask = BIT(1) | BIT(2);
return this->update_register(ES8156_REG13_DAC_MUTE, mask, mute_state ? mask : 0);
if (!this->read_byte(ES8156_REG13_DAC_MUTE, &reg13)) {
return false;
}
ESP_LOGV(TAG, "Read ES8156_REG13_DAC_MUTE: %u", reg13);
assign_bits(reg13, BIT(1) | BIT(2), mute_state);
ESP_LOGV(TAG, "Setting ES8156_REG13_DAC_MUTE to %u (muted: %s)", reg13, YESNO(mute_state));
return this->write_byte(ES8156_REG13_DAC_MUTE, reg13);
}
} // namespace esphome::es8156
+35 -11
View File
@@ -138,9 +138,12 @@ bool ES8311::configure_clock_() {
}
// Register 0x02
ES8311_ERROR_CHECK(
this->update_register(ES8311_REG02_CLK_MANAGER, 0xF8,
((coefficient->pre_div - 1) << 5) | (encode_pre_mult(coefficient->pre_mult) << 3)));
uint8_t reg02;
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, &reg02));
reg02 &= 0x07;
reg02 |= (coefficient->pre_div - 1) << 5;
reg02 |= encode_pre_mult(coefficient->pre_mult) << 3;
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02));
// Register 0x03
const uint8_t reg03 = (coefficient->fs_mode << 6) | coefficient->adc_osr;
@@ -154,13 +157,23 @@ bool ES8311::configure_clock_() {
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG05_CLK_MANAGER, reg05));
// Register 0x06
uint8_t reg06 = coefficient->bclk_div < 19 ? coefficient->bclk_div - 1 : coefficient->bclk_div;
if (this->sclk_inverted_)
reg06 |= BIT(5);
ES8311_ERROR_CHECK(this->update_register(ES8311_REG06_CLK_MANAGER, BIT(5) | 0x1F, reg06));
uint8_t reg06;
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, &reg06));
assign_bits(reg06, BIT(5), this->sclk_inverted_);
reg06 &= 0xE0;
if (coefficient->bclk_div < 19) {
reg06 |= (coefficient->bclk_div - 1) << 0;
} else {
reg06 |= (coefficient->bclk_div) << 0;
}
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG06_CLK_MANAGER, reg06));
// Register 0x07
ES8311_ERROR_CHECK(this->update_register(ES8311_REG07_CLK_MANAGER, 0x3F, coefficient->lrck_h));
uint8_t reg07;
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG07_CLK_MANAGER, &reg07));
reg07 &= 0xC0;
reg07 |= coefficient->lrck_h << 0;
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG07_CLK_MANAGER, reg07));
// Register 0x08
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG08_CLK_MANAGER, coefficient->lrck_l));
@@ -171,7 +184,10 @@ bool ES8311::configure_clock_() {
bool ES8311::configure_format_() {
// Configure I2S mode and format
ES8311_ERROR_CHECK(this->update_register(ES8311_REG00_RESET, BIT(6), 0));
uint8_t reg00;
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG00_RESET, &reg00));
reg00 &= 0xBF;
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG00_RESET, reg00));
// Configure SDP in resolution
uint8_t reg09 = calculate_resolution_value(this->resolution_in_);
@@ -200,9 +216,17 @@ bool ES8311::configure_mic_() {
}
bool ES8311::set_mute_state_(bool mute_state) {
uint8_t reg31;
this->is_muted_ = mute_state;
const uint8_t mask = BIT(6) | BIT(5);
return this->update_register(ES8311_REG31_DAC, mask, mute_state ? mask : 0);
if (!this->read_byte(ES8311_REG31_DAC, &reg31)) {
return false;
}
assign_bits(reg31, BIT(6) | BIT(5), mute_state);
return this->write_byte(ES8311_REG31_DAC, reg31);
}
} // namespace esphome::es8311
@@ -1,18 +1,20 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from .. import CONF_ESPECTRE_ID, ESPectreComponent
from .. import CONF_ESPECTRE_ID, ESPectreComponent, espectre_ns
DEPENDENCIES = ["espectre"]
RecalibrateButton = espectre_ns.class_("RecalibrateButton", button.Button)
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
RecalibrateButton, entity_category=ENTITY_CATEGORY_CONFIG
).extend({cv.GenerateID(CONF_ESPECTRE_ID): cv.use_id(ESPectreComponent)})
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_ESPECTRE_ID])
await buttons.create(config, None, apply="recalibrate()")
var = await button.new_button(config)
await cg.register_parented(var, config[CONF_ESPECTRE_ID])
@@ -0,0 +1,19 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESPECTRE
#include "esphome/components/button/button.h"
#include "../espectre.h"
namespace esphome::espectre {
class RecalibrateButton final : public button::Button, public Parented<ESPectreComponent> {
protected:
void press_action() override { this->parent_->recalibrate(); }
};
} // namespace esphome::espectre
#endif // USE_ESPECTRE
@@ -17,20 +17,16 @@ void IPAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const n
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
uint8_t sensor = 0;
for (const auto &ip : ips) {
if (!ip.is_set())
continue;
if (sensor == ETHERNET_INFO_IP_SENSOR_COUNT)
break;
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
sensor++;
}
#endif
}
void DNSAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
@@ -18,19 +18,14 @@ class IPAddressEthernetInfo final : public Component,
void setup() override;
void dump_config() override;
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
#endif
// EthernetIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
protected:
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
// Sized to the highest configured address_N
std::array<text_sensor::TextSensor *, ETHERNET_INFO_IP_SENSOR_COUNT> ip_sensors_{};
#endif
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
};
class DNSAddressEthernetInfo final : public Component,
@@ -12,7 +12,6 @@ from esphome.types import ConfigType
DEPENDENCIES = ["ethernet"]
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
_request_ip_sensor_slot = cg.slot_counter("ETHERNET_INFO_IP_SENSOR_COUNT")
IPAddressEthernetInfo = ethernet_info_ns.class_(
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.Component
@@ -58,13 +57,10 @@ async def to_code(config: ConfigType) -> None:
if conf := config.get(CONF_IP_ADDRESS):
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
indexes = [x for x in range(5) if f"address_{x}" in conf]
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
for _ in range(max(indexes, default=-1) + 1):
_request_ip_sensor_slot(str(ip_info))
for x in indexes:
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
cg.add(ip_info.add_ip_sensors(x, sens))
for x in range(5):
if sensor_conf := conf.get(f"address_{x}"):
sens = await text_sensor.new_text_sensor(sensor_conf)
cg.add(ip_info.add_ip_sensors(x, sens))
if conf := config.get(CONF_DNS_ADDRESS):
dns_info = await text_sensor.new_text_sensor(conf)
await cg.register_component(dns_info, conf)
+11 -7
View File
@@ -1,5 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -9,15 +10,15 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_EVENT,
CONF_WEB_SERVER,
DEVICE_CLASS_BUTTON,
DEVICE_CLASS_DOORBELL,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_MOTION,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -43,13 +44,11 @@ EventPtr = Event.operator("ptr")
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_EVENT_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTEventComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent),
cv.GenerateID(): cv.declare_id(Event),
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
@@ -105,7 +104,12 @@ async def setup_event_core_(
setup_device_class(config)
await entity_helpers.setup_entity_integrations(var, config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_event(
+17 -25
View File
@@ -1,7 +1,6 @@
#include "ezo_pmp.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::ezo_pmp {
@@ -32,24 +31,11 @@ static const uint16_t EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED = 65;
static const uint16_t EZO_PMP_COMMAND_READ_CALIBRATION_STATUS = 129;
static const uint16_t EZO_PMP_COMMAND_READ_PUMP_VOLTAGE = 257;
#ifdef USE_TEXT_SENSOR
enum DosingMode : uint8_t {
DOSING_MODE_NONE,
DOSING_MODE_VOLUME,
DOSING_MODE_VOLUME_OVER_TIME,
DOSING_MODE_CONSTANT_FLOW_RATE,
DOSING_MODE_CONTINUOUS,
};
PROGMEM_STRING_TABLE(DosingModeStrings, "None", "Volume", "Volume/Time", "Constant Flow Rate", "Continuous");
static void publish_dosing_mode(text_sensor::TextSensor *sensor, DosingMode mode) {
if (sensor == nullptr)
return;
const ProgmemStr name = DosingModeStrings::get_progmem_str(mode, DOSING_MODE_NONE);
if (ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(name)) != 0)
sensor->publish_state(name);
}
#endif
static const std::string DOSING_MODE_NONE = "None";
static const std::string DOSING_MODE_VOLUME = "Volume";
static const std::string DOSING_MODE_VOLUME_OVER_TIME = "Volume/Time";
static const std::string DOSING_MODE_CONSTANT_FLOW_RATE = "Constant Flow Rate";
static const std::string DOSING_MODE_CONTINUOUS = "Continuous";
void EzoPMP::dump_config() {
LOG_I2C_DEVICE(this);
@@ -226,7 +212,8 @@ void EzoPMP::read_command_result_() {
#ifdef USE_TEXT_SENSOR
if (!this->is_dosing_flag_ && !this->is_paused_flag_) {
// If pump is not paused and not dispensing
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_NONE);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
}
#endif
@@ -298,25 +285,29 @@ void EzoPMP::read_command_result_() {
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_VOLUME);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_VOLUME_OVER_TIME);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_CONSTANT_FLOW_RATE);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE)
this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE);
#endif
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_CONTINUOUS);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS)
this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS);
#endif
break;
@@ -327,7 +318,8 @@ void EzoPMP::read_command_result_() {
this->is_paused_->publish_state(this->is_paused_flag_);
#endif
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_NONE);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
#endif
break;
+12 -8
View File
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DIRECTION,
@@ -28,11 +29,11 @@ from esphome.const import (
CONF_SPEED_LEVEL_STATE_TOPIC,
CONF_SPEED_STATE_TOPIC,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -77,16 +78,14 @@ FanPresetSetTrigger = fan_ns.class_(
)
_FAN_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
RESTORE_MODES, upper=True, space="_"
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTFanComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent),
cv.Optional(CONF_DIRECTION_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
@@ -241,8 +240,10 @@ _TRIGGER_AUTOMATIONS = (
async def setup_fan_core_(var, config):
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (
direction_state_topic := config.get(CONF_DIRECTION_STATE_TOPIC)
) is not None:
@@ -276,6 +277,9 @@ async def setup_fan_core_(var, config):
if (speed_command_topic := config.get(CONF_SPEED_COMMAND_TOPIC)) is not None:
cg.add(mqtt_.set_custom_speed_command_topic(speed_command_topic))
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_parent_callback_automation(
var, "add_on_state_callback", (Fan.operator("ptr"), "x"), conf
+15 -37
View File
@@ -23,7 +23,10 @@ CONF_REQUEST_VOLTAGE = "request_voltage"
fusb302b_ns = cg.esphome_ns.namespace("fusb302b")
FUSB302B = fusb302b_ns.class_("FUSB302B", cg.Component, i2c.I2CDevice)
PdState = fusb302b_ns.enum("PdState", is_class=True)
ConnectForwarder = fusb302b_ns.class_("ConnectForwarder")
DisconnectForwarder = fusb302b_ns.class_("DisconnectForwarder")
PowerReadyForwarder = fusb302b_ns.class_("PowerReadyForwarder")
ErrorForwarder = fusb302b_ns.class_("ErrorForwarder")
# USB PD fixed supply voltages are 5, 9, 12, 15 and 20 V; the highest one that does not exceed the
# requested voltage is chosen.
@@ -47,43 +50,18 @@ CONFIG_SCHEMA = cv.All(
cv.only_on_esp32,
)
_STATE_PARAMS = [(PdState, "state"), (PdState, "previous")]
_CONNECTED = "fusb302b::is_connected_state"
# Each state trigger fires on the change described by its filter.
_STATE_TRIGGERS = (
# A source is attached, including after recovering from a communication error.
(CONF_ON_CONNECT, f"{_CONNECTED}(state) && !{_CONNECTED}(previous)"),
# The source is removed, or is no longer known to be attached because communication failed.
(CONF_ON_DISCONNECT, f"!{_CONNECTED}(state) && {_CONNECTED}(previous)"),
# Negotiation is finished: a new PD contract is in place, or the source does not speak PD.
(
CONF_ON_POWER_READY,
(
"state == fusb302b::PdState::PD_STATE_EXPLICIT_CONTRACT"
" || state == fusb302b::PdState::PD_STATE_PD_TIMEOUT"
),
),
# Communication with the chip fails.
(
CONF_ON_ERROR,
(
"state == fusb302b::PdState::PD_STATE_ERROR"
" && previous != fusb302b::PdState::PD_STATE_ERROR"
),
),
)
_CALLBACK_AUTOMATIONS = (
*(
automation.CallbackAutomation(
key,
"add_on_state_callback",
params=_STATE_PARAMS,
forward=[],
when=when,
)
for key, when in _STATE_TRIGGERS
automation.CallbackAutomation(
CONF_ON_CONNECT, "add_on_state_callback", forwarder=ConnectForwarder
),
automation.CallbackAutomation(
CONF_ON_DISCONNECT, "add_on_state_callback", forwarder=DisconnectForwarder
),
automation.CallbackAutomation(
CONF_ON_POWER_READY, "add_on_state_callback", forwarder=PowerReadyForwarder
),
automation.CallbackAutomation(
CONF_ON_ERROR, "add_on_state_callback", forwarder=ErrorForwarder
),
automation.CallbackAutomation(
CONF_ON_REQUEST_FAILED, "add_on_request_failed_callback"
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <type_traits>
#include "esphome/core/automation.h"
#include "pd.h"
namespace esphome::fusb302b {
/// Triggers when a source is attached, including after recovering from a communication error.
struct ConnectForwarder {
Automation<> *automation;
void operator()(PdState state, PdState previous) const {
if (is_connected_state(state) && !is_connected_state(previous))
this->automation->trigger();
}
};
/// Triggers when the source is removed, or is no longer known to be attached because communication failed.
struct DisconnectForwarder {
Automation<> *automation;
void operator()(PdState state, PdState previous) const {
if (!is_connected_state(state) && is_connected_state(previous))
this->automation->trigger();
}
};
/// Triggers when negotiation is finished: a new PD contract is in place, or the source does not speak PD.
struct PowerReadyForwarder {
Automation<> *automation;
void operator()(PdState state, PdState /*previous*/) const {
if (state == PdState::PD_STATE_EXPLICIT_CONTRACT || state == PdState::PD_STATE_PD_TIMEOUT)
this->automation->trigger();
}
};
/// Triggers when communication with the chip fails.
struct ErrorForwarder {
Automation<> *automation;
void operator()(PdState state, PdState previous) const {
if (state == PdState::PD_STATE_ERROR && previous != PdState::PD_STATE_ERROR)
this->automation->trigger();
}
};
static_assert(sizeof(ConnectForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<ConnectForwarder>);
static_assert(sizeof(DisconnectForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<DisconnectForwarder>);
static_assert(sizeof(PowerReadyForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<PowerReadyForwarder>);
static_assert(sizeof(ErrorForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<ErrorForwarder>);
} // namespace esphome::fusb302b
+6 -2
View File
@@ -265,8 +265,12 @@ bool FUSB302B::init_chip_() {
return false;
// Let the chip retransmit unacknowledged messages up to 3 times
if (!this->update_register(FUSB_CONTROL3, FUSB_CONTROL3_N_RETRIES_MASK | FUSB_CONTROL3_AUTO_RETRY,
(3 << FUSB_CONTROL3_N_RETRIES_SHIFT) | FUSB_CONTROL3_AUTO_RETRY))
uint8_t control3 = 0;
if (!this->read_byte(FUSB_CONTROL3, &control3))
return false;
control3 =
(control3 & ~FUSB_CONTROL3_N_RETRIES_MASK) | (3 << FUSB_CONTROL3_N_RETRIES_SHIFT) | FUSB_CONTROL3_AUTO_RETRY;
if (!this->write_byte(FUSB_CONTROL3, control3))
return false;
if (!this->write_byte(FUSB_POWER, FUSB_POWER_ALL))
+3 -1
View File
@@ -63,7 +63,9 @@ CONFIG_SCHEMA = cv.Schema(
def _validate_model(config: ConfigType) -> None:
climate_conf = fv.get_declaring_config(config[CONF_GREE_ID])
full_config = fv.full_config.get()
climate_path = full_config.get_path_for_id(config[CONF_GREE_ID])[:-1]
climate_conf = full_config.get_config_for_path(climate_path)
if climate_conf[CONF_MODEL] not in SUPPORTED_MODELS:
raise cv.Invalid(
"Gree switches are only supported for the "
+11 -8
View File
@@ -1,10 +1,12 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..climate import CONF_HAIER_ID, HonClimate
from ..climate import CONF_HAIER_ID, HonClimate, haier_ns
CODEOWNERS = ["@paveldn"]
SelfCleaningButton = haier_ns.class_("SelfCleaningButton", button.Button)
SteriCleaningButton = haier_ns.class_("SteriCleaningButton", button.Button)
# Haier buttons
@@ -18,18 +20,19 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_SELF_CLEANING): button.button_schema(
button.ApplyButton,
SelfCleaningButton,
icon=ICON_SPRAY_BOTTLE,
),
cv.Optional(CONF_STERI_CLEANING): button.button_schema(
button.ApplyButton,
SteriCleaningButton,
icon=ICON_SPRAY_BOTTLE,
),
}
)
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_HAIER_ID])
await buttons(CONF_SELF_CLEANING, None, apply="start_self_cleaning()")
await buttons(CONF_STERI_CLEANING, None, apply="start_steri_cleaning()")
async def to_code(config):
for button_type in [CONF_SELF_CLEANING, CONF_STERI_CLEANING]:
if conf := config.get(button_type):
btn = await button.new_button(conf)
await cg.register_parented(btn, config[CONF_HAIER_ID])
@@ -0,0 +1,7 @@
#include "self_cleaning.h"
namespace esphome::haier {
void SelfCleaningButton::press_action() { this->parent_->start_self_cleaning(); }
} // namespace esphome::haier
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome::haier {
class SelfCleaningButton final : public button::Button, public Parented<HonClimate> {
public:
SelfCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::haier
@@ -0,0 +1,7 @@
#include "steri_cleaning.h"
namespace esphome::haier {
void SteriCleaningButton::press_action() { this->parent_->start_steri_cleaning(); }
} // namespace esphome::haier
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome::haier {
class SteriCleaningButton final : public button::Button, public Parented<HonClimate> {
public:
SteriCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::haier
+3 -1
View File
@@ -52,10 +52,12 @@ CONFIG_SCHEMA = cv.Schema(
def _final_validate(config) -> None:
full_config = fv.full_config.get()
for switch_type in [CONF_BEEPER, CONF_QUIET_MODE]:
# Check switches that are only supported for HonClimate
if config.get(switch_type):
climate_conf = fv.get_declaring_config(config[CONF_HAIER_ID])
climate_path = full_config.get_path_for_id(config[CONF_HAIER_ID])[:-1]
climate_conf = full_config.get_config_for_path(climate_path)
protocol_type = climate_conf.get(CONF_PROTOCOL)
if protocol_type.casefold() != PROTOCOL_HON.casefold():
raise cv.Invalid(
@@ -1,9 +1,10 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ICON_AIR_FILTER
from esphome.types import ConfigType
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
DEPENDENCIES = ["hoermann_hcp"]
@@ -12,6 +13,10 @@ CONF_VENT = "vent"
ICON_GARAGE_OPEN_VARIANT = "mdi:garage-open-variant"
HoermannHcpVentButton = hoermann_hcp_ns.class_("HoermannHcpVentButton", button.Button)
HoermannHcpHalfOpenButton = hoermann_hcp_ns.class_(
"HoermannHcpHalfOpenButton", button.Button
)
BUTTON_KEYS = (CONF_VENT, CONF_HALF_OPEN)
@@ -20,10 +25,10 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(CONF_HOERMANN_HCP_ID): cv.use_id(HoermannHcp),
cv.Optional(CONF_VENT): button.button_schema(
button.ApplyButton, icon=ICON_AIR_FILTER
HoermannHcpVentButton, icon=ICON_AIR_FILTER
),
cv.Optional(CONF_HALF_OPEN): button.button_schema(
button.ApplyButton, icon=ICON_GARAGE_OPEN_VARIANT
HoermannHcpHalfOpenButton, icon=ICON_GARAGE_OPEN_VARIANT
),
}
),
@@ -32,6 +37,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_HOERMANN_HCP_ID])
await buttons(CONF_VENT, None, apply="vent_door()")
await buttons(CONF_HALF_OPEN, None, apply="half_open_door()")
parent = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
for key in BUTTON_KEYS:
if (conf := config.get(key)) is not None:
await button.new_button(conf, parent)
@@ -0,0 +1,34 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hoermann_hcp.h"
namespace esphome::hoermann_hcp {
// The door commands the cover has no equivalent for. A refused command is already reported by the hub and
// leaves nothing to correct here, because a button carries no state of its own.
class HoermannHcpButton : public button::Button {
public:
explicit HoermannHcpButton(HoermannHcp *parent) : parent_(parent) {}
protected:
HoermannHcp *const parent_;
};
class HoermannHcpVentButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->vent_door(); }
};
class HoermannHcpHalfOpenButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->half_open_door(); }
};
} // namespace esphome::hoermann_hcp
@@ -3,8 +3,9 @@ import esphome.codegen as cg
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME
from esphome.core import coroutine_with_priority
from esphome.core import ID, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns
@@ -20,6 +21,9 @@ CONF_MD5_URL = "md5_url"
OtaHttpRequestComponent = http_request_ns.class_(
"OtaHttpRequestComponent", OTAComponent
)
OtaHttpRequestComponentFlashAction = http_request_ns.class_(
"OtaHttpRequestComponentFlashAction", automation.Action
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -64,13 +68,38 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
)
automation.register_apply_action(
@automation.register_action(
"ota.http_request.flash",
OtaHttpRequestComponentFlashAction,
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
automation.ApplyField(CONF_MD5_URL, "set_md5_url", cg.std_string),
automation.ApplyField(CONF_MD5, "set_md5", cg.std_string),
automation.ApplyField(CONF_PASSWORD, "set_password", cg.std_string),
automation.ApplyField(CONF_USERNAME, "set_username", cg.std_string),
automation.ApplyField(CONF_URL, "set_url", cg.std_string),
automation.ApplyCall("flash()"),
synchronous=True,
)
async def ota_http_request_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if md5_url := config.get(CONF_MD5_URL):
template_ = await cg.templatable(md5_url, args, cg.std_string)
cg.add(var.set_md5_url(template_))
if md5_str := config.get(CONF_MD5):
template_ = await cg.templatable(md5_str, args, cg.std_string)
cg.add(var.set_md5(template_))
if password_str := config.get(CONF_PASSWORD):
template_ = await cg.templatable(password_str, args, cg.std_string)
cg.add(var.set_password(template_))
if username_str := config.get(CONF_USERNAME):
template_ = await cg.templatable(username_str, args, cg.std_string)
cg.add(var.set_username(template_))
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
return var
@@ -0,0 +1,40 @@
#pragma once
#include "ota_http_request.h"
#include "esphome/core/automation.h"
namespace esphome::http_request {
template<typename... Ts> class OtaHttpRequestComponentFlashAction final : public Action<Ts...> {
public:
OtaHttpRequestComponentFlashAction(OtaHttpRequestComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, md5_url)
TEMPLATABLE_VALUE(std::string, md5)
TEMPLATABLE_VALUE(std::string, password)
TEMPLATABLE_VALUE(std::string, url)
TEMPLATABLE_VALUE(std::string, username)
void play(const Ts &...x) override {
if (this->md5_url_.has_value()) {
this->parent_->set_md5_url(this->md5_url_.value(x...));
}
if (this->md5_.has_value()) {
this->parent_->set_md5(this->md5_.value(x...));
}
if (this->password_.has_value()) {
this->parent_->set_password(this->password_.value(x...));
}
if (this->username_.has_value()) {
this->parent_->set_username(this->username_.value(x...));
}
this->parent_->set_url(this->url_.value(x...));
this->parent_->flash();
// Normally never reached due to reboot
}
protected:
OtaHttpRequestComponent *parent_;
};
} // namespace esphome::http_request
+3 -41
View File
@@ -41,13 +41,11 @@ from esphome.const import (
CONF_FREQUENCY,
CONF_I2C_ID,
CONF_ID,
CONF_INVERTED,
CONF_LOW_POWER_MODE,
CONF_SCAN,
CONF_SCL,
CONF_SDA,
CONF_TIMEOUT,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
@@ -70,7 +68,6 @@ ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
HostI2CBus = i2c_ns.class_("HostI2CBus", I2CBus, cg.Component)
SoftwareI2CBus = i2c_ns.class_("SoftwareI2CBus", I2CBus, cg.Component)
I2CDevice = i2c_ns.class_("I2CDevice")
ESP32_I2C_CAPABILITIES = {
@@ -111,8 +108,6 @@ def validate_device(value: str) -> str:
def _bus_declare_type(value: Any) -> ID:
if CORE.is_esp32:
return cv.declare_id(IDFI2CBus)(value)
if CORE.is_libretiny:
return cv.declare_id(SoftwareI2CBus)(value)
if CORE.using_arduino:
return cv.declare_id(ArduinoI2CBus)(value)
if CORE.using_zephyr:
@@ -133,28 +128,7 @@ def _rp2040_i2c_controller(pin: int) -> int:
return (pin // 2) % 2
def _i2c_pin_number(value: Any) -> Any:
# The software bus on LibreTiny drives the pins through the GPIO pin objects and
# switches them between input with pull-up and output at runtime.
if CORE.is_libretiny:
return pins.internal_gpio_input_pullup_pin_schema(value)
return pins.internal_gpio_pin_number(value)
def validate_config(config: ConfigType) -> ConfigType:
if CORE.is_libretiny:
if int(config[CONF_FREQUENCY]) < 1:
raise cv.Invalid("The frequency must be at least 1Hz", [CONF_FREQUENCY])
# no default pins on LibreTiny, the bus is bit-banged on any two GPIOs
for key in (CONF_SDA, CONF_SCL):
if key not in config:
raise cv.Invalid(f"'{key}' is required on this platform", [key])
if config[key].get(CONF_INVERTED):
# the software bus pulls a line low by writing a low level to the pin
raise cv.Invalid(
f"'{CONF_INVERTED}' is not supported on the I2C pins",
[key, CONF_INVERTED],
)
if CORE.is_esp32:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
@@ -207,7 +181,7 @@ CONFIG_SCHEMA = cv.All(
esp8266="SDA",
rp2="SDA",
nrf52="SDA",
): _i2c_pin_number,
): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
@@ -217,7 +191,7 @@ CONFIG_SCHEMA = cv.All(
esp8266="SCL",
rp2="SCL",
nrf52="SCL",
): _i2c_pin_number,
): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
@@ -228,7 +202,6 @@ CONFIG_SCHEMA = cv.All(
rp2="50kHz",
nrf52="100kHz",
host="50kHz",
bk72xx="100kHz",
): cv.All(
cv.frequency,
cv.float_range(min=0, min_included=False),
@@ -252,7 +225,6 @@ CONFIG_SCHEMA = cv.All(
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(
[
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2,
@@ -371,16 +343,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
if CONF_LOW_POWER_MODE in config:
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
elif CORE.is_libretiny:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_sda_pin(await cg.gpio_pin_expression(config[CONF_SDA])))
cg.add(var.set_scl_pin(await cg.gpio_pin_expression(config[CONF_SCL])))
cg.add(var.set_frequency(int(config[CONF_FREQUENCY])))
cg.add(var.set_scan(config[CONF_SCAN]))
if CONF_TIMEOUT in config:
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
else:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -500,6 +462,7 @@ _platform_filter = filter_source_files_from_platform(
"i2c_bus_arduino.cpp": {
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
@@ -507,7 +470,6 @@ _platform_filter = filter_source_files_from_platform(
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"i2c_bus_software.cpp": {PlatformFramework.BK72XX_ARDUINO},
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
"i2c_bus_host.cpp": {PlatformFramework.HOST_NATIVE},
}
-265
View File
@@ -1,265 +0,0 @@
#ifdef USE_LIBRETINY
#include "i2c_bus_software.h"
#include <cinttypes>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::i2c {
ESPHOME_LOG_TAG(TAG, "i2c.software");
// Maximum bytes to log in hex format (truncates larger transfers)
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
// Clock pulses used to free a device that is holding SDA low
static constexpr uint8_t RECOVERY_CLOCK_PULSES = 9;
// A line is released by switching the pin to input with pull-up and pulled low by driving it as output.
static inline void release_line(InternalGPIOPin *pin) { pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
static inline void pull_line_low(InternalGPIOPin *pin) {
pin->digital_write(false);
pin->pin_mode(gpio::FLAG_OUTPUT);
}
void SoftwareI2CBus::setup() {
this->update_half_period_();
this->sda_pin_->setup();
this->scl_pin_->setup();
release_line(this->sda_pin_);
release_line(this->scl_pin_);
if (!this->recover_()) {
// A line that stays low would be read as an ACK for every address and as zero data
ESP_LOGE(TAG, "SDA or SCL is held low, the bus cannot be used");
this->mark_failed();
return;
}
this->initialized_ = true;
if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_();
}
}
void SoftwareI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, "I2C Bus (software):");
LOG_PIN(" SDA Pin: ", this->sda_pin_);
LOG_PIN(" SCL Pin: ", this->scl_pin_);
ESP_LOGCONFIG(TAG,
" Frequency: %" PRIu32 " Hz\n"
" Clock stretch timeout: %" PRIu32 " us",
this->frequency_, this->timeout_);
if (this->recovered_) {
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
}
if (this->scan_) {
ESP_LOGI(TAG, "Results from bus scan:");
if (this->scan_results_.empty()) {
ESP_LOGI(TAG, "Found no devices");
} else {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
}
}
// Half of one clock period in microseconds, rounded up. The frequency is validated to be non-zero.
void SoftwareI2CBus::update_half_period_() {
this->half_period_us_ = (500000 + this->frequency_ - 1) / this->frequency_;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode SoftwareI2CBus::switch_frequency(uint32_t frequency) {
if (frequency == 0)
return ERROR_INVALID_ARGUMENT;
this->frequency_ = frequency;
this->update_half_period_();
return ERROR_OK;
}
#endif
bool SoftwareI2CBus::recover_() {
this->timed_out_ = false;
// A device may still be stretching the clock, give SCL the configured time to rise before looking at SDA
this->release_scl_();
// A device that was interrupted in the middle of a transfer can hold SDA low; clock it out.
bool clocked = false;
for (uint8_t i = 0; i < RECOVERY_CLOCK_PULSES && !this->sda_pin_->digital_read(); i++) {
pull_line_low(this->scl_pin_);
delayMicroseconds(this->half_period_us_);
// wait for the rising edge, the device may stretch the clock
if (!this->release_scl_())
break;
delayMicroseconds(this->half_period_us_);
clocked = true;
}
bool ok = this->sda_pin_->digital_read() && this->scl_pin_->digital_read();
if (ok && clocked) {
// end the interrupted transfer with a STOP so every device returns to idle
this->recovered_ = true;
pull_line_low(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
release_line(this->scl_pin_);
delayMicroseconds(this->half_period_us_);
release_line(this->sda_pin_);
}
if (!ok) {
ESP_LOGW(TAG, "Bus recovery failed, a line is held low");
}
this->timed_out_ = false;
return ok;
}
// Release SCL and wait for it to go high, a device may stretch the clock.
bool SoftwareI2CBus::release_scl_() {
release_line(this->scl_pin_);
uint32_t start = micros();
while (!this->scl_pin_->digital_read()) {
if (micros() - start > this->timeout_) {
this->timed_out_ = true;
return false;
}
}
return true;
}
void SoftwareI2CBus::start_() {
// Also generates a repeated start when SCL is low and SDA is released
release_line(this->sda_pin_);
// After a byte the device may still hold SDA low for its ACK. Without SDA being high before SCL rises there is
// no falling edge, so the (repeated) start would not be seen. Wait until the device lets go.
uint32_t start = micros();
while (!this->sda_pin_->digital_read()) {
if (micros() - start > this->timeout_) {
this->timed_out_ = true;
return;
}
}
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
pull_line_low(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
pull_line_low(this->scl_pin_);
}
void SoftwareI2CBus::stop_() {
if (this->timed_out_) {
// SCL is stuck low, a STOP cannot be generated; just release both lines
release_line(this->scl_pin_);
release_line(this->sda_pin_);
return;
}
pull_line_low(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
release_line(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
}
/// Shift out one byte, returns true if the device acknowledged it.
bool SoftwareI2CBus::write_byte_(uint8_t data) {
for (uint8_t mask = 0x80; mask != 0 && !this->timed_out_; mask >>= 1) {
if (data & mask) {
release_line(this->sda_pin_);
} else {
pull_line_low(this->sda_pin_);
}
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
pull_line_low(this->scl_pin_);
}
if (this->timed_out_)
return false;
release_line(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
if (!this->release_scl_())
return false;
delayMicroseconds(this->half_period_us_);
bool ack = !this->sda_pin_->digital_read();
pull_line_low(this->scl_pin_);
return ack;
}
uint8_t SoftwareI2CBus::read_byte_(bool ack) {
uint8_t data = 0;
release_line(this->sda_pin_);
for (uint8_t i = 0; i < 8 && !this->timed_out_; i++) {
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
data = (data << 1) | (this->sda_pin_->digital_read() ? 1 : 0);
pull_line_low(this->scl_pin_);
}
if (this->timed_out_)
return data;
if (ack) {
pull_line_low(this->sda_pin_);
}
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
pull_line_low(this->scl_pin_);
release_line(this->sda_pin_);
return data;
}
ErrorCode SoftwareI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
uint8_t *read_buffer, size_t read_count) {
if (!this->initialized_) {
ESP_LOGD(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED;
}
// After a failed recovery the bus stays blocked until a later attempt frees both lines
if (this->bus_stuck_ && !this->recover_()) {
return ERROR_TIMEOUT;
}
this->bus_stuck_ = false;
ESP_LOGV_HEX(TAG, write_buffer, write_count, I2C_MAX_LOG_BYTES, "0x%02X TX %s", address);
this->timed_out_ = false;
ErrorCode err = ERROR_OK;
this->start_();
// An empty transfer only addresses the device (used by the bus scan)
if (write_count != 0 || read_count == 0) {
if (!this->write_byte_(address << 1)) {
err = ERROR_NOT_ACKNOWLEDGED;
}
for (size_t i = 0; err == ERROR_OK && i < write_count; i++) {
if (!this->write_byte_(write_buffer[i])) {
ESP_LOGVV(TAG, "TX of byte %u to 0x%02X not acknowledged", (unsigned) i, address);
err = ERROR_NOT_ACKNOWLEDGED;
}
}
}
if (err == ERROR_OK && read_count != 0) {
if (write_count != 0) {
this->start_(); // repeated start
}
if (!this->write_byte_((address << 1) | 1)) {
err = ERROR_NOT_ACKNOWLEDGED;
}
for (size_t i = 0; err == ERROR_OK && !this->timed_out_ && i < read_count; i++) {
read_buffer[i] = this->read_byte_(i + 1 != read_count);
}
}
this->stop_();
if (this->timed_out_) {
ESP_LOGVV(TAG, "Bus line held low during transfer to 0x%02X for more than %" PRIu32 " us", address, this->timeout_);
this->bus_stuck_ = !this->recover_();
return ERROR_TIMEOUT;
}
return err;
}
} // namespace esphome::i2c
#endif // USE_LIBRETINY
-55
View File
@@ -1,55 +0,0 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "i2c_bus.h"
namespace esphome::i2c {
/// Bit-banged I2C controller for LibreTiny chips without an Arduino Wire implementation (BK72xx).
/// SDA and SCL can be any two GPIO pins; both lines are driven open-drain style (pulled low or released),
/// so the bus needs pull-ups (the internal pull-up is enabled while released).
class SoftwareI2CBus final : public I2CBus, public Component {
public:
void setup() override;
void dump_config() override;
ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
size_t read_count) override;
float get_setup_priority() const override { return setup_priority::BUS; }
void set_scan(bool scan) { this->scan_ = scan; }
void set_sda_pin(InternalGPIOPin *sda_pin) { this->sda_pin_ = sda_pin; }
void set_scl_pin(InternalGPIOPin *scl_pin) { this->scl_pin_ = scl_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
/// Maximum time in microseconds a device may hold SCL low (clock stretching).
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
protected:
bool recover_();
void update_half_period_();
void start_();
void stop_();
bool release_scl_();
bool write_byte_(uint8_t data);
uint8_t read_byte_(bool ack);
InternalGPIOPin *sda_pin_{nullptr};
InternalGPIOPin *scl_pin_{nullptr};
uint32_t frequency_{100000};
uint32_t timeout_{1000};
uint32_t half_period_us_{5};
bool timed_out_{false};
bool recovered_{false};
bool initialized_{false};
bool bus_stuck_{false};
};
} // namespace esphome::i2c
#endif // USE_LIBRETINY
@@ -36,7 +36,7 @@ void I2SAudioSpeakerBase::setup() {
return;
}
// Initialize volume control. When audio_dac is configured, this sets every DAC's volume and mute state.
// Initialize volume control. When audio_dac is configured, this sets the DAC volume and mute state.
// When no audio_dac is configured, this initializes software volume control.
this->set_volume(this->volume_);
this->set_mute_state(this->mute_state_);
@@ -101,7 +101,9 @@ void I2SAudioSpeakerBase::loop() {
this->state_ = speaker::STATE_RUNNING;
#ifdef USE_AUDIO_DAC
// The task raises TASK_RUNNING only after enabling the channel, so the clocks are already running
this->notify_audio_dacs_started_();
if (this->audio_dac_ != nullptr) {
this->audio_dac_->on_audio_started();
}
#endif // USE_AUDIO_DAC
}
if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
@@ -202,7 +204,7 @@ void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) {
#ifdef USE_AUDIO_DAC
if (this->has_audio_dac_()) {
if (this->audio_dac_ != nullptr) {
return; // Hardware volume; the ramp stays at unity
}
#endif // USE_AUDIO_DAC
@@ -425,7 +427,7 @@ void I2SAudioSpeakerBase::drain_lockstep_(uint32_t extra_frames) {
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
#ifdef USE_AUDIO_DAC
if (this->has_audio_dac_()) {
if (this->audio_dac_ != nullptr) {
return; // Hardware volume; the ramp is never targeted
}
#endif // USE_AUDIO_DAC
+3 -1
View File
@@ -42,7 +42,9 @@ def _final_validate(config: dict[str, Any]) -> None:
# Get the transmitter configuration
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
transmitter_config = fv.get_declaring_config(transmitter_id)
full_config = fv.full_config.get()
transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1]
transmitter_config = full_config.get_config_for_path(transmitter_path)
# Check if carrier_duty_percent set to 0 or 100
# Note: remote_transmitter schema requires this field and validates 1-100%,
@@ -35,7 +35,11 @@ def _final_validate(config: ConfigType) -> None:
if CONF_REMOTE_TRANSMITTER_ID not in config:
return
transmitter_config = fv.get_declaring_config(config[CONF_REMOTE_TRANSMITTER_ID])
full_config = fv.full_config.get()
transmitter_path = full_config.get_path_for_id(config[CONF_REMOTE_TRANSMITTER_ID])[
:-1
]
transmitter_config = full_config.get_config_for_path(transmitter_path)
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
if duty_percent is not None and duty_percent != 100:
+35 -13
View File
@@ -2,29 +2,51 @@ import esphome.codegen as cg
from esphome.components import button
from esphome.components.const import CONF_QUERY_PARAMS
import esphome.config_validation as cv
from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_RESTART
from esphome.const import (
CONF_FACTORY_RESET,
CONF_ID,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_DATABASE,
ICON_RESTART,
ICON_RESTART_ALERT,
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
FactoryResetButton = ld2410_ns.class_("FactoryResetButton", button.Button)
QueryButton = ld2410_ns.class_("QueryButton", button.Button)
RestartButton = ld2410_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
button.ApplyButton
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
FactoryResetButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
),
cv.Optional(CONF_QUERY_PARAMS): button.button_schema(
QueryButton,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
await buttons(
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="read_all_info()")
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
@@ -0,0 +1,7 @@
#include "factory_reset_button.h"
namespace esphome::ld2410 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class FactoryResetButton final : public button::Button, public Parented<LD2410Component> {
public:
FactoryResetButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,7 @@
#include "query_button.h"
namespace esphome::ld2410 {
void QueryButton::press_action() { this->parent_->read_all_info(); }
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class QueryButton final : public button::Button, public Parented<LD2410Component> {
public:
QueryButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,7 @@
#include "restart_button.h"
namespace esphome::ld2410 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class RestartButton final : public button::Button, public Parented<LD2410Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
+120 -66
View File
@@ -9,12 +9,27 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
namespace esphome::ld2410 {
ESPHOME_LOG_TAG(TAG, "ld2410");
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8,
};
enum DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
enum LightFunction : uint8_t {
LIGHT_FUNCTION_OFF = 0x00,
LIGHT_FUNCTION_BELOW = 0x01,
@@ -54,17 +69,73 @@ enum AckData : uint8_t {
COMMAND_STATUS = 7,
};
// Index + 1 is the BaudRate value; the select options use the same order
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
// Same order as DISTANCE_RESOLUTIONS
PROGMEM_STRING_TABLE(DistanceResolutionStrings, "0.2m", "0.75m");
// The light function and out pin level select options are in enum order
static_assert(LIGHT_FUNCTION_OFF == 0 && LIGHT_FUNCTION_BELOW == 1 && LIGHT_FUNCTION_ABOVE == 2,
"LightFunction is the select option index");
static_assert(OUT_PIN_LEVEL_LOW == 0 && OUT_PIN_LEVEL_HIGH == 1, "OutPinLevel is the select option index");
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
struct Uint8ToString {
const uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = {
{"0.2m", DISTANCE_RESOLUTION_0_2},
{"0.75m", DISTANCE_RESOLUTION_0_75},
};
constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = {
{DISTANCE_RESOLUTION_0_2, "0.2m"},
{DISTANCE_RESOLUTION_0_75, "0.75m"},
};
constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = {
{"off", LIGHT_FUNCTION_OFF},
{"below", LIGHT_FUNCTION_BELOW},
{"above", LIGHT_FUNCTION_ABOVE},
};
constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = {
{LIGHT_FUNCTION_OFF, "off"},
{LIGHT_FUNCTION_BELOW, "below"},
{LIGHT_FUNCTION_ABOVE, "above"},
};
constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = {
{"low", OUT_PIN_LEVEL_LOW},
{"high", OUT_PIN_LEVEL_HIGH},
};
constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
{OUT_PIN_LEVEL_LOW, "low"},
{OUT_PIN_LEVEL_HIGH, "high"},
};
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
for (const auto &entry : arr) {
if (strcmp(str, entry.str) == 0)
return entry.value;
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value)
return entry.str;
}
return ""; // Not found
}
// Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
@@ -124,17 +195,17 @@ void LD2410Component::dump_config() {
#endif
#ifdef USE_SENSOR
ESP_LOGCONFIG(TAG, "Sensors:");
LOG_SENSOR_WITH_DEDUP(" ", "Light", this->light_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "DetectionDistance", this->detection_distance_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "MovingTargetDistance", this->moving_target_distance_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "StillTargetDistance", this->still_target_distance_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "StillTargetEnergy", this->still_target_energy_sensor_);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_);
for (auto &s : this->gate_move_sensors_) {
LOG_SENSOR_WITH_DEDUP(" ", "GateMove", s);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s);
}
for (auto &s : this->gate_still_sensors_) {
LOG_SENSOR_WITH_DEDUP(" ", "GateStill", s);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s);
}
#endif
#ifdef USE_TEXT_SENSOR
@@ -201,7 +272,20 @@ void LD2410Component::restart_and_read_all_info() {
}
void LD2410Component::loop() {
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
}
}
void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
@@ -381,16 +465,11 @@ void LD2410Component::handle_ack_data_() {
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
break; // too short to hold the resolution
}
const auto index = ld24xx::find_index(DISTANCE_RESOLUTIONS, this->buffer_data_[10]);
if (!index) {
ESP_LOGW(TAG, "Unknown distance resolution %u", this->buffer_data_[10]);
break;
}
ESP_LOGV(TAG, "Distance resolution: %s",
LOG_STR_ARG(DistanceResolutionStrings::get_log_str(*index, DistanceResolutionStrings::LAST_INDEX)));
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
#ifdef USE_SELECT
if (this->distance_resolution_select_ != nullptr) {
this->distance_resolution_select_->publish_state(*index);
this->distance_resolution_select_->publish_state(distance_resolution);
}
#endif
break;
@@ -403,14 +482,16 @@ void LD2410Component::handle_ack_data_() {
this->light_function_ = this->buffer_data_[10];
this->light_threshold_ = this->buffer_data_[11];
this->out_pin_level_ = this->buffer_data_[12];
ESP_LOGV(TAG, "Light function: %u, threshold: %u, out pin level: %u", this->light_function_,
this->light_threshold_, this->out_pin_level_);
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_,
out_pin_level_str);
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
this->light_function_select_->publish_state(static_cast<size_t>(this->light_function_));
this->light_function_select_->publish_state(light_function_str);
}
if (this->out_pin_level_select_ != nullptr) {
this->out_pin_level_select_->publish_state(static_cast<size_t>(this->out_pin_level_));
this->out_pin_level_select_->publish_state(out_pin_level_str);
}
#endif
#ifdef USE_NUMBER
@@ -547,29 +628,15 @@ void LD2410Component::set_bluetooth(bool enable) {
}
void LD2410Component::set_distance_resolution(const char *state) {
const int16_t index = progmem_string_table_find<DistanceResolutionStrings>(state, strlen(state));
if (index >= 0) {
this->set_distance_resolution(static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
}
}
void LD2410Component::set_distance_resolution(DistanceResolution resolution) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {resolution, 0x00};
const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
void LD2410Component::set_baud_rate(const char *state) {
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
if (index >= 0) {
this->set_baud_rate(static_cast<BaudRate>(index + 1));
}
}
void LD2410Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {rate, 0x00};
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -648,16 +715,7 @@ void LD2410Component::set_max_distances_timeout() {
this->set_config_mode_(false);
}
void LD2410Component::set_gate_threshold(number::Number *n) {
// Found by pointer: up to 18 compares per write beat storing the gate in each number
uint8_t gate = 0;
while (gate < TOTAL_GATES && this->gate_move_threshold_numbers_[gate] != n &&
this->gate_still_threshold_numbers_[gate] != n)
gate++;
if (gate == TOTAL_GATES) {
ESP_LOGW(TAG, "Threshold number is not a known gate");
return;
}
void LD2410Component::set_gate_threshold(uint8_t gate) {
number::Number *motionsens = this->gate_move_threshold_numbers_[gate];
number::Number *stillsens = this->gate_still_threshold_numbers_[gate];
@@ -701,15 +759,11 @@ void LD2410Component::set_light_out_control() {
}
#endif
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
if (const auto index = this->light_function_select_->active_index()) {
this->light_function_ = *index;
}
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
}
if (this->out_pin_level_select_ != nullptr) {
if (const auto index = this->out_pin_level_select_->active_index()) {
this->out_pin_level_ = *index;
}
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
}
#endif
this->set_config_mode_(true);
+3 -24
View File
@@ -37,25 +37,6 @@ using namespace ld24xx;
static constexpr uint8_t MAX_LINE_LENGTH = 50;
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8,
};
enum DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
// Device values of the distance resolution select options, in option order; in flash on ESP8266
static constexpr uint8_t DISTANCE_RESOLUTIONS[] PROGMEM = {DISTANCE_RESOLUTION_0_2, DISTANCE_RESOLUTION_0_75};
class LD2410Component final : public Component, public uart::UARTDevice {
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(out_pin_presence_status)
@@ -106,7 +87,7 @@ class LD2410Component final : public Component, public uart::UARTDevice {
void set_gate_still_threshold_number(uint8_t gate, number::Number *n);
void set_gate_move_threshold_number(uint8_t gate, number::Number *n);
void set_max_distances_timeout();
void set_gate_threshold(number::Number *n);
void set_gate_threshold(uint8_t gate);
#endif
#ifdef USE_SENSOR
void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s);
@@ -118,9 +99,7 @@ class LD2410Component final : public Component, public uart::UARTDevice {
void restart_and_read_all_info();
void set_bluetooth(bool enable);
void set_distance_resolution(const char *state);
void set_distance_resolution(DistanceResolution resolution);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void factory_reset();
protected:
@@ -150,8 +129,8 @@ class LD2410Component final : public Component, public uart::UARTDevice {
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
#endif
#ifdef USE_SENSOR
std::array<sensor::SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
std::array<sensor::SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
#endif
};
+25 -35
View File
@@ -1,5 +1,3 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
@@ -20,7 +18,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
GateThresholdNumber = ld2410_ns.class_("GateThresholdNumber", number.Number)
LightThresholdNumber = ld2410_ns.class_("LightThresholdNumber", number.Number)
MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.Number)
CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate"
CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate"
@@ -33,25 +35,25 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Inclusive(CONF_TIMEOUT, TIMEOUT_GROUP): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
unit_of_measurement=UNIT_SECOND,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
),
cv.Inclusive(CONF_MAX_MOVE_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Inclusive(CONF_MAX_STILL_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
number.ApplyNumber,
LightThresholdNumber,
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_LIGHTBULB,
@@ -64,14 +66,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(f"g{x}"): cv.Schema(
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
number.ApplyNumber,
GateThresholdNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
number.ApplyNumber,
GateThresholdNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
@@ -87,14 +89,8 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
numbers = number.sub_numbers(config, parent=hub)
timeouts = {"apply": "set_max_distances_timeout()", "publish": "before"}
await numbers(
CONF_TIMEOUT,
hub.set_timeout_number,
min_value=0,
max_value=65535,
step=1,
**timeouts,
CONF_TIMEOUT, hub.set_timeout_number, min_value=0, max_value=65535, step=1
)
await numbers(
CONF_MAX_MOVE_DISTANCE_GATE,
@@ -102,7 +98,6 @@ async def to_code(config: ConfigType) -> None:
min_value=2,
max_value=8,
step=1,
**timeouts,
)
await numbers(
CONF_MAX_STILL_DISTANCE_GATE,
@@ -110,7 +105,6 @@ async def to_code(config: ConfigType) -> None:
min_value=2,
max_value=8,
step=1,
**timeouts,
)
await numbers(
CONF_LIGHT_THRESHOLD,
@@ -118,25 +112,21 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=255,
step=1,
apply="set_light_out_control()",
publish="before",
)
threshold = {
"min_value": 0,
"max_value": 100,
"step": 1,
"apply": "set_gate_threshold(entity)",
"publish": "before",
}
for x in range(9):
if gate_conf := config.get(f"g{x}"):
await numbers.create(
gate_conf[CONF_MOVE_THRESHOLD],
partial(hub.set_gate_move_threshold_number, x),
**threshold,
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = cg.new_Pvariable(move_config[CONF_ID], x)
await number.register_number(
n, move_config, min_value=0, max_value=100, step=1
)
await numbers.create(
gate_conf[CONF_STILL_THRESHOLD],
partial(hub.set_gate_still_threshold_number, x),
**threshold,
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID], x)
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
@@ -0,0 +1,12 @@
#include "gate_threshold_number.h"
namespace esphome::ld2410 {
GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {}
void GateThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_gate_threshold(this->gate_);
}
} // namespace esphome::ld2410
@@ -0,0 +1,17 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class GateThresholdNumber final : public number::Number, public Parented<LD2410Component> {
public:
GateThresholdNumber(uint8_t gate);
protected:
uint8_t gate_;
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,10 @@
#include "light_threshold_number.h"
namespace esphome::ld2410 {
void LightThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_light_out_control();
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class LightThresholdNumber final : public number::Number, public Parented<LD2410Component> {
public:
LightThresholdNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,10 @@
#include "max_distance_timeout_number.h"
namespace esphome::ld2410 {
void MaxDistanceTimeoutNumber::control(float value) {
this->publish_state(value);
this->parent_->set_max_distances_timeout();
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class MaxDistanceTimeoutNumber final : public number::Number, public Parented<LD2410Component> {
public:
MaxDistanceTimeoutNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -52,7 +52,6 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums and tables; the hub maps by index
await selects(
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
@@ -4,7 +4,7 @@ namespace esphome::ld2410 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
this->parent_->set_baud_rate(this->option_at(index));
}
} // namespace esphome::ld2410
@@ -4,8 +4,7 @@ namespace esphome::ld2410 {
void DistanceResolutionSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_distance_resolution(
static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
this->parent_->set_distance_resolution(this->option_at(index));
}
} // namespace esphome::ld2410
+8 -15
View File
@@ -11,7 +11,10 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch)
EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch)
CONF_ENGINEERING_MODE = "engineering_mode"
@@ -19,13 +22,13 @@ CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema(
switch.ApplySwitch,
EngineeringModeSwitch,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_PULSE,
),
cv.Optional(CONF_BLUETOOTH): switch.switch_schema(
switch.ApplySwitch,
BluetoothSwitch,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_BLUETOOTH,
@@ -36,15 +39,5 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(
CONF_ENGINEERING_MODE,
hub.set_engineering_mode_switch,
apply="set_engineering_mode(state)",
publish="before",
)
await switches(
CONF_BLUETOOTH,
hub.set_bluetooth_switch,
apply="set_bluetooth(state)",
publish="before",
)
await switches(CONF_ENGINEERING_MODE, hub.set_engineering_mode_switch)
await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)
@@ -0,0 +1,10 @@
#include "bluetooth_switch.h"
namespace esphome::ld2410 {
void BluetoothSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_bluetooth(state);
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class BluetoothSwitch final : public switch_::Switch, public Parented<LD2410Component> {
public:
BluetoothSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,10 @@
#include "engineering_mode_switch.h"
namespace esphome::ld2410 {
void EngineeringModeSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_engineering_mode(state);
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class EngineeringModeSwitch final : public switch_::Switch, public Parented<LD2410Component> {
public:
EngineeringModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::ld2410
+15 -3
View File
@@ -1,5 +1,6 @@
#include "ld2410s.h"
#include "ld2410s_number.h"
#include <algorithm>
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
@@ -78,7 +79,7 @@ void LD2410SComponent::dump_config() {
LOG_BINARY_SENSOR(" ", "Has Target", this->target_binary_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR_WITH_DEDUP(" ", "Distance", this->distance_sensor_);
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Distance", this->distance_sensor_);
#endif
#ifdef USE_LD2410S_VERSION
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
@@ -94,8 +95,19 @@ void LD2410SComponent::dump_config() {
}
void LD2410SComponent::loop() {
if (!this->read_available<16>([this](uint8_t b) { this->receive_byte_(b); })) {
ESP_LOGV(TAG, "UART read failed");
uint8_t chunk[16];
// Take the count once so one pass handles a bounded number of bytes
size_t avail = this->available();
while (avail > 0) {
const size_t len = std::min(avail, sizeof(chunk));
if (!this->read_array(chunk, len)) {
ESP_LOGV(TAG, "UART read of %u bytes failed", static_cast<unsigned>(len));
break;
}
for (size_t i = 0; i < len; i++) {
this->receive_byte_(chunk[i]);
}
avail -= len;
}
const uint32_t now = App.get_loop_component_start_time();
if (this->sequence_ == nullptr) {
Loaded 100 of 316 files, more files were not shown because too many files have changed in this diff. Show more