Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2026-03-31 20:15:36 -10:00
61 changed files with 1193 additions and 679 deletions
+1 -1
View File
@@ -339,7 +339,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4
uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
+7 -1
View File
@@ -261,7 +261,13 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
uint32_t val;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian — direct load on LE platforms
memcpy(&val, ptr, 4);
#else
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
#endif
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
}
@@ -126,7 +126,7 @@ void BMP581Component::setup() {
}
// verify id
if (chip_id != BMP581_ASIC_ID) {
if (chip_id != BMP581_ASIC_ID && chip_id != BMP585_ASIC_ID) {
ESP_LOGE(TAG, "Unknown chip ID");
this->error_code_ = ERROR_WRONG_CHIP_ID;
+2 -1
View File
@@ -8,7 +8,8 @@
namespace esphome::bmp581_base {
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
static const uint8_t BMP585_ASIC_ID = 0x51;
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
// BMP581 Register Addresses
enum {
@@ -432,6 +432,9 @@ class ESP32BLETracker : public Component,
bool scan_continuous_;
bool scan_active_;
bool scan_continuous_before_ota_{false};
#ifdef USE_OTA_STATE_LISTENER
bool scan_continuous_before_ota_{false};
#endif
bool ble_was_disabled_{true};
bool raw_advertisements_{false};
bool parse_advertisements_{false};
@@ -448,6 +448,13 @@ void Esp32HostedUpdate::perform(bool force) {
return;
}
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
if (this->firmware_url_.empty()) {
ESP_LOGW(TAG, "No firmware URL available, run check first");
return;
}
#endif
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
-1
View File
@@ -675,7 +675,6 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01);
}
out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
out_data->display_status = this->get_display_state() ? 1 : 0;
this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01);
out_data->health_mode = this->get_health_mode() ? 1 : 0;
@@ -17,6 +17,7 @@
namespace esphome::http_request {
static const char *const TAG = "http_request.idf";
static constexpr uint32_t ERROR_DURATION_MS = 1000;
struct UserData {
const std::vector<std::string> &lower_case_collect_headers;
@@ -57,7 +58,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
this->status_momentary_error("failed", ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
return nullptr;
}
@@ -74,7 +75,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
} else if (method == "PATCH") {
method_idf = HTTP_METHOD_PATCH;
} else {
this->status_momentary_error("failed", 1000);
this->status_momentary_error("failed", ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
return nullptr;
}
@@ -112,6 +113,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.event_handler = http_event_handler;
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == nullptr) {
this->status_momentary_error("failed", ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized");
return nullptr;
}
std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
container->set_parent(this);
@@ -129,7 +135,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
esp_err_t err = esp_http_client_open(client, body_len);
if (err != ESP_OK) {
this->status_momentary_error("failed", 1000);
this->status_momentary_error("failed", ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return nullptr;
@@ -151,7 +157,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
if (err != ESP_OK) {
this->status_momentary_error("failed", 1000);
this->status_momentary_error("failed", ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return nullptr;
@@ -176,7 +182,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
err = esp_http_client_set_redirection(client);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
this->status_momentary_error("failed", 1000);
this->status_momentary_error("failed", ERROR_DURATION_MS);
esp_http_client_cleanup(client);
return nullptr;
}
@@ -189,7 +195,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
this->status_momentary_error("failed", 1000);
this->status_momentary_error("failed", ERROR_DURATION_MS);
esp_http_client_cleanup(client);
return nullptr;
}
@@ -214,7 +220,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
this->status_momentary_error("failed", 1000);
this->status_momentary_error("failed", ERROR_DURATION_MS);
return container;
}
@@ -1,18 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace internal_temperature {
namespace esphome::internal_temperature {
class InternalTemperatureSensor : public sensor::Sensor, public PollingComponent {
public:
#if defined(USE_ESP32) || (defined(USE_ZEPHYR) && defined(USE_NRF52))
void setup() override;
#endif // USE_ESP32 || (USE_ZEPHYR && USE_NRF52)
void dump_config() override;
void update() override;
};
} // namespace internal_temperature
} // namespace esphome
} // namespace esphome::internal_temperature
@@ -0,0 +1,41 @@
#ifdef USE_BK72XX
#include "esphome/core/log.h"
#include "internal_temperature.h"
extern "C" {
uint32_t temp_single_get_current_temperature(uint32_t *temp_value);
}
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.bk72xx";
void InternalTemperatureSensor::update() {
float temperature = NAN;
bool success = false;
uint32_t raw, result;
result = temp_single_get_current_temperature(&raw);
success = (result == 0);
#if defined(USE_LIBRETINY_VARIANT_BK7231N)
temperature = raw * -0.38f + 156.0f;
#elif defined(USE_LIBRETINY_VARIANT_BK7231T)
temperature = raw * 0.04f;
#else // USE_LIBRETINY_VARIANT
temperature = raw * 0.128f;
#endif // USE_LIBRETINY_VARIANT
if (success && std::isfinite(temperature)) {
this->publish_state(temperature);
} else {
ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature);
if (!this->has_state()) {
this->publish_state(NAN);
}
}
}
} // namespace esphome::internal_temperature
#endif // USE_BK72XX
@@ -0,0 +1,10 @@
#include "esphome/core/log.h"
#include "internal_temperature.h"
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature";
void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); }
} // namespace esphome::internal_temperature
@@ -1,7 +1,8 @@
#include "internal_temperature.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include "internal_temperature.h"
#if defined(USE_ESP32_VARIANT_ESP32)
// there is no official API available on the original ESP32
extern "C" {
@@ -13,70 +14,20 @@ uint8_t temprature_sens_read();
defined(USE_ESP32_VARIANT_ESP32S3)
#include "driver/temperature_sensor.h"
#endif // USE_ESP32_VARIANT
#endif // USE_ESP32
#ifdef USE_RP2040
#include "Arduino.h"
#endif // USE_RP2040
#ifdef USE_BK72XX
extern "C" {
uint32_t temp_single_get_current_temperature(uint32_t *temp_value);
}
#endif // USE_BK72XX
#if defined(USE_ZEPHYR) && defined(USE_NRF52)
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#endif // USE_ZEPHYR && USE_NRF52
namespace esphome {
namespace internal_temperature {
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.esp32";
static const char *const TAG = "internal_temperature";
#if defined(USE_ZEPHYR) && defined(USE_NRF52)
static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp);
#endif // USE_ZEPHYR && USE_NRF52
#ifdef USE_ESP32
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
static temperature_sensor_handle_t tsensNew = NULL;
#endif // USE_ESP32_VARIANT
#endif // USE_ESP32
void InternalTemperatureSensor::update() {
#if defined(USE_ZEPHYR) && defined(USE_NRF52)
struct sensor_value value;
int result = sensor_sample_fetch(DIE_TEMPERATURE_SENSOR);
if (result != 0) {
ESP_LOGE(TAG, "Failed to fetch nRF52 die temperature sample (%d)", result);
if (!this->has_state()) {
this->publish_state(NAN);
}
return;
}
result = sensor_channel_get(DIE_TEMPERATURE_SENSOR, SENSOR_CHAN_DIE_TEMP, &value);
if (result != 0) {
ESP_LOGE(TAG, "Failed to get nRF52 die temperature (%d)", result);
if (!this->has_state()) {
this->publish_state(NAN);
}
return;
}
const float temperature = value.val1 + (value.val2 / 1000000.0f);
if (std::isfinite(temperature)) {
this->publish_state(temperature);
} else {
ESP_LOGD(TAG, "Ignoring invalid nRF52 temperature (value=%.1f)", temperature);
if (!this->has_state()) {
this->publish_state(NAN);
}
}
#else
float temperature = NAN;
bool success = false;
#ifdef USE_ESP32
#if defined(USE_ESP32_VARIANT_ESP32)
uint8_t raw = temprature_sens_read();
ESP_LOGV(TAG, "Raw temperature value: %d", raw);
@@ -92,23 +43,7 @@ void InternalTemperatureSensor::update() {
ESP_LOGE(TAG, "Reading failed (%d)", result);
}
#endif // USE_ESP32_VARIANT
#endif // USE_ESP32
#ifdef USE_RP2040
temperature = analogReadTemp();
success = (temperature != 0.0f);
#endif // USE_RP2040
#ifdef USE_BK72XX
uint32_t raw, result;
result = temp_single_get_current_temperature(&raw);
success = (result == 0);
#if defined(USE_LIBRETINY_VARIANT_BK7231N)
temperature = raw * -0.38f + 156.0f;
#elif defined(USE_LIBRETINY_VARIANT_BK7231T)
temperature = raw * 0.04f;
#else // USE_LIBRETINY_VARIANT
temperature = raw * 0.128f;
#endif // USE_LIBRETINY_VARIANT
#endif // USE_BK72XX
if (success && std::isfinite(temperature)) {
this->publish_state(temperature);
} else {
@@ -117,18 +52,9 @@ void InternalTemperatureSensor::update() {
this->publish_state(NAN);
}
}
#endif // USE_ZEPHYR && USE_NRF52
}
void InternalTemperatureSensor::setup() {
#if defined(USE_ZEPHYR) && defined(USE_NRF52)
if (!device_is_ready(DIE_TEMPERATURE_SENSOR)) {
ESP_LOGE(TAG, "nRF52 die temperature sensor device %s not ready", DIE_TEMPERATURE_SENSOR->name);
this->mark_failed();
return;
}
#endif // USE_ZEPHYR && USE_NRF52
#ifdef USE_ESP32
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
@@ -148,10 +74,8 @@ void InternalTemperatureSensor::setup() {
return;
}
#endif // USE_ESP32_VARIANT
#endif // USE_ESP32
}
void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); }
} // namespace esphome::internal_temperature
} // namespace internal_temperature
} // namespace esphome
#endif // USE_ESP32
@@ -0,0 +1,31 @@
#ifdef USE_RP2040
#include "esphome/core/log.h"
#include "internal_temperature.h"
#include "Arduino.h"
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.rp2040";
void InternalTemperatureSensor::update() {
float temperature = NAN;
bool success = false;
temperature = analogReadTemp();
success = (temperature != 0.0f);
if (success && std::isfinite(temperature)) {
this->publish_state(temperature);
} else {
ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature);
if (!this->has_state()) {
this->publish_state(NAN);
}
}
}
} // namespace esphome::internal_temperature
#endif // USE_RP2040
@@ -0,0 +1,56 @@
#if defined(USE_ZEPHYR) && defined(USE_NRF52)
#include "esphome/core/log.h"
#include "internal_temperature.h"
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.zephyr";
static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp);
void InternalTemperatureSensor::update() {
struct sensor_value value;
int result = sensor_sample_fetch(DIE_TEMPERATURE_SENSOR);
if (result != 0) {
ESP_LOGE(TAG, "Failed to fetch nRF52 die temperature sample (%d)", result);
if (!this->has_state()) {
this->publish_state(NAN);
}
return;
}
result = sensor_channel_get(DIE_TEMPERATURE_SENSOR, SENSOR_CHAN_DIE_TEMP, &value);
if (result != 0) {
ESP_LOGE(TAG, "Failed to get nRF52 die temperature (%d)", result);
if (!this->has_state()) {
this->publish_state(NAN);
}
return;
}
const float temperature = value.val1 + (value.val2 / 1000000.0f);
if (std::isfinite(temperature)) {
this->publish_state(temperature);
} else {
ESP_LOGD(TAG, "Ignoring invalid nRF52 temperature (value=%.1f)", temperature);
if (!this->has_state()) {
this->publish_state(NAN);
}
}
}
void InternalTemperatureSensor::setup() {
if (!device_is_ready(DIE_TEMPERATURE_SENSOR)) {
ESP_LOGE(TAG, "nRF52 die temperature sensor device %s not ready", DIE_TEMPERATURE_SENSOR->name);
this->mark_failed();
return;
}
}
} // namespace esphome::internal_temperature
#endif // USE_ZEPHYR && USE_NRF52
@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
@@ -11,6 +12,7 @@ from esphome.const import (
PLATFORM_RP2040,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
PlatformFramework,
)
from esphome.core import CORE
@@ -39,3 +41,18 @@ async def to_code(config):
if CORE.using_zephyr and CORE.is_nrf52:
zephyr_add_prj_conf("SENSOR", True)
zephyr_add_prj_conf("TEMP_NRF5", True)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"internal_temperature_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"internal_temperature_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"internal_temperature_bk72xx.cpp": {
PlatformFramework.BK72XX_ARDUINO,
},
"internal_temperature_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
}
)
+2 -8
View File
@@ -380,7 +380,8 @@ async def to_code(configs):
# This must be done after all widgets are created
for comp in helpers.lvgl_components_required:
cg.add_define(f"USE_LVGL_{comp.upper()}")
lv_image_formats = df.get_color_formats().copy()
# Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending
lv_image_formats = {"RGB565", "ARGB8888"}
if {
"transform_rotation",
"transform_scale",
@@ -388,10 +389,6 @@ async def to_code(configs):
"transform_scale_y",
} & styles_used:
df.add_define("LV_COLOR_SCREEN_TRANSP", "1")
lv_image_formats.add("ARGB8888")
lv_image_formats.add(
"RGB565"
) # Currently always need RGB565 for the display buffer
for use in helpers.lv_uses:
df.add_define(f"LV_USE_{use.upper()}")
cg.add_define(f"USE_LVGL_{use.upper()}")
@@ -401,9 +398,6 @@ async def to_code(configs):
metadata = get_image_metadata(image_id.id)
image_type = IMAGE_TYPE[metadata.image_type]
transparent = metadata.transparency != CONF_OPAQUE
if transparent:
# Internal draw layer will use ARGB8888
lv_image_formats.add("ARGB8888")
if image_type == ImageBinary:
lv_image_formats.add("I1")
if image_type == ImageGrayscale:
+2 -2
View File
@@ -136,7 +136,7 @@ async def update_to_code(config, action_id, template_arg, args):
widget.type.w_type.value_property is not None
and widget.type.w_type.value_property in config
):
lv.event_send(widget.obj, UPDATE_EVENT, nullptr)
lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(
@@ -455,6 +455,6 @@ async def obj_refresh_to_code(config, action_id, template_arg, args):
widget.type.w_type.value_property is not None
and widget.type.w_type.value_property in config
):
lv.event_send(widget.obj, UPDATE_EVENT, nullptr)
lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr)
return await action_to_code(widget, do_refresh, action_id, template_arg, args)
+1 -4
View File
@@ -52,10 +52,6 @@ def get_remapped_uses():
return get_data(KEY_REMAPPED_USES, set())
def get_color_formats():
return get_data(KEY_COLOR_FORMATS, set())
def add_warning(msg: str):
get_warnings().add(msg)
@@ -541,6 +537,7 @@ CONF_END_ANGLE = "end_angle"
CONF_END_VALUE = "end_value"
CONF_ENTER_BUTTON = "enter_button"
CONF_ENTRIES = "entries"
CONF_EXT_CLICK_AREA = "ext_click_area"
CONF_FLAGS = "flags"
CONF_FLEX_FLOW = "flex_flow"
CONF_FLEX_ALIGN_MAIN = "flex_align_main"
-5
View File
@@ -253,14 +253,10 @@ class MockLv:
A mock object that can be used to generate LVGL calls.
"""
# Mapping for LVGL 9
ATTR_MAP = {"event_send": "obj_send_event", "dither": "bg_dither_mode"}
def __init__(self, base):
self.base = base
def __getattr__(self, attr: str) -> "MockLv":
attr = MockLv.ATTR_MAP.get(attr, attr)
return MockLv(f"{self.base}{attr}")
def append(self, expression):
@@ -314,7 +310,6 @@ class ReturnStatement(ExpressionStatement):
class LvExpr(MockLv):
def __getattr__(self, attr: str) -> "MockLv":
attr = MockLv.ATTR_MAP.get(attr, attr)
return LvExpr(f"{self.base}{attr}")
def append(self, expression):
+13 -13
View File
@@ -343,26 +343,26 @@ void IndicatorLine::set_value(int value) {
}
void IndicatorLine::update_length_() {
uint32_t actual_needle_length;
auto radius = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2;
auto cx = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2;
auto cy = lv_obj_get_height(lv_obj_get_parent(this->obj)) / 2;
auto radius = clamp_at_most(cx, cy);
auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN);
auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN);
if (LV_COORD_IS_PCT(radial_offset)) {
radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100;
}
if (LV_COORD_IS_PCT(length)) {
actual_needle_length = radius * LV_COORD_GET_PCT(length) / 100;
length = radius * LV_COORD_GET_PCT(length) / 100;
} else if (length < 0) {
actual_needle_length = radius + length;
} else {
actual_needle_length = length;
length += radius;
}
auto x = lv_trigo_cos(this->angle_) / 32768.0f;
auto y = lv_trigo_sin(this->angle_) / 32768.0f;
// radius here also represents the offset of the scale center from top left
this->points_[0].x = radius + radial_offset * x;
this->points_[0].y = radius + radial_offset * y;
this->points_[1].x = x * actual_needle_length + radius;
this->points_[1].y = y * actual_needle_length + radius;
this->points_[1].x = radius + x * (radial_offset + length);
this->points_[1].y = radius + y * (radial_offset + length);
lv_obj_refresh_self_size(this->obj);
lv_obj_invalidate(this->obj);
}
@@ -682,15 +682,15 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en
auto *line_dsc = static_cast<lv_draw_line_dsc_t *>(lv_draw_task_get_draw_dsc(task));
int tick = line_dsc->base.id2;
if (tick >= range_start && tick <= range_end) {
unsigned range = range_end - range_start;
int ratio;
if (local) {
int range = range_end - range_start;
tick -= range_start;
ratio = range == 0 ? 0 : (tick * 255) / range;
} else {
range = lv_scale_get_total_tick_count(scale) - 1;
// total tick count is guaranteed to be at least 2.
ratio = (line_dsc->base.id1 * 255) / (lv_scale_get_total_tick_count(scale) - 1);
}
if (range == 0)
range = 1;
auto ratio = (tick * 255) / range;
line_dsc->color = lv_color_mix(color_end, color_start, ratio);
line_dsc->width += width;
}
+2 -2
View File
@@ -12,7 +12,7 @@ from ..lvcode import (
UPDATE_EVENT,
LambdaContext,
ReturnStatement,
lv,
lv_obj,
lvgl_static,
)
from ..types import LV_EVENT, LvNumber, lvgl_ns
@@ -40,7 +40,7 @@ async def to_code(config):
await widget.set_property(
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
)
lv.event_send(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
event_code = (
LV_EVENT.VALUE_CHANGED
if not config[CONF_UPDATE_ON_RELEASE]
+53 -14
View File
@@ -146,26 +146,41 @@ def point_schema(value):
# All LVGL styles and their validators
STYLE_PROPS = {
BASE_PROPS = {
"align": df.CHILD_ALIGNMENTS.one_of,
"arc_opa": lvalid.opacity,
"anim_duration": lvalid.lv_milliseconds,
"arc_color": lvalid.lv_color,
"arc_opa": lvalid.opacity,
"arc_rounded": lvalid.lv_bool,
"arc_width": lvalid.pixels,
"anim_time": lvalid.lv_milliseconds,
"base_dir": df.LvConstant("LV_BASE_DIR_", "LTR", "RTL", "AUTO").one_of,
"bg_color": lvalid.lv_color,
"bg_grad": lv_gradient,
"bg_grad_color": lvalid.lv_color,
"bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of,
"bg_grad_dir": LV_GRAD_DIR.one_of,
"bg_grad_opa": lvalid.opacity,
"bg_grad_stop": lvalid.stop_value,
"bg_image_opa": lvalid.opacity,
"bg_image_recolor": lvalid.lv_color,
"bg_image_recolor_opa": lvalid.opacity,
"bg_image_src": lvalid.lv_image,
"bg_image_tiled": lvalid.lv_bool,
"bg_main_opa": lvalid.opacity,
"bg_main_stop": lvalid.stop_value,
"bg_opa": lvalid.opacity,
"blend_mode": df.LvConstant(
"LV_BLEND_MODE_",
"NORMAL",
"ADDITIVE",
"SUBTRACTIVE",
"MULTIPLY",
"DIFFERENCE",
).one_of,
"blur_backdrop": lvalid.lv_bool,
"blur_quality": df.LvConstant(
"LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION"
).one_of,
"blur_radius": lvalid.lv_positive_int,
"border_color": lvalid.lv_color,
"border_opa": lvalid.opacity,
"border_post": lvalid.lv_bool,
@@ -175,33 +190,53 @@ STYLE_PROPS = {
"border_width": lvalid.lv_positive_int,
"clip_corner": lvalid.lv_bool,
"color_filter_opa": lvalid.opacity,
"drop_shadow_color": lvalid.lv_color,
"drop_shadow_offset_x": lvalid.lv_int,
"drop_shadow_offset_y": lvalid.lv_int,
"drop_shadow_opa": lvalid.opacity,
"drop_shadow_quality": df.LvConstant(
"LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION"
).one_of,
"drop_shadow_radius": lvalid.lv_positive_int,
"height": lvalid.size,
"image_opa": lvalid.opacity,
"image_recolor": lvalid.lv_color,
"image_recolor_opa": lvalid.opacity,
"length": lvalid.pixels_or_percent,
"line_color": lvalid.lv_color,
"line_dash_gap": lvalid.lv_positive_int,
"line_dash_width": lvalid.lv_positive_int,
"line_opa": lvalid.opacity,
"line_rounded": lvalid.lv_bool,
"line_width": lvalid.lv_positive_int,
"margin_bottom": lvalid.padding,
"margin_left": lvalid.padding,
"margin_right": lvalid.padding,
"margin_top": lvalid.padding,
"max_height": lvalid.pixels_or_percent,
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"opa": lvalid.opacity,
"opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity,
"outline_pad": lvalid.padding,
"outline_width": lvalid.pixels,
"length": lvalid.pixels_or_percent,
"pad_all": lvalid.padding,
"pad_bottom": lvalid.padding,
"pad_left": lvalid.padding,
"pad_radial": lvalid.padding,
"pad_right": lvalid.padding,
"pad_top": lvalid.padding,
"radial_offset": lvalid.size,
"radius": lvalid.lv_fraction,
"recolor": lvalid.lv_color,
"recolor_opa": lvalid.opacity,
"rotary_sensitivity": lvalid.lv_positive_int,
"shadow_color": lvalid.lv_color,
"shadow_offset_x": lvalid.lv_int,
"shadow_offset_y": lvalid.lv_int,
"shadow_ofs_x": lvalid.lv_int,
"shadow_ofs_y": lvalid.lv_int,
"shadow_opa": lvalid.opacity,
"shadow_spread": lvalid.lv_int,
"shadow_width": lvalid.lv_positive_int,
@@ -216,7 +251,9 @@ STYLE_PROPS = {
"text_letter_space": lvalid.lv_positive_int,
"text_line_space": lvalid.lv_positive_int,
"text_opa": lvalid.opacity,
"transform_angle": lvalid.lv_angle,
"text_outline_stroke_color": lvalid.lv_color,
"text_outline_stroke_opa": lvalid.opacity,
"text_outline_stroke_width": lvalid.lv_positive_int,
"transform_height": lvalid.pixels_or_percent,
"transform_pivot_x": lvalid.pixels_or_percent,
"transform_pivot_y": lvalid.pixels_or_percent,
@@ -226,20 +263,17 @@ STYLE_PROPS = {
"transform_scale_y": lvalid.scale,
"transform_skew_x": lvalid.lv_angle,
"transform_skew_y": lvalid.lv_angle,
"transform_zoom": lvalid.scale,
"transform_width": lvalid.pixels_or_percent,
"translate_radial": lvalid.lv_int,
"translate_x": lvalid.pixels_or_percent,
"translate_y": lvalid.pixels_or_percent,
"max_height": lvalid.pixels_or_percent,
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"radius": lvalid.lv_fraction,
"width": lvalid.size,
"x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent,
}
STYLE_REMAP = {
"anim_time": "anim_duration",
"transform_angle": "transform_rotation",
"transform_zoom": "transform_scale",
"zoom": "scale",
@@ -249,6 +283,10 @@ STYLE_REMAP = {
"r_mod": "length",
}
STYLE_PROPS = BASE_PROPS | {
p: BASE_PROPS[v] for p, v in STYLE_REMAP.items() if v in BASE_PROPS
}
def remap_property(prop, record=True):
"""
@@ -394,6 +432,7 @@ def obj_schema(widget_type: WidgetType):
return (
part_schema(widget_type.parts)
.extend(ALIGN_TO_SCHEMA)
.extend({cv.Optional(df.CONF_EXT_CLICK_AREA): lvalid.pixels})
.extend(automation_schema(widget_type.w_type))
.extend(
{
+4 -27
View File
@@ -4,26 +4,12 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import ID
from .defines import (
CONF_STYLE_DEFINITIONS,
CONF_THEME,
CONF_TOP_LAYER,
LValidator,
literal,
)
from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal
from .helpers import add_lv_use
from .lvcode import LambdaContext, LocalVariable, lv
from .lvcode import LambdaContext, lv
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property
from .types import ObjUpdateAction, lv_obj_t, lv_style_t
from .widgets import (
Widget,
add_widgets,
collect_parts,
set_obj_properties,
theme_widget_map,
wait_for_widgets,
)
from .widgets.obj import obj_spec
from .types import ObjUpdateAction, lv_style_t
from .widgets import collect_parts, theme_widget_map, wait_for_widgets
def has_style_props(config) -> bool:
@@ -112,12 +98,3 @@ async def theme_to_code(config):
for state, props in states.items()
}
theme_widget_map[w_name] = styles
async def add_top_layer(lv_component, config):
top_layer = lv.disp_get_layer_top(lv_component.var.get_disp())
if top_conf := config.get(CONF_TOP_LAYER):
with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj:
top_w = Widget(top_layer_obj, obj_spec, top_conf)
await set_obj_properties(top_w, top_conf)
await add_widgets(top_w, top_conf)
+2 -2
View File
@@ -13,8 +13,8 @@ from ..lvcode import (
LambdaContext,
LvConditional,
LvContext,
lv,
lv_add,
lv_obj,
lvgl_static,
)
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns
@@ -39,7 +39,7 @@ async def to_code(config):
widget.add_state(LV_STATE.CHECKED)
cond.else_()
widget.clear_state(LV_STATE.CHECKED)
lv.event_send(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
control.add(switch_id.publish_state(v))
switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda())
await cg.register_component(switch, config)
+2 -2
View File
@@ -10,8 +10,8 @@ from ..lvcode import (
UPDATE_EVENT,
LambdaContext,
LvContext,
lv,
lv_add,
lv_obj,
lvgl_static,
)
from ..types import LV_EVENT, LvText, lvgl_ns
@@ -33,7 +33,7 @@ async def to_code(config):
await wait_for_widgets()
async with LambdaContext([(cg.std_string, "text_value")]) as control:
await widget.set_property("text", "text_value.c_str()")
lv.event_send(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
control.add(textvar.publish_state(widget.get_value()))
async with LambdaContext(EVENT_ARG) as lamb:
lv_add(textvar.publish_state(widget.get_value()))
+3
View File
@@ -15,6 +15,7 @@ from .defines import (
CONF_ALIGN,
CONF_ALIGN_TO,
CONF_ALIGN_TO_LAMBDA_ID,
CONF_EXT_CLICK_AREA,
DIRECTIONS,
LV_EVENT_MAP,
LV_EVENT_TRIGGERS,
@@ -113,6 +114,8 @@ async def generate_align_tos(config: dict):
x = align_to[CONF_X]
y = align_to[CONF_Y]
lv.obj_align_to(w.obj, target, align, x, y)
if ext_click_area := w.config.get(CONF_EXT_CLICK_AREA):
lv.obj_set_ext_click_area(w.obj, ext_click_area)
action_id = config[CONF_ALIGN_TO_LAMBDA_ID]
var = new_Pvariable(action_id, await context.get_lambda())
+5 -2
View File
@@ -1,5 +1,4 @@
import sys
from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.automation import register_action
@@ -405,7 +404,11 @@ class Widget:
# Map of widgets to their config, used for trigger generation
widget_map: dict[Any, Widget] = {}
widget_map: dict[ID, Widget] = {}
def is_widget_completed(name: ID) -> bool:
return name in widget_map
class LvScrActType(WidgetType):
@@ -42,7 +42,6 @@ from ..defines import (
CONF_SRC,
CONF_START_ANGLE,
addr,
get_color_formats,
literal,
)
from ..lv_validation import (
@@ -99,7 +98,6 @@ class CanvasType(WidgetType):
# RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel)
if config[CONF_TRANSPARENT]:
color_format = "LV_COLOR_FORMAT_ARGB8888"
get_color_formats().add("ARGB8888")
else:
color_format = "LV_COLOR_FORMAT_NATIVE"
+20 -6
View File
@@ -1,12 +1,15 @@
from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv
from esphome.const import CONF_ITEMS, CONF_MODE
from esphome.core import CORE
from esphome.cpp_types import std_string
from .. import LvContext
from ..defines import CONF_MAIN, KEYBOARD_MODES, literal
from ..helpers import add_lv_use, lvgl_components_required
from ..helpers import lvgl_components_required
from ..types import LvCompound, LvType
from . import Widget, WidgetType, get_widgets
from . import Widget, WidgetType, get_widgets, is_widget_completed
from .buttonmatrix import CONF_BUTTONMATRIX
from .textarea import CONF_TEXTAREA, lv_textarea_t
CONF_KEYBOARD = "keyboard"
@@ -41,16 +44,27 @@ class KeyboardType(WidgetType):
)
def get_uses(self):
return CONF_KEYBOARD, CONF_TEXTAREA
return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX
async def to_code(self, w: Widget, config: dict):
lvgl_components_required.add("KEY_LISTENER")
lvgl_components_required.add(CONF_KEYBOARD)
add_lv_use("btnmatrix")
if mode := config.get(CONF_MODE):
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
if ta := await get_widgets(config, CONF_TEXTAREA):
await w.set_property(CONF_TEXTAREA, ta[0].obj)
if textarea := config.get(CONF_TEXTAREA):
# If a textarea is configured, it must be generated before the keyboard can attach it.
# If not yet configured, defer the attachment code.
async def add_textarea():
async with LvContext():
await w.set_property(
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
)
if is_widget_completed(textarea):
await add_textarea()
else:
CORE.add_job(add_textarea)
keyboard_spec = KeyboardType()
+1 -1
View File
@@ -35,7 +35,7 @@ class LabelType(WidgetType):
if (value := config.get(CONF_TEXT)) is not None:
await w.set_property(CONF_TEXT, await lv_text.process(value))
await w.set_property(CONF_LONG_MODE, config)
await w.set_property(CONF_RECOLOR, config)
await w.set_property(CONF_RECOLOR, config, processor=lv_bool)
label_spec = LabelType()
+8 -11
View File
@@ -17,11 +17,6 @@ lv_point_t = cg.global_ns.struct("lv_point_t")
lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t")
LINE_SCHEMA = {
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
}
async def process_coord(coord):
if isinstance(coord, Lambda):
return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t))
@@ -34,15 +29,17 @@ class LineType(WidgetType):
CONF_LINE,
LvType("LvLineType", parents=(LvCompound,)),
(CONF_MAIN,),
LINE_SCHEMA,
schema={cv.Required(CONF_POINTS): cv.ensure_list(point_schema)},
modify_schema={cv.Optional(CONF_POINTS): cv.ensure_list(point_schema)},
)
async def to_code(self, w: Widget, config):
points = [
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
for p in config[CONF_POINTS]
]
lv_add(w.var.set_points(points))
if CONF_POINTS in config:
points = [
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
for p in config[CONF_POINTS]
]
lv_add(w.var.set_points(points))
line_spec = LineType()
+35 -28
View File
@@ -56,11 +56,11 @@ from ..lv_validation import (
lv_float,
lv_image,
lv_int,
lv_positive_int,
opacity,
padding,
pixels,
pixels_or_percent,
pixels_or_percent_validator,
requires_component,
size,
)
@@ -88,7 +88,10 @@ CONF_COLOR_START = "color_start"
CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top"
CONF_IMAGE_ID = "image_id"
CONF_INDICATORS = "indicators"
CONF_DASH_GAP = "dash_gap"
CONF_DASH_WIDTH = "dash_width"
CONF_LINE_ID = "line_id"
CONF_ROUNDED = "rounded"
CONF_LABEL_GAP = "label_gap"
CONF_MAJOR = "major"
CONF_METER = "meter"
@@ -135,9 +138,12 @@ INDICATOR_LINE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): cv.int_,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_ROUNDED, default=True): lv_bool,
cv.Optional(CONF_DASH_GAP): lv_positive_int,
cv.Optional(CONF_DASH_WIDTH): lv_positive_int,
cv.Optional(CONF_R_MOD): padding,
cv.Optional(CONF_LENGTH): pixels_or_percent_validator,
cv.Optional(CONF_RADIAL_OFFSET, 0): pixels_or_percent_validator,
cv.Optional(CONF_LENGTH): pixels_or_percent,
cv.Optional(CONF_RADIAL_OFFSET): pixels_or_percent,
cv.Optional(CONF_VALUE, default=0.0): lv_float,
cv.Optional(CONF_OPA, default=1.0): opacity,
}
@@ -249,17 +255,17 @@ SCALE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_COUNT, default=12): cv.int_range(min=2),
cv.Optional(CONF_WIDTH, default=2): cv.positive_int,
cv.Optional(CONF_LENGTH, default=10): size,
cv.Optional(CONF_RADIAL_OFFSET, default=0): size,
cv.Optional(CONF_LENGTH, default=10): cv.positive_int,
cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int,
cv.Optional(CONF_COLOR, default=0x808080): lv_color,
cv.Optional(CONF_MAJOR): cv.Schema(
{
cv.Optional(CONF_STRIDE, default=3): cv.positive_int,
cv.Optional(CONF_WIDTH, default=5): size,
cv.Optional(CONF_LENGTH, default="15%"): size,
cv.Optional(CONF_RADIAL_OFFSET, default=0): size,
cv.Optional(CONF_LENGTH, default=12): cv.positive_int,
cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_LABEL_GAP, default=4): size,
cv.Optional(CONF_LABEL_GAP, default=4): cv.int_,
}
),
}
@@ -466,11 +472,15 @@ class MeterType(WidgetType):
CONF_OPA: v[CONF_OPA],
CONF_LINE_WIDTH: v[CONF_WIDTH],
"line_color": v[CONF_COLOR],
"line_rounded": True,
"line_rounded": v[CONF_ROUNDED],
CONF_ALIGN: CHILD_ALIGNMENTS.TOP_LEFT,
CONF_LENGTH: length,
CONF_RADIAL_OFFSET: v[CONF_RADIAL_OFFSET],
}
if radial_offset := v.get(CONF_RADIAL_OFFSET):
props[CONF_RADIAL_OFFSET] = radial_offset
for option in (CONF_DASH_WIDTH, CONF_DASH_GAP):
if option in v:
props["line_" + option] = v[option]
lw = await widget_to_code(props, line_indicator_type, scale_var)
await set_indicator_values(lw, v)
@@ -478,10 +488,8 @@ class MeterType(WidgetType):
add_lv_use(CONF_IMAGE)
src = v[CONF_SRC]
src_data = get_image_metadata(src.id)
pivot_x = await pixels.process(v[CONF_PIVOT_X])
pivot_y = await pixels.process(
v.get(CONF_PIVOT_Y, src_data.height // 2)
)
pivot_x = v[CONF_PIVOT_X]
pivot_y = v.get(CONF_PIVOT_Y, src_data.height // 2)
props = {
CONF_X: src_data.width // 2 - pivot_x,
"transform_pivot_x": pivot_x,
@@ -511,11 +519,12 @@ class MeterType(WidgetType):
lv_obj.set_style_line_width(
scale_var, await size.process(ticks[CONF_WIDTH]), LV_PART.ITEMS
)
lv_obj.set_style_radial_offset(
scale_var,
await size.process(ticks[CONF_RADIAL_OFFSET]),
LV_PART.ITEMS,
)
if radial_offset := ticks.get(CONF_RADIAL_OFFSET):
lv_obj.set_style_radial_offset(
scale_var,
-radial_offset,
LV_PART.ITEMS,
)
lv_obj.set_style_line_color(
scale_var,
await lv_color.process(ticks[CONF_COLOR]),
@@ -536,11 +545,12 @@ class MeterType(WidgetType):
await size.process(major[CONF_LENGTH]),
LV_PART.INDICATOR,
)
lv_obj.set_style_radial_offset(
scale_var,
await size.process(ticks[CONF_RADIAL_OFFSET]),
LV_PART.INDICATOR,
)
if radial_offset := major.get(CONF_RADIAL_OFFSET):
lv_obj.set_style_radial_offset(
scale_var,
-radial_offset,
LV_PART.INDICATOR,
)
lv_obj.set_style_line_width(
scale_var,
await size.process(major[CONF_WIDTH]),
@@ -553,12 +563,9 @@ class MeterType(WidgetType):
)
# Set label gap (padding)
label_gap = await size.process(major[CONF_LABEL_GAP])
if isinstance(label_gap, int):
label_gap -= DEFAULT_LABEL_GAP
lv_obj.set_style_pad_radial(
scale_var,
label_gap,
major[CONF_LABEL_GAP] - DEFAULT_LABEL_GAP,
LV_PART.INDICATOR,
)
else:
+5 -3
View File
@@ -33,6 +33,7 @@ from ..styles import LVStyle
from ..types import LV_EVENT, lv_obj_t
from . import Widget, WidgetType, add_widgets, set_obj_properties, widget_to_code
from .button import button_spec, lv_button_t
from .img import CONF_IMAGE
from .label import CONF_LABEL
from .obj import obj_spec
@@ -41,7 +42,7 @@ CONF_MSGBOX = "msgbox"
OUTER_STYLE = LVStyle(
"msgbox_outer",
{
"bg_opa": 128,
"bg_opa": 0.5,
"bg_color": "black",
"border_width": 0,
"pad_all": 0,
@@ -119,6 +120,7 @@ async def msgbox_to_code(top_layer, conf):
CONF_BUTTON,
CONF_LABEL,
CONF_MSGBOX,
CONF_IMAGE,
*button_spec.get_uses(),
)
if CONF_BUTTON_STYLE in conf:
@@ -156,7 +158,7 @@ async def msgbox_to_code(top_layer, conf):
with LocalVariable(
"close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox)
) as close_btn:
lv_obj.remove_event_cb(close_btn, nullptr)
lv_obj.remove_event(close_btn, 0)
lv_obj.add_event_cb(
close_btn,
await close_action.get_lambda(),
@@ -170,6 +172,6 @@ async def msgbox_to_code(top_layer, conf):
async def msgboxes_to_code(lv_component, config):
top_layer = lv.disp_get_layer_top(lv_component.get_disp())
top_layer = lv_expr.disp_get_layer_top(lv_component.get_disp())
for conf in config.get(CONF_MSGBOXES, ()):
await msgbox_to_code(top_layer, conf)
+1 -2
View File
@@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_SIZE, CONF_TEXT
from ..defines import CONF_MAIN, get_color_formats
from ..defines import CONF_MAIN
from ..lv_validation import color, lv_color, lv_int, lv_text
from ..lvcode import LocalVariable, lv
from ..schemas import TEXT_SCHEMA
@@ -44,7 +44,6 @@ class QrCodeType(WidgetType):
return CONF_CANVAS, CONF_IMAGE
async def to_code(self, w: Widget, config):
get_color_formats().add("ARGB8888")
await w.set_property(
CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR))
)
+2 -2
View File
@@ -26,7 +26,7 @@ from ..schemas import container_schema, part_schema
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
from .button import button_spec
from .buttonmatrix import buttonmatrix_spec
from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec
from .obj import obj_spec
CONF_TABVIEW = "tabview"
@@ -73,7 +73,7 @@ class TabviewType(WidgetType):
)
def get_uses(self):
return "btnmatrix", TYPE_FLEX
return CONF_BUTTONMATRIX, TYPE_FLEX
async def to_code(self, w: Widget, config: dict):
await w.set_property(
+1 -1
View File
@@ -129,6 +129,6 @@ async def tileview_select(config, action_id, template_arg, args):
lv.tileview_set_tile_by_index(
widgets[0].obj, column, row, literal(config[CONF_ANIMATED])
)
lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
lv_obj.send_event(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
return await action_to_code(widgets, do_select, action_id, template_arg, args)
+137 -137
View File
@@ -597,173 +597,173 @@ void MixerSpeaker::audio_mixer_task(void *params) {
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
{ // Ensure C++ objects fall out of scope to ensure proper cleanup before stopping the task
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
if (output_transfer_buffer == nullptr) {
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
if (output_transfer_buffer == nullptr) {
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
bool sent_finished = false;
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
FixedVector<SourceSpeaker *> speakers_with_data;
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
speakers_with_data.init(this_mixer->source_speakers_.size());
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
while (true) {
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
break;
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
const uint32_t output_frames_free =
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
speakers_with_data.clear();
transfer_buffers_with_data.clear();
bool sent_finished = false;
for (auto &speaker : this_mixer->source_speakers_) {
if (speaker->is_running() && !speaker->get_pause_state()) {
// Speaker is running and not paused, so it possibly can provide audio data
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
if (transfer_buffer.use_count() == 0) {
// No transfer buffer allocated, so skip processing this speaker
continue;
}
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
FixedVector<SourceSpeaker *> speakers_with_data;
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
speakers_with_data.init(this_mixer->source_speakers_.size());
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
if (transfer_buffer->available() > 0) {
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
transfer_buffers_with_data.push_back(transfer_buffer);
speakers_with_data.push_back(speaker);
while (true) {
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
break;
}
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);
const uint32_t output_frames_free =
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
speakers_with_data.clear();
transfer_buffers_with_data.clear();
for (auto &speaker : this_mixer->source_speakers_) {
if (speaker->is_running() && !speaker->get_pause_state()) {
// Speaker is running and not paused, so it possibly can provide audio data
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
if (transfer_buffer.use_count() == 0) {
// No transfer buffer allocated, so skip processing this speaker
continue;
}
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
if (transfer_buffer->available() > 0) {
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
transfer_buffers_with_data.push_back(transfer_buffer);
speakers_with_data.push_back(speaker);
}
}
}
}
if (transfer_buffers_with_data.empty()) {
// No audio available for transferring, block task temporarily
delay(TASK_DELAY_MS);
continue;
}
if (transfer_buffers_with_data.empty()) {
// No audio available for transferring, block task temporarily
delay(TASK_DELAY_MS);
continue;
}
uint32_t frames_to_mix = output_frames_free;
uint32_t frames_to_mix = output_frames_free;
if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) {
// Only one speaker has audio data, just copy samples over
if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) {
// Only one speaker has audio data, just copy samples over
audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info();
audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info();
if (active_stream_info.get_sample_rate() ==
this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) {
// Speaker's sample rate matches the output speaker's, copy directly
if (active_stream_info.get_sample_rate() ==
this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) {
// Speaker's sample rate matches the output speaker's, copy directly
const uint32_t frames_available_in_buffer =
active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available());
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), active_stream_info,
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
const uint32_t frames_available_in_buffer =
active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available());
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()),
active_stream_info, reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
// Set playback delay for newly contributing source
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[0]->playback_delay_frames_.store(
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
// Set playback delay for newly contributing source
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[0]->playback_delay_frames_.store(
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
}
// Update source speaker pending frames
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
// Update output transfer buffer length and pipeline frame count
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
} else {
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
if (!this_mixer->output_speaker_->is_stopped()) {
if (!sent_finished) {
this_mixer->output_speaker_->finish();
sent_finished = true; // Avoid repeatedly sending the finish command
}
} else {
// Speaker has finished writing the current audio, update the stream information and restart the speaker
this_mixer->audio_stream_info_ =
audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_,
active_stream_info.get_sample_rate());
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
this_mixer->output_speaker_->start();
// Reset pipeline frame count since we're starting fresh with a new sample rate
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
sent_finished = false;
}
}
} else {
// Determine how many frames to mix
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(
transfer_buffers_with_data[i]->available());
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
}
int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start());
audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
// Mix two streams together
for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) {
mix_audio_samples(primary_buffer, primary_stream_info,
reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()),
speakers_with_data[i]->get_audio_stream_info(),
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
if (i != transfer_buffers_with_data.size() - 1) {
// Need to mix more streams together, point primary buffer and stream info to the already mixed output
primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end());
primary_stream_info = this_mixer->audio_stream_info_.value();
}
}
// Update source speaker pending frames
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
// Get current pipeline depth for delay calculation (before incrementing)
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
// Update output transfer buffer length and pipeline frame count
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
// Set playback delay for newly contributing sources
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
}
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[i]->decrease_buffer_length(
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
}
// Update output transfer buffer length and pipeline frame count (once, not per source)
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
} else {
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
if (!this_mixer->output_speaker_->is_stopped()) {
if (!sent_finished) {
this_mixer->output_speaker_->finish();
sent_finished = true; // Avoid repeatedly sending the finish command
}
} else {
// Speaker has finished writing the current audio, update the stream information and restart the speaker
this_mixer->audio_stream_info_ =
audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_,
active_stream_info.get_sample_rate());
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
this_mixer->output_speaker_->start();
// Reset pipeline frame count since we're starting fresh with a new sample rate
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
sent_finished = false;
}
}
} else {
// Determine how many frames to mix
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
const uint32_t frames_available_in_buffer =
speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available());
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
}
int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start());
audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
// Mix two streams together
for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) {
mix_audio_samples(primary_buffer, primary_stream_info,
reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()),
speakers_with_data[i]->get_audio_stream_info(),
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
if (i != transfer_buffers_with_data.size() - 1) {
// Need to mix more streams together, point primary buffer and stream info to the already mixed output
primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end());
primary_stream_info = this_mixer->audio_stream_info_.value();
}
}
// Get current pipeline depth for delay calculation (before incrementing)
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
// Set playback delay for newly contributing sources
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
}
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[i]->decrease_buffer_length(
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
}
// Update output transfer buffer length and pipeline frame count (once, not per source)
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
}
}
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
}
// Reset pipeline frame count since the task is stopping
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
output_transfer_buffer.reset();
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
@@ -0,0 +1,139 @@
#include "modbus_helpers.h"
#include "esphome/core/log.h"
namespace esphome::modbus::helpers {
static const char *const TAG = "modbus_helpers";
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
switch (value_type) {
case SensorValueType::U_WORD:
case SensorValueType::S_WORD:
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD:
case SensorValueType::FP32:
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::S_DWORD_R:
case SensorValueType::FP32_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
data.push_back((value & 0xFFFF000000000000) >> 48);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF000000000000) >> 48);
break;
default:
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type));
break;
}
}
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask) {
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
if (offset > data.size()) {
ESP_LOGE(TAG, "not enough data for value");
return value;
}
size_t size = data.size() - offset;
bool error = false;
switch (sensor_value_type) {
case SensorValueType::U_WORD:
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset),
bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::U_DWORD:
case SensorValueType::FP32:
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
} else {
error = true;
}
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::FP32_R:
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
} else {
error = true;
}
break;
case SensorValueType::S_WORD:
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::S_DWORD:
if (size >= 4) {
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
} else {
error = true;
}
break;
case SensorValueType::S_DWORD_R: {
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
// Currently the high word is at the low position
// the sign bit is therefore at low before the switch
uint32_t sign_bit = (value & 0x8000) << 16;
value = mask_and_shift_by_rightbit(
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
} else {
error = true;
}
} break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
// Ignore bitmask for QWORD
if (size >= 8) {
value = get_data<uint64_t>(data, offset);
} else {
error = true;
}
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R: {
// Ignore bitmask for QWORD
if (size >= 8) {
uint64_t tmp = get_data<uint64_t>(data, offset);
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
} else {
error = true;
}
} break;
case SensorValueType::RAW:
default:
break;
}
if (error)
ESP_LOGE(TAG, "not enough data for value");
return value;
}
} // namespace esphome::modbus::helpers
+101
View File
@@ -1,6 +1,8 @@
#pragma once
#include <string>
#include <vector>
#include <cmath>
#include "esphome/core/helpers.h"
#include "esphome/components/modbus/modbus_definitions.h"
@@ -103,4 +105,103 @@ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
}
// Extract data from modbus response buffer
/** Extract data from modbus response buffer
* @param T one of supported integer data types int_8,int_16,int_32,int_64
* @param data modbus response buffer (uint8_t)
* @param buffer_offset offset in bytes.
* @return value of type T extracted from buffer
*/
template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
if (sizeof(T) == sizeof(uint8_t)) {
return T(data[buffer_offset]);
}
if (sizeof(T) == sizeof(uint16_t)) {
return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
}
if (sizeof(T) == sizeof(uint32_t)) {
return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
}
if (sizeof(T) == sizeof(uint64_t)) {
return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
(static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
}
static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
sizeof(T) == sizeof(uint64_t),
"Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
return T{};
}
/** Extract coil data from modbus response buffer
* Responses for coil are packed into bytes .
* coil 3 is bit 3 of the first response byte
* coil 9 is bit 2 of the second response byte
* @param coil number of the cil
* @param data modbus response buffer (uint8_t)
* @return content of coil register
*/
inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
auto data_byte = coil / 8;
return (data[data_byte] & (1 << (coil % 8))) > 0;
}
/** Extract bits from value and shift right according to the bitmask
* if the bitmask is 0x00F0 we want the values frrom bit 5 - 8.
* the result is then shifted right by the position if the first right set bit in the mask
* Useful for modbus data where more than one value is packed in a 16 bit register
* Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as
* D15 - D8 = hour, D7 - D0 = minute
* To get the hours use mask 0xFF00 and 0x00FF for the minute
* @param data an integral value between 16 aand 32 bits,
* @param bitmask the bitmask to apply
*/
template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
auto result = (mask & data);
if (result == 0 || mask == 0xFFFFFFFF) {
return result;
}
for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
if (pos < 32 && (mask & (1UL << pos)) != 0)
return result >> pos;
}
return 0;
}
/** Convert float value to vector<uint16_t> suitable for sending
* @param data target for payload
* @param value float value to convert
* @param value_type defines if 16/32 or FP32 is used
* @return vector containing the modbus register words in correct order
*/
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
/** Convert vector<uint8_t> response payload to number.
* @param data payload with the data to convert
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
* @param offset offset to the data in data
* @param bitmask bitmask used for masking and shifting
* @return 64-bit number of the payload
*/
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask);
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
int64_t val;
if (value_type_is_float(value_type)) {
val = bit_cast<uint32_t>(value);
} else {
val = llroundf(value);
}
std::vector<uint16_t> data;
number_to_payload(data, val, value_type);
return data;
}
} // namespace esphome::modbus::helpers
@@ -15,10 +15,10 @@ void ModbusBinarySensor::parse_and_publish(const std::vector<uint8_t> &data) {
case ModbusRegisterType::DISCRETE_INPUT:
case ModbusRegisterType::COIL:
// offset for coil is the actual number of the coil not the byte offset
value = coil_from_vector(this->offset, data);
value = modbus::helpers::coil_from_vector(this->offset, data);
break;
default:
value = get_data<uint16_t>(data, this->offset) & this->bitmask;
value = modbus::helpers::get_data<uint16_t>(data, this->offset) & this->bitmask;
break;
}
// Is there a lambda registered
@@ -140,7 +140,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
std::vector<uint16_t> payload;
payload.reserve(server_register->register_count * 2);
number_to_payload(payload, value, server_register->value_type);
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
@@ -258,7 +258,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
// Actually write to the registers:
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
return server_register->write_lambda(number);
})) {
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
@@ -517,7 +517,8 @@ void ModbusController::loop() {
void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
const std::vector<uint8_t> &data) {
ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data<uint16_t>(data, 0), get_data<int16_t>(data, 1));
ESP_LOGV(TAG, "Command ACK 0x%X %d ", modbus::helpers::get_data<uint16_t>(data, 0),
modbus::helpers::get_data<int16_t>(data, 1));
}
void ModbusController::dump_sensors_() {
@@ -710,132 +711,5 @@ bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) {
other.register_type == this->register_type && other.function_code == this->function_code;
}
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
switch (value_type) {
case SensorValueType::U_WORD:
case SensorValueType::S_WORD:
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD:
case SensorValueType::FP32:
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::S_DWORD_R:
case SensorValueType::FP32_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
data.push_back((value & 0xFFFF000000000000) >> 48);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF000000000000) >> 48);
break;
default:
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d",
static_cast<uint16_t>(value_type));
break;
}
}
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask) {
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
size_t size = data.size() - offset;
bool error = false;
switch (sensor_value_type) {
case SensorValueType::U_WORD:
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::U_DWORD:
case SensorValueType::FP32:
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
} else {
error = true;
}
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::FP32_R:
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
} else {
error = true;
}
break;
case SensorValueType::S_WORD:
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::S_DWORD:
if (size >= 4) {
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
} else {
error = true;
}
break;
case SensorValueType::S_DWORD_R: {
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
// Currently the high word is at the low position
// the sign bit is therefore at low before the switch
uint32_t sign_bit = (value & 0x8000) << 16;
value = mask_and_shift_by_rightbit(
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
} else {
error = true;
}
} break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
// Ignore bitmask for QWORD
if (size >= 8) {
value = get_data<uint64_t>(data, offset);
} else {
error = true;
}
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R: {
// Ignore bitmask for QWORD
if (size >= 8) {
uint64_t tmp = get_data<uint64_t>(data, offset);
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
} else {
error = true;
}
} break;
case SensorValueType::RAW:
default:
break;
}
if (error)
ESP_LOGE(TAG, "not enough data for value");
return value;
}
} // namespace modbus_controller
} // namespace esphome
@@ -59,83 +59,38 @@ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
return modbus::helpers::qword_from_hex_str(value, pos);
}
// Extract data from modbus response buffer
/** Extract data from modbus response buffer
* @param T one of supported integer data types int_8,int_16,int_32,int_64
* @param data modbus response buffer (uint8_t)
* @param buffer_offset offset in bytes.
* @return value of type T extracted from buffer
*/
template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
if (sizeof(T) == sizeof(uint8_t)) {
return T(data[buffer_offset]);
}
if (sizeof(T) == sizeof(uint16_t)) {
return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
}
if (sizeof(T) == sizeof(uint32_t)) {
return get_data<uint16_t>(data, buffer_offset) << 16 | get_data<uint16_t>(data, (buffer_offset + 2));
}
if (sizeof(T) == sizeof(uint64_t)) {
return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
(static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
}
template<typename T>
ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
return modbus::helpers::get_data<T>(data, buffer_offset);
}
/** Extract coil data from modbus response buffer
* Responses for coil are packed into bytes .
* coil 3 is bit 3 of the first response byte
* coil 9 is bit 2 of the second response byte
* @param coil number of the cil
* @param data modbus response buffer (uint8_t)
* @return content of coil register
*/
ESPDEPRECATED("Use modbus::helpers::coil_from_vector() instead. Removed in 2026.10.0", "2026.4.0")
inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
auto data_byte = coil / 8;
return (data[data_byte] & (1 << (coil % 8))) > 0;
return modbus::helpers::coil_from_vector(coil, data);
}
/** Extract bits from value and shift right according to the bitmask
* if the bitmask is 0x00F0 we want the values frrom bit 5 - 8.
* the result is then shifted right by the position if the first right set bit in the mask
* Useful for modbus data where more than one value is packed in a 16 bit register
* Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as
* D15 - D8 = hour, D7 - D0 = minute
* To get the hours use mask 0xFF00 and 0x00FF for the minute
* @param data an integral value between 16 aand 32 bits,
* @param bitmask the bitmask to apply
*/
template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
auto result = (mask & data);
if (result == 0 || mask == 0xFFFFFFFF) {
return result;
}
for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
if ((mask & (1UL << pos)) != 0)
return result >> pos;
}
return 0;
template<typename N>
ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
N mask_and_shift_by_rightbit(N data, uint32_t mask) {
return modbus::helpers::mask_and_shift_by_rightbit(data, mask);
}
/** Convert float value to vector<uint16_t> suitable for sending
* @param data target for payload
* @param value float value to convert
* @param value_type defines if 16/32 or FP32 is used
* @return vector containing the modbus register words in correct order
*/
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
modbus::helpers::number_to_payload(data, value, value_type);
}
/** Convert vector<uint8_t> response payload to number.
* @param data payload with the data to convert
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
* @param offset offset to the data in data
* @param bitmask bitmask used for masking and shifting
* @return 64-bit number of the payload
*/
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask);
ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask) {
return modbus::helpers::payload_to_number(data, sensor_value_type, offset, bitmask);
}
ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
return modbus::helpers::float_to_payload(value, value_type);
}
class ModbusController;
@@ -517,7 +472,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
* @return float value of data
*/
inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
int64_t number = modbus::helpers::payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
float float_value;
if (modbus::helpers::value_type_is_float(item.sensor_value_type)) {
@@ -529,19 +484,5 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem
return float_value;
}
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
int64_t val;
if (modbus::helpers::value_type_is_float(value_type)) {
val = bit_cast<uint32_t>(value);
} else {
val = llroundf(value);
}
std::vector<uint16_t> data;
number_to_payload(data, val, value_type);
return data;
}
} // namespace modbus_controller
} // namespace esphome
@@ -62,7 +62,7 @@ void ModbusNumber::control(float value) {
this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data);
});
} else {
data = float_to_payload(write_value, this->sensor_value_type);
data = modbus::helpers::float_to_payload(write_value, this->sensor_value_type);
ESP_LOGD(TAG,
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)",
@@ -34,7 +34,7 @@ void ModbusFloatOutput::write_state(float value) {
}
// lambda didn't set payload
if (data.empty()) {
data = float_to_payload(value, this->sensor_value_type);
data = modbus::helpers::float_to_payload(value, this->sensor_value_type);
}
ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)",
@@ -9,7 +9,7 @@ static const char *const TAG = "modbus_controller.select";
void ModbusSelect::dump_config() { LOG_SELECT(TAG, "Modbus Controller Select", this); }
void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
int64_t value = payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask);
int64_t value = modbus::helpers::payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask);
ESP_LOGD(TAG, "New select value %lld from payload", value);
@@ -61,7 +61,7 @@ void ModbusSelect::control(size_t index) {
}
if (data.empty()) {
number_to_payload(data, *mapval, this->sensor_value_type);
modbus::helpers::number_to_payload(data, *mapval, this->sensor_value_type);
} else {
ESP_LOGV(TAG, "Using payload from write lambda");
}
@@ -33,10 +33,10 @@ void ModbusSwitch::parse_and_publish(const std::vector<uint8_t> &data) {
case ModbusRegisterType::DISCRETE_INPUT:
case ModbusRegisterType::COIL:
// offset for coil is the actual number of the coil not the byte offset
value = coil_from_vector(this->offset, data);
value = modbus::helpers::coil_from_vector(this->offset, data);
break;
default:
value = get_data<uint16_t>(data, this->offset) & this->bitmask;
value = modbus::helpers::get_data<uint16_t>(data, this->offset) & this->bitmask;
break;
}
@@ -317,57 +317,59 @@ void ResamplerSpeaker::resample_task(void *params) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING);
std::unique_ptr<audio::AudioResampler> resampler =
make_unique<audio::AudioResampler>(this_resampler->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS),
this_resampler->target_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
{ // Ensure C++ objects fall out of scope for proper cleanup before stopping the task
std::unique_ptr<audio::AudioResampler> resampler = make_unique<audio::AudioResampler>(
this_resampler->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS),
this_resampler->target_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
esp_err_t err = resampler->start(this_resampler->audio_stream_info_, this_resampler->target_stream_info_,
this_resampler->taps_, this_resampler->filters_);
esp_err_t err = resampler->start(this_resampler->audio_stream_info_, this_resampler->target_stream_info_,
this_resampler->taps_, this_resampler->filters_);
if (err == ESP_OK) {
std::shared_ptr<RingBuffer> temp_ring_buffer =
RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
if (err == ESP_OK) {
std::shared_ptr<RingBuffer> temp_ring_buffer =
RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
if (!temp_ring_buffer) {
err = ESP_ERR_NO_MEM;
} else {
this_resampler->ring_buffer_ = temp_ring_buffer;
resampler->add_source(this_resampler->ring_buffer_);
if (!temp_ring_buffer) {
err = ESP_ERR_NO_MEM;
} else {
this_resampler->ring_buffer_ = temp_ring_buffer;
resampler->add_source(this_resampler->ring_buffer_);
this_resampler->output_speaker_->set_audio_stream_info(this_resampler->target_stream_info_);
resampler->add_sink(this_resampler->output_speaker_);
}
}
if (err == ESP_OK) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
} else if (err == ESP_ERR_NO_MEM) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
} else if (err == ESP_ERR_NOT_SUPPORTED) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
}
while (err == ESP_OK) {
uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) {
break;
this_resampler->output_speaker_->set_audio_stream_info(this_resampler->target_stream_info_);
resampler->add_sink(this_resampler->output_speaker_);
}
}
// Stop gracefully if the decoder is done
int32_t ms_differential = 0;
audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential);
if (resampler_state == audio::AudioResamplerState::FINISHED) {
break;
} else if (resampler_state == audio::AudioResamplerState::FAILED) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
break;
if (err == ESP_OK) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
} else if (err == ESP_ERR_NO_MEM) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
} else if (err == ESP_ERR_NOT_SUPPORTED) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
}
while (err == ESP_OK) {
uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) {
break;
}
// Stop gracefully if the decoder is done
int32_t ms_differential = 0;
audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential);
if (resampler_state == audio::AudioResamplerState::FINISHED) {
break;
} else if (resampler_state == audio::AudioResamplerState::FAILED) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
break;
}
}
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
}
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
resampler.reset();
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
@@ -0,0 +1,3 @@
import esphome.codegen as cg
time_based_ns = cg.esphome_ns.namespace("time_based")
@@ -11,7 +11,8 @@ from esphome.const import (
CONF_STOP_ACTION,
)
time_based_ns = cg.esphome_ns.namespace("time_based")
from .. import time_based_ns
TimeBasedCover = time_based_ns.class_("TimeBasedCover", cover.Cover, cg.Component)
CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop"
+49 -18
View File
@@ -10,6 +10,10 @@ namespace tormatic {
static const char *const TAG = "tormatic.cover";
// Time to poll the UART when flushing after desync. At 9600 baud, a full
// 12-byte message takes ~12.5ms, so 15ms guarantees all bytes have arrived.
static constexpr uint32_t DRAIN_TIMEOUT_MS = 15;
using namespace esphome::cover;
void Tormatic::setup() {
@@ -256,32 +260,51 @@ void Tormatic::stop_at_target_() {
// Read a GateStatus from the unit. The unit only sends messages in response to
// status requests or commands, so a message needs to be sent first.
optional<GateStatus> Tormatic::read_gate_status_() {
if (this->available() < sizeof(MessageHeader)) {
if (!this->pending_hdr_) {
if (this->available() < sizeof(MessageHeader)) {
return {};
}
this->pending_hdr_ = this->read_data_<MessageHeader>();
if (!this->pending_hdr_) {
return {};
}
// Sanity check: valid messages have small payloads (3-4 bytes). A large
// or impossible payload_size means the stream is out of sync (corrupted
// byte, dropped data, etc.). Flush the buffer so we can resync on the
// next request/response cycle.
if (this->pending_hdr_->payload_size() > sizeof(CommandRequestReply)) {
ESP_LOGW(TAG, "Unexpected payload size %" PRIu32 ", flushing rx buffer", this->pending_hdr_->payload_size());
this->pending_hdr_.reset();
this->drain_rx_();
return {};
}
}
// Wait for all payload bytes to arrive before processing.
if (this->available() < this->pending_hdr_->payload_size()) {
return {};
}
auto o_hdr = this->read_data_<MessageHeader>();
if (!o_hdr) {
ESP_LOGE(TAG, "Timeout reading message header");
return {};
}
auto hdr = o_hdr.value();
auto hdr = *this->pending_hdr_;
this->pending_hdr_.reset();
switch (hdr.type) {
case STATUS: {
if (hdr.payload_size() != sizeof(StatusReply)) {
ESP_LOGE(TAG, "Header specifies payload size %" PRIu32 " but size of StatusReply is %zu", hdr.payload_size(),
sizeof(StatusReply));
this->drain_rx_(hdr.payload_size());
return {};
}
// Read a StatusReply requested by update().
auto o_status = this->read_data_<StatusReply>();
if (!o_status) {
return {};
}
auto status = o_status.value();
return status.state;
return o_status->state;
}
case COMMAND:
@@ -344,16 +367,24 @@ template<typename T> optional<T> Tormatic::read_data_() {
return obj;
}
// Drain up to n amount of bytes from the uart rx buffer.
// Drain bytes from the uart rx buffer. When n > 0, drain exactly n bytes
// (caller must ensure they are available). When n == 0, poll for 15ms to
// guarantee a full packet time at 9600 baud has elapsed, consuming any
// bytes still in transit.
void Tormatic::drain_rx_(uint16_t n) {
uint8_t data;
uint16_t count = 0;
while (this->available()) {
this->read_byte(&data);
count++;
if (n > 0 && count >= n) {
return;
if (n > 0) {
for (uint16_t i = 0; i < n; i++) {
if (!this->read_byte(&data)) {
return;
}
}
} else {
uint32_t start = millis();
while (millis() - start < DRAIN_TIMEOUT_MS) {
if (this->available()) {
this->read_byte(&data);
}
}
}
}
@@ -43,6 +43,7 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom
void handle_gate_status_(GateStatus s);
uint32_t seq_tx_{0};
optional<MessageHeader> pending_hdr_{};
GateStatus current_status_{PAUSED};
+4
View File
@@ -85,6 +85,10 @@ class UARTComponent {
// @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful.
virtual UARTFlushResult flush() = 0;
// Returns true if the underlying transport is connected and operational.
// Hardware UARTs always return true. USB-backed UARTs override to reflect actual connection state.
virtual bool is_connected() { return true; }
// Sets the maximum time to wait for TX to drain during flush().
// Only meaningful on ESP32 (IDF). Other platforms ignore this value.
// @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely.
@@ -147,6 +147,20 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
// uart_param_config must be called after uart_driver_install and before any
// other uart_set_*() calls. The driver installation resets the UART peripheral
// registers to their default state, overwriting any previously configured baud
// rate or framing settings. Calling uart_param_config here ensures the requested
// settings are applied after the reset and before pin routing, inversion, and
// threshold configuration.
uart_config_t uart_config = this->get_config_();
err = uart_param_config(this->uart_num_, &uart_config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
@@ -214,22 +228,15 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
// Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install().
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install()
err = uart_set_mode(this->uart_num_, mode);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
uart_config_t uart_config = this->get_config_();
err = uart_param_config(this->uart_num_, &uart_config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Register ISR callback to wake the main loop when UART data arrives.
// The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to
+1
View File
@@ -140,6 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override { return this->input_buffer_.get_available(); }
bool is_connected() override { return this->initialised_.load(); }
uart::UARTFlushResult flush() override;
void check_logger_conflict() override {}
void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
+69 -7
View File
@@ -22,6 +22,8 @@ static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
static constexpr uint32_t RECONNECT_DELAY_MS = 500; // Delay between home ID query attempts after reconnect
static constexpr uint8_t MAX_QUERY_RETRIES = 5; // Max attempts to query home ID after reconnect
static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
// Calculate Z-Wave frame checksum
@@ -38,7 +40,10 @@ ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; }
void ZWaveProxy::setup() {
this->setup_time_ = App.get_loop_component_start_time();
this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
this->was_connected_ = this->parent_->is_connected();
if (this->was_connected_) {
this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
}
}
float ZWaveProxy::get_setup_priority() const {
@@ -84,6 +89,14 @@ void ZWaveProxy::loop() {
this->api_connection_ = nullptr; // Unsubscribe if disconnected
}
const bool connected = this->parent_->is_connected();
if (this->was_connected_ != connected) {
this->on_connection_changed_(connected);
}
if (this->reconnect_time_ != 0) {
this->retry_home_id_query_();
}
this->process_uart_();
this->status_clear_warning();
}
@@ -167,6 +180,55 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en
}
}
void ZWaveProxy::on_connection_changed_(bool connected) {
this->was_connected_ = connected;
if (connected) {
ESP_LOGD(TAG, "Modem reconnected");
this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
this->buffer_index_ = 0;
this->last_response_ = 0;
this->in_bootloader_ = false;
// Defer the query — the modem needs time to initialize after power is applied
this->reconnect_time_ = App.get_loop_component_start_time();
this->query_retries_ = 0;
} else {
ESP_LOGW(TAG, "Modem disconnected");
this->clear_home_id_();
}
}
void ZWaveProxy::retry_home_id_query_() {
if (this->home_id_ready_) {
// Got the home ID, cancel remaining retries
this->reconnect_time_ = 0;
return;
}
if (App.get_loop_component_start_time() - this->reconnect_time_ <= RECONNECT_DELAY_MS) {
return; // Not yet time for next attempt
}
this->reconnect_time_ = App.get_loop_component_start_time(); // Reset timer for next retry
this->query_retries_++;
if (this->query_retries_ <= MAX_QUERY_RETRIES) {
ESP_LOGD(TAG, "Querying Home ID (attempt %u)", this->query_retries_);
this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
} else {
ESP_LOGW(TAG, "Failed to read Home ID after %u attempts", MAX_QUERY_RETRIES);
this->reconnect_time_ = 0;
}
}
void ZWaveProxy::clear_home_id_() {
static constexpr uint8_t ZERO_HOME_ID[ZWAVE_HOME_ID_SIZE] = {};
if (this->set_home_id_(ZERO_HOME_ID)) {
this->send_homeid_changed_msg_();
}
this->home_id_ready_ = false;
this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
this->buffer_index_ = 0;
this->last_response_ = 0;
this->in_bootloader_ = false;
}
bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) {
if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) {
ESP_LOGV(TAG, "Home ID unchanged");
@@ -309,7 +371,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START;
switch (byte) {
case ZWAVE_FRAME_TYPE_START:
ESP_LOGVV(TAG, "Received START");
ESP_LOGV(TAG, "Received START");
if (this->in_bootloader_) {
ESP_LOGD(TAG, "Exited bootloader mode");
this->in_bootloader_ = false;
@@ -318,7 +380,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_LENGTH;
return;
case ZWAVE_FRAME_TYPE_BL_MENU:
ESP_LOGVV(TAG, "Received BL_MENU");
ESP_LOGV(TAG, "Received BL_MENU");
if (!this->in_bootloader_) {
ESP_LOGD(TAG, "Entered bootloader mode");
this->in_bootloader_ = true;
@@ -327,16 +389,16 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
this->parsing_state_ = ZWAVE_PARSING_STATE_READ_BL_MENU;
return;
case ZWAVE_FRAME_TYPE_BL_BEGIN_UPLOAD:
ESP_LOGVV(TAG, "Received BL_BEGIN_UPLOAD");
ESP_LOGV(TAG, "Received BL_BEGIN_UPLOAD");
break;
case ZWAVE_FRAME_TYPE_ACK:
ESP_LOGVV(TAG, "Received ACK");
ESP_LOGV(TAG, "Received ACK");
break;
case ZWAVE_FRAME_TYPE_NAK:
ESP_LOGW(TAG, "Received NAK");
ESP_LOGV(TAG, "Received NAK");
break;
case ZWAVE_FRAME_TYPE_CAN:
ESP_LOGW(TAG, "Received CAN");
ESP_LOGV(TAG, "Received CAN");
break;
default:
ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte);
@@ -65,6 +65,9 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
protected:
bool set_home_id_(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed.
void clear_home_id_(); // Clear home ID and notify API clients
void on_connection_changed_(bool connected); // Handle modem connect/disconnect transitions
void retry_home_id_query_(); // Retry home ID query after reconnect
void send_homeid_changed_msg_(api::APIConnection *conn = nullptr);
void send_simple_command_(uint8_t command_id);
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
@@ -80,14 +83,17 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
// Pointers and 32-bit values (aligned together)
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
uint32_t setup_time_{0}; // Time when setup() was called
uint32_t reconnect_time_{0}; // Timestamp of reconnect detection (0 = no pending query)
// Small values (grouped by size to minimize padding)
uint16_t buffer_index_{0}; // Index for populating the data buffer
uint16_t end_frame_after_{0}; // Payload reception ends after this index
uint8_t last_response_{0}; // Last response type sent
uint8_t query_retries_{0}; // Number of home ID query attempts after reconnect
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module
bool was_connected_{false}; // Previous UART connection state for edge detection
};
extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+31 -3
View File
@@ -232,7 +232,7 @@ lvgl:
- roller:
id: lv_roller
visible_row_count: 2
anim_time: 500ms
anim_duration: 500ms
options:
- Nov
- Dec
@@ -317,20 +317,27 @@ lvgl:
align: top_left
- container:
align: center
anim_duration: 1s
arc_opa: COVER
arc_color: 0xFF0000
arc_rounded: false
arc_width: 3
anim_time: 1s
base_dir: auto
bg_color: light_blue
bg_grad_color: light_blue
bg_grad_dir: hor
bg_grad_opa: cover
bg_grad_stop: 128
bg_image_opa: transp
bg_image_recolor: light_blue
bg_image_recolor_opa: 50%
bg_main_opa: cover
bg_main_stop: 0
bg_opa: 20%
blend_mode: normal
blur_backdrop: false
blur_quality: auto
blur_radius: 0
border_color: 0x00FF00
border_opa: cover
border_post: true
@@ -338,7 +345,15 @@ lvgl:
border_width: 4
clip_corner: false
color_filter_opa: transp
drop_shadow_color: 0x000000
drop_shadow_offset_x: 5
drop_shadow_offset_y: 5
drop_shadow_opa: cover
drop_shadow_quality: precision
drop_shadow_radius: 10
ext_click_area: 100px
height: 50%
image_opa: cover
image_recolor: light_blue
image_recolor_opa: cover
line_width: 10
@@ -346,6 +361,10 @@ lvgl:
line_dash_gap: 10
line_rounded: false
line_color: light_blue
margin_bottom: 4
margin_left: 4
margin_right: 4
margin_top: 4
opa: cover
opa_layered: cover
outline_color: light_blue
@@ -355,8 +374,12 @@ lvgl:
pad_all: 10px
pad_bottom: 10px
pad_left: 10px
pad_radial: 0
pad_right: 10px
pad_top: 10px
recolor: 0xFF0000
recolor_opa: transp
rotary_sensitivity: 256
shadow_color: light_blue
shadow_opa: cover
shadow_spread: 5
@@ -368,6 +391,9 @@ lvgl:
text_letter_space: 4
text_line_space: 4
text_opa: cover
text_outline_stroke_color: 0x000000
text_outline_stroke_opa: cover
text_outline_stroke_width: 2
transform_rotation: 90
transform_height: 100
transform_pivot_x: 50%
@@ -377,8 +403,10 @@ lvgl:
transform_scale_y: 0.8
transform_skew_x: 10
transform_skew_y: 20
transform_width: 100
shadow_offset_x: 3
shadow_offset_y: 3
translate_radial: 0
translate_x: 10
translate_y: 10
max_height: 100
@@ -1053,7 +1081,7 @@ lvgl:
- ticks:
width: 1
count: 61
length: 20%
length: 20
radial_offset: 5
color: 0xFFFFFF
major:
@@ -0,0 +1,105 @@
esphome:
name: test-multi-click
host:
api:
batch_delay: 0ms
services:
- service: run_all_tests
then:
# Prime the binary sensor with an initial OFF state.
# trigger_on_initial_state defaults to false, so the first
# state change from unknown won't fire callbacks.
- binary_sensor.template.publish:
id: test_button
state: false
- delay: 50ms
# Test 1: Single click (ON < 50ms, OFF >= 30ms)
- binary_sensor.template.publish:
id: test_button
state: true
- delay: 20ms
- binary_sensor.template.publish:
id: test_button
state: false
# Wait for single click trigger (30ms) + cooldown (100ms) + margin
- delay: 200ms
# Test 2: Double click (ON < 50ms, OFF < 25ms, ON < 50ms, OFF >= 25ms)
- binary_sensor.template.publish:
id: test_button
state: true
- delay: 20ms
- binary_sensor.template.publish:
id: test_button
state: false
- delay: 15ms
- binary_sensor.template.publish:
id: test_button
state: true
- delay: 20ms
- binary_sensor.template.publish:
id: test_button
state: false
# Wait for double click trigger (25ms) + cooldown (100ms) + margin
- delay: 200ms
# Test 3: Long press (ON >= 80ms)
- binary_sensor.template.publish:
id: test_button
state: true
- delay: 100ms
- binary_sensor.template.publish:
id: test_button
state: false
logger:
level: VERBOSE
globals:
- id: single_click_count
type: int
initial_value: "0"
- id: double_click_count
type: int
initial_value: "0"
- id: long_press_count
type: int
initial_value: "0"
binary_sensor:
- platform: template
name: "Test Button"
id: test_button
on_multi_click:
# Single press
- timing:
- ON for at most 50ms
- OFF for at least 30ms
invalid_cooldown: 100ms
then:
- lambda: |-
id(single_click_count) += 1;
ESP_LOGI("multi_click_test", "SINGLE_CLICK count=%d", id(single_click_count));
# Double press
- timing:
- ON for at most 50ms
- OFF for at most 25ms
- ON for at most 50ms
- OFF for at least 25ms
invalid_cooldown: 100ms
then:
- lambda: |-
id(double_click_count) += 1;
ESP_LOGI("multi_click_test", "DOUBLE_CLICK count=%d", id(double_click_count));
# Long press
- timing:
- ON for at least 80ms
invalid_cooldown: 100ms
then:
- lambda: |-
id(long_press_count) += 1;
ESP_LOGI("multi_click_test", "LONG_PRESS count=%d", id(long_press_count));
@@ -0,0 +1,82 @@
"""Integration test for on_multi_click binary sensor automation.
Tests that on_multi_click correctly triggers for single click, double click,
and long press patterns using a template binary sensor with timing
orchestrated entirely in YAML.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_multi_click_trigger(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that on_multi_click triggers for single, double, and long press patterns."""
loop = asyncio.get_running_loop()
single_click_pattern = re.compile(r"SINGLE_CLICK count=(\d+)")
double_click_pattern = re.compile(r"DOUBLE_CLICK count=(\d+)")
long_press_pattern = re.compile(r"LONG_PRESS count=(\d+)")
single_click_future: asyncio.Future[int] = loop.create_future()
double_click_future: asyncio.Future[int] = loop.create_future()
long_press_future: asyncio.Future[int] = loop.create_future()
def check_output(line: str) -> None:
"""Check log output for multi-click trigger messages."""
if m := single_click_pattern.search(line):
if not single_click_future.done():
single_click_future.set_result(int(m.group(1)))
elif m := double_click_pattern.search(line):
if not double_click_future.done():
double_click_future.set_result(int(m.group(1)))
elif (m := long_press_pattern.search(line)) and not long_press_future.done():
long_press_future.set_result(int(m.group(1)))
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
_entities, services = await client.list_entities_services()
test_service = next((s for s in services if s.name == "run_all_tests"), None)
assert test_service is not None, "run_all_tests service not found"
# Kick off the entire test sequence (runs in YAML with delays)
await client.execute_service(test_service, {})
# Wait for all three triggers
try:
count = await asyncio.wait_for(single_click_future, timeout=5.0)
except TimeoutError:
pytest.fail(
"Timeout waiting for SINGLE_CLICK - on_multi_click did not trigger."
)
assert count == 1, f"Expected single click count=1, got {count}"
try:
count = await asyncio.wait_for(double_click_future, timeout=5.0)
except TimeoutError:
pytest.fail(
"Timeout waiting for DOUBLE_CLICK - on_multi_click did not trigger."
)
assert count == 1, f"Expected double click count=1, got {count}"
try:
count = await asyncio.wait_for(long_press_future, timeout=5.0)
except TimeoutError:
pytest.fail(
"Timeout waiting for LONG_PRESS - on_multi_click did not trigger."
)
assert count == 1, f"Expected long press count=1, got {count}"