Compare commits

..
Author SHA1 Message Date
J. Nick Koston 2df80fbf71 Merge branch 'dev' into ota-inflate-table-decode 2026-10-25 23:49:16 -10:00
J. Nick Koston 470f7eb51d [esphome] Read a stored byte before advancing the OTA output cursor
C leaves the order of *d->dest++ and the read on the right of the
assignment open, so dest could move first; a read that needs input
flushes the output up to dest where the ack follows the write, and
would write the byte before it was stored. The test harness can now
take the output inside the read callback the same way, and every
stream runs through it.
2026-10-25 23:05:30 -10:00
J. Nick Koston e7b2dfed57 Merge branch 'dev' into ota-inflate-table-decode 2026-10-25 22:51:40 -10:00
J. Nick Koston 83080c4560 [esphome] Wrap OTA back references as an index and test stored blocks after Huffman ones
Forming a pointer before the ring is undefined behaviour even when it is
wrapped before use; wrap the ring position instead. A new test stream
puts stored blocks after dynamic ones, so they start with read ahead
bits in the bit buffer.
2026-10-25 21:52:28 -10:00
J. Nick Koston 104cb09475 [esphome] Update the inflate session size in its comment 2026-10-25 21:44:28 -10:00
J. Nick Koston a7824ed4e7 [esphome] Trim the OTA decoder's flash and heap
The careful path, which only decodes the last few bytes of each read,
uses the canonical decoder alone instead of the lookup table too. The
canonical decoder accumulates each length's first code and symbol index
as it walks, so the trees no longer store them, and the trans bound
check, which the symbol count already guarantees, goes with the size
field. 94 bytes less flash and 132 bytes less heap on a BK72xx, same
speed on a BK7238.
2026-10-25 21:43:44 -10:00
J. Nick Koston 6420db90b1 [esphome] Let the OTA decoder own its ring and finish each window in the fast loop
The decoder now starts dest at the ring and starts it over once a call
has filled it, so callers no longer set the output bounds and cannot
point them outside the ring. The fast loop runs to the end of the ring,
leaving a match that does not fit to the resume path, instead of handing
the last 258 bytes of every window to the careful path; match copies
run in at most two pieces instead of testing for the wrap on every byte.

The careful path decodes long codes through the same canonical decoder
as the fast loop, the distance lookup table is 6 bits for a 30 symbol
alphabet (1.3 KB more heap than before this change instead of 2.2 KB),
and the tests share one stream table.
2026-10-25 21:33:18 -10:00
J. Nick Koston d3c2a7c734 [esphome] Decode OTA deflate with lookup tables and a fast loop
Inflating dominated compressed uploads on slow chips: the uzlib decoder
read every Huffman code one bit at a time. Decode from a 32 bit bit
buffer through 9 bit lookup tables, run a fast loop with the bit buffer
and cursors in locals while a whole symbol fits in the input and the
output, and write each output byte once, into the ring window.

About 5 times faster on a BK7231N, a BK7238 and an ESP32, for about
830 bytes of flash and 2.2 KB more heap while a compressed upload runs.
2026-10-25 21:05:35 -10:00
652 changed files with 10181 additions and 11179 deletions

No files matched your search

+3 -8
View File
@@ -217,10 +217,9 @@ file does, and it is the authority when they disagree. The most useful starting
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
end of `setup()` when the entities need a state at boot.
@@ -444,16 +443,12 @@ file does, and it is the authority when they disagree. The most useful starting
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
`publish="momentary"` is only for legacy action switches (restart, factory reset): it publishes off and runs
the body only when turned on. Never use it for a new entity; an action is a button.
```python
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
```
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
to still drive a state when the restore mode is disabled.
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
already applies `inverted`.
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_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)
+13 -30
View File
@@ -7,7 +7,6 @@ import re
import textwrap
from esphome.build_helpers import pch
from esphome.build_helpers.ccache import CCACHE_PCH_MIN_VERSION
from esphome.components.esp32 import (
KEY_SRC_CMAKE,
get_esp32_variant,
@@ -516,7 +515,7 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
# Where CMake puts the .gch of the src component; ccache reads the checksum
# next to it in place of the .gch
_PCH_SUM_PATH = "build/esp-idf/src/CMakeFiles/__idf_src.dir/cmake_pch.hxx.gch.sum"
# Where the CMake gate records its choice
# Where the Windows gate records its choice
_PCH_CHOICE_VAR = "ESPHOME_PCH"
@@ -532,32 +531,16 @@ def _pch_cmake_block() -> str:
block = f"""target_precompile_headers(${{COMPONENT_LIB}} PRIVATE
{headers}
)"""
if pch.pch_forced():
if not pch.pch_needs_gcc_check():
return f"\n# ESPHome precompiled header\n{block}\n"
# Only CMake knows the compiler version before the first configure, and
# the ccache IDF found is the one the build runs
gcc_gate = ""
if pch.pch_needs_gcc_check():
gcc_gate = f"""if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
message(STATUS "ESPHome: GCC ${{CMAKE_CXX_COMPILER_VERSION}} cannot load a precompiled header on Windows; compiling without it")
set(_esphome_pch OFF)
endif()
"""
min_ccache = ".".join(map(str, CCACHE_PCH_MIN_VERSION))
# Before the first configure only CMake knows the compiler version
return f"""
# ESPHome precompiled header, unless a check below rules it out
set(_esphome_pch ON)
{gcc_gate}if(CCACHE_ENABLE AND CCACHE_FOUND)
execute_process(COMMAND "${{CCACHE_FOUND}}" --version OUTPUT_VARIABLE _esphome_ccache ERROR_QUIET)
if(_esphome_ccache MATCHES "ccache version ([0-9.]+)")
if(CMAKE_MATCH_1 VERSION_LESS {min_ccache})
message(STATUS "ESPHome: ccache ${{CMAKE_MATCH_1}} can reuse stale objects with a precompiled header; compiling without it (upgrade ccache to {min_ccache} or newer, or set ESPHOME_PCH_ENABLE=1 to force)")
set(_esphome_pch OFF)
endif()
endif()
endif()
set({_PCH_CHOICE_VAR} ${{_esphome_pch}} CACHE BOOL "ESPHome precompiled header in use" FORCE)
if(_esphome_pch)
# ESPHome precompiled header, unless GCC bug 14940 keeps it from loading
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
message(STATUS "ESPHome: GCC ${{CMAKE_CXX_COMPILER_VERSION}} cannot load a precompiled header on Windows; compiling without it")
set({_PCH_CHOICE_VAR} OFF CACHE BOOL "ESPHome precompiled header in use" FORCE)
else()
set({_PCH_CHOICE_VAR} ON CACHE BOOL "ESPHome precompiled header in use" FORCE)
{textwrap.indent(block, " ")}
endif()
"""
@@ -574,10 +557,10 @@ def write_pch_checksum() -> None:
if not pch.pch_enabled():
return
# Written whatever the gate chose last time: a build tree without that
# choice, or a reconfigure that turns the pch on, must not see a stale sum
if pch.pch_forced() or get_cmake_cache_value(_PCH_CHOICE_VAR) == "ON":
pch.log_pch_in_use()
# The gate's choice, cached by configure
if pch.pch_needs_gcc_check() and get_cmake_cache_value(_PCH_CHOICE_VAR) != "ON":
return
pch.log_pch_in_use()
checksum = pch.pch_checksum(
CORE.relative_src_path(),
pch.PCH_DEFAULT_HEADERS,
+1 -48
View File
@@ -3,68 +3,21 @@ resolution, and default ``CCACHE_*`` values."""
from __future__ import annotations
import functools
import logging
import os
from pathlib import Path
import re
from esphome.framework_helpers import (
strip_win_long_path_prefix,
tool_version_probe,
tool_version_runs,
)
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
from esphome.helpers import FALSY_ENV_STRINGS, TRUTHY_ENV_STRINGS
_LOGGER = logging.getLogger(__name__)
# Before 4.7 ccache stores a pch consumer whose header is newer than one
# second without that header, so a later build reuses it after the header
# changed (#21621)
CCACHE_PCH_MIN_VERSION = (4, 7)
_VERSION_RE = re.compile(r"ccache version (\d+(?:\.\d+)*)")
@functools.cache
def _ccache_probe(ccache: str) -> str | Exception:
"""``ccache --version``, run once per binary per process."""
return tool_version_probe(ccache)
def _ccache_runs(ccache: str) -> bool:
"""Return True when the ``ccache`` found on PATH actually runs."""
return tool_version_runs(
ccache,
"Ignoring ccache at %s because it failed to run; compiling without ccache",
probe=_ccache_probe,
)
@functools.cache
def _ccache_too_old_for_pch(ccache: str) -> bool:
output = _ccache_probe(ccache)
if not isinstance(output, str) or (found := _VERSION_RE.search(output)) is None:
return False
if tuple(map(int, found[1].split("."))) >= CCACHE_PCH_MIN_VERSION:
return False
_LOGGER.info(
"ccache %s at %s can reuse stale objects with a precompiled header; "
"compiling without it (upgrade ccache to %s or newer, or set "
"ESPHOME_PCH_ENABLE=1 to force)",
found[1],
ccache,
".".join(map(str, CCACHE_PCH_MIN_VERSION)),
)
return True
def ccache_breaks_pch() -> bool:
"""Whether the ccache this build uses is too old for a precompiled header.
ESP-IDF builds check the ccache IDF finds in CMake instead (build_gen.espidf)."""
return (ccache := resolve_ccache_path()) is not None and _ccache_too_old_for_pch(
ccache
)
+4 -19
View File
@@ -12,11 +12,7 @@ import re
import subprocess
import sys
from esphome.build_helpers.ccache import (
ccache_breaks_pch,
effective_ccache_basedir,
parse_enable_env,
)
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
from esphome.const import PLATFORM_NRF52
from esphome.helpers import write_file_if_changed
@@ -121,16 +117,9 @@ def pch_needs_gcc_check() -> bool:
return sys.platform == "win32" and parse_enable_env("ESPHOME_PCH_ENABLE") is None
def _ccache_rules_out_pch() -> bool:
"""An old ccache, unless ``ESPHOME_PCH_ENABLE=1`` forces the pch on.
ESP-IDF runs the same check in CMake (build_gen.espidf)."""
return not pch_forced() and ccache_breaks_pch()
def pch_usable(cxx: Sequence[Path | str]) -> bool:
"""The knob plus the ccache and host rules; ``ESPHOME_PCH_ENABLE=1``
skips the rules."""
if not pch_enabled() or _ccache_rules_out_pch():
"""The knob plus the host rule; ``ESPHOME_PCH_ENABLE=1`` skips the rule."""
if not pch_enabled():
return False
if not pch_needs_gcc_check():
return True
@@ -173,11 +162,7 @@ def pch_script_enabled() -> bool:
"""Whether this PlatformIO build takes the pch script."""
from esphome.core import CORE
return (
pch_enabled()
and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
and not _ccache_rules_out_pch()
)
return pch_enabled() and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
def pch_header_text(include_headers: Iterable[str]) -> str:
-4
View File
@@ -90,10 +90,6 @@ class AirtonClimate : public climate_ir::ClimateIR {
bool set_display_state(bool state);
bool set_vertical_direction_state(VerticalDirection direction);
void send_settings();
void control_vertical_direction(VerticalDirection direction) {
if (this->set_vertical_direction_state(direction))
this->send_settings();
}
void set_setting(AirtonSetting setting, bool state) {
const bool changed = setting == AirtonSetting::AIRTON_SETTING_DISPLAY ? this->set_display_state(state)
: this->set_sleep_mode_state(state);
+4 -4
View File
@@ -9,16 +9,18 @@ from ..climate import (
CONF_VERTICAL_DIRECTION,
VERTICAL_DIRECTIONS,
AirtonClimate,
VerticalDirection,
airton_ns,
)
CODEOWNERS = ["@Procsiab"]
VerticalDirectionSelect = airton_ns.class_("VerticalDirectionSelect", select.Select)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_AIRTON_ID): cv.use_id(AirtonClimate),
cv.Optional(CONF_VERTICAL_DIRECTION): select.select_schema(
select.ApplySelect,
VerticalDirectionSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
@@ -32,6 +34,4 @@ async def to_code(config: ConfigType) -> None:
CONF_VERTICAL_DIRECTION,
hub.set_vertical_direction_select,
options=list(VERTICAL_DIRECTIONS),
# The options are in the order of the enum; the hub publishes the state
apply=f"control_vertical_direction(static_cast<{VerticalDirection}>(index))",
)
@@ -0,0 +1,17 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../airton.h"
namespace esphome::airton {
class VerticalDirectionSelect : public select::Select, public Parented<AirtonClimate> {
protected:
// The options are in the order of the enum, so the index is the direction; the parent publishes the state
void control(size_t index) override {
if (this->parent_->set_vertical_direction_state(static_cast<VerticalDirection>(index)))
this->parent_->send_settings();
}
};
} // namespace esphome::airton
@@ -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):
+73 -62
View File
@@ -11,52 +11,7 @@ ESPHOME_LOG_TAG(TAG, "apds9960");
this->mark_failed(LOG_STR("Setting up APDS9960 registers failed")); \
return; \
}
static constexpr i2c::I2CRegValue APDS9960_TIMING[] PROGMEM = {
// ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
{0x81, 0xDB},
// WTime (Wait time, 0x83) -> 0xF6 (27ms)
{0x83, 0xF6},
// PPulse (0x8E) -> 0x87 (16us, 8 pulses)
{0x8E, 0x87},
// POffset UR (0x9D) -> 0 (no offset)
{0x9D, 0x00},
// POffset DL (0x9E) -> 0 (no offset)
{0x9E, 0x00},
// Config 1 (0x8D) -> 0x60 (no wtime factor)
{0x8D, 0x60},
};
static constexpr i2c::I2CRegValue APDS9960_INTERRUPT_GESTURE[] PROGMEM = {
// Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
{0x8C, 0x11},
// Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
{0x90, 0x01},
// Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
{0x9F, 0x00},
// GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
{0xA0, 0x28},
// GPexTh (0xA1, gesture exit threshold) -> 0x1E
{0xA1, 0x1E},
// GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
{0xA2, 0x40},
};
static constexpr i2c::I2CRegValue APDS9960_GESTURE_OFFSETS[] PROGMEM = {
// GOffsetU (0xA4) -> 0x00 (no offset)
{0xA4, 0x00},
// GOffsetD (0xA5) -> 0x00 (no offset)
{0xA5, 0x00},
// GOffsetL (0xA7) -> 0x00 (no offset)
{0xA7, 0x00},
// GOffsetR (0xA9) -> 0x00 (no offset)
{0xA9, 0x00},
// GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
{0xA6, 0xC9},
// GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
// 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
{0xAA, 0x00},
};
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
void APDS9960::setup() {
uint8_t id;
@@ -70,27 +25,83 @@ void APDS9960::setup() {
return;
}
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_TIMING));
// ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
APDS9960_WRITE_BYTE(0x81, 0xDB);
// WTime (Wait time, 0x83) -> 0xF6 (27ms)
APDS9960_WRITE_BYTE(0x83, 0xF6);
// PPulse (0x8E) -> 0x87 (16us, 8 pulses)
APDS9960_WRITE_BYTE(0x8E, 0x87);
// POffset UR (0x9D) -> 0 (no offset)
APDS9960_WRITE_BYTE(0x9D, 0x00);
// POffset DL (0x9E) -> 0 (no offset)
APDS9960_WRITE_BYTE(0x9E, 0x00);
// Config 1 (0x8D) -> 0x60 (no wtime factor)
APDS9960_WRITE_BYTE(0x8D, 0x60);
// Control (0x8F): led drive (0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA),
// proximity gain (0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X), ambient light gain (0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x)
APDS9960_ERROR_CHECK(this->update_register(
0x8F, 0b11001111,
(this->led_drive_ & 0b11) << 6 | (this->proximity_gain_ & 0b11) << 2 | (this->ambient_gain_ & 0b11)));
// Control (0x8F) ->
uint8_t val = 0;
APDS9960_ERROR_CHECK(this->read_byte(0x8F, &val));
val &= 0b00111111;
// led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
val |= (this->led_drive_ & 0b11) << 6;
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_INTERRUPT_GESTURE));
val &= 0b11110011;
// proximity gain, 0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X
val |= (this->proximity_gain_ & 0b11) << 2;
// GConf 2 (0xA3): gesture gain (0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x),
// gesture led drive (0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA), gesture wait time
// (0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms, 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms)
APDS9960_ERROR_CHECK(this->update_register(
0xA3, 0b01111111,
(this->gesture_gain_ & 0b11) << 5 | (this->gesture_led_drive_ & 0b11) << 3 | (this->gesture_wait_time_ & 0b111)));
val &= 0b11111100;
// ambient light gain, 0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x
val |= (this->ambient_gain_ & 0b11) << 0;
APDS9960_WRITE_BYTE(0x8F, val);
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_GESTURE_OFFSETS));
// Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
APDS9960_WRITE_BYTE(0x8C, 0x11);
// Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
APDS9960_WRITE_BYTE(0x90, 0x01);
// Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
APDS9960_WRITE_BYTE(0x9F, 0x00);
// GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
APDS9960_WRITE_BYTE(0xA0, 0x28);
// GPexTh (0xA1, gesture exit threshold) -> 0x1E
APDS9960_WRITE_BYTE(0xA1, 0x1E);
// GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
APDS9960_WRITE_BYTE(0xA2, 0x40);
// GConf 2 (0xA3, gesture config 2) ->
APDS9960_ERROR_CHECK(this->read_byte(0xA3, &val));
val &= 0b10011111;
// gesture gain, 0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x
val |= (this->gesture_gain_ & 0b11) << 5;
val &= 0b11100111;
// gesture led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
val |= (this->gesture_led_drive_ & 0b11) << 3;
val &= 0b11111000;
// gesture wait time
// 0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms
// 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms
val |= (this->gesture_wait_time_ & 0b111) << 0;
APDS9960_WRITE_BYTE(0xA3, val);
// GOffsetU (0xA4) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA4, 0x00);
// GOffsetD (0xA5) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA5, 0x00);
// GOffsetL (0xA7) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA7, 0x00);
// GOffsetR (0xA9) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA9, 0x00);
// GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
APDS9960_WRITE_BYTE(0xA6, 0xC9);
// GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
// 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
APDS9960_WRITE_BYTE(0xAA, 0x00);
// Enable (0x80) ->
uint8_t val = 0;
val = 0;
val |= (0b1) << 0; // power on
val |= (this->is_color_enabled_() & 0b1) << 1;
val |= (this->is_proximity_enabled_() & 0b1) << 2;
@@ -98,7 +109,7 @@ void APDS9960::setup() {
val |= 0b0 << 4; // color interrupt disabled
val |= 0b0 << 5; // proximity interrupt disabled
val |= (this->is_gesture_enabled_() & 0b1) << 6; // proximity is required for gestures
APDS9960_ERROR_CHECK(this->write_byte(0x80, val));
APDS9960_WRITE_BYTE(0x80, val);
}
bool APDS9960::is_color_enabled_() const {
#ifdef USE_SENSOR
+2 -11
View File
@@ -1555,17 +1555,8 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()
void APIConnection::send_event(event::Event *event) {
const uint8_t type_index = event->get_last_event_type_index();
const size_t size_before = this->deferred_batch_.size();
if (this->deferred_batch_.contains(event, EventResponse::MESSAGE_TYPE)) {
// Sending now would overtake the older event of this entity that still waits in the batch
this->schedule_message_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE, type_index);
} else {
this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE, type_index);
}
// Events are rare, so a queued one sends the batch now rather than after batch_delay
if (this->deferred_batch_.size() > size_before)
this->make_batch_due_now_();
this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE,
event->get_last_event_type_index());
}
uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
uint32_t remaining_size) {
+68 -5
View File
@@ -2,7 +2,6 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_deferred_batch.h"
#include "api_frame_helper.h"
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
@@ -21,6 +20,7 @@
#include "esphome/core/string_ref.h"
#include <functional>
#include <limits>
#include <vector>
namespace esphome {
@@ -656,6 +656,73 @@ class APIConnection final : public APIServerConnectionBase {
// Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size);
// Generic batching mechanism for both state updates and entity info
struct DeferredBatch {
// Sentinel value for unused aux_data_index
static constexpr uint8_t AUX_DATA_UNUSED = std::numeric_limits<uint8_t>::max();
struct BatchItem {
EntityBase *entity; // 4 bytes - Entity pointer
uint16_t message_type; // 2 bytes - Message type for protocol and dispatch
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
};
std::vector<BatchItem> items;
uint32_t batch_start_time{0};
// No pre-allocation - log connections never use batching, and for
// connections that do, buffers are released after initial sync anyway
// Add item to the batch (with deduplication)
void add_item(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = AUX_DATA_UNUSED) {
// Dedup: O(n) scan but optimized for RAM over performance
// Skip deduplication for events - they are edge-triggered, every occurrence matters
#ifdef USE_EVENT
if (message_type != EventResponse::MESSAGE_TYPE)
#endif
{
for (const auto &item : this->items) {
if (item.entity == entity && item.message_type == message_type)
return; // Already queued
}
}
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
}
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
// Swap to front avoids expensive vector::insert which shifts all elements
this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
if (this->items.size() > 1) {
std::swap(this->items.front(), this->items.back());
}
}
// Clear all items
void clear() {
items.clear();
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
// Release excess capacity - only releases if items already empty
void release_buffer() {
// Safe to call: batch is processed before release_buffer is called,
// and if any items remain (partial processing), we must not clear them.
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
if (items.empty()) {
std::vector<BatchItem>().swap(items);
}
}
};
// DeferredBatch here (16 bytes, 4-byte aligned)
DeferredBatch deferred_batch_;
@@ -728,10 +795,6 @@ class APIConnection final : public APIServerConnectionBase {
static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390; // MTU
bool schedule_batch_();
// Sends the batch on the next loop, so a queued event does not wait for batch_delay; queued states go with it
void make_batch_due_now_() {
this->deferred_batch_.batch_start_time = App.get_loop_component_start_time() - this->get_batch_delay_ms_();
}
void process_batch_();
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
__attribute__((noinline));
@@ -1,88 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_pb2.h"
#include "esphome/core/entity_base.h"
#include <algorithm>
#include <cstdint>
#include <limits>
#include <vector>
namespace esphome::api {
// Generic batching mechanism for both state updates and entity info
struct DeferredBatch {
// Sentinel value for unused aux_data_index
static constexpr uint8_t AUX_DATA_UNUSED = std::numeric_limits<uint8_t>::max();
struct BatchItem {
EntityBase *entity; // 4 bytes - Entity pointer
uint16_t message_type; // 2 bytes - Message type for protocol and dispatch
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
};
std::vector<BatchItem> items;
uint32_t batch_start_time{0};
// No pre-allocation - log connections never use batching, and for
// connections that do, buffers are released after initial sync anyway
// O(n) scan but optimized for RAM over performance
bool contains(EntityBase *entity, uint16_t message_type) const {
for (const auto &item : this->items) {
if (item.entity == entity && item.message_type == message_type)
return true;
}
return false;
}
// Add item to the batch (with deduplication)
void add_item(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = AUX_DATA_UNUSED) {
// Skip deduplication for events - they are edge-triggered, every occurrence matters
#ifdef USE_EVENT
if (message_type != EventResponse::MESSAGE_TYPE)
#endif
{
if (this->contains(entity, message_type))
return; // Already queued
}
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
}
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
// Shift rather than swap, so the queued items keep their order (two events of one entity must not swap)
this->items.push_back({});
std::move_backward(this->items.begin(), this->items.end() - 1, this->items.end());
this->items.front() = {entity, message_type, estimated_size, AUX_DATA_UNUSED};
}
// Clear all items
void clear() {
items.clear();
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
// Release excess capacity - only releases if items already empty
void release_buffer() {
// Safe to call: batch is processed before release_buffer is called,
// and if any items remain (partial processing), we must not clear them.
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
if (items.empty()) {
std::vector<BatchItem>().swap(items);
}
}
};
} // namespace esphome::api
#endif // USE_API
+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
@@ -58,7 +58,7 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const ble_device_base::Ser
return {};
}
const auto &raw = service_data.data;
auto raw = service_data.data;
if (raw.size() < 13) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
+47 -49
View File
@@ -4,9 +4,7 @@
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/core/application.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
namespace esphome::bedjet {
@@ -42,7 +40,7 @@ void BedJetHub::upgrade_firmware() {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name().c_str(), status);
}
}
@@ -66,7 +64,7 @@ bool BedJetHub::set_fan_index(uint8_t fan_speed_index) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
@@ -84,7 +82,7 @@ bool BedJetHub::set_target_temp(float temp_c) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
@@ -95,7 +93,7 @@ bool BedJetHub::set_time_remaining(uint8_t hours, uint8_t mins) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
@@ -105,10 +103,11 @@ bool BedJetHub::send_button(BedjetButton button) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name(),
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name().c_str(),
LOG_STR_ARG(bedjet_button_to_string(button)), status);
} else {
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name(), LOG_STR_ARG(bedjet_button_to_string(button)));
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name().c_str(),
LOG_STR_ARG(bedjet_button_to_string(button)));
}
return status == 0;
}
@@ -126,9 +125,9 @@ uint16_t BedJetHub::get_time_remaining() {
uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) {
if (!this->is_connected()) {
if (!this->parent_->enabled) {
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name());
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
} else {
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name());
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
}
return -1;
}
@@ -145,16 +144,16 @@ uint8_t BedJetHub::set_notify_(const bool enable) {
status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
}
} else {
status = esp_ble_gattc_unregister_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
}
}
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name(), enable, status);
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
return status;
}
@@ -165,7 +164,7 @@ bool BedJetHub::discover_characteristics_() {
if (!this->char_handle_cmd_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name());
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_cmd_ = chr->handle;
@@ -175,7 +174,7 @@ bool BedJetHub::discover_characteristics_() {
if (!this->char_handle_status_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name());
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_status_ = chr->handle;
@@ -205,14 +204,14 @@ bool BedJetHub::discover_characteristics_() {
if (!this->char_handle_name_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name());
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_name_ = chr->handle;
auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
this->char_handle_name_, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name(), status);
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
}
}
}
@@ -223,7 +222,7 @@ bool BedJetHub::discover_characteristics_() {
" - Status char: 0x%x\n"
" - config descriptor: 0x%x\n"
" - Name char: 0x%x",
this->get_name(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
this->char_handle_name_);
return result;
@@ -242,7 +241,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
auto result = this->discover_characteristics_();
if (result) {
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name());
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name().c_str());
this->node_state = espbt::ClientState::ESTABLISHED;
this->set_notify_(true);
@@ -254,7 +253,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
this->dispatch_state_(true);
} else {
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name());
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name().c_str());
this->parent()->set_enabled(false);
this->status_set_warning();
this->dispatch_state_(false);
@@ -266,15 +265,15 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
if (param->write.status == ESP_GATT_INVALID_ATTR_LEN) {
// This probably means that our hack for notify_en (8 bit vs 16 bit) didn't work right.
// Should we try to fall back to BLEClient's way?
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name(),
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
param->write.handle, param->write.status);
} else {
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name(), param->write.handle,
param->write.status);
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
param->write.handle, param->write.status);
}
break;
}
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name(), param->write.handle,
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
param->write.status);
break;
}
@@ -307,11 +306,9 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} else if (param->read.handle == this->char_handle_name_) {
// The data should represent the name.
if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
ESP_LOGV(TAG, "[%s] Got BedJet name: '%.*s'", this->get_name(), static_cast<int>(param->read.value_len),
reinterpret_cast<const char *>(param->read.value));
const size_t len = std::min<size_t>(param->read.value_len, sizeof(this->name_) - 1);
memcpy(this->name_, param->read.value, len);
this->name_[len] = '\0';
std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
this->set_name_(bedjet_name);
}
}
break;
@@ -325,8 +322,8 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
// doesn't break anything.
if (param->reg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", this->get_name(),
param->reg_for_notify.handle, this->char_handle_status_);
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
break;
}
@@ -338,8 +335,8 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
// This event is not handled by the parent BLEClient, so we need to do this either way.
if (param->unreg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", this->get_name(),
param->unreg_for_notify.handle, this->char_handle_status_);
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
break;
}
@@ -353,13 +350,13 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break;
if (param->notify.conn_id != this->parent_->get_conn_id()) {
ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x", this->get_name(),
this->parent_->get_conn_id(), param->notify.conn_id);
ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x",
this->get_name().c_str(), this->parent_->get_conn_id(), param->notify.conn_id);
// FIXME: bug in BLEClient holding wrong conn_id.
}
if (param->notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name(),
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
this->char_handle_status_, param->notify.handle);
break;
}
@@ -375,13 +372,13 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
if (!this->force_refresh_ && this->codec_->compare(param->notify.value, param->notify.value_len)) {
// If the packet is meaningfully different, trigger children as well
this->force_refresh_ = true;
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name());
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name().c_str());
}
if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
// Set reentrant flag to prevent processing multiple packets.
this->processing_ = true;
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name(),
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name().c_str(),
this->last_notify_, delta, this->force_refresh_ ? "y" : "n");
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
@@ -391,7 +388,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name());
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
}
} else {
this->status_packet_ready_();
@@ -400,7 +397,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break;
}
default:
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name(), event);
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
break;
}
}
@@ -439,7 +436,7 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
return status;
}
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name(),
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name().c_str(),
enable ? "true" : "false", handle, this->parent_->get_conn_id());
return ESP_GATT_OK;
}
@@ -471,7 +468,7 @@ void BedJetHub::setup_time_() {
void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
if (!this->is_connected()) {
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name());
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
return;
}
@@ -480,7 +477,7 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
if (status) {
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
} else {
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name(), hour, minute);
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
}
}
@@ -498,7 +495,7 @@ void BedJetHub::dump_config() {
"BedJet Hub '%s'\n"
" ble_client.app_id: %d\n"
" ble_client.conn_id: %d",
this->get_name(), this->parent()->app_id, this->parent()->get_conn_id());
this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id());
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size());
for (auto *child : this->children_) {
@@ -516,9 +513,10 @@ void BedJetHub::dispatch_status_() {
auto *status = this->codec_->get_status_packet();
if (!this->is_connected()) {
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name());
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name().c_str());
} else if (status != nullptr) {
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name(), this->children_.size(), status);
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name().c_str(), this->children_.size(),
status);
for (auto *child : this->children_) {
child->on_status(status);
}
@@ -532,13 +530,13 @@ void BedJetHub::dispatch_status_() {
// We can try to unregister for notifications now, and then re-register, hoping to clear it up...
// But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name());
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
} else if (diff > NOTIFY_WARN_THRESHOLD) {
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name(), diff / 1000);
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name().c_str(), diff / 1000);
}
if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying", this->get_name(), this->timeout_);
ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying", this->get_name().c_str(), this->timeout_);
// set_enabled(false) will only close the connection if state != IDLE.
this->parent()->set_state(espbt::ClientState::CONNECTING);
this->parent()->set_enabled(false);
+10 -3
View File
@@ -119,7 +119,13 @@ class BedJetHub final : public esphome::ble_client::BLEClientNode, public Pollin
float get_setup_priority() const override { return setup_priority::BLUETOOTH; }
/** @return The BedJet's configured name, or the MAC address if not discovered yet. */
const char *get_name() const { return this->name_[0] == '\0' ? this->parent_->address_str() : this->name_; }
std::string get_name() {
if (this->name_.empty()) {
return this->parent_->address_str();
} else {
return this->name_;
}
}
/* BLEClient overrides */
@@ -145,8 +151,9 @@ class BedJetHub final : public esphome::ble_client::BLEClientNode, public Pollin
uint8_t set_notify_(bool enable);
/** Send the `BedjetPacket` to the device. */
uint8_t write_bedjet_packet_(BedjetPacket *pkt);
// Only used in log lines; longer names are truncated
char name_[24]{};
void set_name_(const std::string &name) { this->name_ = name; }
std::string name_;
uint32_t last_notify_ = 0;
inline void status_packet_ready_();
@@ -62,13 +62,6 @@ struct SpiData {
static SpiData *spi_data = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// BDK 3.0.78 makes REG_WRITE a statement expression that yields the volatile assignment,
// a deprecated volatile read under clang
static inline void reg_write(uint32_t addr, uint32_t value) {
// NOLINTNEXTLINE(performance-no-int-to-ptr) - MMIO register at a fixed address
*reinterpret_cast<volatile uint32_t *>(addr) = value;
}
static void set_spi_ctrl_register(uint32_t bit, bool val) {
uint32_t value = REG_READ(SPI_CTRL);
if (val == 0) {
@@ -76,7 +69,7 @@ static void set_spi_ctrl_register(uint32_t bit, bool val) {
} else if (val == 1) {
value |= bit;
}
reg_write(SPI_CTRL, value);
REG_WRITE(SPI_CTRL, value);
}
#ifdef BEKEN_SPI_IS_N_LIKE
@@ -87,7 +80,7 @@ static void set_spi_config_register(uint32_t bit, bool val) {
} else if (val == 1) {
value |= bit;
}
reg_write(SPI_CONFIG, value);
REG_WRITE(SPI_CONFIG, value);
}
#endif
@@ -143,7 +136,7 @@ static void spi_set_clock(uint32_t max_hz) {
param = REG_READ(SPI_CTRL);
param &= ~(SPI_CKR_MASK << SPI_CKR_POSI);
param |= (div << SPI_CKR_POSI);
reg_write(SPI_CTRL, param);
REG_WRITE(SPI_CTRL, param);
#ifdef BEKEN_SPI_IS_N_LIKE
const int divisor = div;
#else
@@ -236,7 +229,7 @@ void BekenSPILEDStripLightOutput::setup() {
value = REG_READ(SPI_CTRL);
value &= ~CTRL_NSSMD_3;
value |= (1 << 17);
reg_write(SPI_CTRL, value);
REG_WRITE(SPI_CTRL, value);
#ifdef BEKEN_SPI_IS_N_LIKE
value = GFUNC_MODE_SPI_DMA;
@@ -286,7 +279,7 @@ void BekenSPILEDStripLightOutput::setup() {
value = REG_READ(SPI_CONFIG);
value &= ~(0xFFF << 8);
value |= ((dma_buffer_size & 0xFFF) << 8);
reg_write(SPI_CONFIG, value);
REG_WRITE(SPI_CONFIG, value);
#else
// T-like variants: No transfer-length register. The SPI controller shifts out
// everything the DMA writes into SPI_DAT.
+9 -5
View File
@@ -1,24 +1,28 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import CONF_BH1745_ID, BH1745Component
from .. import CONF_BH1745_ID, BH1745Component, bh1745_ns
DEPENDENCIES = ["bh1745"]
BH1745InterruptPinOutput = bh1745_ns.class_(
"BH1745InterruptPinOutput", output.BinaryOutput
)
CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(output.ApplyBinaryOutput),
cv.GenerateID(): cv.declare_id(BH1745InterruptPinOutput),
cv.GenerateID(CONF_BH1745_ID): cv.use_id(BH1745Component),
}
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
hub = await cg.get_variable(config[CONF_BH1745_ID])
await output.sub_binary_outputs(config, parent=hub).create(
config, None, apply="set_interrupt_state(state)"
)
await cg.register_parented(var, hub)
cg.add(hub.enable_interrupt_output())
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/output/binary_output.h"
#include "esphome/core/helpers.h"
#include "../bh1745.h"
namespace esphome::bh1745 {
/// Drives the BH1745 INT pin, which switches the LEDs on breakout boards such as the Pimoroni one.
class BH1745InterruptPinOutput : public output::BinaryOutput, public Parented<BH1745Component> {
protected:
void write_state(bool state) override { this->parent_->set_interrupt_state(state); }
};
} // namespace esphome::bh1745
+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
+26 -54
View File
@@ -21,10 +21,9 @@ public ble_api.h.
"""
import logging
import re
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components import libretiny, wifi
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
@@ -33,30 +32,14 @@ from esphome.components.libretiny.const import (
FAMILY_BK7251,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ENABLE_ON_BOOT,
CONF_ESPHOME,
CONF_ID,
CONF_PLATFORMIO_OPTIONS,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE, EsphomeError
import esphome.final_validate as fv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
from esphome.core import EsphomeError
from esphome.types import ConfigType
DEPENDENCIES = ["bk72xx"]
CODEOWNERS = ["@Bl00d-B0b"]
DOMAIN = "bk72xx_ble"
BEKEN_BDK_VERSION_OPTION = "custom_versions.beken-bdk"
# Older BDKs lay the BLE stack out differently and can crash at boot or erase the
# bootloader with BLE compiled in; the libretiny platform builds with this one
MIN_BDK_VERSION = (3, 0, 78)
# Older LibreTiny releases arm the Beken MCU sleep with wifi power save, which halts
# the device once the station stops while BLE runs (libretiny-eu/libretiny#414)
MIN_LIBRETINY_VERSION = (1, 14, 0)
_LOGGER = logging.getLogger(__name__)
bk72xx_ble_ns = cg.esphome_ns.namespace("bk72xx_ble")
@@ -97,45 +80,20 @@ def _unsupported_family_message(family: str) -> str | None:
return None
def check_bdk_version(value: str | None) -> None:
"""Reject a beken-bdk override older than the release the BLE stack needs."""
if value is None:
return
# LibreTiny appends the value to the package URL as a git ref and only builds
# 3.0.x releases; the 2021.06.07 tag is the pre 3.0 SDK
match = re.fullmatch(r"v?(\d+)\.(\d+)\.(\d+)", str(value))
if match is not None:
version = tuple(int(g) for g in match.groups())
if version[:2] == MIN_BDK_VERSION[:2] and version >= MIN_BDK_VERSION:
return
minimum = ".".join(str(part) for part in MIN_BDK_VERSION)
raise cv.Invalid(
f"bk72xx_ble needs beken-bdk {minimum} or a newer 3.0.x release, not "
f"'{value}': with BLE compiled in, an older BDK can crash at boot or erase "
"the bootloader"
)
def check_libretiny_version(version: cv.Version) -> None:
"""Reject a LibreTiny release older than the one whose WiFi sleep is safe with BLE."""
if (version.major, version.minor, version.patch) >= MIN_LIBRETINY_VERSION:
return
minimum = ".".join(str(part) for part in MIN_LIBRETINY_VERSION)
raise cv.Invalid(
f"bk72xx_ble needs LibreTiny {minimum} or newer, not {version}: older releases "
"arm the Beken MCU sleep with wifi power save and the device halts once the "
"station stops while BLE runs"
)
def _final_validate(config: ConfigType) -> None:
# Warn only: a hard error here would break the validate-only CI fixtures,
# which run on a BLE 4.2 board. The hard error is raised at codegen.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_LOGGER.warning("%s (this configuration cannot compile)", msg)
options = fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS) or {}
check_bdk_version(options.get(BEKEN_BDK_VERSION_OPTION))
check_libretiny_version(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
# sleep. With the BLE controller running, that sleep never wakes up once the
# station is stopped (adapter restart after failed roams, wifi.disable): the
# device is dead until a power cycle (esphome#18592). Keep power save off
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
wifi.force_power_save_off(
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -158,6 +116,20 @@ async def to_code(config: ConfigType) -> None:
# so this =1 is the single CFG_SUPPORT_BLE define emitted.
cg.add_platformio_option("custom_options.sys_config#h", ["CFG_SUPPORT_BLE=1"])
# Pin the Beken BDK release the BLE 5.x stack is validated against. The
# bundled 3.0.33 has an older BLE header/library layout — and with
# CFG_SUPPORT_BLE=1 the SDK runs its BLE init unconditionally during boot
# (the reason the libretiny platform sets =0 when BLE is unused), so a
# mismatched BDK can crash the device before WiFi comes up regardless of
# enable_on_boot. Pinning here makes a plain config build against the
# validated BDK without any manual platformio_options.
_LOGGER.warning(
"bk72xx_ble builds with beken-bdk 3.0.78 instead of the platform's bundled "
"default: the default's older BLE layout can crash the device at boot when "
"BLE is compiled in"
)
cg.add_platformio_option("custom_versions.beken-bdk", "3.0.78")
# The BDK exposes the controller's BLE address as `common_default_bdaddr` on
# BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is
# derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++
+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;
}
@@ -1,7 +1,7 @@
#include "bme680_bsec.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <string>
namespace esphome::bme680_bsec {
#ifdef USE_BSEC
@@ -9,7 +9,7 @@ ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
PROGMEM_STRING_TABLE(IaqAccuracyStrings, "Stabilizing", "Uncertain", "Calibrating", "Calibrated");
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
std::vector<BME680BSECComponent *>
BME680BSECComponent::instances; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -386,11 +386,10 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX));
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
@@ -436,9 +435,8 @@ void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, b
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value) {
if (!sensor ||
(sensor->has_state() && ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(value)) == 0)) {
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
return;
}
sensor->publish_state(value);
+1 -1
View File
@@ -77,7 +77,7 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
int64_t get_time_ns_();
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value);
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
void snapshot_state_(); // Fetch the current BSEC library state and save it in the bsec_state_data_ member (volatile
// memory)
+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'",
+1 -2
View File
@@ -80,8 +80,7 @@ void BP1658CJ::loop() {
this->update_ = false;
}
void BP1658CJ::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(state * 0x3FF);
void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
if (this->pwm_amounts_[channel] != value) {
this->update_ = true;
this->update_channel_ = channel;
+18 -1
View File
@@ -2,12 +2,15 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
namespace esphome::bp1658cj {
class BP1658CJ final : public Component {
public:
class Channel;
void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
void set_max_power_color_channels(uint8_t max_power_color_channels) {
@@ -26,9 +29,23 @@ class BP1658CJ final : public Component {
/// Send new values if they were updated.
void loop() override;
void write_channel(uint8_t channel, float state);
class Channel final : public output::FloatOutput {
public:
void set_parent(BP1658CJ *parent) { parent_ = parent; }
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
void write_state(float state) override {
auto amount = static_cast<uint16_t>(state * 0x3FF);
this->parent_->set_channel_value_(this->channel_, amount);
}
BP1658CJ *parent_;
uint8_t channel_;
};
protected:
void set_channel_value_(uint8_t channel, uint16_t value);
void write_bit_(bool value);
void write_byte_(uint8_t data);
void write_buffer_(uint8_t *buffer, uint8_t size);
+9 -5
View File
@@ -1,3 +1,4 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -7,15 +8,16 @@ from . import BP1658CJ
DEPENDENCIES = ["bp1658cj"]
Channel = BP1658CJ.class_("Channel", output.FloatOutput)
CONF_BP1658CJ_ID = "bp1658cj_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
}
)
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
@@ -24,6 +26,8 @@ FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
async def to_code(config: ConfigType) -> None:
await output.sub_float_outputs(config, parent=config[CONF_BP1658CJ_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_BP1658CJ_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
+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
@@ -14,7 +14,11 @@ void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
if (value == this->last_)
return;
this->last_ = value;
this->publish_state(StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX));
const ProgmemStr name = StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX);
char text[32];
ESPHOME_strncpy_P(text, reinterpret_cast<ESPHOME_PGM_P>(name), sizeof(text));
text[sizeof(text) - 1] = '\0';
this->publish_state(text);
}
} // namespace esphome::bq25186
+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_);
+4 -7
View File
@@ -2,7 +2,6 @@
#include "cc1101pa.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cmath>
#include <cstring>
@@ -15,9 +14,6 @@ namespace esphome::cc1101 {
ESPHOME_LOG_TAG(TAG, "cc1101");
static constexpr size_t TX_FIFO_SIZE = 64;
// MDMCFG2 MOD_FORMAT values
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
int e_tmp;
float m_tmp = std::frexp(value, &e_tmp);
@@ -242,6 +238,8 @@ void CC1101Component::loop() {
}
void CC1101Component::dump_config() {
static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK",
"4-FSK", "UNUSED", "UNUSED", "MSK"};
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
XTAL_FREQUENCY / (1 << 16);
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
@@ -255,9 +253,8 @@ void CC1101Component::dump_config() {
" Symbol Rate: %.0f baud\n"
" Filter Bandwidth: %.1f Hz\n"
" Output Power: %.1f dBm",
this->chip_id_, freq, this->state_.CHANNR,
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
this->output_power_effective_);
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
symbol_rate, bw, this->output_power_effective_);
LOG_PIN(" CS Pin: ", this->cs_);
}
+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_() {
+12 -7
View File
@@ -163,21 +163,26 @@ void CW2017::update() {
if (!this->ready_)
return;
bool ok = true;
if (this->battery_level_sensor_ != nullptr) {
uint8_t soc = 0;
ok = this->read_byte(REG_SOC, &soc) && soc <= 100;
if (ok)
if (this->read_byte(REG_SOC, &soc) && soc <= 100) {
this->battery_level_sensor_->publish_state(soc);
} else {
this->status_set_warning();
return;
}
}
if (ok && this->battery_voltage_sensor_ != nullptr) {
if (this->battery_voltage_sensor_ != nullptr) {
uint16_t raw = 0;
ok = this->read_byte_16(REG_VCELL, &raw);
if (ok)
if (this->read_byte_16(REG_VCELL, &raw)) {
this->battery_voltage_sensor_->publish_state((raw & VCELL_MASK) * VCELL_VOLTS_PER_LSB);
} else {
this->status_set_warning();
return;
}
}
this->status_comm_result(ok);
this->status_clear_warning();
}
void CW2017::dump_config() {
+4 -2
View File
@@ -12,8 +12,8 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
public:
explicit CW2017(const uint8_t *profile) : profile_(profile) {}
SUB_SENSOR(battery_level)
SUB_SENSOR(battery_voltage)
void set_battery_level_sensor(sensor::Sensor *sensor) { this->battery_level_sensor_ = sensor; }
void set_battery_voltage_sensor(sensor::Sensor *sensor) { this->battery_voltage_sensor_ = sensor; }
void setup() override;
void update() override;
@@ -29,6 +29,8 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
void wait_ready_(uint8_t attempt);
const uint8_t *profile_;
sensor::Sensor *battery_level_sensor_{nullptr};
sensor::Sensor *battery_voltage_sensor_{nullptr};
bool ready_{false};
bool restart_pending_{false};
uint8_t init_attempts_{0};
+13 -6
View File
@@ -48,14 +48,15 @@ void DAC7678Output::dump_config() {
}
}
void DAC7678Output::register_channel(uint8_t channel) {
this->min_channel_ = std::min(this->min_channel_, channel);
this->max_channel_ = std::max(this->max_channel_, channel);
ESP_LOGV(TAG, "Registered channel: %01u", channel);
void DAC7678Output::register_channel(DAC7678Channel *channel) {
auto c = channel->channel_;
this->min_channel_ = std::min(this->min_channel_, c);
this->max_channel_ = std::max(this->max_channel_, c);
channel->set_parent(this);
ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_);
}
void DAC7678Output::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(roundf(state * 0xFFF));
void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
if (channel >= std::size(this->dac_input_reg_))
return;
if (this->dac_input_reg_[channel] != value) {
@@ -70,4 +71,10 @@ void DAC7678Output::write_channel(uint8_t channel, float state) {
this->status_clear_warning();
}
void DAC7678Channel::write_state(float state) {
const float input_rounded = roundf(state * this->full_scale_);
auto input = static_cast<uint16_t>(input_rounded);
this->parent_->set_channel_value_(this->channel_, input);
}
} // namespace esphome::dac7678
+23 -2
View File
@@ -1,17 +1,34 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::dac7678 {
class DAC7678Output;
class DAC7678Channel final : public output::FloatOutput, public Parented<DAC7678Output> {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
friend class DAC7678Output;
const uint16_t full_scale_ = 0xFFF;
void write_state(float state) override;
uint8_t channel_;
};
/// DAC7678 float output component.
class DAC7678Output final : public Component, public i2c::I2CDevice {
public:
DAC7678Output() {}
void register_channel(uint8_t channel);
void write_channel(uint8_t channel, float state);
void register_channel(DAC7678Channel *channel);
void set_internal_reference(const bool value) { this->internal_reference_ = value; }
@@ -20,8 +37,12 @@ class DAC7678Output final : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
friend DAC7678Channel;
bool internal_reference_;
void set_channel_value_(uint8_t channel, uint16_t value);
uint8_t min_channel_{0xFF};
uint8_t max_channel_{0x00};
uint16_t dac_input_reg_[8] = {
+11 -8
View File
@@ -2,17 +2,19 @@ import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
from . import DAC7678Output
from . import DAC7678Output, dac7678_ns
DEPENDENCIES = ["dac7678"]
DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput)
CONF_DAC7678_ID = "dac7678_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_ID): cv.declare_id(DAC7678Channel),
cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
}
@@ -24,9 +26,10 @@ FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DAC7678_ID])
await output.sub_float_outputs(config, parent=hub).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
cg.add(hub.register_channel(config[CONF_CHANNEL]))
async def to_code(config: ConfigType) -> MockObj:
paren = await cg.get_variable(config[CONF_DAC7678_ID])
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(paren.register_channel(var))
await output.register_output(var, config)
return var
+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:"); }
+16 -18
View File
@@ -1,7 +1,5 @@
#pragma once
#include <algorithm>
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
@@ -29,12 +27,9 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
TEMPLATABLE_VALUE(float, det_max4)
void play(const Ts &...x) override {
// Worst case: stop, factory reset, six settings, save, start
if (!this->parent_->has_queue_room(10))
return;
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, false);
this->parent_->enqueue(make_unique<PowerCommand>(0));
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_FACTORY_RESET);
this->parent_->enqueue(make_unique<FactoryResetCommand>());
}
if (this->det_min1_.has_value() && this->det_max1_.has_value()) {
// Each segment is read once, so the guard and the command see the same values.
@@ -42,10 +37,10 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
if (min1 >= 0) {
const float max1 = this->det_max1_.value(x...);
if (max1 >= 0) {
this->parent_->enqueue_det_range_cfg(min1, max1, this->det_min2_.value_or(x..., -1),
this->det_max2_.value_or(x..., -1), this->det_min3_.value_or(x..., -1),
this->det_max3_.value_or(x..., -1), this->det_min4_.value_or(x..., -1),
this->det_max4_.value_or(x..., -1));
this->parent_->enqueue(make_unique<DetRangeCfgCommand>(
min1, max1, this->det_min2_.value_or(x..., -1), this->det_max2_.value_or(x..., -1),
this->det_min3_.value_or(x..., -1), this->det_max3_.value_or(x..., -1),
this->det_min4_.value_or(x..., -1), this->det_max4_.value_or(x..., -1)));
}
}
}
@@ -53,35 +48,38 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
float detect = this->delay_after_detect_.value(x...);
float disappear = this->delay_after_disappear_.value(x...);
if (detect >= 0 && disappear >= 0) {
this->parent_->enqueue_set_latency(detect, disappear);
this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
}
}
if (this->start_after_power_on_.has_value()) {
int8_t val = this->start_after_power_on_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSOR_CFG_START, val);
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(val));
}
}
if (this->turn_on_led_.has_value()) {
int8_t val = this->turn_on_led_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_LED_MODE, val);
this->parent_->enqueue(make_unique<LedModeCommand>(val));
}
}
if (this->presence_via_uart_.has_value()) {
int8_t val = this->presence_via_uart_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_UART_OUTPUT, val);
this->parent_->enqueue(make_unique<UartOutputCommand>(val));
}
}
if (this->sensitivity_.has_value()) {
int8_t val = this->sensitivity_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSITIVITY, std::min<int8_t>(val, 9));
if (val > 9) {
val = 9;
}
this->parent_->enqueue(make_unique<SensitivityCommand>(val));
}
}
this->parent_->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, true);
this->parent_->enqueue(make_unique<SaveCfgCommand>());
this->parent_->enqueue(make_unique<PowerCommand>(1));
}
};
@@ -0,0 +1,332 @@
#include "commands.h"
#include <cmath>
#include "esphome/core/log.h"
#include "dfrobot_sen0395.h"
namespace esphome::dfrobot_sen0395 {
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395.commands");
uint8_t Command::execute(DfrobotSen0395Component *parent) {
this->parent_ = parent;
if (this->cmd_sent_) {
if (this->parent_->read_message_()) {
std::string message(this->parent_->read_buffer_);
if (message.rfind("is not recognized as a CLI command") != std::string::npos) {
ESP_LOGD(TAG, "Command not recognized properly by sensor");
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
return 0;
} else {
this->parent_->find_prompt_();
return 1; // Command done
}
}
uint8_t rc = on_message(message);
if (rc == 2) {
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
return 0;
} else {
this->parent_->find_prompt_();
return 1; // Command done
}
} else if (rc == 0) {
return 0;
} else {
this->parent_->find_prompt_();
return 1;
}
}
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
ESP_LOGD(TAG, "Command timeout");
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
} else {
return 1; // Command done
}
}
} else if (this->parent_->send_cmd_(this->cmd_.c_str(), this->cmd_duration_ms_)) {
this->cmd_sent_ = true;
}
return 0; // Command not done yet
}
uint8_t ReadStateCommand::execute(DfrobotSen0395Component *parent) {
this->parent_ = parent;
if (this->parent_->read_message_()) {
std::string message(this->parent_->read_buffer_);
if (message.rfind("$JYBSS,0, , , *") != std::string::npos) {
this->parent_->set_detected_(false);
this->parent_->set_active(true);
return 1; // Command done
} else if (message.rfind("$JYBSS,1, , , *") != std::string::npos) {
this->parent_->set_detected_(true);
this->parent_->set_active(true);
return 1; // Command done
}
}
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
return 1; // Command done, timeout
}
return 0; // Command not done yet.
}
uint8_t ReadStateCommand::on_message(std::string &message) { return 1; }
uint8_t PowerCommand::on_message(std::string &message) {
if (message == "sensor stopped already") {
this->parent_->set_active(false);
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
return 1; // Command done
} else if (message == "sensor started already") {
this->parent_->set_active(true);
ESP_LOGI(TAG, "Started sensor (already started)");
return 1; // Command done
} else if (message == "new parameter isn't save, can't startSensor") {
this->parent_->set_active(false);
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
return 1; // Command done
} else if (message == "Done") {
this->parent_->set_active(this->power_on_);
if (this->power_on_) {
ESP_LOGI(TAG, "Started sensor");
} else {
ESP_LOGI(TAG, "Stopped sensor");
}
return 1; // Command done
}
return 0; // Command not done yet.
}
DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3,
float min4, float max4) {
// TODO: Print warning when values are rounded
if (min1 < 0 || max1 < 0) {
this->min1_ = min1 = 0;
this->max1_ = max1 = 0;
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
ESP_LOGW(TAG, "DetRangeCfgCommand invalid input parameters. Using range config 0 0.");
this->cmd_ = "detRangeCfg -1 0 0";
} else if (min2 < 0 || max2 < 0) {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15f, max1 / 0.15f);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f, min3 / 0.15f, max3 / 0.15f);
this->cmd_ = buf;
} else {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
this->min4_ = min4 = roundf(min4 / 0.15f) * 0.15f;
this->max4_ = max4 = roundf(max4 / 0.15f) * 0.15f;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f,
min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f);
this->cmd_ = buf;
}
this->min1_ = min1;
this->max1_ = max1;
this->min2_ = min2;
this->max2_ = max2;
this->min3_ = min3;
this->max3_ = max3;
this->min4_ = min4;
this->max4_ = max4;
};
uint8_t DetRangeCfgCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure range config. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG,
"Updated detection area config:\n"
" Detection area 1 from %.02fm to %.02fm.",
this->min1_, this->max1_);
if (this->min2_ >= 0 && this->max2_ >= 0) {
ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_);
}
if (this->min3_ >= 0 && this->max3_ >= 0) {
ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_);
}
if (this->min4_ >= 0 && this->max4_ >= 0) {
ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_);
}
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet.
}
SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
// max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32
char buf[32];
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
this->cmd_ = buf;
};
uint8_t SetLatencyCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG,
"Updated output latency config:\n"
" Signal that someone was detected is delayed by %.03f s.\n"
" Signal that nobody is detected anymore is delayed by %.03f s.",
this->delay_after_detection_, this->delay_after_disappear_);
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SensorCfgStartCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure sensor startup behavior. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Updated sensor startup behavior:");
if (startup_mode_) {
this->parent_->set_start_after_boot(true);
ESP_LOGI(TAG, "Sensor will start automatically after power-on.");
} else {
this->parent_->set_start_after_boot(false);
ESP_LOGI(TAG, "Sensor needs to be started manually after power-on.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t FactoryResetCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot factory reset. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Sensor factory reset done.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t ResetSystemCommand::on_message(std::string &message) {
if (message == "leapMMW:/>") {
ESP_LOGI(TAG, "Restarted sensor.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SaveCfgCommand::on_message(std::string &message) {
if (message == "no parameter has changed") {
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t LedModeCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set led mode. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set led mode done.");
if (this->active_) {
this->parent_->set_led_active(true);
ESP_LOGI(TAG, "Sensor LED will blink.");
} else {
this->parent_->set_led_active(false);
ESP_LOGI(TAG, "Turned off LED.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t UartOutputCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set uart output mode. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set uart mode done.");
if (this->active_) {
this->parent_->set_uart_presence_active(true);
ESP_LOGI(TAG, "Presence information is sent via UART and GPIO.");
} else {
this->parent_->set_uart_presence_active(false);
ESP_LOGI(TAG, "Presence information is only sent via GPIO.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SensitivityCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set sensitivity. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", this->sensitivity_);
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
} // namespace esphome::dfrobot_sen0395
@@ -0,0 +1,152 @@
#pragma once
#include <cstdint>
#include <string>
#include "esphome/core/helpers.h"
namespace esphome::dfrobot_sen0395 {
class DfrobotSen0395Component;
// Use command queue and time stamps to avoid blocking.
// When component has run time, check if minimum time (1s) between
// commands has passed. After that run a command from the queue.
class Command {
public:
virtual ~Command() = default;
virtual uint8_t execute(DfrobotSen0395Component *parent);
virtual uint8_t on_message(std::string &message) = 0;
protected:
DfrobotSen0395Component *parent_{nullptr};
std::string cmd_;
bool cmd_sent_{false};
int8_t retries_left_{2};
uint32_t cmd_duration_ms_{1000};
uint32_t timeout_ms_{1500};
};
class ReadStateCommand : public Command {
public:
ReadStateCommand() { timeout_ms_ = 500; }
uint8_t execute(DfrobotSen0395Component *parent) override;
uint8_t on_message(std::string &message) override;
};
class PowerCommand : public Command {
public:
PowerCommand(bool power_on) : power_on_(power_on) {
if (power_on) {
cmd_ = "sensorStart";
} else {
cmd_ = "sensorStop";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool power_on_;
};
class DetRangeCfgCommand : public Command {
public:
DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, float min4, float max4);
uint8_t on_message(std::string &message) override;
protected:
float min1_, max1_, min2_, max2_, min3_, max3_, min4_, max4_;
// TODO: Set min max values in component, so they can be published as sensor.
};
class SetLatencyCommand : public Command {
public:
SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
uint8_t on_message(std::string &message) override;
protected:
float delay_after_detection_;
float delay_after_disappear_;
};
class SensorCfgStartCommand : public Command {
public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd);
}
uint8_t on_message(std::string &message) override;
protected:
bool startup_mode_;
};
class FactoryResetCommand : public Command {
public:
FactoryResetCommand() { cmd_ = "factoryReset 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; };
uint8_t on_message(std::string &message) override;
};
class ResetSystemCommand : public Command {
public:
ResetSystemCommand() { cmd_ = "resetSystem"; }
uint8_t on_message(std::string &message) override;
};
class SaveCfgCommand : public Command {
public:
SaveCfgCommand() {
cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89";
cmd_duration_ms_ = 3000;
timeout_ms_ = 3500;
}
uint8_t on_message(std::string &message) override;
};
class LedModeCommand : public Command {
public:
LedModeCommand(bool active) : active_(active) {
if (active) {
cmd_ = "setLedMode 1 0";
} else {
cmd_ = "setLedMode 1 1";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool active_;
};
class UartOutputCommand : public Command {
public:
UartOutputCommand(bool active) : active_(active) {
if (active) {
cmd_ = "setUartOutput 1 1";
} else {
cmd_ = "setUartOutput 1 0";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool active_;
};
class SensitivityCommand : public Command {
public:
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9)
sensitivity_ = sensitivity = 9;
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd);
};
uint8_t on_message(std::string &message) override;
protected:
uint8_t sensitivity_;
};
} // namespace esphome::dfrobot_sen0395
@@ -1,27 +1,13 @@
#include "dfrobot_sen0395.h"
#include <cmath>
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::dfrobot_sen0395 {
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395");
static constexpr char ASCII_CR = 0x0D;
static constexpr char ASCII_LF = 0x0A;
static constexpr float DET_RANGE_STEP_M = 0.15f;
static constexpr float LATENCY_STEP_S = 0.025f;
// What each CommandType cannot do while the sensor runs; empty for commands that do not need it stopped
PROGMEM_STRING_TABLE(BusyActions, "", "configure range config", "configure output latency",
"configure sensor startup behavior", "factory reset", "", "", "set led mode",
"set uart output mode", "set sensitivity");
static_assert(BusyActions::COUNT == static_cast<size_t>(CommandType::COMMAND_TYPE_SENSITIVITY) + 1,
"BusyActions needs one entry per CommandType");
const char ASCII_CR = 0x0D;
const char ASCII_LF = 0x0A;
void DfrobotSen0395Component::dump_config() {
ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:");
@@ -37,270 +23,23 @@ void DfrobotSen0395Component::dump_config() {
}
void DfrobotSen0395Component::loop() {
if (this->cmd_queue_.empty()) {
this->read_state_();
return;
if (cmd_queue_.is_empty()) {
// Command queue empty. Read sensor state.
cmd_queue_.enqueue(make_unique<ReadStateCommand>());
}
// Commands are non-blocking and need to be called repeatedly
if (this->run_command_(this->cmd_queue_.front(), App.get_loop_component_start_time())) {
this->cmd_queue_.pop();
this->cmd_sent_ = false;
this->retries_left_ = 2;
// Commands are non-blocking and need to be called repeatedly.
if (cmd_queue_.process(this)) {
// Dequeue if command is done
cmd_queue_.dequeue();
}
}
bool DfrobotSen0395Component::enqueue(CommandType type, uint8_t arg) {
if (!this->cmd_queue_.push({type, arg})) {
ESP_LOGE(TAG, "Command queue is full");
return false;
}
return true;
int8_t DfrobotSen0395Component::enqueue(std::unique_ptr<Command> cmd) {
return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move
}
bool DfrobotSen0395Component::has_queue_room(size_t count) const {
if (COMMAND_QUEUE_SIZE - this->cmd_queue_.size() >= count)
return true;
ESP_LOGE(TAG, "Command queue is full");
return false;
}
void DfrobotSen0395Component::enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3,
float max3, float min4, float max4) {
// TODO: Print warning when values are rounded
const float values[8] = {min1, max1, min2, max2, min3, max3, min4, max4};
int16_t steps[8];
bool in_use = true;
for (size_t i = 0; i < 8; i += 2) {
in_use = in_use && values[i] >= 0 && values[i + 1] >= 0;
for (size_t j = i; j < i + 2; j++) {
steps[j] = in_use ? static_cast<int16_t>(clamp(roundf(values[j] / DET_RANGE_STEP_M), 0.0f, 32767.0f)) : -1;
}
}
if (steps[0] < 0) {
ESP_LOGW(TAG, "Invalid detection range, using 0 0");
steps[0] = steps[1] = 0;
}
if (this->enqueue(CommandType::COMMAND_TYPE_DET_RANGE_CFG))
memcpy(this->det_range_steps_, steps, sizeof(steps));
}
void DfrobotSen0395Component::enqueue_set_latency(float delay_after_detection, float delay_after_disappear) {
if (!this->enqueue(CommandType::COMMAND_TYPE_SET_LATENCY))
return;
// 1638.375 s is 65535 steps
this->latency_steps_[0] =
static_cast<uint16_t>(clamp(std::round(delay_after_detection / LATENCY_STEP_S), 0.0f, 65535.0f));
this->latency_steps_[1] =
static_cast<uint16_t>(clamp(std::round(delay_after_disappear / LATENCY_STEP_S), 0.0f, 65535.0f));
}
void DfrobotSen0395Component::enqueue_saved_setting(CommandType type, bool value) {
const bool was_active = this->active_;
if (!this->has_queue_room(was_active ? 4 : 2))
return;
if (was_active)
this->enqueue(CommandType::COMMAND_TYPE_POWER, false);
this->enqueue(type, value);
this->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
if (was_active)
this->enqueue(CommandType::COMMAND_TYPE_POWER, true);
}
bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
// The interval between two commands must be larger than the command duration
const uint32_t duration = cmd.type == CommandType::COMMAND_TYPE_SAVE_CFG ? 3000 : 1000;
if (!this->cmd_sent_) {
if (now - this->ts_last_cmd_sent_ > duration) {
char buf[64]; // "detRangeCfg -1" + 8 * " 32767" + null
this->format_command(cmd, buf, sizeof(buf));
ESP_LOGD(TAG, "Sending command: %s", buf);
this->write_str(buf);
this->ts_last_cmd_sent_ = now;
this->cmd_sent_ = true;
}
return false; // Command not done yet
}
if (this->read_message_()) {
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
if (!this->handle_reply_(cmd))
return false; // Command not done yet
this->read_message_(); // Skip the prompt
return true;
}
ESP_LOGD(TAG, "Command not recognized properly by sensor");
} else if (now - this->ts_last_cmd_sent_ <= duration + 500) {
return false; // Command not done yet
} else {
ESP_LOGD(TAG, "Command timeout");
}
if (this->retries_left_ == 0) {
this->read_message_(); // Skip the prompt
return true; // Give up
}
this->retries_left_--;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
return false;
}
void DfrobotSen0395Component::read_state_() {
if (!this->read_message_())
return;
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
this->set_detected_(false);
this->set_active(true);
} else if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
this->set_detected_(true);
this->set_active(true);
}
}
static size_t append_cfg_key(char *buf, size_t size, size_t pos) {
return buf_append_str(buf, size, pos, " 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89");
}
void DfrobotSen0395Component::format_command(const Command &cmd, char *buf, size_t size) const {
buf[0] = '\0';
switch (cmd.type) {
case CommandType::COMMAND_TYPE_POWER:
if (cmd.arg) {
buf_append_str(buf, size, 0, "sensorStart");
} else {
buf_append_str(buf, size, 0, "sensorStop");
}
break;
case CommandType::COMMAND_TYPE_DET_RANGE_CFG: {
size_t pos = buf_append_str(buf, size, 0, "detRangeCfg -1");
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
pos = buf_append_printf(buf, size, pos, " %d %d", this->det_range_steps_[i], this->det_range_steps_[i + 1]);
}
break;
}
case CommandType::COMMAND_TYPE_SET_LATENCY:
buf_append_printf(buf, size, 0, "setLatency %.03f %.03f", this->latency_steps_[0] * LATENCY_STEP_S,
this->latency_steps_[1] * LATENCY_STEP_S);
break;
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
buf_append_printf(buf, size, 0, "sensorCfgStart %d", cmd.arg);
break;
case CommandType::COMMAND_TYPE_FACTORY_RESET:
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "factoryReset"));
break;
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
buf_append_str(buf, size, 0, "resetSystem");
break;
case CommandType::COMMAND_TYPE_SAVE_CFG:
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "saveCfg"));
break;
case CommandType::COMMAND_TYPE_LED_MODE:
buf_append_printf(buf, size, 0, "setLedMode 1 %d", cmd.arg ? 0 : 1);
break;
case CommandType::COMMAND_TYPE_UART_OUTPUT:
buf_append_printf(buf, size, 0, "setUartOutput 1 %d", cmd.arg ? 1 : 0);
break;
case CommandType::COMMAND_TYPE_SENSITIVITY:
buf_append_printf(buf, size, 0, "setSensitivity %d", cmd.arg);
break;
}
}
bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
const char *message = this->read_buffer_;
auto is = [message](ESPHOME_PGM_P text) { return ESPHOME_strcmp_P(message, text) == 0; };
// Commands with their own replies
switch (cmd.type) {
case CommandType::COMMAND_TYPE_POWER:
if (is(ESPHOME_PSTR("sensor stopped already"))) {
this->set_active(false);
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
} else if (is(ESPHOME_PSTR("sensor started already"))) {
this->set_active(true);
ESP_LOGI(TAG, "Started sensor (already started)");
} else if (is(ESPHOME_PSTR("new parameter isn't save, can't startSensor"))) {
this->set_active(false);
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
} else if (is(ESPHOME_PSTR("Done"))) {
this->set_active(cmd.arg);
ESP_LOGI(TAG, "%s", cmd.arg ? LOG_STR_LITERAL("Started sensor") : LOG_STR_LITERAL("Stopped sensor"));
} else {
return false;
}
return true;
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
if (!is(ESPHOME_PSTR("leapMMW:/>")))
return false;
ESP_LOGI(TAG, "Restarted sensor.");
return true;
case CommandType::COMMAND_TYPE_SAVE_CFG:
if (is(ESPHOME_PSTR("no parameter has changed"))) {
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
} else if (is(ESPHOME_PSTR("Done"))) {
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
} else {
return false;
}
return true;
default:
break;
}
// Configuration commands, which need the sensor stopped
if (is(ESPHOME_PSTR("sensor is not stopped"))) {
ESP_LOGE(TAG, "Cannot %s. Sensor is not stopped!",
LOG_STR_ARG(BusyActions::get_log_str(static_cast<uint8_t>(cmd.type), 0)));
return true;
}
if (!is(ESPHOME_PSTR("Done")))
return false;
switch (cmd.type) {
case CommandType::COMMAND_TYPE_DET_RANGE_CFG:
ESP_LOGI(TAG, "Updated detection area config:");
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
ESP_LOGI(TAG, " Detection area %u from %.02fm to %.02fm.", static_cast<unsigned>(i / 2 + 1),
this->det_range_steps_[i] * DET_RANGE_STEP_M, this->det_range_steps_[i + 1] * DET_RANGE_STEP_M);
}
break;
case CommandType::COMMAND_TYPE_SET_LATENCY:
ESP_LOGI(TAG,
"Updated output latency config:\n"
" Signal that someone was detected is delayed by %.03f s.\n"
" Signal that nobody is detected anymore is delayed by %.03f s.",
this->latency_steps_[0] * LATENCY_STEP_S, this->latency_steps_[1] * LATENCY_STEP_S);
break;
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
this->set_start_after_boot(cmd.arg);
ESP_LOGI(TAG, "Updated sensor startup behavior: %s",
cmd.arg ? LOG_STR_LITERAL("sensor will start automatically after power-on.")
: LOG_STR_LITERAL("sensor needs to be started manually after power-on."));
break;
case CommandType::COMMAND_TYPE_FACTORY_RESET:
ESP_LOGI(TAG, "Sensor factory reset done.");
break;
case CommandType::COMMAND_TYPE_LED_MODE:
this->set_led_active(cmd.arg);
ESP_LOGI(TAG, "Set led mode done. %s",
cmd.arg ? LOG_STR_LITERAL("Sensor LED will blink.") : LOG_STR_LITERAL("Turned off LED."));
break;
case CommandType::COMMAND_TYPE_UART_OUTPUT:
this->set_uart_presence_active(cmd.arg);
ESP_LOGI(TAG, "Set uart mode done. %s",
cmd.arg ? LOG_STR_LITERAL("Presence information is sent via UART and GPIO.")
: LOG_STR_LITERAL("Presence information is only sent via GPIO."));
break;
case CommandType::COMMAND_TYPE_SENSITIVITY:
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", cmd.arg);
break;
default:
break;
}
return true;
}
bool DfrobotSen0395Component::read_message_() {
uint8_t DfrobotSen0395Component::read_message_() {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
@@ -323,11 +62,33 @@ bool DfrobotSen0395Component::read_message_() {
this->read_buffer_[this->read_pos_] = 0;
this->read_pos_ = 0;
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
return true; // Full message in buffer
return 1; // Full message in buffer
} else {
this->read_pos_++;
}
this->read_pos_++;
}
return false; // No full message yet
return 0; // No full message yet
}
uint8_t DfrobotSen0395Component::find_prompt_() {
if (this->read_message_()) {
std::string message(this->read_buffer_);
if (message.rfind("leapMMW:/>") != std::string::npos) {
return 1; // Prompt found
}
}
return 0; // Not found yet
}
uint8_t DfrobotSen0395Component::send_cmd_(const char *cmd, uint32_t duration) {
// The interval between two commands must be larger than the specified duration (in ms).
if (millis() - ts_last_cmd_sent_ > duration) {
this->write_str(cmd);
ts_last_cmd_sent_ = millis();
return 1; // Command sent
}
// Could not send command yet as command duration did not fully pass yet.
return 0;
}
void DfrobotSen0395Component::set_detected_(bool detected) {
@@ -338,4 +99,44 @@ void DfrobotSen0395Component::set_detected_(bool detected) {
#endif
}
int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
if (this->is_full()) {
ESP_LOGE(TAG, "Command queue is full");
return -1;
} else if (this->is_empty()) {
front_++;
}
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
return 1;
}
std::unique_ptr<Command> CircularCommandQueue::dequeue() {
if (this->is_empty())
return nullptr;
std::unique_ptr<Command> dequeued_cmd = std::move(commands_[front_]);
if (front_ == rear_) {
front_ = -1;
rear_ = -1;
} else {
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
}
return dequeued_cmd;
}
bool CircularCommandQueue::is_empty() { return front_ == -1; }
bool CircularCommandQueue::is_full() { return (rear_ + 1) % COMMAND_QUEUE_SIZE == front_; }
// Run execute method of first in line command.
// Execute is non-blocking and has to be called until it returns 1.
uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) {
if (!is_empty()) {
return commands_[front_]->execute(parent);
} else {
return 1;
}
}
} // namespace esphome::dfrobot_sen0395
@@ -3,7 +3,6 @@
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
@@ -12,30 +11,29 @@
#include "esphome/components/switch/switch.h"
#endif
#include "commands.h"
namespace esphome::dfrobot_sen0395 {
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
// Keep in sync with the BusyActions table in dfrobot_sen0395.cpp
enum class CommandType : uint8_t {
COMMAND_TYPE_POWER,
COMMAND_TYPE_DET_RANGE_CFG,
COMMAND_TYPE_SET_LATENCY,
COMMAND_TYPE_SENSOR_CFG_START,
COMMAND_TYPE_FACTORY_RESET,
COMMAND_TYPE_RESET_SYSTEM,
COMMAND_TYPE_SAVE_CFG,
COMMAND_TYPE_LED_MODE,
COMMAND_TYPE_UART_OUTPUT,
COMMAND_TYPE_SENSITIVITY,
};
// forward declaration due to circular dependency
class DfrobotSen0395Component;
/// A queued command. arg is the on/off flag, startup mode or sensitivity; the detection range and
/// latency values are held by the component.
struct Command {
CommandType type;
uint8_t arg;
static const uint8_t COMMAND_QUEUE_SIZE = 20;
class CircularCommandQueue {
public:
int8_t enqueue(std::unique_ptr<Command> cmd);
std::unique_ptr<Command> dequeue();
bool is_empty();
bool is_full();
uint8_t process(DfrobotSen0395Component *parent);
protected:
int front_{-1};
int rear_{-1};
std::unique_ptr<Command> commands_[COMMAND_QUEUE_SIZE];
};
class DfrobotSen0395Component final : public uart::UARTDevice, public Component {
@@ -95,21 +93,9 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
}
#endif
// Commands run one at a time from a queue, at least 1 s apart, so loop() never blocks
bool enqueue(CommandType type, uint8_t arg = 0);
/// True when count more commands fit; logs when they do not, so a stop/start sequence is never queued in part
bool has_queue_room(size_t count) const;
/// Queue a detection range config; segments are used up to the first one with a negative bound
void enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3, float max3, float min4,
float max4);
/// Queue an output latency config, both delays in seconds
void enqueue_set_latency(float delay_after_detection, float delay_after_disappear);
/// Queue a setting change, stopping the sensor around it and saving it when the sensor is running
void enqueue_saved_setting(CommandType type, bool value);
int8_t enqueue(std::unique_ptr<Command> cmd);
/// Queue a sensor reset
void reset_system() { this->enqueue(CommandType::COMMAND_TYPE_RESET_SYSTEM); }
/// Write the text sent to the sensor for cmd into buf
void format_command(const Command &cmd, char *buf, size_t size) const;
void reset_system() { this->enqueue(make_unique<ResetSystemCommand>()); }
protected:
#ifdef USE_BINARY_SENSOR
@@ -123,22 +109,17 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
bool start_after_boot_{false};
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
size_t read_pos_{0};
bool cmd_sent_{false};
uint8_t retries_left_{2};
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> cmd_queue_;
CircularCommandQueue cmd_queue_;
uint32_t ts_last_cmd_sent_{0};
// Range and latency of the queued commands, in sensor steps (0.15 m, 0.025 s); a later command overwrites them
int16_t det_range_steps_[8]{};
uint16_t latency_steps_[2]{};
bool read_message_();
void read_state_();
/// Run the command at the front of the queue; true when it is done
bool run_command_(const Command &cmd, uint32_t now);
/// Handle a reply to a sent command; true when the command is done
bool handle_reply_(const Command &cmd);
uint8_t read_message_();
uint8_t find_prompt_();
uint8_t send_cmd_(const char *cmd, uint32_t duration);
void set_detected_(bool detected);
friend class Command;
friend class ReadStateCommand;
};
} // namespace esphome::dfrobot_sen0395
@@ -2,49 +2,63 @@ import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
from esphome.cpp_generator import MockObjClass
from esphome.types import ConfigType
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component, dfrobot_sen0395_ns
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
DEPENDENCIES = ["dfrobot_sen0395"]
CommandType = dfrobot_sen0395_ns.enum("CommandType", is_class=True)
dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395")
DfrobotSen0395Switch = dfrobot_sen0395_ns.class_(
"DfrobotSen0395Switch",
switch.Switch,
cg.Component,
cg.Parented.template(DfrobotSen0395Component),
)
_SWITCH_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(
Sen0395PowerSwitch = dfrobot_sen0395_ns.class_(
"Sen0395PowerSwitch", DfrobotSen0395Switch
)
Sen0395LedSwitch = dfrobot_sen0395_ns.class_("Sen0395LedSwitch", DfrobotSen0395Switch)
Sen0395UartPresenceSwitch = dfrobot_sen0395_ns.class_(
"Sen0395UartPresenceSwitch", DfrobotSen0395Switch
)
Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_(
"Sen0395StartAfterBootSwitch", DfrobotSen0395Switch
)
def _switch_schema(class_: MockObjClass) -> cv.Schema:
return (
switch.switch_schema(
class_,
entity_category=ENTITY_CATEGORY_CONFIG,
)
.extend(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(
DfrobotSen0395Component
),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.typed_schema(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component),
"sensor_active": _switch_schema(Sen0395PowerSwitch),
"turn_on_led": _switch_schema(Sen0395LedSwitch),
"presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch),
"start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch),
}
)
# Per type: the hub setter that stores the switch, and the command its writes queue
_SWITCHES = {
"sensor_active": (
lambda hub: hub.set_sensor_active_switch,
f"enqueue({CommandType.COMMAND_TYPE_POWER}, state)",
),
"turn_on_led": (
lambda hub: hub.set_turn_on_led_switch,
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_LED_MODE}, state)",
),
"presence_via_uart": (
lambda hub: hub.set_presence_via_uart_switch,
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_UART_OUTPUT}, state)",
),
"start_after_boot": (
lambda hub: hub.set_start_after_boot_switch,
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_SENSOR_CFG_START}, state)",
),
}
CONFIG_SCHEMA = cv.typed_schema(dict.fromkeys(_SWITCHES, _SWITCH_SCHEMA))
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
setter, apply = _SWITCHES[config[CONF_TYPE]]
await switch.sub_switches(config, parent=hub).create(
config, setter(hub), apply=apply
)
parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
var = await switch.new_switch(config)
await cg.register_component(var, config)
await cg.register_parented(var, parent)
cg.add(getattr(parent, f"set_{config[CONF_TYPE]}_switch")(var))
@@ -0,0 +1,46 @@
#include "dfrobot_sen0395_switch.h"
namespace esphome::dfrobot_sen0395 {
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique<PowerCommand>(state)); }
void Sen0395LedSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<LedModeCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395UartPresenceSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<UartOutputCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395StartAfterBootSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
} // namespace esphome::dfrobot_sen0395
@@ -0,0 +1,32 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "../dfrobot_sen0395.h"
namespace esphome::dfrobot_sen0395 {
class DfrobotSen0395Switch : public switch_::Switch, public Component, public Parented<DfrobotSen0395Component> {};
class Sen0395PowerSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395LedSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395UartPresenceSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395StartAfterBootSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
} // namespace esphome::dfrobot_sen0395
@@ -34,7 +34,10 @@ DFRobotSEN0465Component = dfrobot_sen0465_base_ns.class_(
"DFRobotSEN0465Component", cg.PollingComponent
)
AlarmType = dfrobot_sen0465_base_ns.enum("AlarmType", is_class=True)
ALARM_TYPES = AlarmType.options("LOW", "HIGH", prefix="ALARM_TYPE_")
ALARM_TYPES = {
"LOW": AlarmType.ALARM_TYPE_LOW,
"HIGH": AlarmType.ALARM_TYPE_HIGH,
}
# One piece of the temperature compensation from the DFRobot_MultiGasSensor library:
# for low < T <= high, compensated = raw / (a * T + b) - (c * T + d)
@@ -72,7 +72,10 @@ void DFRobotSEN0465Component::dump_config() {
if (this->alarm_type_ == AlarmType::ALARM_TYPE_OFF) {
ESP_LOGCONFIG(TAG, " Alarm: off");
} else {
const float threshold = this->alarm_threshold_ / pow10_int(this->alarm_decimals_);
float scale = 1.0f;
for (uint8_t i = 0; i < this->alarm_decimals_; i++)
scale *= 10.0f;
const float threshold = this->alarm_threshold_ / scale;
ESP_LOGCONFIG(TAG, " Alarm: %s %.*f",
this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? LOG_STR_LITERAL("above") : LOG_STR_LITERAL("below"),
this->alarm_decimals_, threshold);
@@ -80,6 +83,9 @@ void DFRobotSEN0465Component::dump_config() {
if (this->setup_incomplete_) {
ESP_LOGCONFIG(TAG, " Setup incomplete: yes");
}
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Concentration", this->concentration_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
@@ -222,14 +228,23 @@ void DFRobotSEN0465Component::handle_data_(const uint8_t *frame) {
return;
}
if (frame[5] > 2) {
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
if (this->concentration_sensor_ != nullptr)
this->concentration_sensor_->publish_state(NAN);
this->status_set_warning();
return;
float concentration = encode_uint16(frame[2], frame[3]);
switch (frame[5]) {
case 0:
break;
case 1:
concentration *= 0.1f;
break;
case 2:
concentration *= 0.01f;
break;
default:
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
if (this->concentration_sensor_ != nullptr)
this->concentration_sensor_->publish_state(NAN);
this->status_set_warning();
return;
}
float concentration = encode_uint16(frame[2], frame[3]) * pow10_int(-frame[5]);
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
if (!this->compensation_.empty())
@@ -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>
@@ -94,6 +95,12 @@ static bool answer_means_done(Operation operation, const char *line) {
}
}
static bool parse_float(const char *str, float &value) {
char *end;
value = strtof(str, &end);
return end != str && *end == '\0';
}
static const LogString *operation_to_str(Operation operation) {
switch (operation) {
case Operation::OPERATION_STOP:
@@ -131,9 +138,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_);
@@ -166,19 +173,29 @@ void DFRobotSEN0609Component::setup() {
switch_::Switch *sw = this->switches_[i];
if (sw == nullptr)
continue;
// A disabled restore mode leaves the switch on, which an inverted switch writes as off
this->publish_switch_(static_cast<SwitchSetting>(i), sw->get_initial_write_state().value_or(!sw->is_inverted()));
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
}
#endif
this->queue_configuration_();
}
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_();
}
@@ -269,6 +286,9 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
return;
}
char *fields[FRAME_FIELDS];
float distance;
float speed;
float energy;
if (split_fields(line + 7, fields, FRAME_FIELDS) < FRAME_FIELDS) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
@@ -288,10 +308,7 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
#endif
return;
}
const optional<float> distance = parse_number<float>(fields[2]);
const optional<float> speed = parse_number<float>(fields[3]);
const optional<float> energy = parse_number<float>(fields[4]);
if (!distance.has_value() || !speed.has_value() || !energy.has_value()) {
if (!parse_float(fields[2], distance) || !parse_float(fields[3], speed) || !parse_float(fields[4], energy)) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
}
@@ -300,7 +317,7 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
this->has_target_binary_sensor_->publish_state(true);
#endif
#ifdef USE_SENSOR
this->publish_target_(*distance, *speed, *energy);
this->publish_target_(distance, speed, energy);
#endif
}
@@ -313,18 +330,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 +459,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]
@@ -1,21 +1,23 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA
from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA, emc2101_ns
DEPENDENCIES = ["emc2101"]
EMC2101Output = emc2101_ns.class_("EMC2101Output", output.FloatOutput)
CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_ID): cv.declare_id(EMC2101Output),
}
)
async def to_code(config: ConfigType) -> None:
await output.sub_float_outputs(config, parent=config[CONF_EMC2101_ID]).create(
config, None, apply="set_duty_cycle(state)"
)
paren = await cg.get_variable(config[CONF_EMC2101_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
await output.register_output(var, config)
@@ -0,0 +1,7 @@
#include "emc2101_output.h"
namespace esphome::emc2101 {
void EMC2101Output::write_state(float state) { this->parent_->set_duty_cycle(state); }
} // namespace esphome::emc2101
@@ -0,0 +1,20 @@
#pragma once
#include "../emc2101.h"
#include "esphome/components/output/float_output.h"
namespace esphome::emc2101 {
/// This class allows to control the EMC2101 output.
class EMC2101Output final : public output::FloatOutput {
public:
EMC2101Output(Emc2101Component *parent) : parent_(parent) {}
protected:
/** Used by ESPHome framework. */
void write_state(float state) override;
Emc2101Component *parent_;
};
} // namespace esphome::emc2101
+15 -12
View File
@@ -4,18 +4,21 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi");
static constexpr size_t EPAPER_MAX_CMD_LOG_BYTES = 128;
PROGMEM_STRING_TABLE(EPaperStateStrings, "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", "Unknown");
static constexpr const char *const EPAPER_STATE_STRINGS[] = {
"IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
};
const LogString *EPaperBase::epaper_state_to_string_() {
return EPaperStateStrings::get_log_str(static_cast<uint8_t>(this->state_), EPaperStateStrings::LAST_INDEX);
const char *EPaperBase::epaper_state_to_string_() {
if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS))
return EPAPER_STATE_STRINGS[idx];
return "Unknown";
}
void EPaperBase::setup() {
@@ -156,7 +159,7 @@ void EPaperBase::update_effective_transform_() {
void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display already in state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
return;
}
this->set_state_(EPaperState::UPDATE);
@@ -209,7 +212,7 @@ void EPaperBase::loop() {
this->high_frequency_loop_.stop();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle in state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
this->waiting_for_idle_last_print_ = millis();
}
#endif
@@ -224,7 +227,7 @@ void EPaperBase::loop() {
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
void EPaperBase::abandon_update_() {
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
LOG_STR_ARG(this->epaper_state_to_string_()));
this->epaper_state_to_string_());
this->status_set_warning(LOG_STR("Busy pin timeout"));
if (this->state_ == EPaperState::TRANSFER_DATA)
this->abort_transfer();
@@ -244,10 +247,10 @@ void EPaperBase::abandon_update_() {
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGE(TAG, "Display is in unhandled state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->set_state_(EPaperState::IDLE);
break;
case EPaperState::IDLE:
@@ -339,7 +342,7 @@ void EPaperBase::process_state_() {
}
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
// allow subclasses to nominate delays
if (delay == 0)
@@ -347,7 +350,7 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
this->next_delay_ = 0;
this->delay_until_ = millis() + delay;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", LOG_STR_ARG(this->epaper_state_to_string_()), delay,
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
// Each step of an update otherwise waits for the next loop iteration; loop() slows down again for long waits
if (state == EPaperState::IDLE) {
+1 -1
View File
@@ -125,7 +125,7 @@ class EPaperBase : public Display,
bool is_using_partial_update_() const { return this->full_update_every_ > 1; }
void process_state_();
const LogString *epaper_state_to_string_();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
virtual bool reset();
+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
+49 -29
View File
@@ -21,23 +21,6 @@ ESPHOME_LOG_TAG(TAG, "es8311");
return false; \
}
static constexpr i2c::I2CRegValue ES8311_POWER_UP[] PROGMEM = {
// Power up analog circuitry
{ES8311_REG0D_SYSTEM, 0x01},
// Enable analog PGA, enable ADC modulator
{ES8311_REG0E_SYSTEM, 0x02},
// Power up DAC
{ES8311_REG12_SYSTEM, 0x00},
// Enable output to HP drive
{ES8311_REG13_SYSTEM, 0x10},
// ADC Equalizer bypass, cancel DC offset in digital domain
{ES8311_REG1C_ADC, 0x6A},
// Bypass DAC equalizer
{ES8311_REG37_DAC, 0x08},
// Power On
{ES8311_REG00_RESET, 0x80},
};
void ES8311::setup() {
// Reset
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x1F));
@@ -50,7 +33,20 @@ void ES8311::setup() {
// Set initial volume
this->set_volume(0.75); // 0.75 = 0xBF = 0dB
ES8311_ERROR_FAILED(this->write_registers(ES8311_POWER_UP));
// Power up analog circuitry
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0D_SYSTEM, 0x01));
// Enable analog PGA, enable ADC modulator
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0E_SYSTEM, 0x02));
// Power up DAC
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG12_SYSTEM, 0x00));
// Enable output to HP drive
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG13_SYSTEM, 0x10));
// ADC Equalizer bypass, cancel DC offset in digital domain
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG1C_ADC, 0x6A));
// Bypass DAC equalizer
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG37_DAC, 0x08));
// Power On
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x80));
}
void ES8311::dump_config() {
@@ -138,9 +134,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 +153,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 +180,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 +212,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
+13 -3
View File
@@ -174,11 +174,21 @@ float ES8388::volume() {
}
bool ES8388::set_mute_state_(bool mute_state) {
uint8_t value = 0;
this->is_muted_ = mute_state;
// Only toggle the DACMute bit; the other bits of this register hold unrelated DAC settings
return this->update_register(ES8388_DACCONTROL3, ES8388_DACCONTROL3_DAC_MUTE,
mute_state ? ES8388_DACCONTROL3_DAC_MUTE : 0);
ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL3, &value));
ESP_LOGV(TAG, "Read ES8388_DACCONTROL3: 0x%02X", value);
// Only toggle the DACMute bit; the other bits of this register hold unrelated
// DAC settings that must be preserved. Previously muting overwrote the whole
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
// could not be unmuted again.
assign_bits(value, ES8388_DACCONTROL3_DAC_MUTE, mute_state);
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
return this->write_byte(ES8388_DACCONTROL3, value);
}
// Set dac power output
+91 -42
View File
@@ -1,5 +1,7 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any
from esphome import pins
import esphome.codegen as cg
@@ -39,20 +41,23 @@ from .const import (
VARIANT_ESP32S31,
esp32_ns,
)
from .gpio_common import ESP32PinSpec
from .gpio_esp32 import ESP32_PIN_SPEC
from .gpio_esp32_c2 import ESP32_C2_PIN_SPEC
from .gpio_esp32_c3 import ESP32_C3_PIN_SPEC
from .gpio_esp32_c5 import ESP32_C5_PIN_SPEC
from .gpio_esp32_c6 import ESP32_C6_PIN_SPEC
from .gpio_esp32_c61 import ESP32_C61_PIN_SPEC
from .gpio_esp32_h2 import ESP32_H2_PIN_SPEC
from .gpio_esp32_h4 import ESP32_H4_PIN_SPEC
from .gpio_esp32_h21 import ESP32_H21_PIN_SPEC
from .gpio_esp32_p4 import ESP32_P4_PIN_SPEC
from .gpio_esp32_s2 import ESP32_S2_PIN_SPEC
from .gpio_esp32_s3 import ESP32_S3_PIN_SPEC, esp32_s3_final_validate_pins
from .gpio_esp32_s31 import ESP32_S31_PIN_SPEC
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
from .gpio_esp32_h4 import esp32_h4_validate_gpio_pin, esp32_h4_validate_supports
from .gpio_esp32_h21 import esp32_h21_validate_gpio_pin, esp32_h21_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_s3 import (
esp32_s3_final_validate_pins,
esp32_s3_validate_gpio_pin,
esp32_s3_validate_supports,
)
from .gpio_esp32_s31 import esp32_s31_validate_gpio_pin, esp32_s31_validate_supports
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
@@ -93,23 +98,66 @@ def _translate_pin(value):
return _lookup_pin(value)
_esp32_pin_specs: dict[str, ESP32PinSpec] = {
VARIANT_ESP32: ESP32_PIN_SPEC,
VARIANT_ESP32C2: ESP32_C2_PIN_SPEC,
VARIANT_ESP32C3: ESP32_C3_PIN_SPEC,
VARIANT_ESP32C5: ESP32_C5_PIN_SPEC,
VARIANT_ESP32C6: ESP32_C6_PIN_SPEC,
VARIANT_ESP32C61: ESP32_C61_PIN_SPEC,
VARIANT_ESP32H2: ESP32_H2_PIN_SPEC,
VARIANT_ESP32H4: ESP32_H4_PIN_SPEC,
VARIANT_ESP32H21: ESP32_H21_PIN_SPEC,
VARIANT_ESP32P4: ESP32_P4_PIN_SPEC,
VARIANT_ESP32S2: ESP32_S2_PIN_SPEC,
VARIANT_ESP32S3: ESP32_S3_PIN_SPEC,
VARIANT_ESP32S31: ESP32_S31_PIN_SPEC,
}
_final_validations: dict[str, Callable[[ConfigType], None]] = {
VARIANT_ESP32S3: esp32_s3_final_validate_pins,
@dataclass
class ESP32ValidationFunctions:
pin_validation: Callable[[int], int]
usage_validation: Callable[[dict[str, Any]], dict[str, Any]]
final_validate: Callable[[ConfigType], None] | None = None
_esp32_validations = {
VARIANT_ESP32: ESP32ValidationFunctions(
pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports
),
VARIANT_ESP32C2: ESP32ValidationFunctions(
pin_validation=esp32_c2_validate_gpio_pin,
usage_validation=esp32_c2_validate_supports,
),
VARIANT_ESP32C3: ESP32ValidationFunctions(
pin_validation=esp32_c3_validate_gpio_pin,
usage_validation=esp32_c3_validate_supports,
),
VARIANT_ESP32C5: ESP32ValidationFunctions(
pin_validation=esp32_c5_validate_gpio_pin,
usage_validation=esp32_c5_validate_supports,
),
VARIANT_ESP32C6: ESP32ValidationFunctions(
pin_validation=esp32_c6_validate_gpio_pin,
usage_validation=esp32_c6_validate_supports,
),
VARIANT_ESP32C61: ESP32ValidationFunctions(
pin_validation=esp32_c61_validate_gpio_pin,
usage_validation=esp32_c61_validate_supports,
),
VARIANT_ESP32H2: ESP32ValidationFunctions(
pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports,
),
VARIANT_ESP32H4: ESP32ValidationFunctions(
pin_validation=esp32_h4_validate_gpio_pin,
usage_validation=esp32_h4_validate_supports,
),
VARIANT_ESP32H21: ESP32ValidationFunctions(
pin_validation=esp32_h21_validate_gpio_pin,
usage_validation=esp32_h21_validate_supports,
),
VARIANT_ESP32P4: ESP32ValidationFunctions(
pin_validation=esp32_p4_validate_gpio_pin,
usage_validation=esp32_p4_validate_supports,
),
VARIANT_ESP32S2: ESP32ValidationFunctions(
pin_validation=esp32_s2_validate_gpio_pin,
usage_validation=esp32_s2_validate_supports,
),
VARIANT_ESP32S3: ESP32ValidationFunctions(
pin_validation=esp32_s3_validate_gpio_pin,
usage_validation=esp32_s3_validate_supports,
final_validate=esp32_s3_final_validate_pins,
),
VARIANT_ESP32S31: ESP32ValidationFunctions(
pin_validation=esp32_s31_validate_gpio_pin,
usage_validation=esp32_s31_validate_supports,
),
}
@@ -126,7 +174,7 @@ def gpio_pin_number_validator(value):
return value
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_pin_specs:
if variant not in _esp32_validations:
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
return value
@@ -134,12 +182,12 @@ def gpio_pin_number_validator(value):
def validate_gpio_pin(pin):
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_pin_specs:
if variant not in _esp32_validations:
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
ignore_pin_validation_warning = pin[CONF_IGNORE_PIN_VALIDATION_ERROR]
try:
pin[CONF_NUMBER] = _esp32_pin_specs[variant].validate_gpio_pin(pin[CONF_NUMBER])
pin[CONF_NUMBER] = _esp32_validations[variant].pin_validation(pin[CONF_NUMBER])
except cv.Invalid as exc:
if not ignore_pin_validation_warning:
raise
@@ -150,10 +198,10 @@ def validate_gpio_pin(pin):
exc,
)
else:
# `ignore_pin_validation_error` only suppresses an error raised by
# validate_gpio_pin above (e.g. SPI flash/PSRAM pins); pin numbers out of
# range still fail validate_supports. If nothing raised, the option is a
# no-op -- warn so the user can clean it up, but don't block the build.
# `ignore_pin_validation_error` only suppresses an error raised by the
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
# numbers). If that didn't raise, the option is a no-op -- warn so the
# user can clean it up, but don't block the build.
if ignore_pin_validation_warning:
_LOGGER.warning(
"GPIO%d has no validation errors to ignore; "
@@ -169,7 +217,7 @@ def validate_supports(value):
is_output = mode[CONF_OUTPUT]
is_open_drain = mode[CONF_OPEN_DRAIN]
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_pin_specs:
if variant not in _esp32_validations:
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
if is_open_drain and not is_output:
@@ -177,7 +225,7 @@ def validate_supports(value):
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
)
return _esp32_pin_specs[variant].validate_supports(value)
return _esp32_validations[variant].usage_validation(value)
# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t
@@ -228,5 +276,6 @@ async def esp32_pin_to_code(config):
def final_validate_pins(full_config: ConfigType) -> None:
"""Run the active variant's pin final-validation, if it defines one."""
if (final := _final_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])) is not None:
final(full_config)
funcs = _esp32_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])
if funcs is not None and funcs.final_validate is not None:
funcs.final_validate(full_config)
+18 -121
View File
@@ -1,129 +1,26 @@
"""Pin checks shared by the ESP32 variant validators."""
from collections.abc import Iterable, Mapping
import logging
from types import MappingProxyType
from logging import Logger
from typing import Any
from esphome.components.const import CONF_HOLD_STATE
import esphome.config_validation as cv
from esphome.const import (
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.pins import check_strapping_pin
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
_NO_PINS: Mapping[int, str] = MappingProxyType({})
_INPUT_ONLY_REJECTED_MODES = (
(CONF_OUTPUT, "does not support output pin mode."),
(CONF_PULLUP, "does not support pullups."),
(CONF_PULLDOWN, "does not support pulldowns."),
)
from esphome.const import CONF_NUMBER
def spi_psram_errors(variant: str, functions: Mapping[int, str]) -> dict[int, str]:
"""Error messages for pins taken by the SPI flash/PSRAM interface."""
return {
num: f"This pin cannot be used on {variant}s and is already used by the"
f" SPI/PSRAM interface (function: {function})"
for num, function in functions.items()
}
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
if num in usb_jtag_pins:
logger.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
num,
)
def unusable_errors(variant: str, pins: Iterable[int]) -> dict[int, str]:
"""Error messages for pins that are missing from the GPIO matrix."""
return {num: f"The pin GPIO{num} is not usable on {variant}s." for num in pins}
class ESP32PinSpec:
"""Pin facts of one ESP32 variant, with the two validators built from them.
`error_pins` fail pin validation, which `ignore_pin_validation_error` bypasses;
`fatal_pins` and pins above `max_pin` fail both validators, so the option cannot bypass them.
`input_only_label` is inserted after "GPIOn" in the input-only errors.
A plain class, not a dataclass: building a dataclass costs ~0.5 ms of import.
"""
__slots__ = (
"error_pins",
"fatal_pins",
"input_only_label",
"input_only_pins",
"max_pin",
"no_hold_pins",
"strapping_pins",
"usb_jtag_pins",
"warning_pins",
)
def __init__(
self,
*,
max_pin: int,
strapping_pins: frozenset[int],
usb_jtag_pins: frozenset[int] = frozenset(),
error_pins: Mapping[int, str] = _NO_PINS,
fatal_pins: Mapping[int, str] = _NO_PINS,
warning_pins: Mapping[int, str] = _NO_PINS,
input_only_pins: frozenset[int] = frozenset(),
input_only_label: str = "",
no_hold_pins: frozenset[int] = frozenset(),
) -> None:
self.max_pin = max_pin
self.strapping_pins = strapping_pins
self.usb_jtag_pins = usb_jtag_pins
self.error_pins = error_pins
self.fatal_pins = fatal_pins
self.warning_pins = warning_pins
self.input_only_pins = input_only_pins
self.input_only_label = input_only_label
self.no_hold_pins = no_hold_pins
def _check_pin_exists(self, num: int) -> None:
if num < 0 or num > self.max_pin:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-{self.max_pin})")
if (message := self.fatal_pins.get(num)) is not None:
raise cv.Invalid(message)
def validate_gpio_pin(self, value: int) -> int:
self._check_pin_exists(value)
if (message := self.error_pins.get(value)) is not None:
raise cv.Invalid(message)
if (message := self.warning_pins.get(value)) is not None:
_LOGGER.warning("%s", message)
if value in self.usb_jtag_pins:
_LOGGER.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
value,
)
return value
def validate_supports(self, value: ConfigType) -> ConfigType:
num = value[CONF_NUMBER]
self._check_pin_exists(num)
hold = value.get(CONF_HOLD_STATE)
if num in self.input_only_pins:
prefix = f"GPIO{num}{self.input_only_label}"
mode = value[CONF_MODE]
for key, problem in _INPUT_ONLY_REJECTED_MODES:
if mode[key]:
raise cv.Invalid(f"{prefix} {problem}", [CONF_MODE, key])
if hold:
raise cv.Invalid(
f"{prefix} is input-only and cannot be held.", [CONF_HOLD_STATE]
)
if hold and num in self.no_hold_pins:
raise cv.Invalid(f"GPIO{num} has no hold function.", [CONF_HOLD_STATE])
if hold and num in self.usb_jtag_pins:
_LOGGER.warning(
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
)
check_strapping_pin(value, self.strapping_pins, _LOGGER)
return value
def check_usb_jtag_hold(
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
) -> None:
num = conf[CONF_NUMBER]
if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins:
logger.warning(
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
)
+75 -24
View File
@@ -1,4 +1,17 @@
from .gpio_common import ESP32PinSpec, unusable_errors
import logging
from typing import Any
from esphome.components.const import CONF_HOLD_STATE
import esphome.config_validation as cv
from esphome.const import (
CONF_INPUT,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.pins import check_strapping_pin
_ESP_SDIO_PINS = {
6: "Flash Clock",
@@ -7,28 +20,66 @@ _ESP_SDIO_PINS = {
11: "Flash Command",
}
ESP32_PIN_SPEC = ESP32PinSpec(
max_pin=39,
strapping_pins=frozenset({0, 2, 5, 12, 15}),
error_pins={
**{
num: "This pin cannot be used on ESP32s and is already used by the flash interface"
f" (function: {function})."
" If you are using an ESP32 module that uses a different flash pin"
" configuration (e.g. ESP32-PICO-V3-02), you can set"
" 'ignore_pin_validation_error: true' to bypass this check."
for num, function in _ESP_SDIO_PINS.items()
},
_ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15}
_LOGGER = logging.getLogger(__name__)
def esp32_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 39:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
if value in _ESP_SDIO_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32s and is already used by the flash interface"
f" (function: {_ESP_SDIO_PINS[value]})."
f" If you are using an ESP32 module that uses a different flash pin"
f" configuration (e.g. ESP32-PICO-V3-02), you can set"
f" 'ignore_pin_validation_error: true' to bypass this check."
)
if 9 <= value <= 10:
_LOGGER.warning(
"Pin %s (9-10) might already be used by the flash interface in QUAD IO flash mode.",
value,
)
if value in (24, 28, 29, 30, 31):
# These pins are not exposed in GPIO mux (reason unknown)
# but they're missing from IO_MUX list in datasheet
**unusable_errors("ESP32", (24, 28, 29, 30, 31)),
},
warning_pins={
num: f"Pin {num} (9-10) might already be used by the flash interface in QUAD IO flash mode."
for num in (9, 10)
},
input_only_pins=frozenset(range(34, 40)),
input_only_label=" (34-39)",
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
no_hold_pins=frozenset({20}),
)
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.")
return value
def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
is_output = mode[CONF_OUTPUT]
is_pullup = mode[CONF_PULLUP]
is_pulldown = mode[CONF_PULLDOWN]
is_hold = value.get(CONF_HOLD_STATE)
if is_input:
# All ESP32 pins support input mode
pass
if is_output and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) does not support output pin mode.",
[CONF_MODE, CONF_OUTPUT],
)
if is_pullup and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP]
)
if is_pulldown and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
)
if is_hold and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) is input-only and cannot be held.",
[CONF_HOLD_STATE],
)
if is_hold and num == 20:
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
raise cv.Invalid("GPIO20 has no hold function.", [CONF_HOLD_STATE])
check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER)
return value
+32 -2
View File
@@ -1,3 +1,33 @@
from .gpio_common import ESP32PinSpec
import logging
from typing import Any
ESP32_C2_PIN_SPEC = ESP32PinSpec(max_pin=20, strapping_pins=frozenset({8, 9}))
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP32C2_STRAPPING_PINS = {8, 9}
_LOGGER = logging.getLogger(__name__)
def esp32_c2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 20:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)")
return value
def esp32_c2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 20:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-20)")
if is_input:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP32C2_STRAPPING_PINS, _LOGGER)
return value
+51 -17
View File
@@ -1,18 +1,52 @@
from .gpio_common import ESP32PinSpec, spi_psram_errors
import logging
from typing import Any
ESP32_C3_PIN_SPEC = ESP32PinSpec(
max_pin=21,
strapping_pins=frozenset({2, 8, 9}),
usb_jtag_pins=frozenset({18, 19}),
error_pins=spi_psram_errors(
"ESP32-C3",
{
12: "SPIHD",
13: "SPIWP",
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
},
),
)
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
_ESP32C3_SPI_PSRAM_PINS = {
12: "SPIHD",
13: "SPIWP",
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
}
_ESP32C3_USB_JTAG_PINS = {18, 19}
_ESP32C3_STRAPPING_PINS = {2, 8, 9}
_LOGGER = logging.getLogger(__name__)
def esp32_c3_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 21:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)")
if value in _ESP32C3_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 21:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-21)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER)
return value
+51 -20
View File
@@ -1,28 +1,59 @@
import esphome.config_validation as cv
from esphome.const import CONF_SCL, CONF_SDA
import logging
from typing import Any
from .gpio_common import ESP32PinSpec, spi_psram_errors
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h
_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3}
ESP32_C5_PIN_SPEC = ESP32PinSpec(
max_pin=28,
strapping_pins=frozenset({2, 7, 27, 28}),
usb_jtag_pins=frozenset({13, 14}),
error_pins=spi_psram_errors(
"ESP32-C5",
{
16: "SPICS0",
17: "SPIQ",
18: "SPIWP",
19: "VDD_SPI",
20: "SPIHD",
21: "SPICLK",
22: "SPID",
},
),
)
_ESP32C5_SPI_PSRAM_PINS = {
16: "SPICS0",
17: "SPIQ",
18: "SPIWP",
19: "VDD_SPI",
20: "SPIHD",
21: "SPICLK",
22: "SPID",
}
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
_ESP32C5_USB_JTAG_PINS = {13, 14}
_LOGGER = logging.getLogger(__name__)
def esp32_c5_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 28:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)")
if value in _ESP32C5_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 28:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-28)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
return value
def esp32_c5_validate_lp_i2c(value):
+50 -20
View File
@@ -1,28 +1,58 @@
import esphome.config_validation as cv
from esphome.const import CONF_SCL, CONF_SDA
import logging
from typing import Any
from .gpio_common import ESP32PinSpec, spi_psram_errors
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h
_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7}
ESP32_C6_PIN_SPEC = ESP32PinSpec(
max_pin=30,
strapping_pins=frozenset({4, 5, 8, 9, 15}),
usb_jtag_pins=frozenset({12, 13}),
error_pins=spi_psram_errors(
"ESP32-C6",
{
24: "SPICS0",
25: "SPIQ",
26: "SPIWP",
27: "VDD_SPI",
28: "SPIHD",
29: "SPICLK",
30: "SPID",
},
),
)
_ESP32C6_SPI_PSRAM_PINS = {
24: "SPICS0",
25: "SPIQ",
26: "SPIWP",
27: "VDD_SPI",
28: "SPIHD",
29: "SPICLK",
30: "SPID",
}
_ESP32C6_USB_JTAG_PINS = {12, 13}
_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15}
_LOGGER = logging.getLogger(__name__)
def esp32_c6_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 30:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-30)")
if value in _ESP32C6_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 30:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-30)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER)
return value
def esp32_c6_validate_lp_i2c(value):
+53 -19
View File
@@ -1,20 +1,54 @@
from .gpio_common import ESP32PinSpec, spi_psram_errors
import logging
from typing import Any
ESP32_C61_PIN_SPEC = ESP32PinSpec(
max_pin=29,
strapping_pins=frozenset({8, 9}),
usb_jtag_pins=frozenset({12, 13}),
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
error_pins=spi_psram_errors(
"ESP32-C61",
{
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
19: "SPIWP",
20: "SPIHD",
21: "VDD_SPI",
},
),
)
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
_ESP32C61_SPI_PSRAM_PINS = {
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
19: "SPIWP",
20: "SPIHD",
21: "VDD_SPI",
}
_ESP32C61_STRAPPING_PINS = {8, 9}
_ESP32C61_USB_JTAG_PINS = {12, 13}
_LOGGER = logging.getLogger(__name__)
def esp32_c61_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 29:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)")
if value in _ESP32C61_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 29:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-29)")
if is_input:
# All ESP32-C61 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER)
return value
+45 -12
View File
@@ -1,13 +1,46 @@
from .gpio_common import ESP32PinSpec
import logging
from typing import Any
ESP32_H2_PIN_SPEC = ESP32PinSpec(
max_pin=27,
strapping_pins=frozenset({8, 9, 25}),
usb_jtag_pins=frozenset({26, 27}),
warning_pins={
num: f"GPIO{num} is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
"Utilizing SPI-reserved pins could cause unexpected failures.\n"
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html"
for num in (6, 7, 15, 16, 17, 18, 19, 20, 21)
},
)
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
_ESP32H2_USB_JTAG_PINS = {26, 27}
_ESP32H2_STRAPPING_PINS = {8, 9, 25}
_LOGGER = logging.getLogger(__name__)
def esp32_h2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 27:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
if value in _ESP32H2_SPI_FLASH_PINS:
_LOGGER.warning(
"GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
"Utilizing SPI-reserved pins could cause unexpected failures.\n"
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html",
value,
)
check_usb_jtag_pin(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
return value
def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 27:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-27)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
return value
+40 -9
View File
@@ -1,10 +1,41 @@
from .gpio_common import ESP32PinSpec
import logging
from typing import Any
ESP32_H21_PIN_SPEC = ESP32PinSpec(
max_pin=25,
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
strapping_pins=frozenset({13, 14}),
usb_jtag_pins=frozenset({17, 18}),
)
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
_ESP32H21_STRAPPING_PINS: set[int] = {13, 14}
_ESP32H21_USB_JTAG_PINS = {17, 18}
_LOGGER = logging.getLogger(__name__)
def esp32_h21_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 25:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)")
check_usb_jtag_pin(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
return value
def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 25:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-25)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER)
return value
Loaded 100 of 652 files, more files were not shown because too many files have changed in this diff. Show more