mirror of
https://github.com/esphome/esphome.git
synced 2026-09-14 08:38:39 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
+10
-86
@@ -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},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
+2
-1
@@ -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"
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
Reference in New Issue
Block a user