Compare commits

...
Author SHA1 Message Date
J. Nick Koston cf55984582 Merge remote-tracking branch 'origin/dev' into dfrobot-sen0395-line-reader
# Conflicts:
#	esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp
#	esphome/components/dfrobot_sen0395/dfrobot_sen0395.h
2026-10-26 04:19:45 -10:00
J. Nick Koston f431cdfb0c [haier_water_heater] Move child entities to sensor, binary_sensor and text_sensor platforms (#21437) 2026-10-26 10:18:05 -04:00
J. Nick Koston ba488eab6e [components] Apply inverted when restoring gree, hbridge, lvgl, opentherm and haier_tundra switches (#21544) 2026-10-26 04:16:56 -10:00
J. Nick Koston f21183c160 [esp32] Add a data driven pin spec for the variant GPIO validators (#21461) 2026-10-26 10:15:09 -04:00
J. Nick Koston 60ca75baad [ld2410] Split frames by their length instead of scanning for footer bytes (#21361) 2026-10-26 04:15:00 -10:00
J. Nick Koston b668cb1e50 [usb_uart] Use FixedRingBuffer for the receive buffer (#21476) 2026-10-26 10:14:32 -04:00
J. Nick Koston ab7d0ca17d [ltr390] Pack members, drop the float tables and expand gain and resolution in the schema (#21195) 2026-10-26 14:11:02 +00:00
J. Nick Koston 43f10a7cfe [core] Turn off the precompiled header with ccache older than 4.7 (#21627) 2026-10-26 04:09:55 -10:00
J. Nick Koston d5e755e407 [dfrobot_sen0395] Queue commands by value instead of allocating them on the heap (#20906) 2026-10-26 04:09:07 -10:00
J. Nick Koston cf67610d43 [xiaomi_ble] Move the Xiaomi sensors onto XiaomiDevice and fix the shared duplicate filter (#21483) 2026-10-26 04:08:39 -10:00
J. Nick Koston bd55bdcd6c [sen6x] Drop the product and serial strings (#21199) 2026-10-26 10:05:57 -04:00
J. Nick Koston f42d1ac645 [airton] Use an Apply select for the vane direction (#21252) 2026-10-26 10:05:13 -04:00
J. Nick Koston 5198c37cd9 [bedjet] Keep the device name in a fixed buffer (#21204) 2026-10-26 10:04:47 -04:00
J. Nick Koston d9fcc292e2 [components] Read hub-restored switch levels with get_initial_write_state (#21543) 2026-10-26 04:03:48 -10:00
J. Nick Koston 8f8e615ee7 [libretiny] Apply a level written before a pin becomes an output (#21603) 2026-10-26 04:03:25 -10:00
J. Nick Koston f91ad8d43c [ltr390] Wait the full integration time at 17, 16 and 13 bit (#21194) 2026-10-26 10:03:11 -04:00
J. Nick Koston 465a8b0386 [fan] Pack the fan traits flags and narrow the speed count to 16 bits (#21340) 2026-10-26 09:52:19 -04:00
J. Nick Koston 9e692b6772 [output] Add ApplyFloatOutput and ApplyBinaryOutput (#21255) 2026-10-26 09:49:49 -04:00
J. Nick Koston 1fd4025a0e [haier] Use Apply switches (#21251) 2026-10-26 09:46:30 -04:00
J. Nick Koston 03b8bd7941 [core] Add bulk push and pop to FixedRingBuffer (#21474) 2026-10-26 09:43:54 -04:00
J. Nick Koston 0e2c906fce [i2c] Put the device address after the bus pointer so derived classes reuse its padding (#21473) 2026-10-26 09:40:37 -04:00
J. Nick Koston d260265cf5 [ld6002b] Use StaticRingBuffer for the command queue (#21479) 2026-10-26 09:40:08 -04:00
J. Nick Koston 2388c3313d [xiaomi_ble] Add a shared XiaomiDevice base for MiBeacon devices (#21481) 2026-10-26 09:39:42 -04:00
J. Nick Koston e717a21bce [restart][shutdown][factory_reset][safe_mode] Use momentary Apply switches (#21519) 2026-10-26 09:37:12 -04:00
J. Nick Koston d47a510b84 [remote_base] Use the byte helpers in roomba, dooya and magiquest (#21513) 2026-10-26 09:09:36 -04:00
J. Nick Koston a16d0c48a0 [core] Add publish="momentary" for Apply switches that trigger an action (#21518) 2026-10-26 09:08:59 -04:00
J. Nick Koston 8541104d87 [switch] Add get_initial_write_state for hubs that restore their switches (#21542) 2026-10-26 09:04:41 -04:00
J. Nick Koston 579c5a5226 [components] Publish PROGMEM table strings on text sensors directly (#21537) 2026-10-26 09:02:38 -04:00
J. Nick Koston a027785fbf [libretiny] Bump LibreTiny to 1.14.0 and build BK7231N and BK7231T with BDK 3.0.78 (#21531) 2026-10-26 09:01:50 -04:00
J. Nick Koston d7f5976525 [http_request] Keep the OTA request strings out of RAM until a flash (#21556) 2026-10-26 09:00:04 -04:00
J. Nick Koston 4c8de264fd [remote_base] Keep the stateless dumpers in flash (#21563) 2026-10-26 08:59:08 -04:00
Bascht74andJ. Nick Koston 2d37ad6836 [api] Keep an entity's events in order under backpressure (#21567)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-25 23:57:11 -10:00
J. Nick Koston f659814b2c Merge remote-tracking branch 'origin/dfrobot-sen0395-no-heap-commands' into dfrobot-sen0395-line-reader 2026-10-19 12:34:39 -10:00
J. Nick Koston 82d09be042 Test the queue room check for a running sensor 2026-10-19 12:34:32 -10:00
J. Nick Koston fbbeb5cc4f Derive the prompt length from one named prompt string 2026-10-19 12:33:52 -10:00
J. Nick Koston 469cfa296e Merge remote-tracking branch 'origin/dfrobot-sen0395-no-heap-commands' into dfrobot-sen0395-line-reader 2026-10-19 08:14:48 -10:00
J. Nick Koston eaad486cda Merge remote-tracking branch 'origin/dev' into dfrobot-sen0395-no-heap-commands 2026-10-19 08:12:50 -10:00
J. Nick Koston 15141a1e4f Merge branch 'dfrobot-sen0395-no-heap-commands' into dfrobot-sen0395-line-reader
# Conflicts:
#	esphome/components/dfrobot_sen0395/__init__.py
2026-10-18 07:32:06 -10:00
J. Nick Koston ec2245e595 Merge remote-tracking branch 'origin/dev' into dfrobot-sen0395-no-heap-commands 2026-10-18 07:31:54 -10:00
J. Nick Koston 494acce857 Merge remote-tracking branch 'origin/dfrobot-sen0395-no-heap-commands' into dfrobot-sen0395-line-reader 2026-10-16 20:06:01 -10:00
J. Nick Koston b4b3aae8b9 Merge remote-tracking branch 'origin/dev' into dfrobot-sen0395-no-heap-commands 2026-10-16 20:05:55 -10:00
J. Nick Koston 769c20a131 [dfrobot_sen0395] Note the buffer size choice, type the test helpers 2026-10-16 20:05:33 -10:00
J. Nick Koston f29530ac47 [dfrobot_sen0395] Collect lines with LineReader 2026-10-16 20:04:29 -10:00
J. Nick Koston 5e2375aa01 Merge remote-tracking branch 'origin/dev' into dfrobot-sen0395-line-reader 2026-10-16 20:01:56 -10:00
J. Nick Koston 3f6eafc880 Queue a stop/start sequence only when it fits whole 2026-10-16 13:39:46 -10:00
J. Nick Koston 2369ddea9e Shorten the range and latency field comment 2026-10-16 13:14:01 -10:00
J. Nick Koston e0af4cace1 Name the test helper for clang-tidy 2026-10-16 10:40:00 -10:00
J. Nick Koston c464a48afb Round detection ranges like before, restore the 0 0 fallback and test the command text 2026-10-16 10:29:36 -10:00
J. Nick Koston 2c693fad5b Poll the state on an empty queue, use StaticRingBuffer and keep strings in flash 2026-10-16 10:14:21 -10:00
J. Nick Koston 3d8bfb7f94 [dfrobot_sen0395] Queue commands by value instead of allocating them 2026-10-16 10:08:26 -10:00
226 changed files with 4399 additions and 3188 deletions

No files matched your search

+4
View File
@@ -443,12 +443,16 @@ 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`)
+30 -13
View File
@@ -7,6 +7,7 @@ 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,
@@ -515,7 +516,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 Windows gate records its choice
# Where the CMake gate records its choice
_PCH_CHOICE_VAR = "ESPHOME_PCH"
@@ -531,16 +532,32 @@ def _pch_cmake_block() -> str:
block = f"""target_precompile_headers(${{COMPONENT_LIB}} PRIVATE
{headers}
)"""
if not pch.pch_needs_gcc_check():
if pch.pch_forced():
return f"\n# ESPHome precompiled header\n{block}\n"
# Before the first configure only CMake knows the compiler version
return f"""
# 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}))
# 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({_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)
set(_esphome_pch OFF)
endif()
"""
min_ccache = ".".join(map(str, CCACHE_PCH_MIN_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)
{textwrap.indent(block, " ")}
endif()
"""
@@ -557,10 +574,10 @@ def write_pch_checksum() -> None:
if not pch.pch_enabled():
return
# 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()
# 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()
checksum = pch.pch_checksum(
CORE.relative_src_path(),
pch.PCH_DEFAULT_HEADERS,
+48 -1
View File
@@ -3,21 +3,68 @@ 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_runs
from esphome.framework_helpers import (
strip_win_long_path_prefix,
tool_version_probe,
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
)
+19 -4
View File
@@ -12,7 +12,11 @@ import re
import subprocess
import sys
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
from esphome.build_helpers.ccache import (
ccache_breaks_pch,
effective_ccache_basedir,
parse_enable_env,
)
from esphome.const import PLATFORM_NRF52
from esphome.helpers import write_file_if_changed
@@ -117,9 +121,16 @@ 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 host rule; ``ESPHOME_PCH_ENABLE=1`` skips the rule."""
if not pch_enabled():
"""The knob plus the ccache and host rules; ``ESPHOME_PCH_ENABLE=1``
skips the rules."""
if not pch_enabled() or _ccache_rules_out_pch():
return False
if not pch_needs_gcc_check():
return True
@@ -162,7 +173,11 @@ 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
return (
pch_enabled()
and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
and not _ccache_rules_out_pch()
)
def pch_header_text(include_headers: Iterable[str]) -> str:
+4
View File
@@ -90,6 +90,10 @@ 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,18 +9,16 @@ from ..climate import (
CONF_VERTICAL_DIRECTION,
VERTICAL_DIRECTIONS,
AirtonClimate,
airton_ns,
VerticalDirection,
)
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(
VerticalDirectionSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
@@ -34,4 +32,6 @@ 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))",
)
@@ -1,17 +0,0 @@
#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
+11 -2
View File
@@ -1555,8 +1555,17 @@ 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) {
this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE,
event->get_last_event_type_index());
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_();
}
uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
uint32_t remaining_size) {
+5 -68
View File
@@ -2,6 +2,7 @@
#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"
@@ -20,7 +21,6 @@
#include "esphome/core/string_ref.h"
#include <functional>
#include <limits>
#include <vector>
namespace esphome {
@@ -656,73 +656,6 @@ 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_;
@@ -795,6 +728,10 @@ 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));
@@ -0,0 +1,88 @@
#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
@@ -58,7 +58,7 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const ble_device_base::Ser
return {};
}
auto raw = service_data.data;
const auto &raw = service_data.data;
if (raw.size() < 13) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
+49 -47
View File
@@ -4,7 +4,9 @@
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/core/application.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
namespace esphome::bedjet {
@@ -40,7 +42,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().c_str(), status);
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name(), status);
}
}
@@ -64,7 +66,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().c_str(), status);
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name(), status);
}
return status == 0;
}
@@ -82,7 +84,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().c_str(), status);
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name(), status);
}
return status == 0;
}
@@ -93,7 +95,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().c_str(), status);
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name(), status);
}
return status == 0;
}
@@ -103,11 +105,10 @@ 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().c_str(),
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name(),
LOG_STR_ARG(bedjet_button_to_string(button)), status);
} else {
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name().c_str(),
LOG_STR_ARG(bedjet_button_to_string(button)));
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name(), LOG_STR_ARG(bedjet_button_to_string(button)));
}
return status == 0;
}
@@ -125,9 +126,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().c_str());
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name());
} else {
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name());
}
return -1;
}
@@ -144,16 +145,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().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name(), 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().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name(), status);
}
}
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name(), enable, status);
return status;
}
@@ -164,7 +165,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().c_str());
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name());
result = false;
} else {
this->char_handle_cmd_ = chr->handle;
@@ -174,7 +175,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().c_str());
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name());
result = false;
} else {
this->char_handle_status_ = chr->handle;
@@ -204,14 +205,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().c_str());
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name());
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().c_str(), status);
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name(), status);
}
}
}
@@ -222,7 +223,7 @@ bool BedJetHub::discover_characteristics_() {
" - Status char: 0x%x\n"
" - config descriptor: 0x%x\n"
" - Name char: 0x%x",
this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
this->get_name(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
this->char_handle_name_);
return result;
@@ -241,7 +242,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().c_str());
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name());
this->node_state = espbt::ClientState::ESTABLISHED;
this->set_notify_(true);
@@ -253,7 +254,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().c_str());
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name());
this->parent()->set_enabled(false);
this->status_set_warning();
this->dispatch_state_(false);
@@ -265,15 +266,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().c_str(),
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name(),
param->write.handle, param->write.status);
} else {
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
param->write.handle, param->write.status);
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name(), param->write.handle,
param->write.status);
}
break;
}
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name(), param->write.handle,
param->write.status);
break;
}
@@ -306,9 +307,11 @@ 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) {
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);
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';
}
}
break;
@@ -322,8 +325,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().c_str(), 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_);
break;
}
@@ -335,8 +338,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().c_str(), 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_);
break;
}
@@ -350,13 +353,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().c_str(), 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(),
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().c_str(),
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name(),
this->char_handle_status_, param->notify.handle);
break;
}
@@ -372,13 +375,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().c_str());
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name());
}
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().c_str(),
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name(),
this->last_notify_, delta, this->force_refresh_ ? "y" : "n");
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
@@ -388,7 +391,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().c_str());
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name());
}
} else {
this->status_packet_ready_();
@@ -397,7 +400,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().c_str(), event);
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name(), event);
break;
}
}
@@ -436,7 +439,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().c_str(),
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name(),
enable ? "true" : "false", handle, this->parent_->get_conn_id());
return ESP_GATT_OK;
}
@@ -468,7 +471,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().c_str());
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name());
return;
}
@@ -477,7 +480,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().c_str(), hour, minute);
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name(), hour, minute);
}
}
@@ -495,7 +498,7 @@ void BedJetHub::dump_config() {
"BedJet Hub '%s'\n"
" ble_client.app_id: %d\n"
" ble_client.conn_id: %d",
this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id());
this->get_name(), 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_) {
@@ -513,10 +516,9 @@ 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().c_str());
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name());
} else if (status != nullptr) {
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name().c_str(), this->children_.size(),
status);
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name(), this->children_.size(), status);
for (auto *child : this->children_) {
child->on_status(status);
}
@@ -530,13 +532,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().c_str());
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name());
} else if (diff > NOTIFY_WARN_THRESHOLD) {
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name().c_str(), diff / 1000);
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name(), 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().c_str(), this->timeout_);
ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying", this->get_name(), 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);
+3 -10
View File
@@ -119,13 +119,7 @@ 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. */
std::string get_name() {
if (this->name_.empty()) {
return this->parent_->address_str();
} else {
return this->name_;
}
}
const char *get_name() const { return this->name_[0] == '\0' ? this->parent_->address_str() : this->name_; }
/* BLEClient overrides */
@@ -151,9 +145,8 @@ 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);
void set_name_(const std::string &name) { this->name_ = name; }
std::string name_;
// Only used in log lines; longer names are truncated
char name_[24]{};
uint32_t last_notify_ = 0;
inline void status_packet_ready_();
@@ -62,6 +62,13 @@ 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) {
@@ -69,7 +76,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
@@ -80,7 +87,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
@@ -136,7 +143,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
@@ -229,7 +236,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;
@@ -279,7 +286,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.
+54 -26
View File
@@ -21,9 +21,10 @@ public ble_api.h.
"""
import logging
import re
import esphome.codegen as cg
from esphome.components import libretiny, wifi
from esphome.components import libretiny
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
@@ -32,14 +33,30 @@ from esphome.components.libretiny.const import (
FAMILY_BK7251,
)
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
from esphome.core import EsphomeError
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.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")
@@ -80,20 +97,45 @@ 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)
# 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)"
)
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])
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -116,20 +158,6 @@ 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++
@@ -14,11 +14,7 @@ void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
if (value == this->last_)
return;
this->last_ = value;
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);
this->publish_state(StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX));
}
} // namespace esphome::bq25186
@@ -42,6 +42,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_LINE_READER")
await uart.new_uart_component(config)
+18 -16
View File
@@ -1,5 +1,7 @@
#pragma once
#include <algorithm>
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
@@ -27,9 +29,12 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
TEMPLATABLE_VALUE(float, det_max4)
void play(const Ts &...x) override {
this->parent_->enqueue(make_unique<PowerCommand>(0));
// 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);
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(make_unique<FactoryResetCommand>());
this->parent_->enqueue(CommandType::COMMAND_TYPE_FACTORY_RESET);
}
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.
@@ -37,10 +42,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(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)));
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));
}
}
}
@@ -48,38 +53,35 @@ 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(make_unique<SetLatencyCommand>(detect, disappear));
this->parent_->enqueue_set_latency(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(make_unique<SensorCfgStartCommand>(val));
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSOR_CFG_START, val);
}
}
if (this->turn_on_led_.has_value()) {
int8_t val = this->turn_on_led_.value(x...);
if (val >= 0) {
this->parent_->enqueue(make_unique<LedModeCommand>(val));
this->parent_->enqueue(CommandType::COMMAND_TYPE_LED_MODE, val);
}
}
if (this->presence_via_uart_.has_value()) {
int8_t val = this->presence_via_uart_.value(x...);
if (val >= 0) {
this->parent_->enqueue(make_unique<UartOutputCommand>(val));
this->parent_->enqueue(CommandType::COMMAND_TYPE_UART_OUTPUT, val);
}
}
if (this->sensitivity_.has_value()) {
int8_t val = this->sensitivity_.value(x...);
if (val >= 0) {
if (val > 9) {
val = 9;
}
this->parent_->enqueue(make_unique<SensitivityCommand>(val));
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSITIVITY, std::min<int8_t>(val, 9));
}
}
this->parent_->enqueue(make_unique<SaveCfgCommand>());
this->parent_->enqueue(make_unique<PowerCommand>(1));
this->parent_->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, true);
}
};
@@ -1,332 +0,0 @@
#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
@@ -1,152 +0,0 @@
#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,13 +1,27 @@
#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");
const char ASCII_CR = 0x0D;
const char ASCII_LF = 0x0A;
static constexpr float DET_RANGE_STEP_M = 0.15f;
static constexpr float LATENCY_STEP_S = 0.025f;
// Sent after a reset with no line end
static constexpr char PROMPT[] PROGMEM = "leapMMW:/>";
// 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");
void DfrobotSen0395Component::dump_config() {
ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:");
@@ -23,72 +37,287 @@ void DfrobotSen0395Component::dump_config() {
}
void DfrobotSen0395Component::loop() {
if (cmd_queue_.is_empty()) {
// Command queue empty. Read sensor state.
cmd_queue_.enqueue(make_unique<ReadStateCommand>());
if (this->cmd_queue_.empty()) {
this->read_state_();
return;
}
// Commands are non-blocking and need to be called repeatedly.
if (cmd_queue_.process(this)) {
// Dequeue if command is done
cmd_queue_.dequeue();
// 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;
}
}
int8_t DfrobotSen0395Component::enqueue(std::unique_ptr<Command> cmd) {
return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move
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;
}
uint8_t DfrobotSen0395Component::read_message_() {
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 (const char *message = this->read_message_()) {
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
if (!this->handle_reply_(cmd, message))
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_() {
const char *message = this->read_message_();
if (message == nullptr)
return;
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
this->set_detected_(false);
this->set_active(true);
} else if (ESPHOME_strstr_P(message, 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) {
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(PROMPT))
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;
}
const char *DfrobotSen0395Component::read_message_() {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);
if (byte == ASCII_CR)
continue;
if (byte >= 0x7F)
byte = '?'; // needs to be valid utf8 string for log functions.
this->read_buffer_[this->read_pos_] = byte;
if (this->read_pos_ == 9 && byte == '>')
this->read_buffer_[++this->read_pos_] = ASCII_LF;
if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
this->read_buffer_[this->read_pos_] = 0;
this->read_pos_ = 0;
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
return 1; // Full message in buffer
} else {
this->read_pos_++;
const char *line = this->line_reader_.feed(byte);
if (line == nullptr && byte == '>' && this->line_reader_.length() == sizeof(PROMPT) - 1) {
line = this->line_reader_.partial();
this->line_reader_.reset();
}
}
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
if (line != nullptr) {
ESP_LOGV(TAG, "Message: %s", line);
return line;
}
this->line_reader_.log_if_overflowed(TAG);
}
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;
return nullptr;
}
void DfrobotSen0395Component::set_detected_(bool detected) {
@@ -99,44 +328,4 @@ 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,6 +3,8 @@
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/line_reader.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
@@ -11,29 +13,30 @@
#include "esphome/components/switch/switch.h"
#endif
#include "commands.h"
namespace esphome::dfrobot_sen0395 {
const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 253; // largest LineReader size with a one byte position
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
// forward declaration due to circular dependency
class DfrobotSen0395Component;
// 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,
};
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];
/// 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;
};
class DfrobotSen0395Component final : public uart::UARTDevice, public Component {
@@ -93,9 +96,21 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
}
#endif
int8_t enqueue(std::unique_ptr<Command> cmd);
// 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);
/// Queue a sensor reset
void reset_system() { this->enqueue(make_unique<ResetSystemCommand>()); }
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;
protected:
#ifdef USE_BINARY_SENSOR
@@ -107,19 +122,24 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
bool led_active_{false};
bool uart_presence_active_{false};
bool start_after_boot_{false};
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
size_t read_pos_{0};
CircularCommandQueue cmd_queue_;
LineReader<MMWAVE_READ_BUFFER_LENGTH> line_reader_;
bool cmd_sent_{false};
uint8_t retries_left_{2};
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> 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]{};
uint8_t read_message_();
uint8_t find_prompt_();
uint8_t send_cmd_(const char *cmd, uint32_t duration);
/// The next complete line, valid until the next call, or nullptr
const char *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, const char *message);
void set_detected_(bool detected);
friend class Command;
friend class ReadStateCommand;
};
} // namespace esphome::dfrobot_sen0395
@@ -2,45 +2,18 @@
namespace esphome::dfrobot_sen0395 {
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique<PowerCommand>(state)); }
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, 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));
}
this->parent_->enqueue_saved_setting(CommandType::COMMAND_TYPE_LED_MODE, state);
}
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));
}
this->parent_->enqueue_saved_setting(CommandType::COMMAND_TYPE_UART_OUTPUT, state);
}
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));
}
this->parent_->enqueue_saved_setting(CommandType::COMMAND_TYPE_SENSOR_CFG_START, state);
}
} // namespace esphome::dfrobot_sen0395
@@ -173,8 +173,8 @@ void DFRobotSEN0609Component::setup() {
switch_::Switch *sw = this->switches_[i];
if (sw == nullptr)
continue;
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
// 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()));
}
#endif
this->queue_configuration_();
+121 -3
View File
@@ -1,13 +1,37 @@
"""Pin checks shared by the ESP32 variant validators."""
from collections.abc import Iterable, Mapping, Set as AbstractSet
import logging
from logging import Logger
from types import MappingProxyType
from typing import Any
from esphome.components.const import CONF_HOLD_STATE
from esphome.const import CONF_NUMBER
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."),
)
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
def check_usb_jtag_pin(
num: int, usb_jtag_pins: AbstractSet[int], logger: Logger
) -> None:
if num in usb_jtag_pins:
logger.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
@@ -17,10 +41,104 @@ def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> Non
def check_usb_jtag_hold(
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
conf: dict[str, Any], usb_jtag_pins: AbstractSet[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
)
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 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)
check_usb_jtag_pin(value, self.usb_jtag_pins, _LOGGER)
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])
check_usb_jtag_hold(value, self.usb_jtag_pins, _LOGGER)
check_strapping_pin(value, self.strapping_pins, _LOGGER)
return value
@@ -6,6 +6,8 @@ from esphome.const import (
)
from esphome.types import ConfigType
AUTO_LOAD = ["factory_reset"]
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
@@ -1,4 +1,4 @@
#include "factory_reset_button.h"
#include "factory_reset_device.h"
#include "esphome/core/defines.h"
@@ -11,7 +11,7 @@
namespace esphome::factory_reset {
ESPHOME_LOG_TAG(TAG, "factory_reset.button");
ESPHOME_LOG_TAG(TAG, "factory_reset");
static void reset_and_reboot() {
global_preferences->reset();
@@ -2,7 +2,7 @@
namespace esphome::factory_reset {
/// Run by the factory reset button.
/// Run by the factory reset button and switch.
void factory_reset_device();
} // namespace esphome::factory_reset
@@ -1,23 +1,21 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT
from esphome.types import ConfigType
from .. import factory_reset_ns
FactoryResetSwitch = factory_reset_ns.class_(
"FactoryResetSwitch", switch.Switch, cg.Component
)
AUTO_LOAD = ["factory_reset"]
CONFIG_SCHEMA = switch.switch_schema(
FactoryResetSwitch,
switch.ApplySwitch,
block_inverted=True,
icon=ICON_RESTART_ALERT,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await switch.sub_switches(config).create(
config,
None,
apply="factory_reset::factory_reset_device()",
publish="momentary",
)
@@ -1,41 +0,0 @@
#include "factory_reset_switch.h"
#include "esphome/core/defines.h"
#ifdef USE_OPENTHREAD
#include "esphome/components/openthread/openthread.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::factory_reset {
ESPHOME_LOG_TAG(TAG, "factory_reset.switch");
void FactoryResetSwitch::dump_config() { LOG_SWITCH("", "Factory Reset Switch", this); }
void FactoryResetSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Resetting");
// Let MQTT settle a bit
delay(100); // NOLINT
#ifdef USE_OPENTHREAD
openthread::global_openthread_component->on_factory_reset(FactoryResetSwitch::factory_reset_callback);
#else
global_preferences->reset();
App.safe_reboot();
#endif
}
}
#ifdef USE_OPENTHREAD
void FactoryResetSwitch::factory_reset_callback() {
global_preferences->reset();
App.safe_reboot();
}
#endif
} // namespace esphome::factory_reset
@@ -1,20 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
namespace esphome::factory_reset {
class FactoryResetSwitch final : public switch_::Switch, public Component {
public:
void dump_config() override;
#ifdef USE_OPENTHREAD
static void factory_reset_callback();
#endif
protected:
void write_state(bool state) override;
};
} // namespace esphome::factory_reset
+4
View File
@@ -185,6 +185,10 @@ _PRESET_MODES_SCHEMA = cv.All(
)
# speed_count_ is a uint16_t in FanTraits
validate_speed_count = cv.int_range(min=1, max=65535)
def validate_preset_modes(value):
# Check against defined schema
value = _PRESET_MODES_SCHEMA(value)
+15 -11
View File
@@ -17,24 +17,24 @@ class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
: flags_{oscillation, speed, direction, 0}, speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
bool supports_oscillation() const { return this->oscillation_; }
bool supports_oscillation() const { return this->flags_.oscillation; }
/// Set whether this fan supports oscillation.
void set_oscillation(bool oscillation) { this->oscillation_ = oscillation; }
void set_oscillation(bool oscillation) { this->flags_.oscillation = oscillation; }
/// Return if this fan supports speed modes.
bool supports_speed() const { return this->speed_; }
bool supports_speed() const { return this->flags_.speed; }
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
void set_speed(bool speed) { this->flags_.speed = speed; }
/// Return how many speed levels the fan has
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
bool supports_direction() const { return this->flags_.direction; }
/// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; }
void set_direction(bool direction) { this->flags_.direction = direction; }
/// Empty when the fan has no preset modes. Set them on the Fan entity.
const FanPresetModes &supported_preset_modes() const;
@@ -60,10 +60,14 @@ class FanTraits {
/// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this).
void set_supported_preset_modes_(const FanPresetModes *preset_modes) { this->preset_modes_ = preset_modes; }
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
int speed_count_{};
// Bit-packed flags to save memory (1 byte instead of 3)
struct FanTraitFlags {
uint8_t oscillation : 1;
uint8_t speed : 1;
uint8_t direction : 1;
uint8_t reserved : 5;
} flags_{};
uint16_t speed_count_{}; // validators cap configs at 65535
const FanPresetModes *preset_modes_{nullptr}; // owned by the Fan entity
};
static_assert(std::is_trivially_copyable_v<FanTraits>, "FanTraits is returned by value from get_traits()");
@@ -5,12 +5,7 @@ namespace esphome::gree {
ESPHOME_LOG_TAG(TAG, "gree.switch");
void GreeModeBitSwitch::setup() {
auto initial = this->get_initial_state_with_restore_mode();
if (initial.has_value()) {
this->write_state(*initial);
}
}
void GreeModeBitSwitch::setup() { this->restore(); }
void GreeModeBitSwitch::dump_config() { log_switch(TAG, " ", this->name_, this); }
+34 -23
View File
@@ -3,20 +3,11 @@ from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_BEEPER, CONF_DISPLAY, ENTITY_CATEGORY_CONFIG
import esphome.final_validate as fv
from esphome.types import ConfigType
from ..climate import (
CONF_HAIER_ID,
CONF_PROTOCOL,
PROTOCOL_HON,
HaierClimateBase,
haier_ns,
)
from ..climate import CONF_HAIER_ID, CONF_PROTOCOL, PROTOCOL_HON, HaierClimateBase
CODEOWNERS = ["@paveldn"]
BeeperSwitch = haier_ns.class_("BeeperSwitch", switch.Switch)
HealthModeSwitch = haier_ns.class_("HealthModeSwitch", switch.Switch)
DisplaySwitch = haier_ns.class_("DisplaySwitch", switch.Switch)
QuietModeSwitch = haier_ns.class_("QuietModeSwitch", switch.Switch)
# Haier switches
CONF_HEALTH_MODE = "health_mode"
@@ -32,26 +23,26 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HaierClimateBase),
cv.Optional(CONF_DISPLAY): switch.switch_schema(
DisplaySwitch,
switch.ApplySwitch,
icon=ICON_LED_ON,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
cv.Optional(CONF_HEALTH_MODE): switch.switch_schema(
HealthModeSwitch,
switch.ApplySwitch,
icon=ICON_LEAF,
default_restore_mode="DISABLED",
),
# Beeper switch is only supported for HonClimate
cv.Optional(CONF_BEEPER): switch.switch_schema(
BeeperSwitch,
switch.ApplySwitch,
icon=ICON_VOLUME_HIGH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
# Quiet mode is only supported for HonClimate
cv.Optional(CONF_QUIET_MODE): switch.switch_schema(
QuietModeSwitch,
switch.ApplySwitch,
icon=ICON_VOLUME_OFF,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
@@ -77,11 +68,31 @@ def _final_validate(config) -> None:
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
parent = await cg.get_variable(config[CONF_HAIER_ID])
for switch_type in [CONF_DISPLAY, CONF_HEALTH_MODE, CONF_BEEPER, CONF_QUIET_MODE]:
if conf := config.get(switch_type):
sw_var = await switch.new_switch(conf)
await cg.register_parented(sw_var, parent)
cg.add(getattr(parent, f"set_{switch_type}_switch")(sw_var))
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_ID])
switches = switch.sub_switches(config, parent=hub)
# The setters skip an unchanged value; the switch reports the state either way
await switches(
CONF_DISPLAY,
hub.set_display_switch,
apply="set_display_state(state)",
publish="after",
)
await switches(
CONF_HEALTH_MODE,
hub.set_health_mode_switch,
apply="set_health_mode(state)",
publish="after",
)
await switches(
CONF_BEEPER,
hub.set_beeper_switch,
apply="set_beeper_state(state)",
publish="after",
)
await switches(
CONF_QUIET_MODE,
hub.set_quiet_mode_switch,
apply="set_quiet_mode_state(state)",
publish="after",
)
@@ -1,12 +0,0 @@
#include "beeper.h"
namespace esphome::haier {
void BeeperSwitch::write_state(bool state) {
if (this->parent_->get_beeper_state() != state) {
this->parent_->set_beeper_state(state);
}
this->publish_state(state);
}
} // namespace esphome::haier
-16
View File
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../hon_climate.h"
namespace esphome::haier {
class BeeperSwitch final : public switch_::Switch, public Parented<HonClimate> {
public:
BeeperSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::haier
@@ -1,12 +0,0 @@
#include "display.h"
namespace esphome::haier {
void DisplaySwitch::write_state(bool state) {
if (this->parent_->get_display_state() != state) {
this->parent_->set_display_state(state);
}
this->publish_state(state);
}
} // namespace esphome::haier
-16
View File
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../haier_base.h"
namespace esphome::haier {
class DisplaySwitch final : public switch_::Switch, public Parented<HaierClimateBase> {
public:
DisplaySwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::haier
@@ -1,12 +0,0 @@
#include "health_mode.h"
namespace esphome::haier {
void HealthModeSwitch::write_state(bool state) {
if (this->parent_->get_health_mode() != state) {
this->parent_->set_health_mode(state);
}
this->publish_state(state);
}
} // namespace esphome::haier
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../haier_base.h"
namespace esphome::haier {
class HealthModeSwitch final : public switch_::Switch, public Parented<HaierClimateBase> {
public:
HealthModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::haier
@@ -1,12 +0,0 @@
#include "quiet_mode.h"
namespace esphome::haier {
void QuietModeSwitch::write_state(bool state) {
if (this->parent_->get_quiet_mode_state() != state) {
this->parent_->set_quiet_mode_state(state);
}
this->publish_state(state);
}
} // namespace esphome::haier
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../hon_climate.h"
namespace esphome::haier {
class QuietModeSwitch final : public switch_::Switch, public Parented<HonClimate> {
public:
QuietModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::haier
@@ -8,9 +8,10 @@ ESPHOME_LOG_TAG(TAG, "haier_tundra.switch");
void HaierTundraSwitch::setup() {
auto initial = this->get_initial_state_with_restore_mode();
if (initial.has_value()) {
const bool state = initial.value();
this->apply_(state, false);
this->publish_state(state);
// apply_() and publish_state() take the level, which an inverted switch flips
const bool level = *initial != this->is_inverted();
this->apply_(level, false);
this->publish_state(level);
}
}
@@ -0,0 +1,37 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_POWER,
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_HEAT,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from esphome.types import ConfigType
from .water_heater import CONF_HAIER_WATER_HEATER_ID, HonWaterHeater
CONF_COMMUNICATION = "communication"
CONF_HEATING = "heating"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_WATER_HEATER_ID): cv.use_id(HonWaterHeater),
cv.Optional(CONF_COMMUNICATION): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_POWER): binary_sensor.binary_sensor_schema(),
cv.Optional(CONF_HEATING): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_HEAT
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_WATER_HEATER_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_COMMUNICATION, hub.set_communication_binary_sensor)
await binary_sensors(CONF_POWER, hub.set_power_binary_sensor)
await binary_sensors(CONF_HEATING, hub.set_heating_binary_sensor)
@@ -14,19 +14,27 @@ namespace esphome::haier_water_heater {
ESPHOME_LOG_TAG(TAG, "haier_water_heater");
using namespace water_heater_protocol;
#ifdef USE_TEXT_SENSOR
// Indexed by HonMode, with Unknown as the fallback for out-of-range values.
PROGMEM_STRING_TABLE(HonModeLabels, "Unknown", "Normal", "ECO", "BPS");
#endif
void HonWaterHeater::setup() {
this->status_set_warning(LOG_STR("Waiting for a valid appliance status frame"));
if (this->communication_sensor_ != nullptr)
this->communication_sensor_->publish_state(false);
this->publish_communication_(false);
this->update_network_status_();
this->set_interval(NETWORK_STATUS_INTERVAL_ID, NETWORK_STATUS_INTERVAL_MS,
[this]() { this->update_network_status_(); });
this->set_interval(STATUS_POLL_INTERVAL_ID, POLL_INTERVAL_MS, [this]() { this->poll_status_(); });
}
void HonWaterHeater::publish_communication_(bool connected) {
#ifdef USE_BINARY_SENSOR
if (this->communication_binary_sensor_ != nullptr)
this->communication_binary_sensor_->publish_state(connected);
#endif
}
void HonWaterHeater::update_network_status_() {
#ifdef USE_NETWORK
const bool ready = network::is_connected();
@@ -87,8 +95,7 @@ void HonWaterHeater::check_timeouts_(uint32_t now) {
this->status_set_warning(LOG_STR("Heater communication timed out"));
ESP_LOGW(TAG, "Heater communication lost: no valid status for %u ms",
static_cast<unsigned>(this->communication_timeout_));
if (this->communication_sensor_ != nullptr)
this->communication_sensor_->publish_state(false);
this->publish_communication_(false);
}
const uint32_t waited = now - this->request_sent_;
switch (this->awaiting_) {
@@ -243,8 +250,7 @@ void HonWaterHeater::process_frame_() {
// Published last so an automation on the sensor sees fresh heater state and can send a control.
if (recovered) {
ESP_LOGI(TAG, "Heater communication established");
if (this->communication_sensor_ != nullptr)
this->communication_sensor_->publish_state(true);
this->publish_communication_(true);
}
}
@@ -267,33 +273,35 @@ void HonWaterHeater::publish_status_(const Status &status) {
}
// The base entity has no Unknown mode; the text sensor preserves the decoded result.
this->mode_ = status.powered ? this->last_active_mode_ : water_heater::WATER_HEATER_MODE_OFF;
if (this->primary_sensor_ != nullptr)
this->primary_sensor_->publish_state(status.current_temperature);
if (this->secondary_sensor_ != nullptr) {
this->secondary_sensor_->publish_state(
#ifdef USE_SENSOR
if (this->primary_temperature_sensor_ != nullptr)
this->primary_temperature_sensor_->publish_state(status.current_temperature);
if (this->secondary_temperature_sensor_ != nullptr) {
this->secondary_temperature_sensor_->publish_state(
status.secondary_temperature <= MAX_REPORTED_TEMPERATURE ? status.secondary_temperature : NAN);
}
if (this->target_sensor_ != nullptr)
this->target_sensor_->publish_state(status.target_temperature);
if (this->heating_time_sensor_ != nullptr)
this->heating_time_sensor_->publish_state(status.estimated_heating_time);
if (this->water_sensor_ != nullptr) {
if (this->target_temperature_sensor_ != nullptr)
this->target_temperature_sensor_->publish_state(status.target_temperature);
if (this->estimated_heating_time_sensor_ != nullptr)
this->estimated_heating_time_sensor_->publish_state(status.estimated_heating_time);
if (this->available_water_sensor_ != nullptr) {
// Three segments of available hot water, published as 0, 33, 66 or 100 percent.
const uint8_t percent = status.available_water * 100 / 3;
this->water_sensor_->publish_state(status.available_water <= 3 ? percent : NAN);
this->available_water_sensor_->publish_state(status.available_water <= 3 ? percent : NAN);
}
if (this->power_sensor_ != nullptr)
this->power_sensor_->publish_state(status.powered);
if (this->heating_sensor_ != nullptr)
this->heating_sensor_->publish_state(status.heating);
if (this->mode_sensor_ != nullptr) {
char label[HonModeLabels::MAX_LENGTH + 1];
ESPHOME_strncpy_P(label,
reinterpret_cast<ESPHOME_PGM_P>(HonModeLabels::get_progmem_str(
static_cast<uint8_t>(status.mode), static_cast<uint8_t>(HonMode::HON_MODE_UNKNOWN))),
sizeof(label));
this->mode_sensor_->publish_state(label);
#endif
#ifdef USE_BINARY_SENSOR
if (this->power_binary_sensor_ != nullptr)
this->power_binary_sensor_->publish_state(status.powered);
if (this->heating_binary_sensor_ != nullptr)
this->heating_binary_sensor_->publish_state(status.heating);
#endif
#ifdef USE_TEXT_SENSOR
if (this->mode_text_sensor_ != nullptr) {
this->mode_text_sensor_->publish_state(HonModeLabels::get_progmem_str(
static_cast<uint8_t>(status.mode), static_cast<uint8_t>(HonMode::HON_MODE_UNKNOWN)));
}
#endif
this->publish_state();
}
@@ -303,15 +311,21 @@ void HonWaterHeater::dump_config() {
" Polling interval: %u ms\n"
" Communication timeout: %u ms",
static_cast<unsigned>(POLL_INTERVAL_MS), static_cast<unsigned>(this->communication_timeout_));
LOG_SENSOR(" ", "Primary temperature", this->primary_sensor_);
LOG_SENSOR(" ", "Secondary temperature", this->secondary_sensor_);
LOG_SENSOR(" ", "Target temperature", this->target_sensor_);
LOG_SENSOR(" ", "Estimated heating time", this->heating_time_sensor_);
LOG_SENSOR(" ", "Available water", this->water_sensor_);
LOG_BINARY_SENSOR(" ", "Communication", this->communication_sensor_);
LOG_BINARY_SENSOR(" ", "Power", this->power_sensor_);
LOG_BINARY_SENSOR(" ", "Heating", this->heating_sensor_);
LOG_TEXT_SENSOR(" ", "Mode", this->mode_sensor_);
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Primary temperature", this->primary_temperature_sensor_);
LOG_SENSOR(" ", "Secondary temperature", this->secondary_temperature_sensor_);
LOG_SENSOR(" ", "Target temperature", this->target_temperature_sensor_);
LOG_SENSOR(" ", "Estimated heating time", this->estimated_heating_time_sensor_);
LOG_SENSOR(" ", "Available water", this->available_water_sensor_);
#endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Communication", this->communication_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Power", this->power_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Heating", this->heating_binary_sensor_);
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Mode", this->mode_text_sensor_);
#endif
}
water_heater::WaterHeaterCallInternal HonWaterHeater::make_call() {
@@ -3,10 +3,16 @@
#include "hon_parser.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/water_heater/water_heater.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/component.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
namespace esphome::haier_water_heater {
@@ -18,15 +24,21 @@ class HonWaterHeater : public Component, public water_heater::WaterHeater, publi
water_heater::WaterHeaterCallInternal make_call() override;
void set_communication_timeout(uint32_t value) { this->communication_timeout_ = value; }
void set_primary_temperature_sensor(sensor::Sensor *value) { this->primary_sensor_ = value; }
void set_secondary_temperature_sensor(sensor::Sensor *value) { this->secondary_sensor_ = value; }
void set_target_temperature_sensor(sensor::Sensor *value) { this->target_sensor_ = value; }
void set_estimated_heating_time_sensor(sensor::Sensor *value) { this->heating_time_sensor_ = value; }
void set_available_water_sensor(sensor::Sensor *value) { this->water_sensor_ = value; }
void set_communication_sensor(binary_sensor::BinarySensor *value) { this->communication_sensor_ = value; }
void set_power_sensor(binary_sensor::BinarySensor *value) { this->power_sensor_ = value; }
void set_heating_sensor(binary_sensor::BinarySensor *value) { this->heating_sensor_ = value; }
void set_mode_sensor(text_sensor::TextSensor *value) { this->mode_sensor_ = value; }
#ifdef USE_SENSOR
SUB_SENSOR(primary_temperature)
SUB_SENSOR(secondary_temperature)
SUB_SENSOR(target_temperature)
SUB_SENSOR(estimated_heating_time)
SUB_SENSOR(available_water)
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(communication)
SUB_BINARY_SENSOR(power)
SUB_BINARY_SENSOR(heating)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(mode)
#endif
protected:
static constexpr uint32_t NETWORK_STATUS_INTERVAL_ID = 0;
@@ -55,6 +67,7 @@ class HonWaterHeater : public Component, public water_heater::WaterHeater, publi
void read_frames_(uint32_t now);
void poll_status_();
void update_network_status_();
void publish_communication_(bool connected);
void send_frame_(water_heater_protocol::FrameType type, const uint8_t *payload = nullptr, size_t size = 0);
void send_request_(uint32_t now, Awaiting kind, water_heater_protocol::FrameType type, const uint8_t *payload,
size_t size, water_heater_protocol::FrameType expected);
@@ -65,15 +78,6 @@ class HonWaterHeater : public Component, public water_heater::WaterHeater, publi
void publish_status_(const water_heater_protocol::Status &status);
water_heater_protocol::FrameParser parser_;
sensor::Sensor *primary_sensor_{nullptr};
sensor::Sensor *secondary_sensor_{nullptr};
sensor::Sensor *target_sensor_{nullptr};
sensor::Sensor *heating_time_sensor_{nullptr};
sensor::Sensor *water_sensor_{nullptr};
binary_sensor::BinarySensor *communication_sensor_{nullptr};
binary_sensor::BinarySensor *power_sensor_{nullptr};
binary_sensor::BinarySensor *heating_sensor_{nullptr};
text_sensor::TextSensor *mode_sensor_{nullptr};
uint32_t request_sent_{0};
uint32_t network_sent_{0};
uint32_t last_status_{0};
@@ -0,0 +1,57 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_TARGET_TEMPERATURE,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_MINUTE,
UNIT_PERCENT,
)
from esphome.types import ConfigType
from .water_heater import CONF_HAIER_WATER_HEATER_ID, HonWaterHeater
CONF_AVAILABLE_WATER = "available_water"
CONF_ESTIMATED_HEATING_TIME = "estimated_heating_time"
CONF_PRIMARY_TEMPERATURE = "primary_temperature"
CONF_SECONDARY_TEMPERATURE = "secondary_temperature"
_TEMPERATURE_SENSOR = sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_WATER_HEATER_ID): cv.use_id(HonWaterHeater),
cv.Optional(CONF_PRIMARY_TEMPERATURE): _TEMPERATURE_SENSOR,
cv.Optional(CONF_SECONDARY_TEMPERATURE): _TEMPERATURE_SENSOR,
cv.Optional(CONF_TARGET_TEMPERATURE): _TEMPERATURE_SENSOR,
cv.Optional(CONF_ESTIMATED_HEATING_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MINUTE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AVAILABLE_WATER): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_WATER_HEATER_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_PRIMARY_TEMPERATURE, hub.set_primary_temperature_sensor)
await sensors(CONF_SECONDARY_TEMPERATURE, hub.set_secondary_temperature_sensor)
await sensors(CONF_TARGET_TEMPERATURE, hub.set_target_temperature_sensor)
await sensors(CONF_ESTIMATED_HEATING_TIME, hub.set_estimated_heating_time_sensor)
await sensors(CONF_AVAILABLE_WATER, hub.set_available_water_sensor)
@@ -0,0 +1,20 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_MODE
from esphome.types import ConfigType
from .water_heater import CONF_HAIER_WATER_HEATER_ID, HonWaterHeater
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_WATER_HEATER_ID): cv.use_id(HonWaterHeater),
cv.Optional(CONF_MODE): text_sensor.text_sensor_schema(),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_WATER_HEATER_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_MODE, hub.set_mode_text_sensor)
@@ -1,48 +1,21 @@
"""Local controller for Haier hOn water heaters."""
import esphome.codegen as cg
from esphome.components import binary_sensor, sensor, text_sensor, uart, water_heater
from esphome.components import uart, water_heater
import esphome.config_validation as cv
from esphome.const import (
CONF_MODE,
CONF_POWER,
CONF_TARGET_TEMPERATURE,
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_TEMPERATURE,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_MINUTE,
UNIT_PERCENT,
)
from esphome.types import ConfigType
CONF_AVAILABLE_WATER = "available_water"
CONF_COMMUNICATION = "communication"
CONF_COMMUNICATION_TIMEOUT = "communication_timeout"
CONF_ESTIMATED_HEATING_TIME = "estimated_heating_time"
CONF_HEATING = "heating"
CONF_PRIMARY_TEMPERATURE = "primary_temperature"
CONF_SECONDARY_TEMPERATURE = "secondary_temperature"
CONF_HAIER_WATER_HEATER_ID = "haier_water_heater_id"
CODEOWNERS = ["@TheLexus"]
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor"]
haier_water_heater_ns = cg.esphome_ns.namespace("haier_water_heater")
HonWaterHeater = haier_water_heater_ns.class_(
"HonWaterHeater", water_heater.WaterHeater, cg.Component, uart.UARTDevice
)
_TEMPERATURE_SENSOR = sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
CONFIG_SCHEMA = (
water_heater.water_heater_schema(HonWaterHeater)
.extend(
@@ -50,29 +23,6 @@ CONFIG_SCHEMA = (
cv.Optional(
CONF_COMMUNICATION_TIMEOUT, default="20s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_PRIMARY_TEMPERATURE): _TEMPERATURE_SENSOR,
cv.Optional(CONF_SECONDARY_TEMPERATURE): _TEMPERATURE_SENSOR,
cv.Optional(CONF_TARGET_TEMPERATURE): _TEMPERATURE_SENSOR,
cv.Optional(CONF_ESTIMATED_HEATING_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MINUTE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AVAILABLE_WATER): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_COMMUNICATION): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_POWER): binary_sensor.binary_sensor_schema(),
cv.Optional(CONF_HEATING): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_HEAT
),
cv.Optional(CONF_MODE): text_sensor.text_sensor_schema(),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -95,16 +45,3 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
cg.add(var.set_communication_timeout(config[CONF_COMMUNICATION_TIMEOUT]))
await uart.register_uart_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_PRIMARY_TEMPERATURE, var.set_primary_temperature_sensor)
await sensors(CONF_SECONDARY_TEMPERATURE, var.set_secondary_temperature_sensor)
await sensors(CONF_TARGET_TEMPERATURE, var.set_target_temperature_sensor)
await sensors(CONF_ESTIMATED_HEATING_TIME, var.set_estimated_heating_time_sensor)
await sensors(CONF_AVAILABLE_WATER, var.set_available_water_sensor)
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_COMMUNICATION, var.set_communication_sensor)
await binary_sensors(CONF_POWER, var.set_power_sensor)
await binary_sensors(CONF_HEATING, var.set_heating_sensor)
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_MODE, var.set_mode_sensor)
+1 -1
View File
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_SPEED_COUNT, default=100): fan.validate_speed_count,
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
@@ -9,8 +9,6 @@ ESPHOME_LOG_TAG(TAG, "switch.hbridge");
float HBridgeSwitch::get_setup_priority() const { return setup_priority::HARDWARE; }
void HBridgeSwitch::setup() {
optional<bool> initial_state = this->get_initial_state_with_restore_mode();
// Like GPIOSwitch does, set the pin state both before and after pin setup()
this->on_pin_->digital_write(false);
this->on_pin_->setup();
@@ -20,8 +18,7 @@ void HBridgeSwitch::setup() {
this->off_pin_->setup();
this->off_pin_->digital_write(false);
if (initial_state.has_value())
this->write_state(initial_state.value());
this->restore();
}
void HBridgeSwitch::dump_config() {
@@ -155,12 +155,9 @@ void HitachiClimate::setup() {
set_swing_v_(swing_vertical(restore->swing_mode));
}
#ifdef USE_SWITCH
// The restored value is the switch state, which an inverted switch flips relative to the frame bit
if (this->mildew_proof_switch_ != nullptr) {
if (auto initial = this->mildew_proof_switch_->get_initial_state_with_restore_mode()) {
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET,
*initial != this->mildew_proof_switch_->is_inverted());
}
if (auto initial = this->mildew_proof_switch_->get_initial_write_state())
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET, *initial);
}
#endif
this->setup_(restore);
@@ -1,7 +1,5 @@
#include "hoermann_hcp_text_sensor.h"
#include <cstring>
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -37,13 +35,7 @@ void HoermannHcpDoorStateTextSensor::update_from_state_() {
return;
this->published_ = true;
this->published_state_ = state;
char text[16];
ESPHOME_strncpy_P(text,
reinterpret_cast<ESPHOME_PGM_P>(
DoorStateStrings::get_progmem_str(static_cast<uint8_t>(state), DoorStateStrings::LAST_INDEX)),
sizeof(text));
text[sizeof(text) - 1] = '\0';
this->publish_state(text);
this->publish_state(DoorStateStrings::get_progmem_str(static_cast<uint8_t>(state), DoorStateStrings::LAST_INDEX));
}
} // namespace esphome::hoermann_hcp
@@ -15,25 +15,34 @@ ESPHOME_LOG_TAG(TAG, "http_request.ota");
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
OtaHttpRequestComponent::Request &OtaHttpRequestComponent::get_request_() {
if (this->request_ == nullptr)
this->request_ = make_unique<Request>();
return *this->request_;
}
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
if (!this->validate_url_(url)) {
this->md5_url_.clear(); // URL was not valid; prevent flashing until it is
if (this->request_ != nullptr)
this->request_->md5_url.clear(); // URL was not valid; prevent flashing until it is
return;
}
this->md5_url_ = url;
this->md5_expected_.clear(); // to be retrieved later
Request &request = this->get_request_();
request.md5_url = url;
request.md5_expected.clear(); // to be retrieved later
}
void OtaHttpRequestComponent::set_url(const std::string &url) {
if (!this->validate_url_(url)) {
this->url_.clear(); // URL was not valid; prevent flashing until it is
if (this->request_ != nullptr)
this->request_->url.clear(); // URL was not valid; prevent flashing until it is
return;
}
this->url_ = url;
this->get_request_().url = url;
}
void OtaHttpRequestComponent::flash() {
if (this->url_.empty()) {
if (this->request_ == nullptr || this->request_->url.empty()) {
ESP_LOGE(TAG, "URL not set; cannot start update");
return;
}
@@ -58,8 +67,8 @@ void OtaHttpRequestComponent::flash() {
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
#endif
this->md5_computed_.clear(); // will be reset at next attempt
this->md5_expected_.clear(); // will be reset at next attempt
this->request_->md5_computed.clear(); // will be reset at next attempt
this->request_->md5_expected.clear(); // will be reset at next attempt
break;
}
}
@@ -81,18 +90,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
md5::MD5Digest md5_receive;
char md5_receive_str[33];
if (this->md5_expected_.empty() && !this->http_get_md5_()) {
if (this->request_->md5_expected.empty() && !this->http_get_md5_()) {
return OTA_MD5_INVALID;
}
ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str());
ESP_LOGD(TAG, "MD5 expected: %s", this->request_->md5_expected.c_str());
auto url_with_auth = this->get_url_with_auth_(this->url_);
auto url_with_auth = this->get_url_with_auth_(this->request_->url);
if (url_with_auth.empty()) {
return OTA_BAD_URL;
}
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->request_->url.c_str());
auto container = this->parent_->get(url_with_auth);
@@ -179,9 +188,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
// verify MD5 is as expected and act accordingly
md5_receive.calculate();
md5_receive.get_hex(md5_receive_str);
this->md5_computed_ = md5_receive_str;
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
this->request_->md5_computed = md5_receive_str;
if (strncmp(this->request_->md5_computed.c_str(), this->request_->md5_expected.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->request_->md5_computed.c_str());
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
@@ -223,11 +232,15 @@ static std::string url_encode(const std::string &str) {
return result;
}
void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); }
void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); }
void OtaHttpRequestComponent::set_password(const std::string &password) {
this->get_request_().password = url_encode(password);
}
void OtaHttpRequestComponent::set_username(const std::string &username) {
this->get_request_().username = url_encode(username);
}
std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
if (this->username_.empty() || this->password_.empty()) {
if (this->request_->username.empty() || this->request_->password.empty()) {
return url;
}
@@ -240,23 +253,23 @@ std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url)
ESP_LOGD(TAG, "Using basic HTTP authentication");
start_char += 3; // skip '://' characters
auto url_with_auth =
url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char);
auto url_with_auth = url.substr(0, start_char) + this->request_->username + ":" + this->request_->password + "@" +
url.substr(start_char);
return url_with_auth;
}
bool OtaHttpRequestComponent::http_get_md5_() {
if (this->md5_url_.empty()) {
if (this->request_->md5_url.empty()) {
return false;
}
auto url_with_auth = this->get_url_with_auth_(this->md5_url_);
auto url_with_auth = this->get_url_with_auth_(this->request_->md5_url);
if (url_with_auth.empty()) {
return false;
}
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->request_->md5_url.c_str());
auto container = this->parent_->get(url_with_auth);
if (container == nullptr) {
ESP_LOGE(TAG, "Failed to connect to MD5 URL");
@@ -273,8 +286,8 @@ bool OtaHttpRequestComponent::http_get_md5_() {
return false;
}
this->md5_expected_.resize(MD5_SIZE);
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
this->request_->md5_expected.resize(MD5_SIZE);
auto result = http_read_fully(container.get(), (uint8_t *) this->request_->md5_expected.data(), MD5_SIZE, MD5_SIZE,
this->parent_->get_timeout());
container->end();
@@ -27,29 +27,36 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_md5_url(const std::string &md5_url);
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
void set_md5(const std::string &md5) { this->get_request_().md5_expected = md5; }
void set_password(const std::string &password);
void set_url(const std::string &url);
void set_username(const std::string &username);
std::string md5_computed() { return this->md5_computed_; }
std::string md5_expected() { return this->md5_expected_; }
std::string md5_computed() { return this->request_ != nullptr ? this->request_->md5_computed : std::string(); }
std::string md5_expected() { return this->request_ != nullptr ? this->request_->md5_expected : std::string(); }
void flash();
protected:
/// Allocated by the first setter, which only runs before a flash, and never freed, so a flash never loses it.
struct Request {
std::string md5_computed;
std::string md5_expected;
std::string md5_url;
std::string password;
std::string username;
std::string url;
};
Request &get_request_();
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container, bool abort_backend);
// Only reached from flash(), which returns early while request_ is null
uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_();
bool validate_url_(const std::string &url);
std::string md5_computed_{};
std::string md5_expected_{};
std::string md5_url_{};
std::string password_{};
std::string username_{};
std::string url_{};
std::unique_ptr<Request> request_;
int status_ = -1;
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
};
+45 -48
View File
@@ -163,21 +163,19 @@ static constexpr uint8_t CMD_DURATION_VALUE = 0x02;
// Bitmasks for target states
static constexpr uint8_t MOVE_BITMASK = 0x01;
static constexpr uint8_t STILL_BITMASK = 0x02;
// Header & Footer size
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
// Command Header & Footer
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
using ld24xx::CMD_FRAME_FOOTER;
using ld24xx::CMD_FRAME_HEADER;
using ld24xx::HEADER_FOOTER_SIZE;
using ld24xx::validate_header_footer;
// Smallest frames whose values can all be read: the last value, data footer and check, then the frame footer
static constexpr uint8_t PERIODIC_FRAME_MIN = DETECT_DISTANCE_HIGH + 3 + HEADER_FOOTER_SIZE;
static constexpr uint8_t ENGINEERING_FRAME_MIN = OUT_PIN_SENSOR + 3 + HEADER_FOOTER_SIZE;
// Data Header & Footer
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
void LD2410Component::dump_config() {
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
char version_s[20];
@@ -328,6 +326,9 @@ void LD2410Component::handle_periodic_data_() {
0x02: Normal mode
*/
bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01;
if (this->buffer_pos_ < (engineering_mode ? ENGINEERING_FRAME_MIN : PERIODIC_FRAME_MIN)) {
return; // too short to hold the values
}
#ifdef USE_SWITCH
if (this->engineering_mode_switch_ != nullptr) {
this->engineering_mode_switch_->publish_state(engineering_mode);
@@ -414,24 +415,15 @@ void set_number_value(number::Number *n, float value) {
}
#endif
bool LD2410Component::handle_ack_data_() {
void LD2410Component::handle_ack_data_() {
ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
if (this->buffer_pos_ < 10) {
ESP_LOGE(TAG, "Invalid length");
return true;
}
if (!ld2410::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)];
ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE));
return true;
}
if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Invalid status");
return true;
return;
}
if (this->buffer_data_[8] || this->buffer_data_[9]) {
ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
return true;
return;
}
switch (this->buffer_data_[COMMAND]) {
@@ -454,6 +446,9 @@ bool LD2410Component::handle_ack_data_() {
break;
case CMD_QUERY_VERSION: {
if (this->buffer_pos_ < 12 + sizeof(this->version_) + HEADER_FOOTER_SIZE) {
break; // too short to hold a version
}
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
char version_s[20];
ld24xx::format_version_str(this->version_, version_s);
@@ -467,6 +462,9 @@ bool LD2410Component::handle_ack_data_() {
}
case CMD_QUERY_DISTANCE_RESOLUTION: {
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
break; // too short to hold the resolution
}
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
#ifdef USE_SELECT
@@ -478,6 +476,9 @@ bool LD2410Component::handle_ack_data_() {
}
case CMD_QUERY_LIGHT_CONTROL: {
if (this->buffer_pos_ < 13 + HEADER_FOOTER_SIZE) {
break; // too short to hold the light settings
}
this->light_function_ = this->buffer_data_[10];
this->light_threshold_ = this->buffer_data_[11];
this->out_pin_level_ = this->buffer_data_[12];
@@ -502,7 +503,7 @@ bool LD2410Component::handle_ack_data_() {
}
case CMD_QUERY_MAC_ADDRESS: {
if (this->buffer_pos_ < 20) {
return false;
break; // too short to hold a MAC address
}
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
@@ -547,12 +548,11 @@ bool LD2410Component::handle_ack_data_() {
break;
case CMD_QUERY: { // Query parameters response
// Config values can contain the footer bytes, so wait for the whole 38 byte frame
if (this->buffer_pos_ < 38) {
return false;
break; // too short to hold the parameters
}
if (this->buffer_data_[10] != HEADER)
return true; // value head=0xAA
return; // value head=0xAA
#ifdef USE_NUMBER
/*
Moving distance range: 13th byte
@@ -582,39 +582,36 @@ bool LD2410Component::handle_ack_data_() {
default:
break;
}
return true;
}
void LD2410Component::readline_(int readch) {
if (readch < 0) {
return; // No data available
}
// Periodic and command frames both carry their count after the header
ld24xx::feed_frame_byte(
this->buffer_data_, this->buffer_pos_, readch, DATA_FRAME_HEADER, ld24xx::cmd_frame_length,
[this](uint8_t frame_len) { return this->handle_frame_(frame_len); }, []() { ESP_LOGW(TAG, "Invalid length"); });
}
if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
this->buffer_data_[this->buffer_pos_++] = readch;
this->buffer_data_[this->buffer_pos_] = 0;
} else {
// We should never get here, but just in case...
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
this->buffer_pos_ = 0;
return;
}
if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
return; // Not enough data to process yet
}
if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
// Dispatches the frame of frame_len bytes at the start of the buffer when its footer is in place
bool LD2410Component::handle_frame_(uint8_t frame_len) {
this->buffer_pos_ = frame_len; // the handlers take the frame length from here
const uint8_t *tail = &this->buffer_data_[frame_len - HEADER_FOOTER_SIZE];
if (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) {
if (!ld2410::validate_header_footer(DATA_FRAME_FOOTER, tail)) {
return false;
}
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Periodic Data: %s");
this->handle_periodic_data_();
this->buffer_pos_ = 0; // Reset position index for next message
} else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Ack Data: %s");
if (this->handle_ack_data_()) {
this->buffer_pos_ = 0; // Reset position index for next message
} else {
ESP_LOGV(TAG, "Ack Data incomplete");
}
return true;
}
if (!ld2410::validate_header_footer(CMD_FRAME_FOOTER, tail)) {
return false;
}
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Ack Data: %s");
this->handle_ack_data_();
return true;
}
void LD2410Component::set_config_mode_(bool enable) {
+2 -1
View File
@@ -106,7 +106,8 @@ class LD2410Component final : public Component, public uart::UARTDevice {
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable);
void handle_periodic_data_();
bool handle_ack_data_();
void handle_ack_data_();
bool handle_frame_(uint8_t frame_len);
void readline_(int readch);
void query_parameters_();
void get_version_();
+70
View File
@@ -3,6 +3,8 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <cstring>
#include <memory>
#include <span>
@@ -53,6 +55,74 @@ inline void format_version_str(const uint8_t *version, std::span<char, 20> buffe
version[2]);
}
// Commands and their answers share this header, a little-endian byte count and this footer
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
// Whole command frame length, or 0 until the count arrives; 32 bit so 0xFFFF cannot wrap to 0
inline uint32_t cmd_frame_length(const uint8_t *buffer, uint8_t count) {
if (count < HEADER_FOOTER_SIZE + 2) {
return 0;
}
return HEADER_FOOTER_SIZE + 2 + encode_value_le<uint16_t>(&buffer[HEADER_FOOTER_SIZE]) + HEADER_FOOTER_SIZE;
}
// Drops the first `from` bytes and anything after them that does not begin a header; returns the bytes kept
inline uint8_t drop_to_next_header(uint8_t *buffer, uint8_t count, uint8_t from, const uint8_t *data_header) {
for (; from < count; from++) {
const size_t n = std::min<size_t>(HEADER_FOOTER_SIZE, count - from);
if (std::memcmp(&buffer[from], CMD_FRAME_HEADER, n) == 0 || std::memcmp(&buffer[from], data_header, n) == 0) {
std::memmove(buffer, &buffer[from], count - from);
return count - from;
}
}
return 0;
}
// Feeds one byte into a frame buffer. A frame opens with the command header or `data_header` and ends at
// `frame_length(buffer, count)` bytes (0 while unknown); `handle_frame(len)` returns false for a footer out
// of place. A dropped or oversized frame calls `on_drop()`. The handler may overwrite `pos`; the splitter
// works from the count it saved.
template<size_t N, typename LengthFn, typename HandleFn, typename DropFn>
void feed_frame_byte(uint8_t (&buffer)[N], uint8_t &pos, uint8_t byte, const uint8_t *data_header,
LengthFn frame_length, HandleFn handle_frame, DropFn on_drop) {
// The first byte picks the header; bytes that do not continue it are dropped
if (pos > 0 && pos < HEADER_FOOTER_SIZE) {
const uint8_t *header = buffer[0] == data_header[0] ? data_header : CMD_FRAME_HEADER;
if (byte != header[pos]) {
pos = 0;
}
}
if (pos == 0 && byte != data_header[0] && byte != CMD_FRAME_HEADER[0]) {
return;
}
buffer[pos++] = byte;
// Drain every whole frame left in the buffer
while (true) {
const uint32_t frame_len = frame_length(buffer, pos);
if (frame_len == 0 || (frame_len <= N && pos < frame_len)) {
return;
}
const uint8_t total = pos;
// Keep only what begins a header: after the frame, or after its first byte when it is dropped
uint8_t from = 1;
if (frame_len <= N && handle_frame(static_cast<uint8_t>(frame_len))) {
from = frame_len;
} else {
on_drop();
}
pos = drop_to_next_header(buffer, total, from, data_header);
if (pos == 0) {
return;
}
}
}
#ifdef USE_SENSOR
template<typename T> using SensorWithDedup = sensor::SensorWithDedup<T>;
#endif
+6 -8
View File
@@ -1066,20 +1066,19 @@ bool LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_
ESP_LOGW(TAG, "Command data too large: %u", len);
return false;
}
if (this->cmd_count_ >= CMD_QUEUE_SIZE) {
if (this->cmd_queue_.size() >= CMD_QUEUE_SIZE) {
ESP_LOGW(TAG, "Command queue full, dropping command 0x%04X", type);
return false;
}
PendingCommand &cmd = this->cmd_queue_[this->cmd_tail_];
PendingCommand cmd;
cmd.type = type;
cmd.len = len;
if (len > 0 && data != nullptr) {
std::memcpy(cmd.data.data(), data, len);
}
this->cmd_queue_.push(cmd);
this->cmd_tail_ = (this->cmd_tail_ + 1) % CMD_QUEUE_SIZE;
this->cmd_count_++;
this->process_command_queue_();
return true;
}
@@ -1146,12 +1145,11 @@ void LD6002BComponent::process_command_queue_() {
return;
}
if (this->cmd_count_ == 0)
if (this->cmd_queue_.empty())
return;
this->active_command_ = this->cmd_queue_[this->cmd_head_];
this->cmd_head_ = (this->cmd_head_ + 1) % CMD_QUEUE_SIZE;
this->cmd_count_--;
this->active_command_ = this->cmd_queue_.front();
this->cmd_queue_.pop();
this->send_generation_++;
this->retries_left_ = CMD_MAX_RETRIES;
+1 -4
View File
@@ -419,10 +419,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
// A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms.
static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000;
std::array<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_{};
uint8_t cmd_head_{0};
uint8_t cmd_tail_{0};
uint8_t cmd_count_{0};
StaticRingBuffer<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_;
bool command_active_{false};
bool command_sent_{false};
PendingCommand active_command_{};
+17 -7
View File
@@ -40,6 +40,7 @@ from .const import (
CONF_UART_PORT,
FAMILIES,
FAMILY_BK7231N,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
@@ -233,14 +234,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 13, 0), "https://github.com/libretiny-eu/libretiny.git"),
"dev": (cv.Version(1, 14, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
cv.Version(1, 14, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.14.0",
),
"recommended": (
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
cv.Version(1, 14, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.14.0",
),
}
@@ -516,6 +517,13 @@ async def component_to_code(config):
if config[CONF_FAMILY] == FAMILY_RTL8710B:
cg.add_platformio_option("custom_versions.freertos", "8.2.3")
# LibreTiny bundles BDK 3.0.33 for these two but already builds BK7238 with 3.0.78,
# where STA+AP works (esphome#19430), the web server OTA does not trip the watchdog
# (esphome#19429) and the no WiFi reboot loop is gone (esphome#18268). A
# platformio_options custom_versions.beken-bdk entry in the config still wins.
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7231T):
cg.add_platformio_option("custom_versions.beken-bdk", "3.0.78")
# force using arduino framework
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
@@ -533,8 +541,10 @@ async def component_to_code(config):
# it for project source files only. GCC uses the last -O flag.
build_src_flags += " -Os"
cg.add_platformio_option("build_src_flags", build_src_flags)
# Must run before the platform's builder scripts are loaded; see the script.
cg.add_platformio_option("extra_scripts", ["pre:scons_dont_inherit.py"])
# LibreTiny before 1.14.0 breaks under SCons 4.11 without this script; see the script
version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if (version.major, version.minor, version.patch) < (1, 14, 0):
cg.add_platformio_option("extra_scripts", ["pre:scons_dont_inherit.py"])
cg.add_platformio_option("extra_scripts", ["pre:ccache.py"])
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
# On other families, patch_linker.py routes .sram.text into the right
@@ -42,11 +42,14 @@ from esphome.components.libretiny.const import (
LibreTinyComponent,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
{IMPORTS}
CODEOWNERS = {CODEOWNERS}
AUTO_LOAD = ["libretiny"]
DOMAIN = "{COMPONENT_LOWER}"
IS_TARGET_PLATFORM = True
COMPONENT_DATA = LibreTinyComponent(
@@ -59,7 +62,7 @@ COMPONENT_DATA = LibreTinyComponent(
)
def _set_core_data(config):
def _set_core_data(config: ConfigType) -> ConfigType:
CORE.data[KEY_LIBRETINY] = {}
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
return config
@@ -76,12 +79,12 @@ PIN_SCHEMA = {PIN_SCHEMA}
CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
async def to_code(config: ConfigType) -> MockObj:
return await libretiny.component_to_code(config)
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
async def pin_to_code(config):
async def pin_to_code(config: ConfigType) -> MockObj:
return await libretiny.gpio.component_pin_to_code(config)
+68 -20
View File
@@ -4,6 +4,11 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
// clang-tidy cannot parse the Realtek SDK headers behind wiring_data.h
#if defined(USE_BK72XX) || (defined(USE_RTL87XX) && !defined(CLANG_TIDY))
#include <wiring_data.h>
#endif
namespace esphome::libretiny {
ESPHOME_LOG_TAG(TAG, "libretiny.gpio");
@@ -26,16 +31,10 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags) {
}
}
struct ISRPinArg {
uint8_t pin;
bool inverted;
};
// The ISR copy is the pin itself, so both share the stored mode and level. Pins are never freed.
ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_;
arg->inverted = inverted_;
return ISRInternalGPIOPin((void *) arg);
return ISRInternalGPIOPin(
const_cast<ArduinoInternalGPIOPin *>(this)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
}
void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
@@ -61,8 +60,59 @@ void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg,
attachInterruptParam(pin_, func, arduino_mode, arg);
}
#ifdef USE_BK72XX
// BDK GPIO config register (REG_GPIO_0_CONFIG, GCFG_OUTPUT_BIT): 0 enables the output, bit 1 is the output level
static constexpr uintptr_t BK_GPIO_CFG_BASE = 0x00802800;
static constexpr uint32_t BK_GPIO_OUTPUT_BIT = 1 << 1;
// LibreTiny's switch to output always starts the pin LOW. Enable the output at its level in one store, then update
// LibreTiny's pin state the way its own switch does (cores/beken-72xx/arduino/src/wiring_digital.c).
static bool bk_enter_output(uint8_t pin, bool level) {
PinInfo *info = pinInfo(pin);
if (info == nullptr || info->gpio > 31)
return false;
PinData *data = pinData(info);
if (pinEnabled(info, PIN_GPIO) && data->gpioMode == OUTPUT)
return false;
pinRemoveMode(info, PIN_PWM);
auto *cfg =
reinterpret_cast<volatile uint32_t *>(BK_GPIO_CFG_BASE + info->gpio * 4); // NOLINT(performance-no-int-to-ptr)
*cfg = level ? BK_GPIO_OUTPUT_BIT : 0;
pinEnable(info, PIN_GPIO);
data->gpioMode = OUTPUT;
return true;
}
#endif
#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)
// The switch to output keeps the HAL data register, so write the level to it first
static void rtl_preset_level(uint8_t pin, bool level) {
PinInfo *info = pinInfo(pin);
if (info == nullptr)
return;
if (info->data == nullptr || info->data->gpio == nullptr)
pinMode(pin, INPUT); // NOLINT - LibreTiny creates the gpio_t on first use
if (info->data != nullptr && info->data->gpio != nullptr)
gpio_write(info->data->gpio, level);
}
#endif
void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) {
pinMode(pin_, flags_to_mode(flags)); // NOLINT
const int mode = flags_to_mode(flags);
this->output_ = flags & gpio::FLAG_OUTPUT;
#ifdef USE_BK72XX
if (mode == OUTPUT && bk_enter_output(this->pin_, this->level_))
return;
#endif
#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)
// The preset avoids a LOW blip when going HIGH, but a LOW only takes after the switch (measured on a BW15),
// so the write below is still needed
if (mode == OUTPUT)
rtl_preset_level(this->pin_, this->level_);
#endif
pinMode(this->pin_, mode); // NOLINT
if (this->output_)
digitalWrite(this->pin_, this->level_); // NOLINT
}
size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const {
@@ -70,10 +120,12 @@ size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const {
}
bool ArduinoInternalGPIOPin::digital_read() {
return bool(digitalRead(pin_)) ^ inverted_; // NOLINT
return bool(digitalRead(this->pin_)) ^ this->inverted_; // NOLINT
}
void ArduinoInternalGPIOPin::digital_write(bool value) {
digitalWrite(pin_, value ^ inverted_); // NOLINT
this->level_ = value ^ this->inverted_;
if (this->output_)
digitalWrite(this->pin_, this->level_); // NOLINT
}
void ArduinoInternalGPIOPin::detach_interrupt() const {
detachInterrupt(pin_); // NOLINT
@@ -86,20 +138,16 @@ namespace esphome {
using namespace libretiny;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
return bool(digitalRead(arg->pin)) ^ arg->inverted; // NOLINT
return reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->digital_read();
}
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
digitalWrite(arg->pin, value ^ arg->inverted); // NOLINT
reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->digital_write(value);
}
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
detachInterrupt(arg->pin);
detachInterrupt(reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->get_pin());
}
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
pinMode(arg->pin, flags_to_mode(flags)); // NOLINT
reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->pin_mode(flags);
}
} // namespace esphome
+6 -1
View File
@@ -30,7 +30,12 @@ class ArduinoInternalGPIOPin final : public GPIOPin {
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
uint8_t pin_;
bool inverted_{};
bool inverted_ : 1 {};
// LibreTiny drops writes to a pin that is not an output; keep the level until it becomes one.
// Remove with bk_enter_output() and rtl_preset_level() once LibreTiny handles this.
// These bits share a byte with the ISR copy, so a pin must not be written from an ISR and the loop at once.
bool output_ : 1 {};
bool level_ : 1 {};
gpio::Flags flags_{};
};
+15 -11
View File
@@ -355,11 +355,12 @@ LN882X_BOARD_PINS = {
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 9,
"WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 20,
"WIRE0_SCL_10": 21,
"WIRE0_SCL_11": 22,
"WIRE0_SCL_12": 25,
"WIRE0_SCL_8": 10,
"WIRE0_SCL_9": 11,
"WIRE0_SCL_10": 20,
"WIRE0_SCL_11": 21,
"WIRE0_SCL_12": 22,
"WIRE0_SCL_13": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
@@ -368,11 +369,12 @@ LN882X_BOARD_PINS = {
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 9,
"WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 20,
"WIRE0_SDA_10": 21,
"WIRE0_SDA_11": 22,
"WIRE0_SDA_12": 25,
"WIRE0_SDA_8": 10,
"WIRE0_SDA_9": 11,
"WIRE0_SDA_10": 20,
"WIRE0_SDA_11": 21,
"WIRE0_SDA_12": 22,
"WIRE0_SDA_13": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_TX": 25,
@@ -397,6 +399,7 @@ LN882X_BOARD_PINS = {
"PA6": 6,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PB04": 20,
"PB4": 20,
@@ -421,7 +424,8 @@ LN882X_BOARD_PINS = {
"D9": 22,
"D10": 3,
"D11": 2,
"D12": 11,
"D12": 10,
"D13": 11,
"A0": 0,
"A1": 1,
"A2": 4,
+25 -44
View File
@@ -1,6 +1,7 @@
#include "ltr390.h"
#include <bitset>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ltr390 {
@@ -16,28 +17,16 @@ static const uint8_t LTR390_GAIN = 0x05;
static const uint8_t LTR390_PART_ID = 0x06;
static const uint8_t LTR390_MAIN_STATUS = 0x07;
static const float GAINVALUES[5] = {1.0, 3.0, 6.0, 9.0, 18.0};
static const float RESOLUTIONVALUE[6] = {4.0, 2.0, 1.0, 0.5, 0.25, 0.125};
static const uint8_t RESOLUTION_BITS[6] = {20, 19, 18, 17, 16, 13};
static constexpr uint8_t LTR390_ALS_DATA = 0x0D;
static constexpr uint8_t LTR390_UVS_DATA = 0x10;
// Request fastest measurement rate - will be slowed by device if conversion rate is slower.
static const float RESOLUTION_SETTING[6] = {0x00, 0x10, 0x20, 0x30, 0x40, 0x50};
static const uint32_t MODEADDRESSES[2] = {0x0D, 0x10};
static constexpr uint8_t GAINVALUES[5] PROGMEM = {1, 3, 6, 9, 18};
static uint8_t gain_value(LTR390GAIN gain) { return progmem_read_byte(&GAINVALUES[gain]); }
static uint8_t resolution_bits(LTR390RESOLUTION res) { return res == LTR390_RESOLUTION_13BIT ? 13 : 20 - res; }
static const float SENSITIVITY_MAX = 2300;
static const float INTG_MAX = RESOLUTIONVALUE[0] * 100;
static const int GAIN_MAX = GAINVALUES[4];
uint32_t little_endian_bytes_to_int(const uint8_t *buffer, uint8_t num_bytes) {
uint32_t value = 0;
for (int i = 0; i < num_bytes; i++) {
value <<= 8;
value |= buffer[num_bytes - i - 1];
}
return value;
}
// UV counts per UVI at 18x gain and 20-bit resolution
static constexpr float SENSITIVITY_MAX = 2300;
static constexpr float GAIN_MAX = 18;
optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
const uint8_t num_bytes = 3;
@@ -60,12 +49,12 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
delay(1);
}
if (!this->read_bytes(MODEADDRESSES[mode], buffer, num_bytes)) {
if (!this->read_bytes(mode == LTR390_MODE_UVS ? LTR390_UVS_DATA : LTR390_ALS_DATA, buffer, num_bytes)) {
ESP_LOGW(TAG, "Reading data from sensor failed!");
return {};
}
return little_endian_bytes_to_int(buffer, num_bytes);
return encode_uint24(buffer[2], buffer[1], buffer[0]);
}
void LTR390Component::read_als_() {
@@ -75,7 +64,8 @@ void LTR390Component::read_als_() {
uint32_t als = *val;
if (this->light_sensor_ != nullptr) {
float lux = ((0.6f * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
// The integration time halves per resolution step, from 400 ms at 20-bit
float lux = ((0.6f * als * (1 << this->res_als_)) / (4.0f * gain_value(this->gain_als_))) * this->wfac_;
this->light_sensor_->publish_state(lux);
}
@@ -92,9 +82,7 @@ void LTR390Component::read_uvs_() {
if (this->uvi_sensor_ != nullptr) {
// Set sensitivity by linearly scaling against known value in the datasheet
float gain_scale_uv = GAINVALUES[this->gain_uv_] / GAIN_MAX;
float intg_scale_uv = (RESOLUTIONVALUE[this->res_uv_] * 100) / INTG_MAX;
float sensitivity_uv = SENSITIVITY_MAX * gain_scale_uv * intg_scale_uv;
float sensitivity_uv = SENSITIVITY_MAX * gain_value(this->gain_uv_) / (GAIN_MAX * (1 << this->res_uv_));
this->uvi_sensor_->publish_state((uv / sensitivity_uv) * this->wfac_);
}
@@ -118,20 +106,13 @@ void LTR390Component::read_mode_(LTR390MODE mode) {
ctrl[LTR390_CTRL_EN] = true;
this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong();
uint32_t int_time{0};
// Set gain, resolution and measurement rate
switch (mode) {
case LTR390_MODE_ALS:
this->reg(LTR390_GAIN) = this->gain_als_;
this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_als_];
int_time = ((uint32_t) RESOLUTIONVALUE[this->res_als_]) * 100;
break;
case LTR390_MODE_UVS:
this->reg(LTR390_GAIN) = this->gain_uv_;
this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_uv_];
int_time = ((uint32_t) RESOLUTIONVALUE[this->res_uv_]) * 100;
break;
}
const bool uv = mode == LTR390_MODE_UVS;
const uint8_t res = uv ? this->res_uv_ : this->res_als_;
this->reg(LTR390_GAIN) = uv ? this->gain_uv_ : this->gain_als_;
// Resolution in bits 6:4, rate 0 (fastest); the device slows it to the conversion time
this->reg(LTR390_MEAS_RATE) = res << 4;
// Conversion time: 400 ms at 20-bit, halved per step (12 ms at 13-bit)
const uint32_t int_time = 400u >> res;
// After the sensor integration time do the following
this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode]() {
@@ -186,12 +167,12 @@ void LTR390Component::setup() {
void LTR390Component::dump_config() {
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG,
" ALS Gain: X%.0f\n"
" ALS Gain: X%u\n"
" ALS Resolution: %u-bit\n"
" UV Gain: X%.0f\n"
" UV Gain: X%u\n"
" UV Resolution: %u-bit",
GAINVALUES[this->gain_als_], RESOLUTION_BITS[this->res_als_], GAINVALUES[this->gain_uv_],
RESOLUTION_BITS[this->res_uv_]);
gain_value(this->gain_als_), resolution_bits(this->res_als_), gain_value(this->gain_uv_),
resolution_bits(this->res_uv_));
}
void LTR390Component::update() {
+13 -12
View File
@@ -15,13 +15,13 @@ enum LTR390CTRL {
// enums from https://github.com/adafruit/Adafruit_LTR390/
// Sensing modes
enum LTR390MODE {
enum LTR390MODE : uint8_t {
LTR390_MODE_ALS,
LTR390_MODE_UVS,
};
// Sensor gain levels
enum LTR390GAIN {
enum LTR390GAIN : uint8_t {
LTR390_GAIN_1 = 0,
LTR390_GAIN_3, // Default
LTR390_GAIN_6,
@@ -30,7 +30,7 @@ enum LTR390GAIN {
};
// Sensor resolution
enum LTR390RESOLUTION {
enum LTR390RESOLUTION : uint8_t {
LTR390_RESOLUTION_20BIT,
LTR390_RESOLUTION_19BIT,
LTR390_RESOLUTION_18BIT, // Default
@@ -68,20 +68,21 @@ class LTR390Component final : public PollingComponent, public i2c::I2CDevice {
void read_mode_(LTR390MODE mode);
void standby_();
bool reading_{false};
uint8_t enabled_modes_{0};
LTR390GAIN gain_als_;
LTR390GAIN gain_uv_;
LTR390RESOLUTION res_als_;
LTR390RESOLUTION res_uv_;
float wfac_;
sensor::Sensor *light_sensor_{nullptr};
sensor::Sensor *als_sensor_{nullptr};
sensor::Sensor *uvi_sensor_{nullptr};
sensor::Sensor *uv_sensor_{nullptr};
float wfac_;
LTR390GAIN gain_als_;
LTR390GAIN gain_uv_;
LTR390RESOLUTION res_als_;
LTR390RESOLUTION res_uv_;
bool reading_{false};
uint8_t enabled_modes_{0};
};
} // namespace esphome::ltr390
+30 -44
View File
@@ -1,3 +1,6 @@
from collections.abc import Callable
from typing import Any
import esphome.codegen as cg
from esphome.components import i2c, sensor
from esphome.components.const import UNIT_COUNTS
@@ -49,6 +52,20 @@ RES_OPTIONS = {
13: LTR390RESOLUTION.LTR390_RESOLUTION_13BIT,
}
def _per_mode(validator: Callable[[Any], Any]) -> Callable[[Any], ConfigType]:
"""One value for both modes, or one each; always returns the per-mode dict."""
return cv.Any(
cv.All(validator, lambda value: {CONF_AMBIENT_LIGHT: value, CONF_UV: value}),
cv.Schema(
{
cv.Required(CONF_AMBIENT_LIGHT): validator,
cv.Required(CONF_UV): validator,
}
),
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -81,24 +98,8 @@ CONFIG_SCHEMA = cv.All(
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_GAIN, default="X18"): cv.Any(
cv.enum(GAIN_OPTIONS),
cv.Schema(
{
cv.Required(CONF_AMBIENT_LIGHT): cv.enum(GAIN_OPTIONS),
cv.Required(CONF_UV): cv.enum(GAIN_OPTIONS),
}
),
),
cv.Optional(CONF_RESOLUTION, default=20): cv.Any(
cv.enum(RES_OPTIONS),
cv.Schema(
{
cv.Required(CONF_AMBIENT_LIGHT): cv.enum(RES_OPTIONS),
cv.Required(CONF_UV): cv.enum(RES_OPTIONS),
}
),
),
cv.Optional(CONF_GAIN, default="X18"): _per_mode(cv.enum(GAIN_OPTIONS)),
cv.Optional(CONF_RESOLUTION, default=20): _per_mode(cv.enum(RES_OPTIONS)),
cv.Optional(CONF_WINDOW_CORRECTION_FACTOR, default=1.0): cv.float_range(
min=1.0
),
@@ -109,36 +110,21 @@ CONFIG_SCHEMA = cv.All(
cv.has_at_least_one_key(CONF_LIGHT, CONF_AMBIENT_LIGHT, CONF_UV_INDEX, CONF_UV),
)
TYPES = {
CONF_LIGHT: "set_light_sensor",
CONF_AMBIENT_LIGHT: "set_als_sensor",
CONF_UV_INDEX: "set_uvi_sensor",
CONF_UV: "set_uv_sensor",
}
async def to_code(config: ConfigType) -> None:
var = await i2c.new_i2c_component(config)
cg.add(var.set_wfac_value(config[CONF_WINDOW_CORRECTION_FACTOR]))
for key, funcName in TYPES.items():
if key in config:
sens = await sensor.new_sensor(config[key])
cg.add(getattr(var, funcName)(sens))
sensors = sensor.sub_sensors(config)
await sensors(CONF_LIGHT, var.set_light_sensor)
await sensors(CONF_AMBIENT_LIGHT, var.set_als_sensor)
await sensors(CONF_UV_INDEX, var.set_uvi_sensor)
await sensors(CONF_UV, var.set_uv_sensor)
gain_value = config[CONF_GAIN]
if isinstance(gain_value, dict):
cg.add(var.set_als_gain_value(gain_value[CONF_AMBIENT_LIGHT]))
cg.add(var.set_uv_gain_value(gain_value[CONF_UV]))
else:
cg.add(var.set_als_gain_value(gain_value))
cg.add(var.set_uv_gain_value(gain_value))
res_value = config[CONF_RESOLUTION]
if isinstance(res_value, dict):
cg.add(var.set_als_res_value(res_value[CONF_AMBIENT_LIGHT]))
cg.add(var.set_uv_res_value(res_value[CONF_UV]))
else:
cg.add(var.set_als_res_value(res_value))
cg.add(var.set_uv_res_value(res_value))
gain = config[CONF_GAIN]
cg.add(var.set_als_gain_value(gain[CONF_AMBIENT_LIGHT]))
cg.add(var.set_uv_gain_value(gain[CONF_UV]))
res = config[CONF_RESOLUTION]
cg.add(var.set_als_res_value(res[CONF_AMBIENT_LIGHT]))
cg.add(var.set_uv_res_value(res[CONF_UV]))
+1 -1
View File
@@ -13,7 +13,7 @@ class LVGLSwitch final : public switch_::Switch, public Component {
public:
LVGLSwitch(std::function<void(bool)> state_lambda) : state_lambda_(std::move(state_lambda)) {}
void setup() override { this->write_state(this->get_initial_state_with_restore_mode().value_or(false)); }
void setup() override { this->restore(false); }
protected:
void write_state(bool value) override { this->state_lambda_(value); }
@@ -12,12 +12,8 @@ PROGMEM_STRING_TABLE(FieldStatusStrings, "OK", "TOO_STRONG", "TOO_WEAK", "UNKNOW
void MT6701SPIFieldStatus::update() {
if (!this->parent_->read_encoder())
return;
char text[FieldStatusStrings::MAX_LENGTH + 1];
ESPHOME_strncpy_P(text,
reinterpret_cast<ESPHOME_PGM_P>(FieldStatusStrings::get_progmem_str(
this->parent_->get_status() & 0x03, FieldStatusStrings::LAST_INDEX)),
sizeof(text));
this->publish_state(text);
this->publish_state(
FieldStatusStrings::get_progmem_str(this->parent_->get_status() & 0x03, FieldStatusStrings::LAST_INDEX));
}
void MT6701SPIFieldStatus::dump_config() {
@@ -15,7 +15,7 @@ void OpenthermSwitch::setup() {
ESP_LOGD(TAG, "Restored state for OpenTherm switch '%s': %d", LOG_STR_ARG(this->get_log_name()), restored.value());
state = restored.value();
}
this->write_state(state);
this->control(state);
}
void OpenthermSwitch::dump_config() {
+38 -1
View File
@@ -15,7 +15,9 @@ from esphome.const import (
CONF_MIN_POWER,
CONF_POWER_SUPPLY,
)
from esphome.core import CORE
from esphome.core import CORE, ID
from esphome.core.entity_helpers import ApplyEntity, SubEntities
from esphome.cpp_generator import MockObj, SafeExpType
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -57,6 +59,8 @@ BinaryOutput = output_ns.class_("BinaryOutput")
BinaryOutputPtr = BinaryOutput.operator("ptr")
FloatOutput = output_ns.class_("FloatOutput", BinaryOutput)
FloatOutputPtr = FloatOutput.operator("ptr")
ApplyBinaryOutput = output_ns.class_("ApplyBinaryOutput", BinaryOutput)
ApplyFloatOutput = output_ns.class_("ApplyFloatOutput", FloatOutput)
async def setup_output_platform_(obj, config):
@@ -87,6 +91,39 @@ async def register_output(var, config):
await setup_output_platform_(var, config)
async def new_output(config: ConfigType, *args: SafeExpType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_output(var, config)
return var
BINARY_APPLY_ENTITY = ApplyEntity(
ApplyBinaryOutput,
((BinaryOutputPtr, "entity"), (cg.uint16, "index"), (cg.bool_, "state")),
)
FLOAT_APPLY_ENTITY = ApplyEntity(
ApplyFloatOutput,
((FloatOutputPtr, "entity"), (cg.uint16, "index"), (cg.float_, "state")),
)
def sub_binary_outputs(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_output; ``apply`` can use ``entity``, ``index`` and ``state``.
``index`` is the constructor argument (default 0), so a hub's channels share one class.
"""
return SubEntities(new_output, config, parent, BINARY_APPLY_ENTITY)
def sub_float_outputs(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Like sub_binary_outputs; ``apply`` gets the scaled level as ``state``."""
return SubEntities(new_output, config, parent, FLOAT_APPLY_ENTITY)
BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(BinaryOutput),
+12
View File
@@ -68,4 +68,16 @@ class BinaryOutput {
#endif
};
/// Calls the codegen-generated Fn(this, index, state) on a state write; the hub is baked into Fn.
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
template<auto Fn> class ApplyBinaryOutput final : public BinaryOutput {
public:
explicit ApplyBinaryOutput(uint16_t index = 0) : index_(index) {}
protected:
void write_state(bool state) override { Fn(this, this->index_, state); }
uint16_t index_; // Fits in the base's tail padding
};
} // namespace esphome::output
+12
View File
@@ -130,4 +130,16 @@ class FloatOutput : public BinaryOutput {
#endif
};
/// Calls the codegen-generated Fn(this, index, state) with the scaled, inverted level; the hub is baked into Fn.
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
template<auto Fn> class ApplyFloatOutput final : public FloatOutput {
public:
explicit ApplyFloatOutput(uint16_t index = 0) : index_(index) {}
protected:
void write_state(float state) override { Fn(this, this->index_, state); }
uint16_t index_; // Fits in the base's tail padding
};
} // namespace esphome::output
@@ -58,7 +58,7 @@ optional<ParseResult> PVVXMiThermometer::parse_header_(const ble_device_base::Se
return {};
}
auto raw = service_data.data;
const auto &raw = service_data.data;
if (raw.size() < 14) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
+43 -3
View File
@@ -1,4 +1,6 @@
from collections.abc import Callable
import dataclasses
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@@ -132,9 +134,22 @@ def add_listener(receiver: MockObj, listener: MockObj) -> None:
cg.add(receiver.register_listener(listener))
@dataclass
class _RemoteBaseData:
# receiver id -> (receiver, its dumpers in registration order)
tables: dict[str, tuple[MockObj, list[MockObj]]] = dataclasses.field(
default_factory=dict
)
# dumper type -> its one flash object, shared by every receiver that lists it
flash_dumpers: dict[str, str] = dataclasses.field(default_factory=dict)
def _get_data() -> _RemoteBaseData:
return CORE.data.setdefault(DOMAIN, _RemoteBaseData())
def _get_dumper_tables() -> dict[str, tuple[MockObj, list[MockObj]]]:
"""Receiver id -> (receiver, its dumpers in registration order)."""
return CORE.data.setdefault(DOMAIN, {})
return _get_data().tables
@coroutine_with_priority(CoroPriority.FINAL)
@@ -255,7 +270,32 @@ def register_trigger(name, type, data_type):
return decorator
def _new_flash_dumper(dumper_id: ID) -> cg.RawExpression:
"""Place a stateless dumper in flash; the receiver only ever calls it."""
flash_dumpers = _get_data().flash_dumpers
if (storage := flash_dumpers.get(str(dumper_id.type))) is None:
# A generated name a config id cannot take; registered so later globals avoid it too
storage_id = cg.unique_global_id(f"{dumper_id.id}__flash", dumper_id.type)
storage = flash_dumpers[str(dumper_id.type)] = storage_id.id
CORE.register_variable(storage_id, cg.MockObj(storage))
CORE.add_global_after_includes(
cg.RawStatement(
f"static_assert(sizeof({dumper_id.type}) == sizeof(void *) && "
f"std::is_trivially_destructible_v<{dumper_id.type}>, "
'"a dumper kept in flash must have no data members or destructor");'
)
)
CORE.add_global_after_includes(
cg.RawStatement(f"static constexpr {dumper_id.type} {storage} PROGMEM{{}};")
)
# A constant address for the receiver's flash table; lambdas get a pointer-like object
address = f"const_cast<{dumper_id.type} *>(&{storage})"
CORE.register_variable(dumper_id, cg.MockObj(address, "->"))
return cg.RawExpression(address)
def register_dumper(name, type, schema=None):
"""Dumpers are stateless, so each is a flash object; one with data members fails the build."""
if schema is None:
schema = {}
registerer = DUMPER_REGISTRY.register(name, type, schema)
@@ -263,7 +303,7 @@ def register_dumper(name, type, schema=None):
def decorator(func):
async def new_func(config, dumper_id):
_request_protocol_if_in_tree(name)
var = cg.new_Pvariable(dumper_id)
var = _new_flash_dumper(dumper_id)
await coroutine(func)(var, config)
return var
@@ -7,108 +7,35 @@ ESPHOME_LOG_TAG(TAG, "remote.dooya");
static constexpr uint32_t HEADER_HIGH_US = 5000;
static constexpr uint32_t HEADER_LOW_US = 1500;
static constexpr uint32_t BIT_ZERO_HIGH_US = 350;
static constexpr uint32_t BIT_ZERO_LOW_US = 750;
static constexpr uint32_t BIT_ONE_HIGH_US = 750;
static constexpr uint32_t BIT_ONE_LOW_US = 350;
static constexpr uint32_t SHORT_US = 350;
static constexpr uint32_t LONG_US = 750;
static constexpr BitTimings BIT_TIMINGS{.one_space = SHORT_US,
.zero_space = LONG_US,
.bit_order = BitOrder::BIT_ORDER_MSB_FIRST,
.one_mark = LONG_US,
.zero_mark = SHORT_US};
void DooyaProtocol::encode(RemoteTransmitData *dst, const DooyaData &data) {
dst->set_carrier_frequency(0);
dst->reserve(2 + 40 * 2u);
dst->item(HEADER_HIGH_US, HEADER_LOW_US);
for (uint32_t mask = 1UL << (23); mask != 0; mask >>= 1) {
if (data.id & mask) {
dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US);
} else {
dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US);
}
}
for (uint32_t mask = 1UL << (7); mask != 0; mask >>= 1) {
if (data.channel & mask) {
dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US);
} else {
dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US);
}
}
for (uint32_t mask = 1UL << (3); mask != 0; mask >>= 1) {
if (data.button & mask) {
dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US);
} else {
dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US);
}
}
for (uint32_t mask = 1UL << (3); mask != 0; mask >>= 1) {
if (data.check & mask) {
dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US);
} else {
dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US);
}
}
// 24 bit id, 8 bit channel, 4 bit button, 4 bit check
const uint8_t bytes[5] = {static_cast<uint8_t>(data.id >> 16), static_cast<uint8_t>(data.id >> 8),
static_cast<uint8_t>(data.id), data.channel,
static_cast<uint8_t>((data.button & 0x0F) << 4 | (data.check & 0x0F))};
dst->item_bytes<BIT_TIMINGS>(bytes, sizeof(bytes));
}
optional<DooyaData> DooyaProtocol::decode(RemoteReceiveData src) {
DooyaData out{
.id = 0,
.channel = 0,
.button = 0,
.check = 0,
uint8_t bytes[5];
if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US) || !src.expect_bytes_last_mark<BIT_TIMINGS>(bytes, sizeof(bytes)))
return {};
return DooyaData{
.id = encode_uint24(bytes[0], bytes[1], bytes[2]),
.channel = bytes[3],
.button = static_cast<uint8_t>(bytes[4] >> 4),
.check = static_cast<uint8_t>(bytes[4] & 0x0F),
};
if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US))
return {};
for (uint8_t i = 0; i < 24; i++) {
if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) {
out.id = (out.id << 1) | 1;
} else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) {
out.id = (out.id << 1) | 0;
} else {
return {};
}
}
for (uint8_t i = 0; i < 8; i++) {
if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) {
out.channel = (out.channel << 1) | 1;
} else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) {
out.channel = (out.channel << 1) | 0;
} else {
return {};
}
}
for (uint8_t i = 0; i < 4; i++) {
if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) {
out.button = (out.button << 1) | 1;
} else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) {
out.button = (out.button << 1) | 0;
} else {
return {};
}
}
for (uint8_t i = 0; i < 3; i++) {
if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) {
out.check = (out.check << 1) | 1;
} else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) {
out.check = (out.check << 1) | 0;
} else {
return {};
}
}
// Last bit is not received properly but can be decoded
if (src.expect_mark(BIT_ONE_HIGH_US)) {
out.check = (out.check << 1) | 1;
} else if (src.expect_mark(BIT_ZERO_HIGH_US)) {
out.check = (out.check << 1) | 0;
} else {
return {};
}
return out;
}
void DooyaProtocol::dump(const DooyaData &data) {
ESP_LOGI(TAG, "Received Dooya: id=0x%08" PRIX32 ", channel=%d, button=%d, check=%d", data.id, data.channel,
@@ -16,6 +16,11 @@ static constexpr uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT;
static constexpr uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT;
static constexpr uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT;
static constexpr uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT;
static constexpr BitTimings MAGIQUEST_BIT_TIMINGS{.one_space = MAGIQUEST_ONE_SPACE,
.zero_space = MAGIQUEST_ZERO_SPACE,
.bit_order = BitOrder::BIT_ORDER_MSB_FIRST,
.one_mark = MAGIQUEST_ONE_MARK,
.zero_mark = MAGIQUEST_ZERO_MARK};
static bool checksum_is_valid(uint32_t wand_id, uint32_t magnitude_and_checksum) {
// The frame's six payload bytes, which start at the wand id's top bit, must sum to zero.
@@ -35,21 +40,10 @@ void MagiQuestProtocol::encode(RemoteTransmitData *dst, const MagiQuestData &dat
// 2 start bits
dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE);
dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE);
for (uint32_t mask = 1 << 31; mask; mask >>= 1) {
if (data.wand_id & mask) {
dst->item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE);
} else {
dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE);
}
}
for (uint16_t mask = 1 << 15; mask; mask >>= 1) {
if (data.magnitude & mask) {
dst->item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE);
} else {
dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE);
}
}
const uint8_t bytes[6] = {static_cast<uint8_t>(data.wand_id >> 24), static_cast<uint8_t>(data.wand_id >> 16),
static_cast<uint8_t>(data.wand_id >> 8), static_cast<uint8_t>(data.wand_id),
static_cast<uint8_t>(data.magnitude >> 8), static_cast<uint8_t>(data.magnitude)};
dst->item_bytes<MAGIQUEST_BIT_TIMINGS>(bytes, sizeof(bytes));
dst->mark(MAGIQUEST_UNIT);
}
@@ -10,18 +10,16 @@ static constexpr uint32_t BIT_ONE_HIGH_US = 3000;
static constexpr uint32_t BIT_ONE_LOW_US = 1000;
static constexpr uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US;
static constexpr uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US;
static constexpr BitTimings BIT_TIMINGS{.one_space = BIT_ONE_LOW_US,
.zero_space = BIT_ZERO_LOW_US,
.bit_order = BitOrder::BIT_ORDER_MSB_FIRST,
.one_mark = BIT_ONE_HIGH_US,
.zero_mark = BIT_ZERO_HIGH_US};
void RoombaProtocol::encode(RemoteTransmitData *dst, const RoombaData &data) {
dst->set_carrier_frequency(38000);
dst->reserve(NBITS * 2u);
for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) {
if (data.data & mask) {
dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US);
} else {
dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US);
}
}
dst->item_bytes<BIT_TIMINGS>(&data.data, 1);
}
optional<RoombaData> RoombaProtocol::decode(RemoteReceiveData src) {
RoombaData out{.data = 0};
@@ -2,6 +2,8 @@ from esphome.components import button
from esphome.const import DEVICE_CLASS_RESTART, ENTITY_CATEGORY_CONFIG, ICON_RESTART
from esphome.types import ConfigType
AUTO_LOAD = ["restart"]
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton,
icon=ICON_RESTART,
@@ -1,11 +1,11 @@
#include "restart_button.h"
#include "restart.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::restart {
ESPHOME_LOG_TAG(TAG, "restart.button");
ESPHOME_LOG_TAG(TAG, "restart");
void restart_device() {
ESP_LOGI(TAG, "Restarting device");
@@ -2,7 +2,7 @@
namespace esphome::restart {
/// Run by the restart button.
/// Run by the restart button and switch.
void restart_device();
} // namespace esphome::restart
@@ -1,20 +1,18 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART
from esphome.types import ConfigType
restart_ns = cg.esphome_ns.namespace("restart")
RestartSwitch = restart_ns.class_("RestartSwitch", switch.Switch, cg.Component)
AUTO_LOAD = ["restart"]
CONFIG_SCHEMA = switch.switch_schema(
RestartSwitch,
switch.ApplySwitch,
icon=ICON_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
block_inverted=True,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await switch.sub_switches(config).create(
config, None, apply="restart::restart_device()", publish="momentary"
)
@@ -1,23 +0,0 @@
#include "restart_switch.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::restart {
ESPHOME_LOG_TAG(TAG, "restart");
void RestartSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Restarting device");
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
}
void RestartSwitch::dump_config() { LOG_SWITCH("", "Restart Switch", this); }
} // namespace esphome::restart
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
namespace esphome::restart {
class RestartSwitch final : public switch_::Switch, public Component {
public:
// User provided, not "= default": `new(p) RestartSwitch()` would zero-fill .bss that is already zero.
RestartSwitch() {}
void dump_config() override;
protected:
void write_state(bool state) override;
};
} // namespace esphome::restart
+101
View File
@@ -39,6 +39,10 @@ RTL87XX_BOARDS = {
"name": "Generic - RTL8710BN (2M/788k)",
"family": FAMILY_RTL8710B,
},
"generic-rtl8710bx-1mb-468k": {
"name": "Generic - RTL8710BX (1M/468k)",
"family": FAMILY_RTL8710B,
},
"generic-rtl8710bx-4mb-980k": {
"name": "Generic - RTL8710BX (4M/980k)",
"family": FAMILY_RTL8710B,
@@ -595,6 +599,103 @@ RTL87XX_BOARD_PINS = {
"A0": 19,
"A1": 41,
},
"generic-rtl8710bx-1mb-468k": {
"SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22,
"SPI0_MOSI": 23,
"SPI0_SCK": 18,
"SPI1_CS": 19,
"SPI1_MISO": 22,
"SPI1_MOSI": 23,
"SPI1_SCK": 18,
"WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30,
"WIRE1_SCL": 18,
"WIRE1_SDA": 23,
"SERIAL0_CTS": 19,
"SERIAL0_RTS": 22,
"SERIAL0_RX": 18,
"SERIAL0_TX": 23,
"SERIAL2_RX": 29,
"SERIAL2_TX": 30,
"ADC1": 19,
"CS0": 19,
"CS1": 19,
"CTS0": 19,
"FCS": 6,
"FD0": 9,
"FD1": 7,
"FD2": 8,
"FD3": 11,
"FSCK": 10,
"MISO0": 22,
"MISO1": 22,
"MOSI0": 23,
"MOSI1": 23,
"PA00": 0,
"PA0": 0,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA14": 14,
"PA15": 15,
"PA18": 18,
"PA19": 19,
"PA22": 22,
"PA23": 23,
"PA29": 29,
"PA30": 30,
"PWM1": 15,
"PWM2": 0,
"PWM3": 12,
"PWM4": 30,
"PWM5": 22,
"RTS0": 22,
"RX0": 18,
"RX2": 29,
"SCK0": 18,
"SCK1": 18,
"SCL1": 18,
"SDA1": 23,
"TX0": 23,
"TX2": 30,
"D0": 0,
"D1": 5,
"D2": 6,
"D3": 7,
"D4": 8,
"D5": 9,
"D6": 10,
"D7": 11,
"D8": 12,
"D9": 14,
"D10": 15,
"D11": 18,
"D12": 19,
"D13": 22,
"D14": 23,
"D15": 29,
"D16": 30,
"A0": 19,
},
"generic-rtl8710bx-4mb-980k": {
"SPI0_CS": 19,
"SPI0_FCS": 6,
+1 -1
View File
@@ -25,7 +25,7 @@ class SafeModeComponent final : public Component {
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
bool get_safe_mode_pending();
/// Set safe mode pending and reboot; run by the safe mode button.
/// Set safe mode pending and reboot; run by the safe mode button and switch.
void restart_in_safe_mode();
void dump_config() override;
+11 -18
View File
@@ -1,29 +1,22 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_SAFE_MODE, ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT
from esphome.types import ConfigType
from .. import SafeModeComponent, safe_mode_ns
from .. import SafeModeComponent
DEPENDENCIES = ["safe_mode"]
SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component)
CONFIG_SCHEMA = (
switch.switch_schema(
SafeModeSwitch,
block_inverted=True,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
)
.extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
block_inverted=True,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
).extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await cg.add_variable(var.set_safe_mode, config[CONF_SAFE_MODE])
switches = switch.sub_switches(config, parent=config[CONF_SAFE_MODE])
await switches.create(
config, None, apply="restart_in_safe_mode()", publish="momentary"
)
@@ -1,26 +0,0 @@
#include "safe_mode_switch.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::safe_mode {
ESPHOME_LOG_TAG(TAG, "safe_mode.switch");
void SafeModeSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Restarting in safe mode");
this->safe_mode_component_->set_safe_mode_pending(true);
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
}
void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); }
} // namespace esphome::safe_mode
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/components/safe_mode/safe_mode.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
namespace esphome::safe_mode {
class SafeModeSwitch final : public switch_::Switch, public Component {
public:
void dump_config() override;
void set_safe_mode(SafeModeComponent *safe_mode_component) { this->safe_mode_component_ = safe_mode_component; }
protected:
SafeModeComponent *safe_mode_component_;
void write_state(bool state) override;
};
} // namespace esphome::safe_mode
@@ -25,7 +25,7 @@ void SeeedMultiChannelRelay::setup() {
switch_::Switch *sw = this->switches_[channel];
if (sw == nullptr)
continue;
const bool on = sw->get_initial_state_with_restore_mode().value_or(sw->is_inverted()) != sw->is_inverted();
const bool on = sw->get_initial_write_state().value_or(false);
assign_bits(value, 1 << channel, on);
}
if (!this->status_comm_result(this->write_byte(CMD_CHANNEL_CTRL, value)))
+24 -28
View File
@@ -1,6 +1,7 @@
#include "sen6x.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes>
#include <cmath>
#include <iterator>
@@ -9,6 +10,14 @@ namespace esphome::sen6x {
ESPHOME_LOG_TAG(TAG, "sen6x");
// Indexed by Sen6xType
PROGMEM_STRING_TABLE(TypeStrings, "SEN62", "SEN63C", "SEN65", "SEN66", "SEN68", "SEN69C", "UNKNOWN");
static SEN6XComponent::Sen6xType infer_type_from_product_name(const char *product_name) {
const int16_t type = progmem_string_table_find<TypeStrings>(product_name, strnlen(product_name, 32));
return type < 0 ? SEN6XComponent::UNKNOWN : static_cast<SEN6XComponent::Sen6xType>(type);
}
#ifdef SEN6X_STATUS_SENSOR_COUNT
// Bit of the status register behind each kind; the CO2 error bit is 9 on SEN66 and 12 on SEN63C/SEN69C
static uint8_t status_bit(Sen6xStatus status, SEN6XComponent::Sen6xType type) {
@@ -133,33 +142,36 @@ void SEN6XComponent::setup() {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->serial_number_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_serial_number, 16);
ESP_LOGI(TAG, "Serial number: %s", this->serial_number_.c_str());
// The last byte of serial_number_ stays 0, so a full 32-byte serial is still terminated
memcpy(this->serial_number_, SEN6XComponent::sensirion_convert_to_string_in_place(raw_serial_number, 16),
sizeof(this->serial_number_) - 1);
ESP_LOGI(TAG, "Serial number: %s", this->serial_number_);
// Step 2: Read product name in next loop iteration
this->set_timeout(0, [this]() {
uint16_t raw_product_name[16];
// One spare zero word so a full 32-byte name is still terminated
uint16_t raw_product_name[17]{};
if (!this->get_register(SEN6X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->product_name_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_product_name, 16);
const char *product_name = SEN6XComponent::sensirion_convert_to_string_in_place(raw_product_name, 16);
Sen6xType inferred_type = this->infer_type_from_product_name_(this->product_name_);
Sen6xType inferred_type = infer_type_from_product_name(product_name);
if (this->sen6x_type_ == UNKNOWN) {
this->sen6x_type_ = inferred_type;
if (inferred_type == UNKNOWN) {
ESP_LOGE(TAG, "Unknown product '%s'", this->product_name_.c_str());
ESP_LOGE(TAG, "Unknown product '%s'", product_name);
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Type inferred from product: %s", this->product_name_.c_str());
ESP_LOGD(TAG, "Type inferred from product: %s", product_name);
} else if (this->sen6x_type_ != inferred_type && inferred_type != UNKNOWN) {
ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", this->product_name_.c_str());
ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", product_name);
}
ESP_LOGI(TAG, "Product: %s", this->product_name_.c_str());
ESP_LOGI(TAG, "Product: %s", product_name);
// Validate configured sensors against detected type and disable unsupported ones
const bool has_voc_nox = (this->sen6x_type_ == SEN65 || this->sen6x_type_ == SEN66 ||
@@ -329,13 +341,13 @@ bool SEN6XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTun
void SEN6XComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"sen6x:\n"
" Product: %s\n"
" Type: %s\n"
" Serial: %s\n"
" Firmware: %u.%u\n"
" Address: 0x%02X\n"
" Startup delay: %" PRIu32 " ms",
this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_,
this->firmware_version_minor_, this->address_, this->startup_delay_ms_);
LOG_STR_ARG(TypeStrings::get_log_str(this->sen6x_type_, TypeStrings::LAST_INDEX)), this->serial_number_,
this->firmware_version_major_, this->firmware_version_minor_, this->address_, this->startup_delay_ms_);
LOG_UPDATE_INTERVAL(this);
if (this->temperature_compensation_.has_value()) {
const auto &comp = this->temperature_compensation_.value();
@@ -812,20 +824,4 @@ void SEN6XComponent::activate_sht_heater() {
this->set_command_wait_(SHT_HEATER_DELAY);
}
SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
if (product_name == "SEN62")
return SEN62;
if (product_name == "SEN63C")
return SEN63C;
if (product_name == "SEN65")
return SEN65;
if (product_name == "SEN66")
return SEN66;
if (product_name == "SEN68")
return SEN68;
if (product_name == "SEN69C")
return SEN69C;
return UNKNOWN;
}
} // namespace esphome::sen6x
+3 -5
View File
@@ -86,9 +86,9 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
void dump_config() override;
void update() override;
enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN };
enum Sen6xType : uint8_t { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN };
void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); }
void set_type(Sen6xType type) { this->sen6x_type_ = type; }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
@@ -139,7 +139,6 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
SETUP_STEP_COUNT,
};
Sen6xType infer_type_from_product_name_(const std::string &product_name);
void run_next_setup_step_();
bool write_setup_step_(SetupStep step);
void finish_setup_();
@@ -167,8 +166,7 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
void parse_and_publish_number_concentration_();
#endif
std::string product_name_;
std::string serial_number_;
char serial_number_[33]{};
#ifdef SEN6X_STATUS_SENSOR_COUNT
StaticVector<StatusSensor, SEN6X_STATUS_SENSOR_COUNT> status_sensors_;
#endif
+10 -3
View File
@@ -78,6 +78,15 @@ sen6x_ns = cg.esphome_ns.namespace("sen6x")
SEN6XComponent = sen6x_ns.class_(
"SEN6XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
)
Sen6xType = sen6x_ns.namespace("SEN6XComponent").enum("Sen6xType")
SEN6X_TYPES = {
"SEN62": Sen6xType.SEN62,
"SEN63C": Sen6xType.SEN63C,
"SEN65": Sen6xType.SEN65,
"SEN66": Sen6xType.SEN66,
"SEN68": Sen6xType.SEN68,
"SEN69C": Sen6xType.SEN69C,
}
_PMC_KEYS = (CONF_PMC_0_5, CONF_PMC_1_0, CONF_PMC_2_5, CONF_PMC_4_0, CONF_PMC_10_0)
@@ -133,9 +142,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SEN6XComponent),
cv.Optional(CONF_TYPE): cv.one_of(
"SEN62", "SEN63C", "SEN65", "SEN66", "SEN68", "SEN69C", upper=True
),
cv.Optional(CONF_TYPE): cv.enum(SEN6X_TYPES, upper=True),
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
@@ -2,6 +2,8 @@ from esphome.components import button
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_POWER
from esphome.types import ConfigType
AUTO_LOAD = ["shutdown"]
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER
)
@@ -1,4 +1,4 @@
#include "shutdown_button.h"
#include "shutdown.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
@@ -12,7 +12,7 @@
namespace esphome::shutdown {
ESPHOME_LOG_TAG(TAG, "shutdown.button");
ESPHOME_LOG_TAG(TAG, "shutdown");
void shutdown_device() {
ESP_LOGI(TAG, "Shutting down");
@@ -2,7 +2,7 @@
namespace esphome::shutdown {
/// Run by the shutdown button.
/// Run by the shutdown button and switch.
void shutdown_device();
} // namespace esphome::shutdown
@@ -1,20 +1,18 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_POWER
from esphome.types import ConfigType
shutdown_ns = cg.esphome_ns.namespace("shutdown")
ShutdownSwitch = shutdown_ns.class_("ShutdownSwitch", switch.Switch, cg.Component)
AUTO_LOAD = ["shutdown"]
CONFIG_SCHEMA = switch.switch_schema(
ShutdownSwitch,
switch.ApplySwitch,
icon=ICON_POWER,
entity_category=ENTITY_CATEGORY_CONFIG,
block_inverted=True,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await switch.sub_switches(config).create(
config, None, apply="shutdown::shutdown_device()", publish="momentary"
)
@@ -1,36 +0,0 @@
#include "shutdown_switch.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#ifdef USE_ESP32
#include <esp_sleep.h>
#endif
#ifdef USE_ESP8266
#include <Esp.h>
#endif
namespace esphome::shutdown {
ESPHOME_LOG_TAG(TAG, "shutdown.switch");
void ShutdownSwitch::dump_config() { LOG_SWITCH("", "Shutdown Switch", this); }
void ShutdownSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Shutting down");
delay(100); // NOLINT
App.safe_shutdown(TEARDOWN_TIMEOUT_REBOOT_MS);
#ifdef USE_ESP8266
ESP.deepSleep(0); // NOLINT(readability-static-accessed-through-instance)
#endif
#ifdef USE_ESP32
esp_deep_sleep_start();
#endif
}
}
} // namespace esphome::shutdown
Loaded 100 of 226 files, more files were not shown because too many files have changed in this diff. Show more