Compare commits

..
Author SHA1 Message Date
J. Nick Koston 0fdb4d44ca Merge branch 'dev' into libretiny-ota-trim-uf2 2026-10-26 04:49:36 -10:00
J. Nick Koston cf0e4f5dec Merge branch 'dev' into libretiny-ota-trim-uf2 2026-10-26 04:34:19 -10:00
J. Nick Koston b2dcc597cf Merge branch 'dev' into libretiny-ota-trim-uf2 2026-10-26 03:58:47 -10:00
J. Nick Koston c63b3ace5b Merge branch 'dev' into libretiny-ota-trim-uf2 2026-10-25 23:48:10 -10:00
J. Nick Koston 5036dc4c35 [libretiny] Leave a UF2 untrimmed when an OTA_PART_INFO is too short
uf2ota rejects a selector shorter than the three bytes of the scheme map;
reading a short zero-filled one as no device scheme would drop its image
and hide that.
2026-10-25 23:31:54 -10:00
J. Nick Koston 6865723622 [libretiny] Leave a UF2 untrimmed when its numbering or a dropped tag would mislead the device
A package whose blocks are not numbered 0..n-1 of n (a block missing or
repeated) is sent as it is, so the device still rejects it by size
instead of seeing a consistent trimmed copy. A block the device acts on
in an image that would be dropped also leaves the package untrimmed:
without its image's OTA_PART_INFO it would be written to the partition
of the image before it.
2026-10-25 23:17:31 -10:00
J. Nick Koston d285b97fd3 [libretiny] Set the --file image inside each OTA type branch
Each branch now picks its own image, --file included, instead of a second
ota_type check after them. A missing ota_image hook on an external
platform that cannot be imported only costs upload size, so it is logged
at debug like process_stacktrace.
2026-10-25 23:01:06 -10:00
J. Nick Koston c125a4424f [libretiny] Pick the OTA upload image through a platform hook
__main__ no longer checks for LibreTiny: an ota_image platform hook,
registered for bk72xx, rtl87xx and ln882x, picks the file an app update
sends, and one helper resolves it, --file included, for both upload
paths. The trim returns None when there is nothing to drop or the file
is not a well formed UF2 (malformed tags included), writes the result
atomically next to the build output, and the tests build their UF2
blocks from the format rather than from the module's internals.
2026-10-25 22:33:09 -10:00
J. Nick Koston d5bf9fd6ed [libretiny] Send only the UF2 images the device writes over OTA
A LibreTiny UF2 holds an image per OTA scheme, including the flasher
images used over UART; the device skips every block of an image whose
scheme it does not run. On single OTA chips (BK72xx, LN882H) that is
the whole flasher image, about 60% of the package. The CLI now drops
the images no device scheme writes before a native or web_server OTA
upload, keeping both slots on dual OTA chips, and renumbers the blocks
since the device takes the update size from the block count.
2026-10-25 21:54:17 -10:00
245 changed files with 3301 additions and 3331 deletions

No files matched your search

+17 -10
View File
@@ -1305,10 +1305,7 @@ def upload_program(
f"{option_string} is only supported with the esphome OTA platform; "
"the web_server OTA path can only update the firmware image."
)
binary = CORE.firmware_bin
if getattr(args, "file", None) is not None:
binary = Path(args.file)
return _upload_via_web_server(config, network_devices, binary)
return _upload_via_web_server(config, network_devices, _app_ota_image(args))
return _upload_via_native_api(config, network_devices, args)
@@ -1364,6 +1361,16 @@ def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
return next(iter(esphome_ota_items(config)), None)
def _app_ota_image(args: ArgsProtocol) -> Path:
"""The file an app update sends: ``--file`` or the build output, as the
target platform's ota_image hook picks it."""
binary = CORE.firmware_bin
if (file := getattr(args, "file", None)) is not None:
binary = Path(file)
ota_image = platform_hooks.get_platform_hook(CORE.target_platform, "ota_image")
return ota_image(binary) if ota_image is not None else binary
def _upload_via_native_api(
config: ConfigType, network_devices: list[str], args: ArgsProtocol
) -> tuple[int, str | None]:
@@ -1413,27 +1420,27 @@ def _upload_via_native_api(
f"flash a build with the option enabled, and then retry {option_string}."
)
binary = CORE.firmware_bin
ota_type = espota2.OTA_TYPE_UPDATE_APP
file = getattr(args, "file", None)
if getattr(args, "partition_table", False):
# Fail fast if the resolved ESPHome OTA config does not enable allow_partition_access.
# The device-side handshake also rejects this with "Device only supports app updates",
# but checking here surfaces the misconfiguration before opening a network connection.
check_partition_access("--partition-table")
binary = CORE.partition_table_bin
binary = Path(file) if file is not None else CORE.partition_table_bin
ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE
elif getattr(args, "bootloader", False):
check_partition_access("--bootloader")
if (
getattr(args, "file", None) is None
file is None
and (native := native_backend())
and (hint := getattr(native, "missing_image_hint", lambda: None)())
):
raise EsphomeError(hint)
binary = CORE.bootloader_bin
binary = Path(file) if file is not None else CORE.bootloader_bin
ota_type = espota2.OTA_TYPE_UPDATE_BOOTLOADER
if getattr(args, "file", None) is not None:
binary = Path(args.file)
else:
binary = _app_ota_image(args)
if ota_type == espota2.OTA_TYPE_UPDATE_PARTITION_TABLE:
_validate_partition_table_binary(binary)
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_CODE,
@@ -9,11 +10,11 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_STATE,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -43,12 +44,12 @@ AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
_ALARM_CONTROL_PANEL_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTAlarmControlPanelComponent")
mqtt.MQTTAlarmControlPanelComponent
),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}),
@@ -165,7 +166,11 @@ _CALLBACK_AUTOMATIONS = (
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
await entity_helpers.setup_entity_integrations(var, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_alarm_control_panel(var, config):
+7 -6
View File
@@ -4,13 +4,14 @@ import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI
from esphome.types import ConfigType
from .. import CONF_AT581X_ID, AT581XComponent
from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns
DEPENDENCIES = ["at581x"]
RFSwitch = at581x_ns.class_("RFSwitch", switch.Switch)
CONFIG_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
RFSwitch,
device_class=DEVICE_CLASS_SWITCH,
icon=ICON_WIFI,
).extend(
@@ -23,7 +24,7 @@ CONFIG_SCHEMA = switch.switch_schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_AT581X_ID])
await switch.sub_switches(config, parent=hub).create(
config, hub.set_rf_power_switch, apply="set_rf_mode(state)", publish="before"
)
at581x_component = await cg.get_variable(config[CONF_AT581X_ID])
s = await switch.new_switch(config)
await cg.register_parented(s, config[CONF_AT581X_ID])
cg.add(at581x_component.set_rf_power_switch(s))
@@ -0,0 +1,10 @@
#include "rf_switch.h"
namespace esphome::at581x {
void RFSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_rf_mode(state);
}
} // namespace esphome::at581x
@@ -0,0 +1,13 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../at581x.h"
namespace esphome::at581x {
class RFSwitch final : public switch_::Switch, public Parented<AT581XComponent> {
protected:
void write_state(bool state) override;
};
} // namespace esphome::at581x
+4 -2
View File
@@ -63,7 +63,6 @@ from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
lazy_load_setup,
lazy_load_validator,
mqtt_component_class,
queue_entity_register,
@@ -622,7 +621,10 @@ async def setup_binary_sensor_core_(var, config):
await entity_helpers.setup_entity_integrations(var, config)
await lazy_load_setup("zigbee", "setup_binary_sensor", var, config)
if "zigbee" in CORE.loaded_integrations:
from esphome.components import zigbee
await zigbee.setup_binary_sensor(var, config)
async def register_binary_sensor(var, config):
+4
View File
@@ -78,3 +78,7 @@ async def pin_to_code(config: ConfigType) -> MockObj:
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
# Called by the OTA upload; the shared libretiny implementation.
ota_image = libretiny.device_ota_image
+3 -6
View File
@@ -1,5 +1,4 @@
#include "bl0939.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -21,7 +20,7 @@ static const uint8_t BL0939_REG_SOFT_RESET = 0x19;
static const uint8_t BL0939_REG_USR_WRPROT = 0x1A;
static const uint8_t BL0939_REG_TPS_CTRL = 0x1B;
static constexpr uint8_t BL0939_INIT[6][6] PROGMEM = {
const uint8_t BL0939_INIT[6][6] = {
// Reset to default
{BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33},
// Enable User Operation Write
@@ -67,10 +66,8 @@ void BL0939::update() {
}
void BL0939::setup() {
uint8_t cmd[sizeof(BL0939_INIT[0])];
for (const auto *i : BL0939_INIT) {
progmem_memcpy(cmd, i, sizeof(cmd));
this->write_array(cmd, sizeof(cmd));
for (auto *i : BL0939_INIT) {
this->write_array(i, 6);
delay(1);
}
this->flush();
@@ -1,7 +1,7 @@
#include "bme680_bsec.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <string>
namespace esphome::bme680_bsec {
#ifdef USE_BSEC
@@ -9,7 +9,7 @@ ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
PROGMEM_STRING_TABLE(IaqAccuracyStrings, "Stabilizing", "Uncertain", "Calibrating", "Calibrated");
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
std::vector<BME680BSECComponent *>
BME680BSECComponent::instances; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -386,11 +386,10 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX));
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
@@ -436,9 +435,8 @@ void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, b
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value) {
if (!sensor ||
(sensor->has_state() && ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(value)) == 0)) {
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
return;
}
sensor->publish_state(value);
+1 -1
View File
@@ -77,7 +77,7 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
int64_t get_time_ns_();
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value);
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
void snapshot_state_(); // Fetch the current BSEC library state and save it in the bsec_state_data_ member (volatile
// memory)
@@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BUTTON
#include "bq25186.h"
#include "esphome/components/button/button.h"
namespace esphome::bq25186 {
/// Writes one SHIP_RST field when pressed
class BQ25186Button final : public button::Button, public Parented<BQ25186Component> {
public:
BQ25186Button(uint8_t mask, uint8_t value) : mask_(mask), value_(value) {}
protected:
void press_action() override { this->parent_->write_ship_rst(this->mask_, this->value_); }
const uint8_t mask_;
const uint8_t value_;
};
} // namespace esphome::bq25186
#endif // USE_BUTTON
+7 -4
View File
@@ -1,3 +1,4 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
@@ -9,10 +10,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
DEPENDENCIES = ["bq25186"]
BQ25186Button = bq25186_ns.class_("BQ25186Button", button.Button)
# SHIP_RST: bit 7 is the software reset, bits 6:5 the shutdown, ship and hardware reset codes
SOFTWARE_RESET_MASK = 0x80
@@ -31,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): button.button_schema(
button.ApplyButton,
BQ25186Button,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
device_class=device_class,
@@ -44,6 +46,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_BQ25186_ID])
for key, mask, value, _, _ in _BUTTONS:
await buttons(key, None, apply=f"write_ship_rst({mask:#04x}, {value:#04x})")
if (conf := config.get(key)) is not None:
var = await button.new_button(conf, mask, value)
await cg.register_parented(var, config[CONF_BQ25186_ID])
+11 -7
View File
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -9,6 +10,7 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_PRESS,
CONF_WEB_SERVER,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_IDENTIFY,
DEVICE_CLASS_RESTART,
@@ -19,12 +21,11 @@ from esphome.const import (
ICON_RESTART,
ICON_RESTART_ALERT,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
ApplyEntity,
SubEntities,
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -52,13 +53,11 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_BUTTON_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTButtonComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): validate_device_class,
@@ -136,7 +135,12 @@ async def setup_button_core_(var: MockObj, config: ConfigType) -> None:
setup_device_class(config)
await entity_helpers.setup_entity_integrations(var, config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_button(var: MockObj, config: ConfigType) -> None:
+4 -7
View File
@@ -2,7 +2,6 @@
#include "cc1101pa.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cmath>
#include <cstring>
@@ -15,9 +14,6 @@ namespace esphome::cc1101 {
ESPHOME_LOG_TAG(TAG, "cc1101");
static constexpr size_t TX_FIFO_SIZE = 64;
// MDMCFG2 MOD_FORMAT values
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
int e_tmp;
float m_tmp = std::frexp(value, &e_tmp);
@@ -242,6 +238,8 @@ void CC1101Component::loop() {
}
void CC1101Component::dump_config() {
static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK",
"4-FSK", "UNUSED", "UNUSED", "MSK"};
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
XTAL_FREQUENCY / (1 << 16);
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
@@ -255,9 +253,8 @@ void CC1101Component::dump_config() {
" Symbol Rate: %.0f baud\n"
" Filter Bandwidth: %.1f Hz\n"
" Output Power: %.1f dBm",
this->chip_id_, freq, this->state_.CHANNR,
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
this->output_power_effective_);
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
symbol_rate, bw, this->output_power_effective_);
LOG_PIN(" CS Pin: ", this->cs_);
}
+12 -8
View File
@@ -3,6 +3,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION_STATE_TOPIC,
@@ -48,15 +49,15 @@ from esphome.const import (
CONF_TEMPERATURE_STEP,
CONF_UNIT_OF_MEASUREMENT,
CONF_VISUAL,
CONF_WEB_SERVER,
UNIT_KELVIN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
TemperatureUnitResolver,
convert_visual_temperatures,
declare_fixed_temperature_unit,
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
setup_entity_temperature_unit,
@@ -176,13 +177,11 @@ _visual_temperatures_in_entity_unit = partial(
_CLIMATE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTClimateComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
@@ -354,8 +353,10 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
cg.add(var.set_visual_max_humidity_override(max_humidity))
setup_entity_temperature_unit(var, config)
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (action_state_topic := config.get(CONF_ACTION_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_action_state_topic(action_state_topic))
if (away_command_topic := config.get(CONF_AWAY_COMMAND_TOPIC)) is not None:
@@ -485,6 +486,9 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
def set_custom_fan_modes(var: MockObj, modes: list[str]) -> None:
"""Point the climate at a shared flash table of custom fan modes."""
+12 -8
View File
@@ -3,6 +3,7 @@ import logging
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -22,6 +23,7 @@ from esphome.const import (
CONF_TILT_COMMAND_TOPIC,
CONF_TILT_STATE_TOPIC,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_AWNING,
DEVICE_CLASS_BLIND,
DEVICE_CLASS_CURTAIN,
@@ -34,10 +36,9 @@ from esphome.const import (
DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -111,13 +112,11 @@ TRIGGERS = {
_COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTCoverComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
cv.requires_component("mqtt"), cv.boolean
),
@@ -203,8 +202,10 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (position_state_topic := config.get(CONF_POSITION_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_position_state_topic(position_state_topic))
if (
@@ -219,6 +220,9 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
cg.add(mqtt_.set_custom_tilt_command_topic(tilt_command_topic))
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_cover(var: MockObj, config: ConfigType) -> None:
if not CORE.has_id(config[CONF_ID]):
+22 -15
View File
@@ -28,23 +28,30 @@ void CSE7766Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
this->read_available<CSE7766_RAW_DATA_SIZE>(
[this](uint8_t byte) {
this->raw_data_[this->raw_data_index_] = byte;
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
return;
}
uint8_t buf[CSE7766_RAW_DATA_SIZE];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
for (size_t i = 0; i < to_read; i++) {
this->raw_data_[this->raw_data_index_] = buf[i];
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
continue;
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
},
avail);
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
}
}
}
bool CSE7766Component::check_byte_() {
+14 -17
View File
@@ -2,6 +2,7 @@ from collections.abc import Callable
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, time, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DATE,
@@ -19,12 +20,12 @@ from esphome.const import (
CONF_TIME_ID,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_WEB_SERVER,
CONF_YEAR,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -37,10 +38,6 @@ DOMAIN = "datetime"
IS_PLATFORM_COMPONENT = True
datetime_ns = cg.esphome_ns.namespace("datetime")
# Matches time.RealTimeClock by name without importing the time component
_RealTimeClock = cg.esphome_ns.namespace("time").class_(
"RealTimeClock", cg.PollingComponent
)
DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase)
DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
@@ -67,7 +64,7 @@ DATETIME_MODES = [
def _validate_time_present(config: ConfigType) -> ConfigType:
config = config.copy()
if CONF_ON_TIME in config and CONF_TIME_ID not in config:
time_id = cv.use_id(_RealTimeClock)(None)
time_id = cv.use_id(time.RealTimeClock)(None)
config[CONF_TIME_ID] = time_id
return config
@@ -76,10 +73,10 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.Schema(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
cv.Optional(CONF_TIME_ID): cv.use_id(_RealTimeClock),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
).add_extra(_validate_time_present)
@@ -90,9 +87,7 @@ def date_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTDateComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent),
cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
}
)
@@ -103,9 +98,7 @@ def time_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTTimeComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent),
cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True),
cv.Optional(CONF_ON_TIME): automation.validate_automation(
{
@@ -122,7 +115,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTDateTimeComponent")
mqtt.MQTTDateTimeComponent
),
cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of(
"DATETIME", upper=True
@@ -139,7 +132,11 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
@setup_entity("datetime")
async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None:
await entity_helpers.setup_entity_integrations(var, config)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
# The state callback carries nothing; the automation gets the value as an ESPTime.
for conf in config.get(CONF_ON_VALUE, []):
await automation.build_callback_automation(
+146 -133
View File
@@ -135,140 +135,153 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
}
void DFPlayer::loop() {
this->read_available([this](uint8_t byte) {
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
return;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
return;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
return;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
return;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
return;
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
});
avail -= to_read;
for (size_t bi = 0; bi < to_read; bi++) {
uint8_t byte = buf[bi];
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
continue;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG,
"Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
continue;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
}
}
}
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
@@ -1,7 +1,10 @@
#include "dfrobot_sen0465_uart.h"
#include <algorithm>
namespace esphome::dfrobot_sen0465_uart {
using dfrobot_sen0465_base::FRAME_HEAD;
using dfrobot_sen0465_base::FRAME_SIZE;
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
@@ -15,21 +18,30 @@ void DFRobotSEN0465UARTComponent::setup() {
}
void DFRobotSEN0465UARTComponent::loop() {
// The tail of a reading the probe sent on its own may precede the answer; the reader skips to the frame head.
const uint8_t *frame = nullptr;
this->read_available<FRAME_SIZE>([&](uint8_t c) { return (frame = this->reader_.feed(c)) == nullptr; });
if (frame == nullptr)
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
uint8_t byte;
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
this->read_byte(&byte);
const size_t available = this->available();
if (available == 0)
return;
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
return;
this->rx_count_ += len;
if (this->rx_count_ < FRAME_SIZE)
return;
this->rx_count_ = 0;
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
this->disable_loop();
this->handle_response_(frame);
this->handle_response_(this->rx_buffer_);
}
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
this->discard_available();
this->reader_.reset();
this->rx_count_ = 0;
this->write_array(frame, FRAME_SIZE);
return true;
}
@@ -38,7 +50,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
this->enable_loop();
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
this->disable_loop();
this->reader_.reset();
this->rx_count_ = 0;
this->handle_no_response_();
});
}
@@ -2,7 +2,6 @@
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/frame_reader.h"
namespace esphome::dfrobot_sen0465_uart {
@@ -18,7 +17,8 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
bool write_frame(const uint8_t *frame) override;
void read_response() override;
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
uint8_t rx_count_{0};
};
} // namespace esphome::dfrobot_sen0465_uart
@@ -5,16 +5,19 @@ import esphome.config_validation as cv
from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_RESTART
from esphome.types import ConfigType
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
DFRobotSEN0609Button = dfrobot_sen0609_ns.class_("DFRobotSEN0609Button", button.Button)
ButtonAction = dfrobot_sen0609_ns.enum("ButtonAction")
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
button.ApplyButton
DFRobotSEN0609Button
),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(DFRobotSEN0609Button),
cv.Optional(CONF_RESTART): button.restart_button_schema(DFRobotSEN0609Button),
}
@@ -22,7 +25,11 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
CONF_FACTORY_RESET,
hub.set_factory_reset_button,
ButtonAction.BUTTON_FACTORY_RESET,
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="query_params()")
await buttons(CONF_RESTART, hub.set_restart_button, apply="restart()")
await buttons(
CONF_QUERY_PARAMS, hub.set_query_button, ButtonAction.BUTTON_QUERY_PARAMS
)
await buttons(CONF_RESTART, hub.set_restart_button, ButtonAction.BUTTON_RESTART)
@@ -0,0 +1,30 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Button final : public button::Button, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Button(ButtonAction action) : action_(action) {}
protected:
void press_action() override {
switch (this->action_) {
case BUTTON_RESTART:
this->parent_->restart();
break;
case BUTTON_FACTORY_RESET:
this->parent_->factory_reset();
break;
case BUTTON_QUERY_PARAMS:
this->parent_->query_params();
break;
}
}
ButtonAction action_;
};
} // namespace esphome::dfrobot_sen0609
@@ -5,6 +5,7 @@
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -174,11 +175,21 @@ void DFRobotSEN0609Component::setup() {
}
void DFRobotSEN0609Component::loop() {
if (this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); })) {
uint8_t buf[MAX_LINE_LENGTH];
size_t avail = this->available();
while (avail > 0) {
const size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
if (!this->read_failed_) {
ESP_LOGW(TAG, "UART read failed");
}
this->read_failed_ = true;
break;
}
this->read_failed_ = false;
} else if (!this->read_failed_) {
ESP_LOGW(TAG, "UART read failed");
this->read_failed_ = true;
avail -= to_read;
for (size_t i = 0; i < to_read; i++)
this->readline_(buf[i]);
}
this->run_sequencer_();
}
@@ -443,15 +454,6 @@ void DFRobotSEN0609Component::control_number(NumberSetting setting, float value)
this->request_write_(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[setting])));
}
void DFRobotSEN0609Component::control_number(number::Number *number, float value) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (this->numbers_[i] == number) {
this->control_number(static_cast<NumberSetting>(i), value);
return;
}
}
}
void DFRobotSEN0609Component::forget_number_changes_(Param param, bool unknown) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (progmem_read_byte(&NUMBER_PARAMS[i]) != param)
@@ -62,6 +62,12 @@ enum SwitchSetting : uint8_t {
SWITCH_COUNT,
};
enum ButtonAction : uint8_t {
BUTTON_RESTART,
BUTTON_FACTORY_RESET,
BUTTON_QUERY_PARAMS,
};
/// Module parameters; a write or read of each is one command. Pending work runs in this order.
enum Param : uint8_t {
PARAM_RUN_APP,
@@ -124,8 +130,6 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
#ifdef USE_NUMBER
void set_number(NumberSetting setting, number::Number *number) { this->numbers_[setting] = number; }
void control_number(NumberSetting setting, float value);
// Finds the setting by pointer so every number shares one Apply function
void control_number(number::Number *number, float value);
#endif
#ifdef USE_SWITCH
void set_switch(SwitchSetting setting, switch_::Switch *sw) { this->switches_[setting] = sw; }
@@ -31,6 +31,7 @@ from .. import (
final_validate_mode,
)
DFRobotSEN0609Number = dfrobot_sen0609_ns.class_("DFRobotSEN0609Number", number.Number)
NumberSetting = dfrobot_sen0609_ns.enum("NumberSetting")
CONF_HOLD_SENSITIVITY = "hold_sensitivity"
@@ -54,7 +55,7 @@ class Setting(NamedTuple):
def _distance(icon: str = ICON_RULER) -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
@@ -64,7 +65,7 @@ def _distance(icon: str = ICON_RULER) -> cv.Schema:
def _duration() -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
@@ -74,7 +75,7 @@ def _duration() -> cv.Schema:
def _plain() -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
)
@@ -186,8 +187,8 @@ async def to_code(config: ConfigType) -> None:
await numbers(
s.key,
lambda n, setting=s.setting: hub.set_number(setting, n),
s.setting,
min_value=s.min_value,
max_value=max_range if s.max_value is None else s.max_value,
step=s.step,
apply="control_number(entity, value)",
)
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Number final : public number::Number, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Number(NumberSetting setting) : setting_(setting) {}
protected:
void control(float value) override { this->parent_->control_number(this->setting_, value); }
NumberSetting setting_;
};
} // namespace esphome::dfrobot_sen0609
+36 -15
View File
@@ -1,5 +1,7 @@
#include "ds1603l.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -8,28 +10,35 @@ namespace esphome::ds1603l {
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
void DS1603L::loop() {
this->read_available([this](uint8_t byte) {
const uint8_t *frame = this->reader_.feed(byte);
if (frame == nullptr) {
if (this->reader_.length() == 0) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
}
return;
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
uint8_t byte;
while (this->available() > 0 && this->read_byte(&byte)) {
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
continue;
}
if (!this->parse_data_(frame)) {
this->rx_buffer_[this->rx_count_++] = byte;
if (this->rx_count_ < FRAME_SIZE) {
continue;
}
if (this->parse_data_()) {
this->rx_count_ = 0;
} else {
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
this->reader_.resync();
this->resync_();
}
});
}
}
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
bool DS1603L::parse_data_(const uint8_t *frame) {
uint8_t header = frame[0];
uint8_t data_h = frame[1];
uint8_t data_l = frame[2];
uint8_t checksum = frame[3];
bool DS1603L::parse_data_() {
uint8_t header = this->rx_buffer_[0];
uint8_t data_h = this->rx_buffer_[1];
uint8_t data_l = this->rx_buffer_[2];
uint8_t checksum = this->rx_buffer_[3];
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
@@ -44,4 +53,16 @@ bool DS1603L::parse_data_(const uint8_t *frame) {
return true;
}
void DS1603L::resync_() {
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
size_t start = 1;
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
start++;
}
this->rx_count_ -= start;
if (this->rx_count_ > 0) {
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
}
}
} // namespace esphome::ds1603l
+6 -4
View File
@@ -6,7 +6,6 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/frame_reader.h"
namespace esphome::ds1603l {
@@ -19,10 +18,13 @@ class DS1603L final : public sensor::Sensor, public Component, public uart::UART
static constexpr uint8_t HEADER_BYTE = 0xFF;
static constexpr size_t FRAME_SIZE = 4;
// Validates the checksum of the frame and publishes it. Returns false if the frame is invalid.
bool parse_data_(const uint8_t *frame);
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
bool parse_data_();
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
void resync_();
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
};
} // namespace esphome::ds1603l
+15 -12
View File
@@ -4,18 +4,21 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi");
static constexpr size_t EPAPER_MAX_CMD_LOG_BYTES = 128;
PROGMEM_STRING_TABLE(EPaperStateStrings, "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", "Unknown");
static constexpr const char *const EPAPER_STATE_STRINGS[] = {
"IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
};
const LogString *EPaperBase::epaper_state_to_string_() {
return EPaperStateStrings::get_log_str(static_cast<uint8_t>(this->state_), EPaperStateStrings::LAST_INDEX);
const char *EPaperBase::epaper_state_to_string_() {
if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS))
return EPAPER_STATE_STRINGS[idx];
return "Unknown";
}
void EPaperBase::setup() {
@@ -156,7 +159,7 @@ void EPaperBase::update_effective_transform_() {
void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display already in state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
return;
}
this->set_state_(EPaperState::UPDATE);
@@ -209,7 +212,7 @@ void EPaperBase::loop() {
this->high_frequency_loop_.stop();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle in state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
this->waiting_for_idle_last_print_ = millis();
}
#endif
@@ -224,7 +227,7 @@ void EPaperBase::loop() {
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
void EPaperBase::abandon_update_() {
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
LOG_STR_ARG(this->epaper_state_to_string_()));
this->epaper_state_to_string_());
this->status_set_warning(LOG_STR("Busy pin timeout"));
if (this->state_ == EPaperState::TRANSFER_DATA)
this->abort_transfer();
@@ -244,10 +247,10 @@ void EPaperBase::abandon_update_() {
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGE(TAG, "Display is in unhandled state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->set_state_(EPaperState::IDLE);
break;
case EPaperState::IDLE:
@@ -339,7 +342,7 @@ void EPaperBase::process_state_() {
}
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
// allow subclasses to nominate delays
if (delay == 0)
@@ -347,7 +350,7 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
this->next_delay_ = 0;
this->delay_until_ = millis() + delay;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", LOG_STR_ARG(this->epaper_state_to_string_()), delay,
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
// Each step of an update otherwise waits for the next loop iteration; loop() slows down again for long waits
if (state == EPaperState::IDLE) {
+1 -1
View File
@@ -125,7 +125,7 @@ class EPaperBase : public Display,
bool is_using_partial_update_() const { return this->full_update_every_ > 1; }
void process_state_();
const LogString *epaper_state_to_string_();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
virtual bool reset();
@@ -1,18 +1,20 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from .. import CONF_ESPECTRE_ID, ESPectreComponent
from .. import CONF_ESPECTRE_ID, ESPectreComponent, espectre_ns
DEPENDENCIES = ["espectre"]
RecalibrateButton = espectre_ns.class_("RecalibrateButton", button.Button)
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
RecalibrateButton, entity_category=ENTITY_CATEGORY_CONFIG
).extend({cv.GenerateID(CONF_ESPECTRE_ID): cv.use_id(ESPectreComponent)})
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_ESPECTRE_ID])
await buttons.create(config, None, apply="recalibrate()")
var = await button.new_button(config)
await cg.register_parented(var, config[CONF_ESPECTRE_ID])
@@ -0,0 +1,19 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESPECTRE
#include "esphome/components/button/button.h"
#include "../espectre.h"
namespace esphome::espectre {
class RecalibrateButton final : public button::Button, public Parented<ESPectreComponent> {
protected:
void press_action() override { this->parent_->recalibrate(); }
};
} // namespace esphome::espectre
#endif // USE_ESPECTRE
@@ -17,20 +17,16 @@ void IPAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const n
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
uint8_t sensor = 0;
for (const auto &ip : ips) {
if (!ip.is_set())
continue;
if (sensor == ETHERNET_INFO_IP_SENSOR_COUNT)
break;
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
sensor++;
}
#endif
}
void DNSAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
@@ -18,19 +18,14 @@ class IPAddressEthernetInfo final : public Component,
void setup() override;
void dump_config() override;
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
#endif
// EthernetIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
protected:
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
// Sized to the highest configured address_N
std::array<text_sensor::TextSensor *, ETHERNET_INFO_IP_SENSOR_COUNT> ip_sensors_{};
#endif
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
};
class DNSAddressEthernetInfo final : public Component,
@@ -12,7 +12,6 @@ from esphome.types import ConfigType
DEPENDENCIES = ["ethernet"]
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
_request_ip_sensor_slot = cg.slot_counter("ETHERNET_INFO_IP_SENSOR_COUNT")
IPAddressEthernetInfo = ethernet_info_ns.class_(
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.Component
@@ -58,13 +57,10 @@ async def to_code(config: ConfigType) -> None:
if conf := config.get(CONF_IP_ADDRESS):
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
indexes = [x for x in range(5) if f"address_{x}" in conf]
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
for _ in range(max(indexes, default=-1) + 1):
_request_ip_sensor_slot(str(ip_info))
for x in indexes:
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
cg.add(ip_info.add_ip_sensors(x, sens))
for x in range(5):
if sensor_conf := conf.get(f"address_{x}"):
sens = await text_sensor.new_text_sensor(sensor_conf)
cg.add(ip_info.add_ip_sensors(x, sens))
if conf := config.get(CONF_DNS_ADDRESS):
dns_info = await text_sensor.new_text_sensor(conf)
await cg.register_component(dns_info, conf)
+11 -7
View File
@@ -1,5 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -9,15 +10,15 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_EVENT,
CONF_WEB_SERVER,
DEVICE_CLASS_BUTTON,
DEVICE_CLASS_DOORBELL,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_MOTION,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -43,13 +44,11 @@ EventPtr = Event.operator("ptr")
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_EVENT_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTEventComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent),
cv.GenerateID(): cv.declare_id(Event),
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
@@ -105,7 +104,12 @@ async def setup_event_core_(
setup_device_class(config)
await entity_helpers.setup_entity_integrations(var, config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_event(
+9 -12
View File
@@ -1,14 +1,14 @@
#include "ezo.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::ezo {
PROGMEM_STRING_TABLE(EzoCommandTypeStrings, "EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION", "EZO_SLOPE",
"EZO_CALIBRATION", "EZO_SLEEP", "EZO_I2C", "EZO_T", "EZO_CUSTOM");
static const char *const EZO_COMMAND_TYPE_STRINGS[] = {"EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION",
"EZO_SLOPE", "EZO_CALIBRATION", "EZO_SLEEP",
"EZO_I2C", "EZO_T", "EZO_CUSTOM"};
PROGMEM_STRING_TABLE(EzoCalibrationTypeStrings, "LOW", "MID", "HIGH");
static const char *const EZO_CALIBRATION_TYPE_STRINGS[] = {"LOW", "MID", "HIGH"};
void EZOSensor::dump_config() {
LOG_SENSOR("", "EZO", this);
@@ -103,8 +103,7 @@ void EZOSensor::loop() {
break;
}
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1],
LOG_STR_ARG(EzoCommandTypeStrings::get_log_str(to_run->command_type, EzoCommandTypeStrings::LAST_INDEX)));
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1], EZO_COMMAND_TYPE_STRINGS[to_run->command_type]);
if (buf[0] == 1) {
std::string payload = reinterpret_cast<char *>(&buf[1]);
@@ -169,9 +168,7 @@ void EZOSensor::add_command_(const char *command, EzoCommandType command_type, u
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
// max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety
char payload[24];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%s,%0.2f"),
LOG_STR_ARG(EzoCalibrationTypeStrings::get_log_str(type, EzoCalibrationTypeStrings::LAST_INDEX)),
value);
snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
@@ -179,7 +176,7 @@ void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) {
// max 8: "I2C,"(4) + uint8(3) + null
char payload[8];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("I2C,%u"), address);
snprintf(payload, sizeof(payload), "I2C,%u", address);
this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C);
} else {
@@ -200,7 +197,7 @@ void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) {
// max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment
char payload[16];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("T,%0.2f"), value);
snprintf(payload, sizeof(payload), "T,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_T);
}
@@ -223,7 +220,7 @@ void EZOSensor::set_calibration_point_high(float value) {
void EZOSensor::set_calibration_generic(float value) {
// exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16
char payload[16];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%0.2f"), value);
snprintf(payload, sizeof(payload), "Cal,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
+17 -25
View File
@@ -1,7 +1,6 @@
#include "ezo_pmp.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::ezo_pmp {
@@ -32,24 +31,11 @@ static const uint16_t EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED = 65;
static const uint16_t EZO_PMP_COMMAND_READ_CALIBRATION_STATUS = 129;
static const uint16_t EZO_PMP_COMMAND_READ_PUMP_VOLTAGE = 257;
#ifdef USE_TEXT_SENSOR
enum DosingMode : uint8_t {
DOSING_MODE_NONE,
DOSING_MODE_VOLUME,
DOSING_MODE_VOLUME_OVER_TIME,
DOSING_MODE_CONSTANT_FLOW_RATE,
DOSING_MODE_CONTINUOUS,
};
PROGMEM_STRING_TABLE(DosingModeStrings, "None", "Volume", "Volume/Time", "Constant Flow Rate", "Continuous");
static void publish_dosing_mode(text_sensor::TextSensor *sensor, DosingMode mode) {
if (sensor == nullptr)
return;
const ProgmemStr name = DosingModeStrings::get_progmem_str(mode, DOSING_MODE_NONE);
if (ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(name)) != 0)
sensor->publish_state(name);
}
#endif
static const std::string DOSING_MODE_NONE = "None";
static const std::string DOSING_MODE_VOLUME = "Volume";
static const std::string DOSING_MODE_VOLUME_OVER_TIME = "Volume/Time";
static const std::string DOSING_MODE_CONSTANT_FLOW_RATE = "Constant Flow Rate";
static const std::string DOSING_MODE_CONTINUOUS = "Continuous";
void EzoPMP::dump_config() {
LOG_I2C_DEVICE(this);
@@ -226,7 +212,8 @@ void EzoPMP::read_command_result_() {
#ifdef USE_TEXT_SENSOR
if (!this->is_dosing_flag_ && !this->is_paused_flag_) {
// If pump is not paused and not dispensing
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_NONE);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
}
#endif
@@ -298,25 +285,29 @@ void EzoPMP::read_command_result_() {
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_VOLUME);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_VOLUME_OVER_TIME);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_CONSTANT_FLOW_RATE);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE)
this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE);
#endif
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_CONTINUOUS);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS)
this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS);
#endif
break;
@@ -327,7 +318,8 @@ void EzoPMP::read_command_result_() {
this->is_paused_->publish_state(this->is_paused_flag_);
#endif
#ifdef USE_TEXT_SENSOR
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_NONE);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
#endif
break;
+12 -8
View File
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DIRECTION,
@@ -28,11 +29,11 @@ from esphome.const import (
CONF_SPEED_LEVEL_STATE_TOPIC,
CONF_SPEED_STATE_TOPIC,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -77,16 +78,14 @@ FanPresetSetTrigger = fan_ns.class_(
)
_FAN_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
RESTORE_MODES, upper=True, space="_"
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTFanComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent),
cv.Optional(CONF_DIRECTION_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
@@ -241,8 +240,10 @@ _TRIGGER_AUTOMATIONS = (
async def setup_fan_core_(var, config):
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (
direction_state_topic := config.get(CONF_DIRECTION_STATE_TOPIC)
) is not None:
@@ -276,6 +277,9 @@ async def setup_fan_core_(var, config):
if (speed_command_topic := config.get(CONF_SPEED_COMMAND_TOPIC)) is not None:
cg.add(mqtt_.set_custom_speed_command_topic(speed_command_topic))
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_parent_callback_automation(
var, "add_on_state_callback", (Fan.operator("ptr"), "x"), conf
+11 -8
View File
@@ -1,10 +1,12 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..climate import CONF_HAIER_ID, HonClimate
from ..climate import CONF_HAIER_ID, HonClimate, haier_ns
CODEOWNERS = ["@paveldn"]
SelfCleaningButton = haier_ns.class_("SelfCleaningButton", button.Button)
SteriCleaningButton = haier_ns.class_("SteriCleaningButton", button.Button)
# Haier buttons
@@ -18,18 +20,19 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_SELF_CLEANING): button.button_schema(
button.ApplyButton,
SelfCleaningButton,
icon=ICON_SPRAY_BOTTLE,
),
cv.Optional(CONF_STERI_CLEANING): button.button_schema(
button.ApplyButton,
SteriCleaningButton,
icon=ICON_SPRAY_BOTTLE,
),
}
)
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_HAIER_ID])
await buttons(CONF_SELF_CLEANING, None, apply="start_self_cleaning()")
await buttons(CONF_STERI_CLEANING, None, apply="start_steri_cleaning()")
async def to_code(config):
for button_type in [CONF_SELF_CLEANING, CONF_STERI_CLEANING]:
if conf := config.get(button_type):
btn = await button.new_button(conf)
await cg.register_parented(btn, config[CONF_HAIER_ID])
@@ -0,0 +1,7 @@
#include "self_cleaning.h"
namespace esphome::haier {
void SelfCleaningButton::press_action() { this->parent_->start_self_cleaning(); }
} // namespace esphome::haier
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome::haier {
class SelfCleaningButton final : public button::Button, public Parented<HonClimate> {
public:
SelfCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::haier
@@ -0,0 +1,7 @@
#include "steri_cleaning.h"
namespace esphome::haier {
void SteriCleaningButton::press_action() { this->parent_->start_steri_cleaning(); }
} // namespace esphome::haier
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome::haier {
class SteriCleaningButton final : public button::Button, public Parented<HonClimate> {
public:
SteriCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::haier
+24 -14
View File
@@ -6,7 +6,6 @@
#include "esphome/components/wifi/wifi_component.h"
#endif
#include "haier_base.h"
#include "esphome/core/progmem.h"
using namespace esphome::climate;
using namespace esphome::uart;
@@ -20,16 +19,28 @@ constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
PROGMEM_STRING_TABLE(ProtocolPhaseStrings, "SENDING_INIT_1", "SENDING_INIT_2", "SENDING_FIRST_STATUS_REQUEST",
"SENDING_FIRST_ALARM_STATUS_REQUEST", "IDLE", "SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_SIGNAL_LEVEL", "SENDING_CONTROL",
"SENDING_ACTION_COMMAND", "SENDING_ALARM_STATUS_REQUEST", "UNKNOWN");
const LogString *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static_assert(ProtocolPhaseStrings::COUNT == ((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1,
"ProtocolPhaseStrings must match the enum ProtocolPhases, with UNKNOWN last");
// UNKNOWN (-1) wraps past the table and reads as the last entry, "UNKNOWN"
return ProtocolPhaseStrings::get_log_str(static_cast<uint8_t>(phase), ProtocolPhaseStrings::LAST_INDEX);
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static const char *phase_names[] = {
"SENDING_INIT_1",
"SENDING_INIT_2",
"SENDING_FIRST_STATUS_REQUEST",
"SENDING_FIRST_ALARM_STATUS_REQUEST",
"IDLE",
"SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST",
"SENDING_SIGNAL_LEVEL",
"SENDING_CONTROL",
"SENDING_ACTION_COMMAND",
"SENDING_ALARM_STATUS_REQUEST",
"UNKNOWN" // Should be the last!
};
static_assert(
(sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1),
"Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases");
int phase_index = (int) phase;
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
return phase_names[phase_index];
}
bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
@@ -60,8 +71,7 @@ HaierClimateBase::~HaierClimateBase() {}
void HaierClimateBase::set_phase(ProtocolPhases phase) {
if (this->protocol_phase_ != phase) {
ESP_LOGV(TAG, "Phase transition: %s => %s", LOG_STR_ARG(phase_to_string_(this->protocol_phase_)),
LOG_STR_ARG(phase_to_string_(phase)));
ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
this->protocol_phase_ = phase;
}
}
@@ -215,7 +225,7 @@ haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_hand
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) {
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type,
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)));
phase_to_string_(this->protocol_phase_));
if (this->protocol_phase_ > ProtocolPhases::IDLE) {
this->set_phase(ProtocolPhases::IDLE);
} else {
+1 -1
View File
@@ -95,7 +95,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_ALARM_STATUS_REQUEST,
NUM_PROTOCOL_PHASES
};
const LogString *phase_to_string_(ProtocolPhases phase);
const char *phase_to_string_(ProtocolPhases phase);
virtual void set_handlers() = 0;
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming)
+1 -1
View File
@@ -564,7 +564,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
default:
// Shouldn't get here
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)), (int) this->protocol_phase_);
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
this->set_phase(ProtocolPhases::SENDING_INIT_1);
break;
}
@@ -93,7 +93,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc
if (this->protocol_phase_ >= ProtocolPhases::IDLE)
return HaierClimateBase::timeout_default_handler_(message_type);
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)));
phase_to_string_(this->protocol_phase_));
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST)
new_phase = ProtocolPhases::SENDING_INIT_1;
@@ -217,7 +217,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
default:
// Shouldn't get here
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)), (int) this->protocol_phase_);
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
this->set_phase(ProtocolPhases::SENDING_INIT_1);
break;
}
@@ -1,9 +1,10 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ICON_AIR_FILTER
from esphome.types import ConfigType
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
DEPENDENCIES = ["hoermann_hcp"]
@@ -12,6 +13,10 @@ CONF_VENT = "vent"
ICON_GARAGE_OPEN_VARIANT = "mdi:garage-open-variant"
HoermannHcpVentButton = hoermann_hcp_ns.class_("HoermannHcpVentButton", button.Button)
HoermannHcpHalfOpenButton = hoermann_hcp_ns.class_(
"HoermannHcpHalfOpenButton", button.Button
)
BUTTON_KEYS = (CONF_VENT, CONF_HALF_OPEN)
@@ -20,10 +25,10 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(CONF_HOERMANN_HCP_ID): cv.use_id(HoermannHcp),
cv.Optional(CONF_VENT): button.button_schema(
button.ApplyButton, icon=ICON_AIR_FILTER
HoermannHcpVentButton, icon=ICON_AIR_FILTER
),
cv.Optional(CONF_HALF_OPEN): button.button_schema(
button.ApplyButton, icon=ICON_GARAGE_OPEN_VARIANT
HoermannHcpHalfOpenButton, icon=ICON_GARAGE_OPEN_VARIANT
),
}
),
@@ -32,6 +37,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_HOERMANN_HCP_ID])
await buttons(CONF_VENT, None, apply="vent_door()")
await buttons(CONF_HALF_OPEN, None, apply="half_open_door()")
parent = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
for key in BUTTON_KEYS:
if (conf := config.get(key)) is not None:
await button.new_button(conf, parent)
@@ -0,0 +1,34 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hoermann_hcp.h"
namespace esphome::hoermann_hcp {
// The door commands the cover has no equivalent for. A refused command is already reported by the hub and
// leaves nothing to correct here, because a button carries no state of its own.
class HoermannHcpButton : public button::Button {
public:
explicit HoermannHcpButton(HoermannHcp *parent) : parent_(parent) {}
protected:
HoermannHcp *const parent_;
};
class HoermannHcpVentButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->vent_door(); }
};
class HoermannHcpHalfOpenButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->half_open_door(); }
};
} // namespace esphome::hoermann_hcp
+3 -41
View File
@@ -41,13 +41,11 @@ from esphome.const import (
CONF_FREQUENCY,
CONF_I2C_ID,
CONF_ID,
CONF_INVERTED,
CONF_LOW_POWER_MODE,
CONF_SCAN,
CONF_SCL,
CONF_SDA,
CONF_TIMEOUT,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
@@ -70,7 +68,6 @@ ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
HostI2CBus = i2c_ns.class_("HostI2CBus", I2CBus, cg.Component)
SoftwareI2CBus = i2c_ns.class_("SoftwareI2CBus", I2CBus, cg.Component)
I2CDevice = i2c_ns.class_("I2CDevice")
ESP32_I2C_CAPABILITIES = {
@@ -111,8 +108,6 @@ def validate_device(value: str) -> str:
def _bus_declare_type(value: Any) -> ID:
if CORE.is_esp32:
return cv.declare_id(IDFI2CBus)(value)
if CORE.is_libretiny:
return cv.declare_id(SoftwareI2CBus)(value)
if CORE.using_arduino:
return cv.declare_id(ArduinoI2CBus)(value)
if CORE.using_zephyr:
@@ -133,28 +128,7 @@ def _rp2040_i2c_controller(pin: int) -> int:
return (pin // 2) % 2
def _i2c_pin_number(value: Any) -> Any:
# The software bus on LibreTiny drives the pins through the GPIO pin objects and
# switches them between input with pull-up and output at runtime.
if CORE.is_libretiny:
return pins.internal_gpio_input_pullup_pin_schema(value)
return pins.internal_gpio_pin_number(value)
def validate_config(config: ConfigType) -> ConfigType:
if CORE.is_libretiny:
if int(config[CONF_FREQUENCY]) < 1:
raise cv.Invalid("The frequency must be at least 1Hz", [CONF_FREQUENCY])
# no default pins on LibreTiny, the bus is bit-banged on any two GPIOs
for key in (CONF_SDA, CONF_SCL):
if key not in config:
raise cv.Invalid(f"'{key}' is required on this platform", [key])
if config[key].get(CONF_INVERTED):
# the software bus pulls a line low by writing a low level to the pin
raise cv.Invalid(
f"'{CONF_INVERTED}' is not supported on the I2C pins",
[key, CONF_INVERTED],
)
if CORE.is_esp32:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
@@ -207,7 +181,7 @@ CONFIG_SCHEMA = cv.All(
esp8266="SDA",
rp2="SDA",
nrf52="SDA",
): _i2c_pin_number,
): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
@@ -217,7 +191,7 @@ CONFIG_SCHEMA = cv.All(
esp8266="SCL",
rp2="SCL",
nrf52="SCL",
): _i2c_pin_number,
): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
@@ -228,7 +202,6 @@ CONFIG_SCHEMA = cv.All(
rp2="50kHz",
nrf52="100kHz",
host="50kHz",
bk72xx="100kHz",
): cv.All(
cv.frequency,
cv.float_range(min=0, min_included=False),
@@ -252,7 +225,6 @@ CONFIG_SCHEMA = cv.All(
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(
[
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2,
@@ -371,16 +343,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
if CONF_LOW_POWER_MODE in config:
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
elif CORE.is_libretiny:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_sda_pin(await cg.gpio_pin_expression(config[CONF_SDA])))
cg.add(var.set_scl_pin(await cg.gpio_pin_expression(config[CONF_SCL])))
cg.add(var.set_frequency(int(config[CONF_FREQUENCY])))
cg.add(var.set_scan(config[CONF_SCAN]))
if CONF_TIMEOUT in config:
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
else:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -500,6 +462,7 @@ _platform_filter = filter_source_files_from_platform(
"i2c_bus_arduino.cpp": {
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
@@ -507,7 +470,6 @@ _platform_filter = filter_source_files_from_platform(
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"i2c_bus_software.cpp": {PlatformFramework.BK72XX_ARDUINO},
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
"i2c_bus_host.cpp": {PlatformFramework.HOST_NATIVE},
}
-265
View File
@@ -1,265 +0,0 @@
#ifdef USE_LIBRETINY
#include "i2c_bus_software.h"
#include <cinttypes>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::i2c {
ESPHOME_LOG_TAG(TAG, "i2c.software");
// Maximum bytes to log in hex format (truncates larger transfers)
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
// Clock pulses used to free a device that is holding SDA low
static constexpr uint8_t RECOVERY_CLOCK_PULSES = 9;
// A line is released by switching the pin to input with pull-up and pulled low by driving it as output.
static inline void release_line(InternalGPIOPin *pin) { pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
static inline void pull_line_low(InternalGPIOPin *pin) {
pin->digital_write(false);
pin->pin_mode(gpio::FLAG_OUTPUT);
}
void SoftwareI2CBus::setup() {
this->update_half_period_();
this->sda_pin_->setup();
this->scl_pin_->setup();
release_line(this->sda_pin_);
release_line(this->scl_pin_);
if (!this->recover_()) {
// A line that stays low would be read as an ACK for every address and as zero data
ESP_LOGE(TAG, "SDA or SCL is held low, the bus cannot be used");
this->mark_failed();
return;
}
this->initialized_ = true;
if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_();
}
}
void SoftwareI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, "I2C Bus (software):");
LOG_PIN(" SDA Pin: ", this->sda_pin_);
LOG_PIN(" SCL Pin: ", this->scl_pin_);
ESP_LOGCONFIG(TAG,
" Frequency: %" PRIu32 " Hz\n"
" Clock stretch timeout: %" PRIu32 " us",
this->frequency_, this->timeout_);
if (this->recovered_) {
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
}
if (this->scan_) {
ESP_LOGI(TAG, "Results from bus scan:");
if (this->scan_results_.empty()) {
ESP_LOGI(TAG, "Found no devices");
} else {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
}
}
// Half of one clock period in microseconds, rounded up. The frequency is validated to be non-zero.
void SoftwareI2CBus::update_half_period_() {
this->half_period_us_ = (500000 + this->frequency_ - 1) / this->frequency_;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode SoftwareI2CBus::switch_frequency(uint32_t frequency) {
if (frequency == 0)
return ERROR_INVALID_ARGUMENT;
this->frequency_ = frequency;
this->update_half_period_();
return ERROR_OK;
}
#endif
bool SoftwareI2CBus::recover_() {
this->timed_out_ = false;
// A device may still be stretching the clock, give SCL the configured time to rise before looking at SDA
this->release_scl_();
// A device that was interrupted in the middle of a transfer can hold SDA low; clock it out.
bool clocked = false;
for (uint8_t i = 0; i < RECOVERY_CLOCK_PULSES && !this->sda_pin_->digital_read(); i++) {
pull_line_low(this->scl_pin_);
delayMicroseconds(this->half_period_us_);
// wait for the rising edge, the device may stretch the clock
if (!this->release_scl_())
break;
delayMicroseconds(this->half_period_us_);
clocked = true;
}
bool ok = this->sda_pin_->digital_read() && this->scl_pin_->digital_read();
if (ok && clocked) {
// end the interrupted transfer with a STOP so every device returns to idle
this->recovered_ = true;
pull_line_low(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
release_line(this->scl_pin_);
delayMicroseconds(this->half_period_us_);
release_line(this->sda_pin_);
}
if (!ok) {
ESP_LOGW(TAG, "Bus recovery failed, a line is held low");
}
this->timed_out_ = false;
return ok;
}
// Release SCL and wait for it to go high, a device may stretch the clock.
bool SoftwareI2CBus::release_scl_() {
release_line(this->scl_pin_);
uint32_t start = micros();
while (!this->scl_pin_->digital_read()) {
if (micros() - start > this->timeout_) {
this->timed_out_ = true;
return false;
}
}
return true;
}
void SoftwareI2CBus::start_() {
// Also generates a repeated start when SCL is low and SDA is released
release_line(this->sda_pin_);
// After a byte the device may still hold SDA low for its ACK. Without SDA being high before SCL rises there is
// no falling edge, so the (repeated) start would not be seen. Wait until the device lets go.
uint32_t start = micros();
while (!this->sda_pin_->digital_read()) {
if (micros() - start > this->timeout_) {
this->timed_out_ = true;
return;
}
}
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
pull_line_low(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
pull_line_low(this->scl_pin_);
}
void SoftwareI2CBus::stop_() {
if (this->timed_out_) {
// SCL is stuck low, a STOP cannot be generated; just release both lines
release_line(this->scl_pin_);
release_line(this->sda_pin_);
return;
}
pull_line_low(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
release_line(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
}
/// Shift out one byte, returns true if the device acknowledged it.
bool SoftwareI2CBus::write_byte_(uint8_t data) {
for (uint8_t mask = 0x80; mask != 0 && !this->timed_out_; mask >>= 1) {
if (data & mask) {
release_line(this->sda_pin_);
} else {
pull_line_low(this->sda_pin_);
}
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
pull_line_low(this->scl_pin_);
}
if (this->timed_out_)
return false;
release_line(this->sda_pin_);
delayMicroseconds(this->half_period_us_);
if (!this->release_scl_())
return false;
delayMicroseconds(this->half_period_us_);
bool ack = !this->sda_pin_->digital_read();
pull_line_low(this->scl_pin_);
return ack;
}
uint8_t SoftwareI2CBus::read_byte_(bool ack) {
uint8_t data = 0;
release_line(this->sda_pin_);
for (uint8_t i = 0; i < 8 && !this->timed_out_; i++) {
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
data = (data << 1) | (this->sda_pin_->digital_read() ? 1 : 0);
pull_line_low(this->scl_pin_);
}
if (this->timed_out_)
return data;
if (ack) {
pull_line_low(this->sda_pin_);
}
delayMicroseconds(this->half_period_us_);
this->release_scl_();
delayMicroseconds(this->half_period_us_);
pull_line_low(this->scl_pin_);
release_line(this->sda_pin_);
return data;
}
ErrorCode SoftwareI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
uint8_t *read_buffer, size_t read_count) {
if (!this->initialized_) {
ESP_LOGD(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED;
}
// After a failed recovery the bus stays blocked until a later attempt frees both lines
if (this->bus_stuck_ && !this->recover_()) {
return ERROR_TIMEOUT;
}
this->bus_stuck_ = false;
ESP_LOGV_HEX(TAG, write_buffer, write_count, I2C_MAX_LOG_BYTES, "0x%02X TX %s", address);
this->timed_out_ = false;
ErrorCode err = ERROR_OK;
this->start_();
// An empty transfer only addresses the device (used by the bus scan)
if (write_count != 0 || read_count == 0) {
if (!this->write_byte_(address << 1)) {
err = ERROR_NOT_ACKNOWLEDGED;
}
for (size_t i = 0; err == ERROR_OK && i < write_count; i++) {
if (!this->write_byte_(write_buffer[i])) {
ESP_LOGVV(TAG, "TX of byte %u to 0x%02X not acknowledged", (unsigned) i, address);
err = ERROR_NOT_ACKNOWLEDGED;
}
}
}
if (err == ERROR_OK && read_count != 0) {
if (write_count != 0) {
this->start_(); // repeated start
}
if (!this->write_byte_((address << 1) | 1)) {
err = ERROR_NOT_ACKNOWLEDGED;
}
for (size_t i = 0; err == ERROR_OK && !this->timed_out_ && i < read_count; i++) {
read_buffer[i] = this->read_byte_(i + 1 != read_count);
}
}
this->stop_();
if (this->timed_out_) {
ESP_LOGVV(TAG, "Bus line held low during transfer to 0x%02X for more than %" PRIu32 " us", address, this->timeout_);
this->bus_stuck_ = !this->recover_();
return ERROR_TIMEOUT;
}
return err;
}
} // namespace esphome::i2c
#endif // USE_LIBRETINY
-55
View File
@@ -1,55 +0,0 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "i2c_bus.h"
namespace esphome::i2c {
/// Bit-banged I2C controller for LibreTiny chips without an Arduino Wire implementation (BK72xx).
/// SDA and SCL can be any two GPIO pins; both lines are driven open-drain style (pulled low or released),
/// so the bus needs pull-ups (the internal pull-up is enabled while released).
class SoftwareI2CBus final : public I2CBus, public Component {
public:
void setup() override;
void dump_config() override;
ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
size_t read_count) override;
float get_setup_priority() const override { return setup_priority::BUS; }
void set_scan(bool scan) { this->scan_ = scan; }
void set_sda_pin(InternalGPIOPin *sda_pin) { this->sda_pin_ = sda_pin; }
void set_scl_pin(InternalGPIOPin *scl_pin) { this->scl_pin_ = scl_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
/// Maximum time in microseconds a device may hold SCL low (clock stretching).
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
protected:
bool recover_();
void update_half_period_();
void start_();
void stop_();
bool release_scl_();
bool write_byte_(uint8_t data);
uint8_t read_byte_(bool ack);
InternalGPIOPin *sda_pin_{nullptr};
InternalGPIOPin *scl_pin_{nullptr};
uint32_t frequency_{100000};
uint32_t timeout_{1000};
uint32_t half_period_us_{5};
bool timed_out_{false};
bool recovered_{false};
bool initialized_{false};
bool bus_stuck_{false};
};
} // namespace esphome::i2c
#endif // USE_LIBRETINY
@@ -2,20 +2,11 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::kamstrup_kmp {
ESPHOME_LOG_TAG(TAG, "kamstrup_kmp");
// KMP units indexed by unit code; the empty first entry doubles as the fallback for unknown codes
PROGMEM_STRING_TABLE(KmpUnits, "", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal", "kCal", "Mcal", "Gcal",
"varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh", "GVAh", "kW", "kW", "MW", "GW", "kvar",
"kvar", "Mvar", "Gvar", "VA", "kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l", "m3",
"l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd", "mm:dd", "",
"bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield", "s", "ms", "days",
"RTC-Q", "Datetime");
void KamstrupKMPComponent::dump_config() {
ESP_LOGCONFIG(TAG, "kamstrup_kmp:");
LOG_UPDATE_INTERVAL(this);
@@ -246,6 +237,8 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
}
void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) {
const char *unit = unit_idx < sizeof(UNITS) / sizeof(UNITS[0]) ? UNITS[unit_idx] : "";
// Standard sensors
if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) {
this->heat_energy_sensor_->publish_state(value);
@@ -270,8 +263,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint
}
}
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value,
LOG_STR_ARG(KmpUnits::get_log_str(unit_idx, 0)));
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value, unit);
}
} // namespace esphome::kamstrup_kmp
@@ -63,6 +63,16 @@ static const uint16_t CMD_TEMP_DIFF = 0x0059;
static const uint16_t CMD_FLOW = 0x004A;
static const uint16_t CMD_VOLUME = 0x0044;
// KMP units
static const char *const UNITS[] = {
"", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal",
"kCal", "Mcal", "Gcal", "varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh",
"GVAh", "kW", "kW", "MW", "GW", "kvar", "kvar", "Mvar", "Gvar", "VA",
"kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l",
"m3", "l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd",
"mm:dd", "", "bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield",
"s", "ms", "days", "RTC-Q", "Datetime"};
class KamstrupKMPComponent final : public PollingComponent, public uart::UARTDevice {
public:
void set_heat_energy_sensor(sensor::Sensor *sensor) { this->heat_energy_sensor_ = sensor; }
+11 -11
View File
@@ -5,26 +5,26 @@ import esphome.config_validation as cv
from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_RESTART
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
FactoryResetButton = ld2410_ns.class_("FactoryResetButton", button.Button)
QueryButton = ld2410_ns.class_("QueryButton", button.Button)
RestartButton = ld2410_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
button.ApplyButton
FactoryResetButton
),
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
cv.Optional(CONF_RESTART): button.restart_button_schema(RestartButton),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(QueryButton),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
await buttons(
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="read_all_info()")
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
@@ -0,0 +1,7 @@
#include "factory_reset_button.h"
namespace esphome::ld2410 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class FactoryResetButton final : public button::Button, public Parented<LD2410Component> {
public:
FactoryResetButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,7 @@
#include "query_button.h"
namespace esphome::ld2410 {
void QueryButton::press_action() { this->parent_->read_all_info(); }
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class QueryButton final : public button::Button, public Parented<LD2410Component> {
public:
QueryButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,7 @@
#include "restart_button.h"
namespace esphome::ld2410 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class RestartButton final : public button::Button, public Parented<LD2410Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
+112 -58
View File
@@ -9,12 +9,27 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
namespace esphome::ld2410 {
ESPHOME_LOG_TAG(TAG, "ld2410");
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8,
};
enum DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
enum LightFunction : uint8_t {
LIGHT_FUNCTION_OFF = 0x00,
LIGHT_FUNCTION_BELOW = 0x01,
@@ -54,17 +69,73 @@ enum AckData : uint8_t {
COMMAND_STATUS = 7,
};
// Index + 1 is the BaudRate value; the select options use the same order
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
// Same order as DISTANCE_RESOLUTIONS
PROGMEM_STRING_TABLE(DistanceResolutionStrings, "0.2m", "0.75m");
// The light function and out pin level select options are in enum order
static_assert(LIGHT_FUNCTION_OFF == 0 && LIGHT_FUNCTION_BELOW == 1 && LIGHT_FUNCTION_ABOVE == 2,
"LightFunction is the select option index");
static_assert(OUT_PIN_LEVEL_LOW == 0 && OUT_PIN_LEVEL_HIGH == 1, "OutPinLevel is the select option index");
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
struct Uint8ToString {
const uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = {
{"0.2m", DISTANCE_RESOLUTION_0_2},
{"0.75m", DISTANCE_RESOLUTION_0_75},
};
constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = {
{DISTANCE_RESOLUTION_0_2, "0.2m"},
{DISTANCE_RESOLUTION_0_75, "0.75m"},
};
constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = {
{"off", LIGHT_FUNCTION_OFF},
{"below", LIGHT_FUNCTION_BELOW},
{"above", LIGHT_FUNCTION_ABOVE},
};
constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = {
{LIGHT_FUNCTION_OFF, "off"},
{LIGHT_FUNCTION_BELOW, "below"},
{LIGHT_FUNCTION_ABOVE, "above"},
};
constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = {
{"low", OUT_PIN_LEVEL_LOW},
{"high", OUT_PIN_LEVEL_HIGH},
};
constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
{OUT_PIN_LEVEL_LOW, "low"},
{OUT_PIN_LEVEL_HIGH, "high"},
};
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
for (const auto &entry : arr) {
if (strcmp(str, entry.str) == 0)
return entry.value;
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value)
return entry.str;
}
return ""; // Not found
}
// Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
@@ -201,7 +272,20 @@ void LD2410Component::restart_and_read_all_info() {
}
void LD2410Component::loop() {
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
}
}
void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
@@ -381,16 +465,11 @@ void LD2410Component::handle_ack_data_() {
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
break; // too short to hold the resolution
}
const auto index = ld24xx::find_index(DISTANCE_RESOLUTIONS, this->buffer_data_[10]);
if (!index) {
ESP_LOGW(TAG, "Unknown distance resolution %u", this->buffer_data_[10]);
break;
}
ESP_LOGV(TAG, "Distance resolution: %s",
LOG_STR_ARG(DistanceResolutionStrings::get_log_str(*index, DistanceResolutionStrings::LAST_INDEX)));
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
#ifdef USE_SELECT
if (this->distance_resolution_select_ != nullptr) {
this->distance_resolution_select_->publish_state(*index);
this->distance_resolution_select_->publish_state(distance_resolution);
}
#endif
break;
@@ -403,14 +482,16 @@ void LD2410Component::handle_ack_data_() {
this->light_function_ = this->buffer_data_[10];
this->light_threshold_ = this->buffer_data_[11];
this->out_pin_level_ = this->buffer_data_[12];
ESP_LOGV(TAG, "Light function: %u, threshold: %u, out pin level: %u", this->light_function_,
this->light_threshold_, this->out_pin_level_);
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_,
out_pin_level_str);
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
this->light_function_select_->publish_state(static_cast<size_t>(this->light_function_));
this->light_function_select_->publish_state(light_function_str);
}
if (this->out_pin_level_select_ != nullptr) {
this->out_pin_level_select_->publish_state(static_cast<size_t>(this->out_pin_level_));
this->out_pin_level_select_->publish_state(out_pin_level_str);
}
#endif
#ifdef USE_NUMBER
@@ -547,29 +628,15 @@ void LD2410Component::set_bluetooth(bool enable) {
}
void LD2410Component::set_distance_resolution(const char *state) {
const int16_t index = progmem_string_table_find<DistanceResolutionStrings>(state, strlen(state));
if (index >= 0) {
this->set_distance_resolution(static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
}
}
void LD2410Component::set_distance_resolution(DistanceResolution resolution) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {resolution, 0x00};
const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
void LD2410Component::set_baud_rate(const char *state) {
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
if (index >= 0) {
this->set_baud_rate(static_cast<BaudRate>(index + 1));
}
}
void LD2410Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {rate, 0x00};
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -648,16 +715,7 @@ void LD2410Component::set_max_distances_timeout() {
this->set_config_mode_(false);
}
void LD2410Component::set_gate_threshold(number::Number *n) {
// Found by pointer: up to 18 compares per write beat storing the gate in each number
uint8_t gate = 0;
while (gate < TOTAL_GATES && this->gate_move_threshold_numbers_[gate] != n &&
this->gate_still_threshold_numbers_[gate] != n)
gate++;
if (gate == TOTAL_GATES) {
ESP_LOGW(TAG, "Threshold number is not a known gate");
return;
}
void LD2410Component::set_gate_threshold(uint8_t gate) {
number::Number *motionsens = this->gate_move_threshold_numbers_[gate];
number::Number *stillsens = this->gate_still_threshold_numbers_[gate];
@@ -701,15 +759,11 @@ void LD2410Component::set_light_out_control() {
}
#endif
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
if (const auto index = this->light_function_select_->active_index()) {
this->light_function_ = *index;
}
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
}
if (this->out_pin_level_select_ != nullptr) {
if (const auto index = this->out_pin_level_select_->active_index()) {
this->out_pin_level_ = *index;
}
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
}
#endif
this->set_config_mode_(true);
+1 -22
View File
@@ -37,25 +37,6 @@ using namespace ld24xx;
static constexpr uint8_t MAX_LINE_LENGTH = 50;
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8,
};
enum DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
// Device values of the distance resolution select options, in option order; in flash on ESP8266
static constexpr uint8_t DISTANCE_RESOLUTIONS[] PROGMEM = {DISTANCE_RESOLUTION_0_2, DISTANCE_RESOLUTION_0_75};
class LD2410Component final : public Component, public uart::UARTDevice {
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(out_pin_presence_status)
@@ -106,7 +87,7 @@ class LD2410Component final : public Component, public uart::UARTDevice {
void set_gate_still_threshold_number(uint8_t gate, number::Number *n);
void set_gate_move_threshold_number(uint8_t gate, number::Number *n);
void set_max_distances_timeout();
void set_gate_threshold(number::Number *n);
void set_gate_threshold(uint8_t gate);
#endif
#ifdef USE_SENSOR
void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s);
@@ -118,9 +99,7 @@ class LD2410Component final : public Component, public uart::UARTDevice {
void restart_and_read_all_info();
void set_bluetooth(bool enable);
void set_distance_resolution(const char *state);
void set_distance_resolution(DistanceResolution resolution);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void factory_reset();
protected:
+25 -35
View File
@@ -1,5 +1,3 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
@@ -20,7 +18,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
GateThresholdNumber = ld2410_ns.class_("GateThresholdNumber", number.Number)
LightThresholdNumber = ld2410_ns.class_("LightThresholdNumber", number.Number)
MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.Number)
CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate"
CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate"
@@ -33,25 +35,25 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Inclusive(CONF_TIMEOUT, TIMEOUT_GROUP): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
unit_of_measurement=UNIT_SECOND,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
),
cv.Inclusive(CONF_MAX_MOVE_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Inclusive(CONF_MAX_STILL_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
number.ApplyNumber,
LightThresholdNumber,
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_LIGHTBULB,
@@ -64,14 +66,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(f"g{x}"): cv.Schema(
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
number.ApplyNumber,
GateThresholdNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
number.ApplyNumber,
GateThresholdNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
@@ -87,14 +89,8 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
numbers = number.sub_numbers(config, parent=hub)
timeouts = {"apply": "set_max_distances_timeout()", "publish": "before"}
await numbers(
CONF_TIMEOUT,
hub.set_timeout_number,
min_value=0,
max_value=65535,
step=1,
**timeouts,
CONF_TIMEOUT, hub.set_timeout_number, min_value=0, max_value=65535, step=1
)
await numbers(
CONF_MAX_MOVE_DISTANCE_GATE,
@@ -102,7 +98,6 @@ async def to_code(config: ConfigType) -> None:
min_value=2,
max_value=8,
step=1,
**timeouts,
)
await numbers(
CONF_MAX_STILL_DISTANCE_GATE,
@@ -110,7 +105,6 @@ async def to_code(config: ConfigType) -> None:
min_value=2,
max_value=8,
step=1,
**timeouts,
)
await numbers(
CONF_LIGHT_THRESHOLD,
@@ -118,25 +112,21 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=255,
step=1,
apply="set_light_out_control()",
publish="before",
)
threshold = {
"min_value": 0,
"max_value": 100,
"step": 1,
"apply": "set_gate_threshold(entity)",
"publish": "before",
}
for x in range(9):
if gate_conf := config.get(f"g{x}"):
await numbers.create(
gate_conf[CONF_MOVE_THRESHOLD],
partial(hub.set_gate_move_threshold_number, x),
**threshold,
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = cg.new_Pvariable(move_config[CONF_ID], x)
await number.register_number(
n, move_config, min_value=0, max_value=100, step=1
)
await numbers.create(
gate_conf[CONF_STILL_THRESHOLD],
partial(hub.set_gate_still_threshold_number, x),
**threshold,
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID], x)
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
@@ -0,0 +1,12 @@
#include "gate_threshold_number.h"
namespace esphome::ld2410 {
GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {}
void GateThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_gate_threshold(this->gate_);
}
} // namespace esphome::ld2410
@@ -0,0 +1,17 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class GateThresholdNumber final : public number::Number, public Parented<LD2410Component> {
public:
GateThresholdNumber(uint8_t gate);
protected:
uint8_t gate_;
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,10 @@
#include "light_threshold_number.h"
namespace esphome::ld2410 {
void LightThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_light_out_control();
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class LightThresholdNumber final : public number::Number, public Parented<LD2410Component> {
public:
LightThresholdNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,10 @@
#include "max_distance_timeout_number.h"
namespace esphome::ld2410 {
void MaxDistanceTimeoutNumber::control(float value) {
this->publish_state(value);
this->parent_->set_max_distances_timeout();
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class MaxDistanceTimeoutNumber final : public number::Number, public Parented<LD2410Component> {
public:
MaxDistanceTimeoutNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -52,7 +52,6 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums and tables; the hub maps by index
await selects(
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
@@ -4,7 +4,7 @@ namespace esphome::ld2410 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
this->parent_->set_baud_rate(this->option_at(index));
}
} // namespace esphome::ld2410
@@ -4,8 +4,7 @@ namespace esphome::ld2410 {
void DistanceResolutionSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_distance_resolution(
static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
this->parent_->set_distance_resolution(this->option_at(index));
}
} // namespace esphome::ld2410
+8 -15
View File
@@ -11,7 +11,10 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch)
EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch)
CONF_ENGINEERING_MODE = "engineering_mode"
@@ -19,13 +22,13 @@ CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema(
switch.ApplySwitch,
EngineeringModeSwitch,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_PULSE,
),
cv.Optional(CONF_BLUETOOTH): switch.switch_schema(
switch.ApplySwitch,
BluetoothSwitch,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_BLUETOOTH,
@@ -36,15 +39,5 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(
CONF_ENGINEERING_MODE,
hub.set_engineering_mode_switch,
apply="set_engineering_mode(state)",
publish="before",
)
await switches(
CONF_BLUETOOTH,
hub.set_bluetooth_switch,
apply="set_bluetooth(state)",
publish="before",
)
await switches(CONF_ENGINEERING_MODE, hub.set_engineering_mode_switch)
await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)
@@ -0,0 +1,10 @@
#include "bluetooth_switch.h"
namespace esphome::ld2410 {
void BluetoothSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_bluetooth(state);
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class BluetoothSwitch final : public switch_::Switch, public Parented<LD2410Component> {
public:
BluetoothSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::ld2410
@@ -0,0 +1,10 @@
#include "engineering_mode_switch.h"
namespace esphome::ld2410 {
void EngineeringModeSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_engineering_mode(state);
}
} // namespace esphome::ld2410
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class EngineeringModeSwitch final : public switch_::Switch, public Parented<LD2410Component> {
public:
EngineeringModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace esphome::ld2410
+14 -2
View File
@@ -1,5 +1,6 @@
#include "ld2410s.h"
#include "ld2410s_number.h"
#include <algorithm>
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
@@ -94,8 +95,19 @@ void LD2410SComponent::dump_config() {
}
void LD2410SComponent::loop() {
if (!this->read_available<16>([this](uint8_t b) { this->receive_byte_(b); })) {
ESP_LOGV(TAG, "UART read failed");
uint8_t chunk[16];
// Take the count once so one pass handles a bounded number of bytes
size_t avail = this->available();
while (avail > 0) {
const size_t len = std::min(avail, sizeof(chunk));
if (!this->read_array(chunk, len)) {
ESP_LOGV(TAG, "UART read of %u bytes failed", static_cast<unsigned>(len));
break;
}
for (size_t i = 0; i < len; i++) {
this->receive_byte_(chunk[i]);
}
avail -= len;
}
const uint32_t now = App.get_loop_component_start_time();
if (this->sequence_ == nullptr) {
+14 -13
View File
@@ -11,20 +11,26 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2412_ID, LD2412Component
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
FactoryResetButton = LD2412_ns.class_("FactoryResetButton", button.Button)
QueryButton = LD2412_ns.class_("QueryButton", button.Button)
RestartButton = LD2412_ns.class_("RestartButton", button.Button)
StartDynamicBackgroundCorrectionButton = LD2412_ns.class_(
"StartDynamicBackgroundCorrectionButton", button.Button
)
CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
button.ApplyButton
FactoryResetButton
),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(QueryButton),
cv.Optional(CONF_RESTART): button.restart_button_schema(RestartButton),
cv.Optional(CONF_START_DYNAMIC_BACKGROUND_CORRECTION): button.button_schema(
button.ApplyButton,
StartDynamicBackgroundCorrectionButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_PULSE,
),
@@ -34,15 +40,10 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="read_all_info()")
await buttons(
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(
CONF_START_DYNAMIC_BACKGROUND_CORRECTION,
hub.set_start_dynamic_background_correction_button,
apply="start_dynamic_background_correction()",
)
@@ -0,0 +1,7 @@
#include "factory_reset_button.h"
namespace esphome::ld2412 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::ld2412
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class FactoryResetButton final : public button::Button, public Parented<LD2412Component> {
public:
FactoryResetButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
@@ -0,0 +1,7 @@
#include "query_button.h"
namespace esphome::ld2412 {
void QueryButton::press_action() { this->parent_->read_all_info(); }
} // namespace esphome::ld2412
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class QueryButton final : public button::Button, public Parented<LD2412Component> {
public:
QueryButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
@@ -0,0 +1,7 @@
#include "restart_button.h"
namespace esphome::ld2412 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace esphome::ld2412
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class RestartButton final : public button::Button, public Parented<LD2412Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
@@ -0,0 +1,9 @@
#include "start_dynamic_background_correction_button.h"
#include "restart_button.h"
namespace esphome::ld2412 {
void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); }
} // namespace esphome::ld2412
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class StartDynamicBackgroundCorrectionButton final : public button::Button, public Parented<LD2412Component> {
public:
StartDynamicBackgroundCorrectionButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
+147 -132
View File
@@ -9,7 +9,6 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cmath>
@@ -18,6 +17,23 @@ namespace esphome::ld2412 {
ESPHOME_LOG_TAG(TAG, "ld2412");
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8,
};
enum DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x03,
DISTANCE_RESOLUTION_0_5 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
enum LightFunction : uint8_t {
LIGHT_FUNCTION_OFF = 0x00,
LIGHT_FUNCTION_BELOW = 0x01,
@@ -66,19 +82,79 @@ enum AckData : uint8_t {
ACK_PAYLOAD = 10,
};
// Index + 1 is the BaudRate value; the select options use the same order
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
// Same order as DISTANCE_RESOLUTIONS
PROGMEM_STRING_TABLE(DistanceResolutionStrings, "0.2m", "0.5m", "0.75m");
// The light function select options are in enum order
static_assert(LIGHT_FUNCTION_OFF == 0 && LIGHT_FUNCTION_BELOW == 1 && LIGHT_FUNCTION_ABOVE == 2,
"LightFunction is the select option index");
// Device values of the out pin level select options, in option order
static constexpr uint8_t OUT_PIN_LEVELS[] PROGMEM = {OUT_PIN_LEVEL_LOW, OUT_PIN_LEVEL_HIGH};
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
struct Uint8ToString {
const uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = {
{"0.2m", DISTANCE_RESOLUTION_0_2},
{"0.5m", DISTANCE_RESOLUTION_0_5},
{"0.75m", DISTANCE_RESOLUTION_0_75},
};
constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = {
{DISTANCE_RESOLUTION_0_2, "0.2m"},
{DISTANCE_RESOLUTION_0_5, "0.5m"},
{DISTANCE_RESOLUTION_0_75, "0.75m"},
};
constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = {
{"off", LIGHT_FUNCTION_OFF},
{"below", LIGHT_FUNCTION_BELOW},
{"above", LIGHT_FUNCTION_ABOVE},
};
constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = {
{LIGHT_FUNCTION_OFF, "off"},
{LIGHT_FUNCTION_BELOW, "below"},
{LIGHT_FUNCTION_ABOVE, "above"},
};
constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = {
{"low", OUT_PIN_LEVEL_LOW},
{"high", OUT_PIN_LEVEL_HIGH},
};
constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
{OUT_PIN_LEVEL_LOW, "low"},
{OUT_PIN_LEVEL_HIGH, "high"},
};
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
for (const auto &entry : arr) {
if (strcmp(str, entry.str) == 0) {
return entry.value;
}
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value) {
return entry.str;
}
}
return ""; // Not found
}
static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Default used when number component is not defined
// Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
@@ -236,7 +312,20 @@ void LD2412Component::restart_and_read_all_info() {
}
void LD2412Component::loop() {
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
}
}
void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
@@ -383,10 +472,8 @@ static inline bool has_value(number::Number *n) { return n != nullptr && n->has_
// Timeout ids 0 (still) and 1 (motion) for a write waiting on a re-query
static constexpr uint32_t GATE_QUERY_TIMEOUT_ID = 0;
static constexpr uint32_t GATE_QUERY_TIMEOUT_MS = 2000;
#endif
// Timeout id for a basic configuration write waiting on a re-query, and how long any such write waits
static constexpr uint32_t BASIC_CONF_TIMEOUT_ID = 2;
static constexpr uint32_t QUERY_TIMEOUT_MS = 2000;
void LD2412Component::handle_ack_data_() {
ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
@@ -437,16 +524,11 @@ void LD2412Component::handle_ack_data_() {
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
break; // too short to hold the resolution
}
const auto index = ld24xx::find_index(DISTANCE_RESOLUTIONS, this->buffer_data_[10]);
if (!index) {
ESP_LOGW(TAG, "Unknown distance resolution %u", this->buffer_data_[10]);
break;
}
ESP_LOGV(TAG, "Distance resolution: %s",
LOG_STR_ARG(DistanceResolutionStrings::get_log_str(*index, DistanceResolutionStrings::LAST_INDEX)));
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
#ifdef USE_SELECT
if (this->distance_resolution_select_ != nullptr) {
this->distance_resolution_select_->publish_state(*index);
this->distance_resolution_select_->publish_state(distance_resolution);
}
#endif
break;
@@ -458,10 +540,11 @@ void LD2412Component::handle_ack_data_() {
}
this->light_function_ = this->buffer_data_[10];
this->light_threshold_ = this->buffer_data_[11];
ESP_LOGV(TAG, "Light function: %u, threshold: %u", this->light_function_, this->light_threshold_);
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
ESP_LOGV(TAG, "Light function: %s, threshold: %u", light_function_str, this->light_threshold_);
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
this->light_function_select_->publish_state(static_cast<size_t>(this->light_function_));
this->light_function_select_->publish_state(light_function_str);
}
#endif
#ifdef USE_NUMBER
@@ -554,44 +637,37 @@ void LD2412Component::handle_ack_data_() {
break;
}
case CMD_QUERY_BASIC_CONF: { // Query parameters response
case CMD_QUERY_BASIC_CONF: // Query parameters response
{
#ifdef USE_NUMBER
if (this->buffer_pos_ < 15 + HEADER_FOOTER_SIZE) {
break; // too short to hold the configuration
}
// True only while a held-back write waits for this answer
const bool pending = this->cancel_timeout(BASIC_CONF_TIMEOUT_ID);
this->basic_conf_read_ = true;
// Min gate: 9th byte, max gate: 10th byte, timeout: 11~12th bytes, output pin level: 13th byte
this->min_gate_ = this->buffer_data_[10];
this->max_gate_ = this->buffer_data_[11];
this->timeout_ = encode_uint16(this->buffer_data_[13], this->buffer_data_[12]);
/*
Moving distance range: 9th byte
Still distance range: 10th byte
*/
set_number_value(this->min_distance_gate_number_, this->buffer_data_[10]);
set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1);
ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10],
this->buffer_data_[11]);
/*
None Duration: 11~12th bytes
*/
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
/*
Output pin configuration: 13th bytes
*/
this->out_pin_level_ = this->buffer_data_[14];
ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->min_gate_, this->max_gate_);
ESP_LOGV(TAG, "timeout_number_: %u", this->timeout_);
// The pending write's user values must survive the answer
#ifdef USE_NUMBER
if (!pending || !has_value(this->min_distance_gate_number_)) {
set_number_value(this->min_distance_gate_number_, this->min_gate_);
}
if (!pending || !has_value(this->max_distance_gate_number_)) {
set_number_value(this->max_distance_gate_number_, this->max_gate_ - 1);
}
if (!pending || !has_value(this->timeout_number_)) {
set_number_value(this->timeout_number_, this->timeout_);
}
#endif
#ifdef USE_SELECT
if (this->out_pin_level_select_ != nullptr && !(pending && this->out_pin_level_select_->has_state())) {
if (const auto index = ld24xx::find_index(OUT_PIN_LEVELS, this->out_pin_level_)) {
this->out_pin_level_select_->publish_state(*index);
}
const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
if (this->out_pin_level_select_ != nullptr) {
this->out_pin_level_select_->publish_state(out_pin_level_str);
}
#endif
if (pending) {
this->set_basic_config();
}
break;
}
#endif
} break;
default:
break;
}
@@ -641,29 +717,15 @@ void LD2412Component::set_bluetooth(bool enable) {
}
void LD2412Component::set_distance_resolution(const char *state) {
const int16_t index = progmem_string_table_find<DistanceResolutionStrings>(state, strlen(state));
if (index >= 0) {
this->set_distance_resolution(static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
}
}
void LD2412Component::set_distance_resolution(DistanceResolution resolution) {
this->set_config_mode_(true);
const uint8_t cmd_value[6] = {resolution, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00};
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
void LD2412Component::set_baud_rate(const char *state) {
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
if (index >= 0) {
this->set_baud_rate(static_cast<BaudRate>(index + 1));
}
}
void LD2412Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {rate, 0x00};
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -697,9 +759,8 @@ void LD2412Component::set_engineering_mode(bool enable) {
void LD2412Component::factory_reset() {
this->set_config_mode_(true);
this->send_command_(CMD_FACTORY_RESET, nullptr, 0);
// Reset discards the settings; the next write must wait for the re-read
this->basic_conf_read_ = false;
#ifdef USE_NUMBER
// Reset discards the thresholds; the next write must wait for the re-read
this->gate_move_thresholds_.read = false;
this->gate_still_thresholds_.read = false;
#endif
@@ -722,22 +783,10 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
void LD2412Component::set_basic_config() {
// Entities that are not configured keep what the module last reported
uint8_t min_gate = this->min_gate_;
uint8_t max_gate = this->max_gate_;
uint16_t timeout = this->timeout_;
uint8_t out_pin_level = this->out_pin_level_;
bool from_module = false;
#ifdef USE_NUMBER
from_module = this->min_distance_gate_number_ == nullptr || this->max_distance_gate_number_ == nullptr ||
this->timeout_number_ == nullptr;
#else
from_module = true;
#endif
#ifndef USE_SELECT
from_module = true;
#endif
uint8_t min_gate = 1;
uint8_t max_gate = TOTAL_GATES;
uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
uint8_t out_pin_level = 0x01;
#ifdef USE_NUMBER
if (this->min_distance_gate_number_ != nullptr) {
@@ -758,23 +807,11 @@ void LD2412Component::set_basic_config() {
#endif
#ifdef USE_SELECT
if (this->out_pin_level_select_ != nullptr) {
const auto index = this->out_pin_level_select_->active_index();
if (!index)
if (!this->out_pin_level_select_->has_state())
return;
out_pin_level = progmem_read_byte(&OUT_PIN_LEVELS[*index]);
} else {
from_module = true;
out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
}
#endif
if (from_module && !this->basic_conf_read_) {
// The module never reported, so the values on hand are the built in defaults
ESP_LOGW(TAG, "Command %02X held back until the module reports its settings", CMD_BASIC_CONF);
this->set_config_mode_(true);
this->query_parameters_();
this->set_config_mode_(false);
this->set_timeout(BASIC_CONF_TIMEOUT_ID, QUERY_TIMEOUT_MS, [this]() { this->basic_conf_timeout_(); });
return;
}
uint8_t value[5] = {
lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
@@ -784,26 +821,6 @@ void LD2412Component::set_basic_config() {
this->set_config_mode_(false);
}
void LD2412Component::basic_conf_timeout_() {
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its settings", CMD_BASIC_CONF);
#ifdef USE_NUMBER
// The values on hand never reached the module, so forgetting them loses nothing
for (number::Number *n : {this->min_distance_gate_number_, this->max_distance_gate_number_, this->timeout_number_}) {
if (n != nullptr) {
n->publish_state(NAN);
}
}
#endif
#ifdef USE_SELECT
// Show the last known or default level until the module reports one
if (this->out_pin_level_select_ != nullptr) {
if (const auto index = ld24xx::find_index(OUT_PIN_LEVELS, this->out_pin_level_)) {
this->out_pin_level_select_->publish_state(*index);
}
}
#endif
}
#ifdef USE_NUMBER
void LD2412Component::send_gate_thresholds_(bool motion) {
GateThresholds &group = this->gate_thresholds_(motion);
@@ -821,9 +838,9 @@ void LD2412Component::send_gate_thresholds_(bool motion) {
}
if (configured < TOTAL_GATES && !group.read) {
// Unread gates would go out as zero, which is maximum sensitivity
ESP_LOGW(TAG, "Command %02X held back until the module reports its settings", command);
ESP_LOGW(TAG, "Command %02X held back until the module reports its gate thresholds", command);
this->send_command_(motion ? CMD_QUERY_MOTION_GATE_SENS : CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
this->set_timeout(GATE_QUERY_TIMEOUT_ID + motion, QUERY_TIMEOUT_MS,
this->set_timeout(GATE_QUERY_TIMEOUT_ID + motion, GATE_QUERY_TIMEOUT_MS,
[this, motion]() { this->gate_query_timeout_(motion); });
return;
}
@@ -831,7 +848,7 @@ void LD2412Component::send_gate_thresholds_(bool motion) {
}
void LD2412Component::gate_query_timeout_(bool motion) {
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its settings",
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its gate thresholds",
motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS);
// The values on hand never reached the module or are stale, so forgetting them loses nothing
for (number::Number *n : this->gate_thresholds_(motion).numbers) {
@@ -864,10 +881,8 @@ void LD2412Component::set_light_out_control() {
}
#endif
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
if (const auto index = this->light_function_select_->active_index()) {
this->light_function_ = *index;
}
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
}
#endif
uint8_t value[2] = {this->light_function_, this->light_threshold_};
+1 -31
View File
@@ -35,28 +35,6 @@ using namespace ld24xx;
static constexpr uint8_t MAX_LINE_LENGTH = 54; // Max characters for serial buffer
static constexpr uint8_t TOTAL_GATES = 14; // Total number of gates supported by the LD2412
static constexpr uint16_t DEFAULT_PRESENCE_TIMEOUT = 5;
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8,
};
enum DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x03,
DISTANCE_RESOLUTION_0_5 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
// Device values of the distance resolution select options, in option order; in flash on ESP8266
static constexpr uint8_t DISTANCE_RESOLUTIONS[] PROGMEM = {DISTANCE_RESOLUTION_0_2, DISTANCE_RESOLUTION_0_5,
DISTANCE_RESOLUTION_0_75};
class LD2412Component final : public Component, public uart::UARTDevice {
#ifdef USE_BINARY_SENSOR
@@ -125,9 +103,7 @@ class LD2412Component final : public Component, public uart::UARTDevice {
void restart_and_read_all_info();
void set_bluetooth(bool enable);
void set_distance_resolution(const char *state);
void set_distance_resolution(DistanceResolution resolution);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void factory_reset();
void start_dynamic_background_correction();
@@ -137,7 +113,6 @@ class LD2412Component final : public Component, public uart::UARTDevice {
void handle_periodic_data_();
void handle_ack_data_();
bool handle_frame_(uint8_t frame_len);
void basic_conf_timeout_();
void readline_(int readch);
void query_parameters_();
void get_version_();
@@ -162,12 +137,7 @@ class LD2412Component final : public Component, public uart::UARTDevice {
uint8_t light_function_ = 0;
uint8_t light_threshold_ = 0;
// Basic configuration as last reported by the module; resent for entities that are not configured
bool basic_conf_read_{false}; // answered since boot or the last factory reset
uint16_t timeout_ = DEFAULT_PRESENCE_TIMEOUT;
uint8_t min_gate_ = 1;
uint8_t max_gate_ = TOTAL_GATES;
uint8_t out_pin_level_ = 1;
uint8_t out_pin_level_ = 0;
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t mac_address_[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
+20 -31
View File
@@ -1,5 +1,3 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
@@ -20,7 +18,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2412_ID, LD2412Component
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number)
LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number)
MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number)
CONF_LIGHT_THRESHOLD = "light_threshold"
CONF_MAX_DISTANCE_GATE = "max_distance_gate"
@@ -33,25 +35,25 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
number.ApplyNumber,
LightThresholdNumber,
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_LIGHTBULB,
),
cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_TIMEOUT): number.number_schema(
number.ApplyNumber,
MaxDistanceTimeoutNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
unit_of_measurement=UNIT_SECOND,
@@ -64,14 +66,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(f"gate_{x}"): (
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
number.ApplyNumber,
GateThresholdNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
number.ApplyNumber,
GateThresholdNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
@@ -93,17 +95,13 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=255,
step=1,
apply="set_light_out_control()",
publish="before",
)
basic = {"apply": "set_basic_config()", "publish": "before"}
await numbers(
CONF_MAX_DISTANCE_GATE,
hub.set_max_distance_gate_number,
min_value=2,
max_value=13,
step=1,
**basic,
)
await numbers(
CONF_MIN_DISTANCE_GATE,
@@ -111,32 +109,23 @@ async def to_code(config: ConfigType) -> None:
min_value=1,
max_value=12,
step=1,
**basic,
)
threshold = {
"min_value": 0,
"max_value": 100,
"step": 1,
"apply": "set_gate_threshold(entity)",
"publish": "before",
}
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
await numbers.create(
gate_conf[CONF_MOVE_THRESHOLD],
partial(hub.set_gate_move_threshold_number, x),
**threshold,
)
await numbers.create(
gate_conf[CONF_STILL_THRESHOLD],
partial(hub.set_gate_still_threshold_number, x),
**threshold,
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = await number.new_number(move_config, min_value=0, max_value=100, step=1)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = await number.new_number(
still_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
await numbers(
CONF_TIMEOUT,
hub.set_timeout_number,
min_value=0,
max_value=900,
step=1,
**basic,
)
@@ -0,0 +1,10 @@
#include "gate_threshold_number.h"
namespace esphome::ld2412 {
void GateThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_gate_threshold(this);
}
} // namespace esphome::ld2412
@@ -0,0 +1,17 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class GateThresholdNumber final : public number::Number, public Parented<LD2412Component> {
public:
// Not "= default": that makes new(p) T() zero-fill the object at every codegen site before the ctor runs.
GateThresholdNumber() {}
protected:
void control(float value) override;
};
} // namespace esphome::ld2412
@@ -0,0 +1,10 @@
#include "light_threshold_number.h"
namespace esphome::ld2412 {
void LightThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_light_out_control();
}
} // namespace esphome::ld2412
@@ -0,0 +1,17 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class LightThresholdNumber final : public number::Number, public Parented<LD2412Component> {
public:
// User provided, not "= default": `new(p) LightThresholdNumber()` would zero-fill .bss that is already zero.
LightThresholdNumber() {}
protected:
void control(float value) override;
};
} // namespace esphome::ld2412
@@ -0,0 +1,10 @@
#include "max_distance_timeout_number.h"
namespace esphome::ld2412 {
void MaxDistanceTimeoutNumber::control(float value) {
this->publish_state(value);
this->parent_->set_basic_config();
}
} // namespace esphome::ld2412
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class MaxDistanceTimeoutNumber final : public number::Number, public Parented<LD2412Component> {
public:
MaxDistanceTimeoutNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2412
+1 -5
View File
@@ -42,7 +42,7 @@ CONFIG_SCHEMA = {
icon=ICON_LIGHTBULB,
),
cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema(
select.ApplySelect,
LightOutControlSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
@@ -52,7 +52,6 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums and tables; the hub maps by index
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
@@ -77,11 +76,8 @@ async def to_code(config: ConfigType) -> None:
hub.set_light_function_select,
options=["off", "below", "above"],
)
# The pin level travels in the basic configuration command, not the light control one
await selects(
CONF_OUT_PIN_LEVEL,
hub.set_out_pin_level_select,
options=["low", "high"],
apply="set_basic_config()",
publish="before",
)
Loaded 100 of 245 files, more files were not shown because too many files have changed in this diff. Show more