mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 02:32:49 +00:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d226de405 | ||
|
|
ac74796263 | ||
|
|
c56bb14037 | ||
|
|
e8eba14c58 | ||
|
|
a4278b8dec | ||
|
|
6d173c7371 | ||
|
|
1779b011c9 | ||
|
|
3c4cf50cb6 | ||
|
|
e869d1f7ab | ||
|
|
e1a996435d | ||
|
|
c5a624a1f2 | ||
|
|
15fb7f775e | ||
|
|
3fa72e61c6 | ||
|
|
163b09a473 | ||
|
|
7afc2768f8 | ||
|
|
56f1a2bf46 | ||
|
|
fee47fe2f1 | ||
|
|
b994e081c3 | ||
|
|
a1f740b71b | ||
|
|
7f144365ba | ||
|
|
928459d16f | ||
|
|
9163359445 | ||
|
|
67307f0eeb | ||
|
|
91e0edc11d | ||
|
|
9fe75e262f | ||
|
|
0132f722a3 | ||
|
|
7d7a78308d | ||
|
|
9c568525b0 | ||
|
|
10f42e3e7c | ||
|
|
25d4b88605 | ||
|
|
d6abf35138 | ||
|
|
66ea99ce65 | ||
|
|
d9298cb7b9 | ||
|
|
34e1cf96d5 | ||
|
|
56cc03c4d4 | ||
|
|
35b416c6c0 | ||
|
|
7f2c3ba3f5 | ||
|
|
a97604d574 | ||
|
|
1146b77836 | ||
|
|
1338902ac5 | ||
|
|
60a1d70805 | ||
|
|
887d4c3a83 | ||
|
|
c1fa73470e | ||
|
|
e41d16ba7e | ||
|
|
d0b0cd6e13 | ||
|
|
18e93a96fc | ||
|
|
01f1139476 | ||
|
|
053cb337aa | ||
|
|
3ac5918ec8 | ||
|
|
bf5d095cf6 | ||
|
|
eae1b99232 | ||
|
|
15936f9a55 | ||
|
|
aac085c5c7 | ||
|
|
fe66ac546a | ||
|
|
a8e5fd0d2f | ||
|
|
93afb0d092 |
+25
-11
@@ -81,8 +81,8 @@ jobs:
|
||||
- name: Install apt packages (cached)
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
determine-jobs:
|
||||
name: Determine which jobs to run
|
||||
@@ -415,8 +415,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
@@ -680,7 +680,7 @@ jobs:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
|
||||
pio_cache_key: tidyesp8266
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino
|
||||
@@ -693,12 +693,16 @@ jobs:
|
||||
ignore_errors: false
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for RP2
|
||||
options: --environment rp2-tidy --grep USE_RP2
|
||||
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
|
||||
pio_cache_key: tidyrp2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for host
|
||||
options: --environment host-tidy --grep USE_HOST
|
||||
pio_cache_key: tidyhost
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for LibreTiny
|
||||
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
pio_cache_key: tidylibretiny
|
||||
|
||||
steps:
|
||||
@@ -707,6 +711,16 @@ jobs:
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
- name: Install apt packages (cached)
|
||||
if: matrix.pio_cache_key == 'tidyhost'
|
||||
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
|
||||
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
|
||||
# exactly.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
@@ -989,14 +1003,14 @@ jobs:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 S3
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM
|
||||
tidy_environment: esp32s3-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 P4
|
||||
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
|
||||
# so their code paths differ -- lint them under the P4 build too.
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
tidy_environment: esp32p4-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 C6
|
||||
@@ -1102,8 +1116,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -219,6 +219,7 @@ esphome/components/friedrich/* @rwrozelle
|
||||
esphome/components/fs3000/* @kahrendt
|
||||
esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
esphome/components/fusb302b/* @remcom
|
||||
esphome/components/gcja5/* @gcormier
|
||||
esphome/components/gdk101/* @Szewcson
|
||||
esphome/components/gl_r01_i2c/* @pkejval
|
||||
@@ -240,6 +241,7 @@ esphome/components/gt911/* @clydebarrow @jesserockz
|
||||
esphome/components/haier/* @paveldn
|
||||
esphome/components/haier/binary_sensor/* @paveldn
|
||||
esphome/components/haier/button/* @paveldn
|
||||
esphome/components/haier/select/* @paveldn
|
||||
esphome/components/haier/sensor/* @paveldn
|
||||
esphome/components/haier/switch/* @paveldn
|
||||
esphome/components/haier/text_sensor/* @paveldn
|
||||
@@ -385,6 +387,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/midea_portasplit/* @Fexiven
|
||||
esphome/components/mipi_dsi/* @clydebarrow
|
||||
esphome/components/mipi_rgb/* @clydebarrow
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "api_connection.h"
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -451,6 +454,15 @@ bool APIConnection::send_disconnect_response_() {
|
||||
DisconnectResponse resp;
|
||||
return this->send_message(resp);
|
||||
}
|
||||
void APIConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
logger::log_crash_to_client();
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::on_disconnect_response() {
|
||||
// Don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
#include "subscribe_state.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
#include "esphome/components/esp32/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
#include "esphome/components/rp2/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
#include "esphome/components/esp8266/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
@@ -298,21 +289,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
esp32::crash_handler_log();
|
||||
esp32::crash_handler_clear();
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
rp2::crash_handler_log();
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
esp8266::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
|
||||
#endif
|
||||
|
||||
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter)
|
||||
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter)
|
||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter)
|
||||
AutorepeatFilterTiming = binary_sensor_ns.struct("AutorepeatFilterTiming")
|
||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter)
|
||||
@@ -236,53 +237,49 @@ async def delayed_off_filter_to_code(config, filter_id):
|
||||
return var
|
||||
|
||||
|
||||
AUTOREPEAT_TIMING_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_DELAY, default=DEFAULT_DELAY
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_OFF, default=DEFAULT_TIME_OFF
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_ON, default=DEFAULT_TIME_ON
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_filter(
|
||||
"autorepeat",
|
||||
AutorepeatFilter,
|
||||
cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_DELAY, default=DEFAULT_DELAY
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_OFF, default=DEFAULT_TIME_OFF
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_ON, default=DEFAULT_TIME_ON
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
cv.Length(max=254),
|
||||
),
|
||||
cv.All(cv.ensure_list(AUTOREPEAT_TIMING_SCHEMA), cv.Length(max=254)),
|
||||
)
|
||||
async def autorepeat_filter_to_code(config, filter_id):
|
||||
if len(config) > 0:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", conf[CONF_DELAY]),
|
||||
("time_off", conf[CONF_TIME_OFF]),
|
||||
("time_on", conf[CONF_TIME_ON]),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
else:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
|
||||
(
|
||||
"time_off",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||
),
|
||||
(
|
||||
"time_on",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||
),
|
||||
)
|
||||
]
|
||||
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings)
|
||||
async def autorepeat_filter_to_code(config: list[ConfigType], filter_id: ID) -> MockObj:
|
||||
config = config or [AUTOREPEAT_TIMING_SCHEMA({})]
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
AutorepeatFilterTiming,
|
||||
("delay", conf[CONF_DELAY].total_milliseconds),
|
||||
("time_off", conf[CONF_TIME_OFF].total_milliseconds),
|
||||
("time_on", conf[CONF_TIME_ON].total_milliseconds),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
# A never-run delay ends the table, so the filter stores no count.
|
||||
timings.append(
|
||||
cg.StructInitializer(
|
||||
AutorepeatFilterTiming, ("delay", cg.RawExpression("SCHEDULER_DONT_RUN"))
|
||||
)
|
||||
)
|
||||
table = cg.shared_progmem_array(
|
||||
"binary_sensor_autorepeat_timings",
|
||||
AutorepeatFilterTiming,
|
||||
cg.ArrayInitializer(*timings),
|
||||
)
|
||||
return cg.new_Pvariable(filter_id, table)
|
||||
|
||||
|
||||
@register_filter("lambda", LambdaFilter, cv.returning_lambda)
|
||||
|
||||
@@ -63,12 +63,12 @@ optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
// AutorepeatFilterBase
|
||||
// AutorepeatFilter
|
||||
// Two independent timers per instance, keyed off two stable addresses inside
|
||||
// the filter: `this` for the timing-step timer, `&active_timing_` for the
|
||||
// on/off timer. Both are unique per instance and don't collide with anything
|
||||
// else, so the self-keyed scheduler API is sufficient.
|
||||
optional<bool> AutorepeatFilterBase::new_value(bool value) {
|
||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
if (this->active_timing_ != 0)
|
||||
return {};
|
||||
@@ -82,18 +82,18 @@ optional<bool> AutorepeatFilterBase::new_value(bool value) {
|
||||
}
|
||||
}
|
||||
|
||||
void AutorepeatFilterBase::next_timing_() {
|
||||
if (this->active_timing_ < this->timings_count_) {
|
||||
App.scheduler.set_timeout(this, this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
|
||||
}
|
||||
if (this->active_timing_ <= this->timings_count_) {
|
||||
this->active_timing_++;
|
||||
void AutorepeatFilter::next_timing_() {
|
||||
const uint32_t delay = this->timings_[this->active_timing_].delay;
|
||||
if (delay != SCHEDULER_DONT_RUN) {
|
||||
App.scheduler.set_timeout(this, delay, [this]() { this->next_timing_(); });
|
||||
}
|
||||
// Only reached with active_timing_ at most the end-marker index, so this never passes it.
|
||||
this->active_timing_++;
|
||||
if (this->active_timing_ == 2)
|
||||
this->next_value_(false);
|
||||
}
|
||||
|
||||
void AutorepeatFilterBase::next_value_(bool val) {
|
||||
void AutorepeatFilter::next_value_(bool val) {
|
||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||
this->output(val);
|
||||
App.scheduler.set_timeout(&this->active_timing_, val ? timing.time_on : timing.time_off,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
|
||||
#include <array>
|
||||
#include <type_traits>
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
@@ -87,39 +87,34 @@ struct AutorepeatFilterTiming {
|
||||
uint32_t time_off;
|
||||
uint32_t time_on;
|
||||
};
|
||||
// Read straight from flash on ESP8266, so every field must stay a word.
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::delay), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::time_off), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::time_on), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(sizeof(AutorepeatFilterTiming) == 3 * sizeof(uint32_t),
|
||||
"AutorepeatFilterTiming is read from flash with word loads");
|
||||
|
||||
/// Non-template base for AutorepeatFilter — all methods in filter.cpp.
|
||||
/// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once.
|
||||
/// Timings live in a PROGMEM table emitted by codegen, ended by an entry whose delay is
|
||||
/// SCHEDULER_DONT_RUN (a step that could never advance anyway). Aligned loads are ESP8266 safe.
|
||||
/// The two scheduled timers are keyed off `this` and `&active_timing_`; since the address
|
||||
/// of `active_timing_` is taken as a scheduler key, the class must not be copied or moved.
|
||||
class AutorepeatFilterBase : public Filter {
|
||||
class AutorepeatFilter : public Filter {
|
||||
public:
|
||||
explicit AutorepeatFilter(const AutorepeatFilterTiming *timings) : timings_(timings) {}
|
||||
AutorepeatFilter(const AutorepeatFilter &) = delete;
|
||||
AutorepeatFilter &operator=(const AutorepeatFilter &) = delete;
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
AutorepeatFilterBase(const AutorepeatFilterBase &) = delete;
|
||||
AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete;
|
||||
|
||||
protected:
|
||||
AutorepeatFilterBase() = default;
|
||||
void next_timing_();
|
||||
void next_value_(bool val);
|
||||
|
||||
const AutorepeatFilterTiming *timings_{nullptr};
|
||||
uint8_t timings_count_{0};
|
||||
uint8_t active_timing_{0};
|
||||
};
|
||||
|
||||
/// Template wrapper that provides inline std::array storage for timings.
|
||||
/// N is set by code generation to match the exact number of timings configured in YAML.
|
||||
template<size_t N> class AutorepeatFilter : public AutorepeatFilterBase {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) {
|
||||
init_array_from(this->timings_storage_, timings);
|
||||
this->timings_ = this->timings_storage_.data();
|
||||
this->timings_count_ = N;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::array<AutorepeatFilterTiming, N> timings_storage_{};
|
||||
uint8_t active_timing_{0}; // before timings_ so it fills the padding after the Filter base
|
||||
const AutorepeatFilterTiming *timings_;
|
||||
};
|
||||
|
||||
class LambdaFilter : public Filter {
|
||||
|
||||
@@ -100,7 +100,8 @@ void BM8563::get_date_(ESPTime &time) {
|
||||
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
|
||||
|
||||
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
|
||||
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
|
||||
// The chip counts weekdays 0-6 from Sunday (not BCD), ESPTime counts 1-7
|
||||
time.day_of_week = (buf[1] & 0x07) + 1;
|
||||
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
|
||||
|
||||
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
|
||||
@@ -115,7 +116,7 @@ void BM8563::get_date_(ESPTime &time) {
|
||||
void BM8563::set_date_(const ESPTime &time) {
|
||||
uint8_t buf[4] = {
|
||||
this->byte_to_bcd2_(time.day_of_month),
|
||||
this->byte_to_bcd2_(time.day_of_week),
|
||||
static_cast<uint8_t>(time.day_of_week - 1),
|
||||
this->byte_to_bcd2_(time.month),
|
||||
this->byte_to_bcd2_(time.year % 100),
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "bme680_bsec.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <string>
|
||||
|
||||
namespace esphome::bme680_bsec {
|
||||
#ifdef USE_BSEC
|
||||
@@ -9,7 +9,7 @@ ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor");
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
PROGMEM_STRING_TABLE(IaqAccuracyStrings, "Stabilizing", "Uncertain", "Calibrating", "Calibrated");
|
||||
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
|
||||
|
||||
std::vector<BME680BSECComponent *>
|
||||
BME680BSECComponent::instances; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -391,15 +391,10 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
|
||||
switch (outputs[i].sensor_id) {
|
||||
case BSEC_OUTPUT_IAQ:
|
||||
case BSEC_OUTPUT_STATIC_IAQ: {
|
||||
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
|
||||
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
|
||||
this->queue_push_([this, accuracy]() {
|
||||
char text[IaqAccuracyStrings::MAX_LENGTH + 1];
|
||||
ESPHOME_strncpy_P(text,
|
||||
reinterpret_cast<ESPHOME_PGM_P>(
|
||||
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX)),
|
||||
sizeof(text));
|
||||
this->publish_sensor_(this->iaq_accuracy_text_sensor_, text);
|
||||
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
|
||||
});
|
||||
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
|
||||
|
||||
@@ -445,7 +440,7 @@ void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, b
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const char *value) {
|
||||
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
|
||||
if (!sensor || (sensor->has_state() && sensor->state == value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
|
||||
int64_t get_time_ns_();
|
||||
|
||||
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
|
||||
void publish_sensor_(text_sensor::TextSensor *sensor, const char *value);
|
||||
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
|
||||
|
||||
void snapshot_state_(); // Fetch the current BSEC library state and save it in the bsec_state_data_ member (volatile
|
||||
// memory)
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace esphome::cse7766 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cse7766");
|
||||
|
||||
// The chip only sends the time since the last pulse once a cycle is longer than 1 s (in us)
|
||||
static constexpr uint32_t MIN_HELD_POWER_CYCLE = 1000000;
|
||||
|
||||
void CSE7766Component::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_transmission_ >= 500) {
|
||||
@@ -154,18 +157,33 @@ void CSE7766Component::parse_data_() {
|
||||
if (power_cycle_exceeds_range) {
|
||||
// Datasheet: power cycle exceeding range means active power is 0
|
||||
have_power = true;
|
||||
this->last_power_cycle_ = 0;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
} else if (have_power) {
|
||||
power = power_coeff / float(power_cycle);
|
||||
this->last_power_cycle_ = power_cycle;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
} else if (this->power_sensor_ != nullptr) {
|
||||
// No valid power measurement from chip - publish 0W to avoid stale readings
|
||||
// This typically happens when current is below the measurable threshold (~50mA)
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
} else if (this->last_power_cycle_ > MIN_HELD_POWER_CYCLE && power_cycle <= 2 * this->last_power_cycle_) {
|
||||
// No new measurement in this frame. With cycles over 1 s the register holds the last
|
||||
// cycle or the time elapsed since the last pulse. While that time is within twice the
|
||||
// last cycle, the next pulse is not overdue, so the last measurement still applies.
|
||||
// Shorter cycles never get here in normal operation and keep the previous behaviour.
|
||||
have_power = true;
|
||||
power = power_coeff / float(this->last_power_cycle_);
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
} else {
|
||||
// The next pulse is overdue: the load has dropped, report 0 W. A load that drops to less
|
||||
// than half also reads 0 W here until its first new pulse arrives.
|
||||
this->last_power_cycle_ = 0;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
float current = 0.0f;
|
||||
|
||||
@@ -36,6 +36,7 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
|
||||
|
||||
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
|
||||
uint8_t raw_data_index_{0};
|
||||
uint16_t cf_pulses_last_{0};
|
||||
uint32_t last_transmission_{0};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
@@ -45,7 +46,8 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
|
||||
sensor::Sensor *reactive_power_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_{nullptr};
|
||||
uint32_t cf_pulses_total_{0};
|
||||
uint16_t cf_pulses_last_{0};
|
||||
// Power cycle of the last frame that carried a new power measurement, 0 if none
|
||||
uint32_t last_power_cycle_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::cse7766
|
||||
|
||||
@@ -250,6 +250,42 @@ def add_metadata(
|
||||
)
|
||||
|
||||
|
||||
KEY_EXTERNAL_WRITERS = "external_writers"
|
||||
|
||||
|
||||
def _get_external_writer_list() -> list[tuple[ID, str]]:
|
||||
"""Get the raw external writer list. Each entry is (display id, owner)."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
|
||||
|
||||
|
||||
def register_external_writer(display_id: ID, owner: str) -> None:
|
||||
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
|
||||
|
||||
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
|
||||
resolves it in place before final validation.
|
||||
"""
|
||||
_get_external_writer_list().append((display_id, owner))
|
||||
|
||||
|
||||
def get_external_writers(display_id: ID) -> list[str]:
|
||||
"""Get the owners registered for a display. Only valid once IDs are resolved."""
|
||||
return [
|
||||
owner
|
||||
for id_, owner in _get_external_writer_list()
|
||||
if id_.id is not None and id_.id == display_id.id
|
||||
]
|
||||
|
||||
|
||||
def has_external_writer(display_id: ID) -> bool:
|
||||
"""Tell a driver's final validation that something else draws into the display.
|
||||
|
||||
LVGL anywhere in the config also counts, as the drivers assumed before.
|
||||
"""
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
|
||||
|
||||
|
||||
async def register_display(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await setup_display_core_(var, config)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
|
||||
// -Os stops inlining it once there are two call sites, which costs a call and
|
||||
// spills per pixel. Without PSRAM there is one call site and it changes
|
||||
// nothing, and draw_pixel_at() stays a virtual call either way.
|
||||
template<ColorBitness BITNESS>
|
||||
static void __attribute__((flatten))
|
||||
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
// BITNESS is a constant here, so to_color()'s three per pixel divisions
|
||||
// become a multiply and shift and its bitness switch folds.
|
||||
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
|
||||
display->draw_pixel_at(
|
||||
x + x_start, y + y_start,
|
||||
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
|
||||
};
|
||||
// The source and frame buffers are row-major, so walking rows keeps the read
|
||||
// and the write sequential. When the display swaps the axes before writing,
|
||||
// walking columns keeps the write sequential instead; with the frame buffer
|
||||
// in PSRAM a strided write costs more than a strided read.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
size_t column_idx = y_offset * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, column_idx++) {
|
||||
size_t source_idx = column_idx;
|
||||
for (int y = 0; y != h; y++, source_idx += line_stride) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
switch (bitness) {
|
||||
case COLOR_BITNESS_565:
|
||||
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
default:
|
||||
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
|
||||
@@ -139,6 +139,11 @@ enum DisplayRotation {
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
/// Whether a rotation swaps the x and y axes.
|
||||
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
|
||||
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
const int EDGES_TRIGON = 3;
|
||||
const int EDGES_TRIANGLE = 3;
|
||||
const int EDGES_TETRAGON = 4;
|
||||
@@ -717,6 +722,12 @@ class Display : public PollingComponent {
|
||||
|
||||
virtual DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
|
||||
/// neighbours along x land a row apart in the frame buffer. Not the same as
|
||||
/// get_width()/get_height() swapping: a display that rotates in hardware
|
||||
/// reports swapped dimensions but writes unswapped pixels.
|
||||
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::display {
|
||||
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
|
||||
@@ -10,6 +11,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
|
||||
|
||||
class ColorUtil {
|
||||
public:
|
||||
/// Reads one packed pixel of the given bitness from a source buffer into the
|
||||
/// value to_color() takes. BITNESS is a template argument so the byte layout
|
||||
/// folds; pass it on to to_color() as a constant too, and its channel scaling
|
||||
/// divides by a constant, which the compiler turns into a multiply and shift.
|
||||
template<ColorBitness BITNESS>
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
|
||||
if constexpr (BITNESS == COLOR_BITNESS_565) {
|
||||
const size_t idx = index * 2;
|
||||
if (big_endian)
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
}
|
||||
}
|
||||
|
||||
static Color to_color(uint32_t colorcode, ColorOrder color_order,
|
||||
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
|
||||
uint8_t first_color, second_color, third_color;
|
||||
|
||||
@@ -40,7 +40,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -198,15 +197,11 @@ def _final_validate(config) -> None:
|
||||
"epaper_spi", require_miso=False, require_mosi=True
|
||||
)(config)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if display.has_external_writer(config[CONF_ID]):
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
|
||||
@@ -106,6 +106,7 @@ class EPaperBase : public Display,
|
||||
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
@@ -2920,6 +2920,7 @@ async def _finalize_arduino_aware_flags():
|
||||
if not arduino_linked:
|
||||
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
|
||||
cg.add_define("USE_ESP32_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
elif not CORE.using_arduino:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-c
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "esp32.crash");
|
||||
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG);
|
||||
|
||||
// RAM copy of the build timestamp. The generated constant lives in flash,
|
||||
// which the panic handler must not read (cache may be disabled during
|
||||
@@ -175,7 +175,8 @@ static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME)
|
||||
|
||||
// Validate the NOINIT record. Runs on every has_data() call; re-running is
|
||||
// harmless and the magic is left alone so the record survives an OTA
|
||||
// rollback reboot, crash_handler_clear() drops it once an API client has it.
|
||||
// rollback reboot, crash_handler_clear() drops it once a log client has it
|
||||
// (logger::log_crash_to_client(), for the API and MQTT).
|
||||
static void read_crash_data() {
|
||||
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
|
||||
s_crash_data_valid = true;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
/// The tag the crash record is logged under, so a client can check the level in force for it
|
||||
inline constexpr const char *CRASH_LOG_TAG = "esp32.crash";
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
void ESP32Preferences::open() {
|
||||
// Runs from app_main() before the logger is initialized; any logging here
|
||||
// must be deferred. See s_open_err and make_preference() below.
|
||||
// Components that need NVS check nvs_handle instead of initializing it again.
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
#include <esp_bt.h>
|
||||
@@ -28,7 +29,6 @@ extern "C" {
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
|
||||
@@ -77,8 +77,9 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
|
||||
|
||||
void ESP32BLE::setup() {
|
||||
global_ble = this;
|
||||
if (!ble_pre_setup_()) {
|
||||
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
|
||||
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
|
||||
if (global_preferences->nvs_handle == 0) {
|
||||
ESP_LOGE(TAG, "NVS unavailable");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -185,15 +186,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ESP32BLE::ble_pre_setup_() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_init_() {
|
||||
if (this->advertising_ != nullptr)
|
||||
|
||||
@@ -185,7 +185,6 @@ class ESP32BLE final : public Component {
|
||||
}
|
||||
this->ble_events_.get_and_reset_dropped_count();
|
||||
}
|
||||
bool ble_pre_setup_();
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void advertising_init_();
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <nvs_flash.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <esp_bt_main.h>
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_OTA
|
||||
|
||||
@@ -47,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
|
||||
CAN_MODES = {
|
||||
"NORMAL": CanMode.CAN_MODE_NORMAL,
|
||||
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
|
||||
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
|
||||
}
|
||||
|
||||
# Currently the driver only supports a subset of the bit rates defined in canbus
|
||||
|
||||
@@ -79,10 +79,13 @@ bool ESP32Can::setup_internal() {
|
||||
}
|
||||
|
||||
// Select TWAI mode based on configuration
|
||||
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
|
||||
|
||||
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
|
||||
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
|
||||
twai_mode = TWAI_MODE_LISTEN_ONLY;
|
||||
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
|
||||
} else if (this->mode_ == CAN_MODE_NO_ACK) {
|
||||
twai_mode = TWAI_MODE_NO_ACK;
|
||||
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
|
||||
}
|
||||
|
||||
twai_general_config_t g_config =
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace esphome::esp32_can {
|
||||
enum CanMode : uint8_t {
|
||||
CAN_MODE_NORMAL = 0,
|
||||
CAN_MODE_LISTEN_ONLY = 1,
|
||||
CAN_MODE_NO_ACK = 2,
|
||||
};
|
||||
|
||||
class ESP32Can final : public canbus::Canbus {
|
||||
|
||||
@@ -42,6 +42,7 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include "esp_now_hosted_rpc.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -52,6 +53,8 @@ ESPHOME_LOG_TAG(TAG, "esp_now_hosted");
|
||||
// single esp-hosted RPC RX thread. Serializing requests keeps the shared
|
||||
// response slot race-free; a sequence number stops a late/stale response from
|
||||
// being mistaken for ours.
|
||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables): the shim is
|
||||
// C-ABI state shared between the main loop and the esp-hosted RX thread.
|
||||
SemaphoreHandle_t g_req_mutex = nullptr;
|
||||
SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands
|
||||
bool g_setup_done = false; // set only after setup fully succeeds
|
||||
@@ -85,6 +88,7 @@ constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20;
|
||||
uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6];
|
||||
size_t g_peer_count = 0;
|
||||
portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// Caller must hold g_peer_lock.
|
||||
int peer_cache_find_locked(const uint8_t *mac) {
|
||||
@@ -231,12 +235,14 @@ esp_err_t ensure_setup() {
|
||||
g_resp_sem = xSemaphoreCreateBinary();
|
||||
if (g_req_mutex == nullptr || g_resp_sem == nullptr)
|
||||
return ESP_ERR_NO_MEM;
|
||||
esp_err_t err;
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK)
|
||||
esp_err_t err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK)
|
||||
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK)
|
||||
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
g_setup_done = true;
|
||||
return ESP_OK;
|
||||
@@ -422,10 +428,11 @@ esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len
|
||||
uint8_t hdr[sizeof(esp_now_hosted_send_req_t)];
|
||||
auto *s = reinterpret_cast<esp_now_hosted_send_req_t *>(hdr);
|
||||
s->has_addr = peer_addr != nullptr ? 1 : 0;
|
||||
if (peer_addr != nullptr)
|
||||
if (peer_addr != nullptr) {
|
||||
memcpy(s->peer_addr, peer_addr, 6);
|
||||
else
|
||||
} else {
|
||||
memset(s->peer_addr, 0, 6);
|
||||
}
|
||||
s->data_len = static_cast<uint16_t>(len);
|
||||
// Fire-and-forget (wait=false): native esp_now_send returns once the frame is
|
||||
// queued, with the real TX result delivered later through the send callback.
|
||||
|
||||
@@ -53,10 +53,14 @@ from .const import (
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
KEY_WAVEFORM_REQUIRED,
|
||||
PREFERENCES_RTC_EBOOT_WORDS,
|
||||
PREFERENCES_RTC_NORMAL_WORDS,
|
||||
THROW_STUBS_HEADER,
|
||||
check_preferences_fit,
|
||||
enable_serial,
|
||||
enable_serial1,
|
||||
esp8266_ns,
|
||||
preferences_flash_words,
|
||||
)
|
||||
from .gpio import PinInitialState, add_pin_initial_states_array
|
||||
|
||||
@@ -416,6 +420,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
cg.add_define("USE_ESP8266_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
|
||||
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
|
||||
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
|
||||
@@ -467,6 +472,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Other projects like Tasmota & ESPEasy also use this
|
||||
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
|
||||
|
||||
cg.add_define("ESP8266_PREFERENCES_RTC_NORMAL_WORDS", PREFERENCES_RTC_NORMAL_WORDS)
|
||||
cg.add_define("ESP8266_PREFERENCES_RTC_EBOOT_WORDS", PREFERENCES_RTC_EBOOT_WORDS)
|
||||
cg.add_define(
|
||||
"ESP8266_PREFERENCES_FLASH_WORDS",
|
||||
preferences_flash_words(config[CONF_RESTORE_FROM_FLASH]),
|
||||
)
|
||||
if config[CONF_RESTORE_FROM_FLASH]:
|
||||
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
|
||||
|
||||
@@ -528,6 +539,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
CORE.add_job(add_pin_initial_states_array)
|
||||
CORE.add_job(finalize_waveform_config)
|
||||
CORE.add_job(finalize_serial_config)
|
||||
CORE.add_job(check_preferences_fit)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
|
||||
# Re-exported from the shared definition; here it indexes the BOARDS
|
||||
# metadata dicts, whose entries in boards.py spell the literal.
|
||||
from esphome.const import KEY_FLASH_SIZE # noqa: F401 # pylint: disable=unused-import
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
KEY_ESP8266 = "esp8266"
|
||||
KEY_BOARD = "board"
|
||||
@@ -24,6 +28,87 @@ KEY_LDSCRIPT = "ldscript"
|
||||
# Crash-decode tools, resolved once per run by __init__
|
||||
KEY_DECODE_TOOLS = "decode_tools"
|
||||
|
||||
# Saved-settings space in 4-byte words; preferences.cpp gets these as defines. A value cannot span the two
|
||||
# RTC regions, and the flash store is smaller when only explicit flash preferences use it.
|
||||
PREFERENCES_RTC_NORMAL_WORDS = 78
|
||||
PREFERENCES_RTC_EBOOT_WORDS = 32
|
||||
PREFERENCES_RTC_WORDS = PREFERENCES_RTC_NORMAL_WORDS + PREFERENCES_RTC_EBOOT_WORDS
|
||||
PREFERENCES_FLASH_WORDS = 128
|
||||
PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE = 64
|
||||
KEY_PREFERENCES = "preferences"
|
||||
|
||||
|
||||
def preferences_flash_words(restore_from_flash: bool) -> int:
|
||||
"""Size of the flash store for this configuration."""
|
||||
return (
|
||||
PREFERENCES_FLASH_WORDS
|
||||
if restore_from_flash
|
||||
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
|
||||
)
|
||||
|
||||
|
||||
def preference_words(value_bytes: int) -> int:
|
||||
"""Words a saved value takes, checksum word included (rtc_pref_bytes_to_words plus one)."""
|
||||
return (value_bytes + 3) // 4 + 1
|
||||
|
||||
|
||||
def reserve_preferences(
|
||||
count: int, value_bytes: int, in_flash: bool | None = None
|
||||
) -> None:
|
||||
"""Record saved values a component creates; check_preferences_fit() adds them all up.
|
||||
|
||||
in_flash is the value's own storage, as passed to make_preference(); None is the two-argument
|
||||
form, which follows restore_from_flash.
|
||||
"""
|
||||
if in_flash is None:
|
||||
in_flash = CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
|
||||
reservations = CORE.data.setdefault(KEY_ESP8266, {}).setdefault(KEY_PREFERENCES, [])
|
||||
reservations.extend([(preference_words(value_bytes), in_flash)] * count)
|
||||
|
||||
|
||||
def _rtc_values_can_split(values: list[int]) -> bool:
|
||||
"""Whether some order fills the two RTC regions without a value spanning them."""
|
||||
total = sum(values)
|
||||
if total <= PREFERENCES_RTC_NORMAL_WORDS:
|
||||
return True
|
||||
# Bit i of reachable is set when some subset of the values sums to i
|
||||
reachable = 1
|
||||
for words in values:
|
||||
reachable |= reachable << words
|
||||
lowest = max(0, total - PREFERENCES_RTC_EBOOT_WORDS)
|
||||
highest = min(total, PREFERENCES_RTC_NORMAL_WORDS)
|
||||
return any(reachable >> normal & 1 for normal in range(lowest, highest + 1))
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def check_preferences_fit() -> None:
|
||||
"""Fail the build when the reserved saved values cannot fit the space preferences.cpp has."""
|
||||
flash_store = preferences_flash_words(
|
||||
CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
|
||||
)
|
||||
reservations = CORE.data[KEY_ESP8266].get(KEY_PREFERENCES, [])
|
||||
flash_words = sum(w for w, in_flash in reservations if in_flash)
|
||||
rtc_values = [w for w, in_flash in reservations if not in_flash]
|
||||
if flash_words > flash_store:
|
||||
raise EsphomeError(
|
||||
f"Saved values need {flash_words} words but the ESP8266 flash store has {flash_store}; "
|
||||
"use fewer or smaller saved values"
|
||||
)
|
||||
if not _rtc_values_can_split(rtc_values):
|
||||
raise EsphomeError(
|
||||
f"Saved values need {sum(rtc_values)} words but ESP8266 RTC memory has {PREFERENCES_RTC_WORDS} in "
|
||||
f"regions of {PREFERENCES_RTC_NORMAL_WORDS} and {PREFERENCES_RTC_EBOOT_WORDS} that a value cannot "
|
||||
"span; use fewer or smaller saved values, or store them in flash"
|
||||
)
|
||||
if sum(rtc_values) > PREFERENCES_RTC_NORMAL_WORDS:
|
||||
_LOGGER.warning(
|
||||
"Saved values reserve %d of %d words of ESP8266 RTC memory; the fit depends on setup order, and "
|
||||
"saved settings that do not reserve are not counted",
|
||||
sum(rtc_values),
|
||||
PREFERENCES_RTC_WORDS,
|
||||
)
|
||||
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
#include "crash_handler.h"
|
||||
#include "rtc_layout.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
@@ -46,11 +47,8 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
|
||||
// Recover the actual code address from a windowed-ABI return address on the stack.
|
||||
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
|
||||
|
||||
// RTC user memory layout for crash backtrace data.
|
||||
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
|
||||
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
|
||||
// Crash data lives in the last esphome::esp8266::RTC_CRASH_WORDS of user RTC memory (rtc_layout.h).
|
||||
// Store 16 raw candidates, filter to real return addresses at log time.
|
||||
static constexpr uint8_t RTC_CRASH_BASE = 174;
|
||||
static constexpr size_t MAX_BACKTRACE = 16;
|
||||
|
||||
// Magic word packs sentinel, version, and count into one uint32_t:
|
||||
@@ -78,7 +76,7 @@ struct RtcCrashData {
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint32_t epc1; // Fault PC, used to filter duplicates
|
||||
};
|
||||
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
|
||||
static_assert(sizeof(RtcCrashData) == esphome::esp8266::RTC_CRASH_WORDS * 4, "RtcCrashData must fill its RTC words");
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
@@ -129,7 +127,7 @@ static const LogString *get_reset_reason(uint32_t reason) {
|
||||
// Returns the number of valid backtrace entries (0 if no data found).
|
||||
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
|
||||
RtcCrashData rtc_data;
|
||||
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
|
||||
if (!system_rtc_mem_read(esphome::esp8266::RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
|
||||
return 0;
|
||||
uint32_t magic = rtc_data.magic;
|
||||
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
|
||||
@@ -233,7 +231,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3
|
||||
data.epc1 = epc1;
|
||||
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
|
||||
|
||||
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
|
||||
system_rtc_mem_write(esphome::esp8266::RTC_CRASH_BASE, &data, sizeof(data));
|
||||
}
|
||||
|
||||
#endif // USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
@@ -6,6 +6,7 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include "preferences.h"
|
||||
#include "rtc_layout.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences_rtc.h"
|
||||
@@ -17,31 +18,26 @@ namespace esphome::esp8266 {
|
||||
ESPHOME_LOG_TAG(TAG, "preferences");
|
||||
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = RTC_USER_MEM_WORDS;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
|
||||
|
||||
// RTC memory layout:
|
||||
// RTC memory layout (sizes come from const.py through codegen):
|
||||
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
|
||||
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
|
||||
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
|
||||
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
|
||||
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = ESP8266_PREFERENCES_RTC_EBOOT_WORDS;
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = ESP8266_PREFERENCES_RTC_NORMAL_WORDS;
|
||||
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
|
||||
static_assert(PREF_TOTAL_WORDS <= RTC_CRASH_BASE - RTC_SYSTEM_USER_BASE,
|
||||
"preference regions overlap the crash handler's RTC words");
|
||||
|
||||
// Maximum preference size in words (limited by uint8_t length_words field)
|
||||
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
||||
|
||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||
|
||||
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
|
||||
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
|
||||
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
|
||||
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
|
||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||
#else
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
|
||||
#endif
|
||||
// Smaller without restore_from_flash, when only explicit flash preferences live here
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = ESP8266_PREFERENCES_FLASH_WORDS;
|
||||
|
||||
static uint32_t
|
||||
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -60,7 +56,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
|
||||
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
|
||||
return false;
|
||||
}
|
||||
if (index < 32 && s_prevent_write) {
|
||||
if (index < RTC_EBOOT_REGION_WORDS && s_prevent_write) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,7 +119,7 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
||||
|
||||
// Maximum buffer for any single preference - bounded by storage sizes.
|
||||
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (78) - a single pref can't span both RTC regions.
|
||||
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
@@ -180,8 +176,8 @@ ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t
|
||||
} else {
|
||||
uint32_t start = this->current_offset;
|
||||
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
|
||||
// Normal: offset 0-95 maps to RTC offset 32-127
|
||||
// Eboot: offset 96-127 maps to RTC offset 0-31
|
||||
// Normal: offset 0-77 maps to RTC offset 32-109
|
||||
// Eboot: offset 78-109 maps to RTC offset 0-31
|
||||
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
|
||||
// start is in normal but end is not -> switch to Eboot
|
||||
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
// User RTC memory is system blocks 64-191, one 4-byte word each. preferences.cpp owns the first
|
||||
// words, crash_handler.cpp the last; const.py hands preferences.cpp its region sizes.
|
||||
static constexpr uint32_t RTC_SYSTEM_USER_BASE = 64;
|
||||
static constexpr uint32_t RTC_USER_MEM_WORDS = 128;
|
||||
static constexpr uint32_t RTC_CRASH_WORDS = 18;
|
||||
static constexpr uint32_t RTC_CRASH_BASE = RTC_SYSTEM_USER_BASE + RTC_USER_MEM_WORDS - RTC_CRASH_WORDS;
|
||||
|
||||
} // namespace esphome::esp8266
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@remcom"]
|
||||
DOMAIN = "fusb302b"
|
||||
@@ -0,0 +1,330 @@
|
||||
#include "pd.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "fusb302b.pd");
|
||||
|
||||
static constexpr uint32_t AMS_TIMEOUT_MS = 2000;
|
||||
// tSinkRequest: delay before repeating a request that the source answered with Wait
|
||||
static constexpr uint32_t SINK_REQUEST_MS = 100;
|
||||
static constexpr uint8_t MAX_WAIT_RETRIES = 10;
|
||||
|
||||
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object flags; only the first (5 V) object carries them
|
||||
static constexpr uint32_t SINK_PDO_HIGHER_CAPABILITY = 1u << 28;
|
||||
static constexpr uint32_t SINK_PDO_USB_COMMS_CAPABLE = 1u << 26;
|
||||
// Operational current advertised for every sink object: 3 A (300 x 10 mA)
|
||||
static constexpr uint16_t SINK_PDO_CURRENT = 300;
|
||||
|
||||
// USB PD 6.4.1.2.3 Source Fixed Supply Power Data Object; other PDO types only report their type
|
||||
static PdContract parse_pdo(uint32_t pdo) {
|
||||
PdContract info{};
|
||||
info.type = static_cast<PdPdoType>(pdo >> 30);
|
||||
if (info.type == PD_PDO_TYPE_FIXED_SUPPLY) {
|
||||
info.max_v = (pdo >> 10) & 0x3FF;
|
||||
info.max_i = pdo & 0x3FF;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object; voltage in 50 mV units, current in 10 mA units
|
||||
static constexpr uint32_t sink_fixed_pdo(uint16_t voltage, uint16_t current) {
|
||||
return (static_cast<uint32_t>(PD_PDO_TYPE_FIXED_SUPPLY) << 30) | (static_cast<uint32_t>(voltage & 0x3FF) << 10) |
|
||||
(current & 0x3FF);
|
||||
}
|
||||
|
||||
void PdMsg::set_header(uint16_t header) {
|
||||
this->type = header & 0x1F;
|
||||
this->spec_rev = static_cast<PdSpecRevision>((header >> 6) & 0x3);
|
||||
this->id = (header >> 9) & 0x7;
|
||||
this->num_of_obj = (header >> 12) & 0x7;
|
||||
this->extended = (header >> 15) & 0x1;
|
||||
}
|
||||
|
||||
uint16_t PdMsg::get_coded_header() const {
|
||||
// Data role UFP (bit 5 = 0), power role sink (bit 8 = 0)
|
||||
return static_cast<uint16_t>(this->type & 0x1F) | (static_cast<uint16_t>(this->spec_rev) << 6) |
|
||||
(static_cast<uint16_t>(this->id & 0x7) << 9) | (static_cast<uint16_t>(this->num_of_obj & 0x7) << 12) |
|
||||
(static_cast<uint16_t>(this->extended) << 15);
|
||||
}
|
||||
|
||||
PdMsg PowerDelivery::make_control_msg_(PdControlMsgType type) const {
|
||||
PdMsg msg;
|
||||
msg.type = type;
|
||||
msg.id = this->msg_counter_ % 8;
|
||||
return msg;
|
||||
}
|
||||
|
||||
PdMsg PowerDelivery::make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const {
|
||||
PdMsg msg;
|
||||
msg.type = type;
|
||||
msg.id = this->msg_counter_ % 8;
|
||||
msg.num_of_obj = len;
|
||||
memcpy(msg.data_objects, objects, len * sizeof(uint32_t));
|
||||
return msg;
|
||||
}
|
||||
|
||||
void PowerDelivery::reset_protocol_() {
|
||||
this->last_received_msg_id_ = 255;
|
||||
this->msg_counter_ = 0;
|
||||
this->active_ams_ = false;
|
||||
this->ps_rdy_expected_ = false;
|
||||
this->retry_request_pending_ = false;
|
||||
this->wait_retries_ = 0;
|
||||
}
|
||||
|
||||
void PowerDelivery::set_state_(PdState state) {
|
||||
if (this->state_.exchange(state) != state)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::set_contract_(const PdContract &contract) {
|
||||
this->accepted_contract_ = contract;
|
||||
const uint32_t packed = PackedContract::from(contract).value;
|
||||
if (this->contract_.exchange(packed) != packed)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::clear_contract_() {
|
||||
this->accepted_contract_ = {};
|
||||
if (this->contract_.exchange(0) != 0)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::set_state_and_contract_(PdState state, const PdContract &contract) {
|
||||
this->accepted_contract_ = contract;
|
||||
const uint32_t packed = PackedContract::from(contract).value;
|
||||
// The contract is stored first, so a reader that sees the new state also sees the new contract
|
||||
const bool contract_changed = this->contract_.exchange(packed) != packed;
|
||||
const bool state_changed = this->state_.exchange(state) != state;
|
||||
if (contract_changed || state_changed)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
void PowerDelivery::set_ams_(bool active) {
|
||||
this->active_ams_ = active;
|
||||
// A new sequence has no accepted request yet, and an ended one can no longer complete
|
||||
this->ps_rdy_expected_ = false;
|
||||
if (active)
|
||||
this->ams_start_ = millis();
|
||||
}
|
||||
|
||||
bool PowerDelivery::check_ams_() {
|
||||
if (this->active_ams_ && millis() - this->ams_start_ > AMS_TIMEOUT_MS)
|
||||
this->set_ams_(false);
|
||||
return this->active_ams_;
|
||||
}
|
||||
|
||||
void PowerDelivery::check_request_retry_() {
|
||||
if (!this->retry_request_pending_ || millis() - this->wait_received_ms_ < SINK_REQUEST_MS)
|
||||
return;
|
||||
if (this->send_request_()) {
|
||||
this->retry_request_pending_ = false;
|
||||
} else if (this->wait_retries_ < MAX_WAIT_RETRIES) {
|
||||
// Try again after another tSinkRequest; failed attempts count against the same limit as Wait replies
|
||||
this->wait_retries_++;
|
||||
this->wait_received_ms_ = millis();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Giving up repeating the request");
|
||||
this->retry_request_pending_ = false;
|
||||
this->request_failed_();
|
||||
}
|
||||
}
|
||||
|
||||
bool PowerDelivery::send_request_() {
|
||||
this->set_ams_(true);
|
||||
if (this->send_message(this->make_data_msg_(PD_DATA_REQUEST, &this->last_rdo_, 1)))
|
||||
return true;
|
||||
ESP_LOGW(TAG, "Sending the request failed");
|
||||
this->set_ams_(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
void PowerDelivery::request_failed_() {
|
||||
// A contract already in place stays valid. Without one, report that negotiation is over at default power.
|
||||
if (this->state_ == PdState::PD_STATE_DEFAULT_CONTRACT)
|
||||
this->set_state_(PdState::PD_STATE_PD_TIMEOUT);
|
||||
}
|
||||
|
||||
void PowerDelivery::check_transition_timeout_() {
|
||||
// The AMS that sent the request only ends on PS_RDY, so an ended AMS while in transition means a timeout
|
||||
if (this->state_ != PdState::PD_STATE_TRANSITION || this->check_ams_())
|
||||
return;
|
||||
ESP_LOGW(TAG, "Source did not report the new supply ready, falling back to default power");
|
||||
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
|
||||
this->on_transition_timeout();
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_transmit_failed_() {
|
||||
// USB PD 6.12.2.2: the counter also moves on after a failed transmission. The source may have received the
|
||||
// message and only its GoodCRC was lost, so reusing the MessageID would make it drop the next one as a repeat.
|
||||
ESP_LOGD(TAG, "Message %u not acknowledged", this->msg_counter_ % 8);
|
||||
this->msg_counter_++;
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_message_(const PdMsg &msg) {
|
||||
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_GOODCRC) {
|
||||
// A GoodCRC carries the MessageID it acknowledges; a duplicate or stray one must not advance the counter
|
||||
if (msg.id != this->msg_counter_ % 8) {
|
||||
ESP_LOGD(TAG, "Ignoring GoodCRC for MessageID %u, expected %u", msg.id, this->msg_counter_ % 8);
|
||||
return;
|
||||
}
|
||||
// Our last message was received, so the next one gets a new MessageID
|
||||
this->msg_counter_++;
|
||||
return;
|
||||
}
|
||||
// Soft_Reset always has MessageID 0, so it must not be dropped as a retransmission of the last message
|
||||
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_SOFT_RESET) {
|
||||
this->handle_soft_reset_(msg);
|
||||
return;
|
||||
}
|
||||
// Retransmissions repeat the MessageID of a message that was already handled
|
||||
if (msg.id == this->last_received_msg_id_)
|
||||
return;
|
||||
this->last_received_msg_id_ = msg.id;
|
||||
|
||||
if (msg.num_of_obj == 0) {
|
||||
this->handle_control_message_(msg);
|
||||
} else {
|
||||
this->handle_data_message_(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_data_message_(const PdMsg &msg) {
|
||||
if (msg.extended || msg.type != PD_DATA_SOURCE_CAP) {
|
||||
// BIST and Vendor_Defined messages must not be answered with Reject, so unsupported data messages are ignored
|
||||
ESP_LOGV(TAG, "Ignoring data message type %u%s", msg.type,
|
||||
msg.extended ? LOG_STR_LITERAL(" (extended)") : LOG_STR_LITERAL(""));
|
||||
return;
|
||||
}
|
||||
this->on_source_caps_received();
|
||||
this->respond_to_source_caps_(msg);
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_control_message_(const PdMsg &msg) {
|
||||
switch (msg.type) {
|
||||
case PD_CNTRL_ACCEPT:
|
||||
if (!this->check_ams_()) {
|
||||
ESP_LOGD(TAG, "Ignoring Accept outside a request");
|
||||
break;
|
||||
}
|
||||
this->ps_rdy_expected_ = true;
|
||||
if (this->requested_contract_ != this->accepted_contract_)
|
||||
this->set_state_and_contract_(PdState::PD_STATE_TRANSITION, this->requested_contract_);
|
||||
break;
|
||||
case PD_CNTRL_PS_RDY:
|
||||
// check_ams_() first, so a PS_RDY that arrives after the sequence timed out is ignored too
|
||||
if (!this->check_ams_() || !this->ps_rdy_expected_) {
|
||||
ESP_LOGD(TAG, "Ignoring PS_RDY without an accepted request");
|
||||
break;
|
||||
}
|
||||
this->set_ams_(false);
|
||||
this->set_state_(PdState::PD_STATE_EXPLICIT_CONTRACT);
|
||||
break;
|
||||
case PD_CNTRL_REJECT:
|
||||
if (!this->check_ams_()) {
|
||||
ESP_LOGD(TAG, "Ignoring Reject outside a request");
|
||||
break;
|
||||
}
|
||||
ESP_LOGW(TAG, "Source rejected the request");
|
||||
this->set_ams_(false);
|
||||
this->request_failed_();
|
||||
break;
|
||||
case PD_CNTRL_WAIT:
|
||||
if (!this->check_ams_()) {
|
||||
ESP_LOGD(TAG, "Ignoring Wait outside a request");
|
||||
break;
|
||||
}
|
||||
this->set_ams_(false);
|
||||
if (this->wait_retries_ < MAX_WAIT_RETRIES) {
|
||||
ESP_LOGD(TAG, "Source asked to wait, repeating the request");
|
||||
this->wait_retries_++;
|
||||
this->retry_request_pending_ = true;
|
||||
this->wait_received_ms_ = millis();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Source kept answering Wait, giving up");
|
||||
this->request_failed_();
|
||||
}
|
||||
break;
|
||||
case PD_CNTRL_PING:
|
||||
case PD_CNTRL_NOT_SUPPORTED:
|
||||
break;
|
||||
case PD_CNTRL_GET_SINK_CAP: {
|
||||
// 5 V first, then the requested voltage. Higher Capability tells the source that 5 V is not enough for
|
||||
// full operation.
|
||||
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
|
||||
uint32_t pdos[2];
|
||||
uint8_t count = 0;
|
||||
pdos[count++] = sink_fixed_pdo(100, SINK_PDO_CURRENT) | SINK_PDO_USB_COMMS_CAPABLE |
|
||||
(request_v > 100 ? SINK_PDO_HIGHER_CAPABILITY : 0);
|
||||
if (request_v > 100)
|
||||
pdos[count++] = sink_fixed_pdo(request_v, SINK_PDO_CURRENT);
|
||||
this->send_message(this->make_data_msg_(PD_DATA_SINK_CAP, pdos, count));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Messages are sent as PD 2.0, which has no Not_Supported message
|
||||
this->send_message(this->make_control_msg_(PD_CNTRL_REJECT));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PowerDelivery::handle_soft_reset_(const PdMsg &msg) {
|
||||
this->reset_protocol_();
|
||||
this->last_received_msg_id_ = msg.id;
|
||||
// If this fails the source escalates to a hard reset. The hook still runs, so the PHY starts waiting for source
|
||||
// capabilities and recovers on its own if neither follows.
|
||||
if (!this->send_message(this->make_control_msg_(PD_CNTRL_ACCEPT))) {
|
||||
ESP_LOGW(TAG, "Accepting the soft reset failed");
|
||||
}
|
||||
// A soft reset keeps the power level, but the outcome of a transition in progress is unknown
|
||||
if (this->state_ == PdState::PD_STATE_TRANSITION)
|
||||
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
|
||||
this->on_soft_reset_received();
|
||||
}
|
||||
|
||||
void PowerDelivery::respond_to_source_caps_(const PdMsg &msg) {
|
||||
// Pick the highest fixed supply PDO that does not exceed the requested voltage.
|
||||
// The first PDO is always the 5 V fixed supply.
|
||||
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
|
||||
PdContract selected{};
|
||||
uint8_t position = 0; // 1-based object position, 0 = none
|
||||
for (uint8_t idx = 0; idx < msg.num_of_obj; idx++) {
|
||||
PdContract info = parse_pdo(msg.data_objects[idx]);
|
||||
if (info.type != PD_PDO_TYPE_FIXED_SUPPLY)
|
||||
continue;
|
||||
if (position == 0 || (info.max_v <= request_v && info.max_v > selected.max_v)) {
|
||||
selected = info;
|
||||
position = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// USB PD 6.4.2 Request Data Object: no USB suspend (bit 24), USB communications capable (bit 25)
|
||||
uint32_t rdo = (1u << 24) | (1u << 25);
|
||||
if (position == 0) {
|
||||
ESP_LOGW(TAG, "No usable PDO in source capabilities, requesting 5 V");
|
||||
// Object 1 (5 V fixed supply), 300 mA maximum, 100 mA operating current
|
||||
selected = PdContract{PD_PDO_TYPE_FIXED_SUPPLY, 100, 30};
|
||||
position = 1;
|
||||
rdo |= (30u << 0) | (10u << 10);
|
||||
} else {
|
||||
// Request the full current the source offers
|
||||
const uint32_t current = selected.max_i;
|
||||
rdo |= (current << 0) | (current << 10);
|
||||
}
|
||||
rdo |= static_cast<uint32_t>(position) << 28;
|
||||
ESP_LOGD(TAG, "Requesting PDO %u: %.2f V", position, selected.max_v * 0.05f);
|
||||
this->requested_contract_ = selected;
|
||||
this->last_rdo_ = rdo;
|
||||
this->retry_request_pending_ = false;
|
||||
this->wait_retries_ = 0;
|
||||
// A repeat would come too late: the source resets if it gets no Request within tSenderResponse (24-30 ms)
|
||||
if (!this->send_request_())
|
||||
this->request_failed_();
|
||||
}
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
static constexpr uint8_t PD_MAX_NUM_DATA_OBJECTS = 7;
|
||||
// Standard Power Range fixed supplies, which are the only ones this sink negotiates
|
||||
static constexpr uint8_t MIN_REQUEST_VOLTAGE = 5;
|
||||
static constexpr uint8_t MAX_REQUEST_VOLTAGE = 20;
|
||||
|
||||
enum PdSpecRevision : uint8_t {
|
||||
PD_SPEC_REV_1 = 0,
|
||||
PD_SPEC_REV_2 = 1,
|
||||
PD_SPEC_REV_3 = 2,
|
||||
};
|
||||
|
||||
enum PdDataMsgType : uint8_t {
|
||||
PD_DATA_SOURCE_CAP = 0x01,
|
||||
PD_DATA_REQUEST = 0x02,
|
||||
PD_DATA_SINK_CAP = 0x04,
|
||||
};
|
||||
|
||||
enum PdControlMsgType : uint8_t {
|
||||
PD_CNTRL_GOODCRC = 0x01,
|
||||
PD_CNTRL_ACCEPT = 0x03,
|
||||
PD_CNTRL_REJECT = 0x04,
|
||||
PD_CNTRL_PING = 0x05,
|
||||
PD_CNTRL_PS_RDY = 0x06,
|
||||
PD_CNTRL_GET_SINK_CAP = 0x08,
|
||||
PD_CNTRL_WAIT = 0x0C,
|
||||
PD_CNTRL_SOFT_RESET = 0x0D,
|
||||
PD_CNTRL_NOT_SUPPORTED = 0x10,
|
||||
};
|
||||
|
||||
enum PdPdoType : uint8_t {
|
||||
PD_PDO_TYPE_FIXED_SUPPLY = 0,
|
||||
PD_PDO_TYPE_BATTERY = 1,
|
||||
PD_PDO_TYPE_VARIABLE_SUPPLY = 2,
|
||||
PD_PDO_TYPE_AUGMENTED = 3,
|
||||
};
|
||||
|
||||
enum class PdState : uint8_t {
|
||||
PD_STATE_DISCONNECTED,
|
||||
/// Negotiation is over without a PD contract (no capabilities, or the request was not accepted); the source
|
||||
/// supplies Type-C default power.
|
||||
PD_STATE_PD_TIMEOUT,
|
||||
PD_STATE_DEFAULT_CONTRACT,
|
||||
PD_STATE_TRANSITION,
|
||||
PD_STATE_EXPLICIT_CONTRACT,
|
||||
PD_STATE_ERROR,
|
||||
};
|
||||
|
||||
/// Returns true for the states in which a source is attached and supplying power.
|
||||
inline bool is_connected_state(PdState state) {
|
||||
return state == PdState::PD_STATE_DEFAULT_CONTRACT || state == PdState::PD_STATE_TRANSITION ||
|
||||
state == PdState::PD_STATE_EXPLICIT_CONTRACT || state == PdState::PD_STATE_PD_TIMEOUT;
|
||||
}
|
||||
|
||||
struct PdContract {
|
||||
PdPdoType type{PD_PDO_TYPE_FIXED_SUPPLY};
|
||||
uint16_t max_v{0}; // 50 mV units
|
||||
uint16_t max_i{0}; // 10 mA units
|
||||
|
||||
bool operator==(const PdContract &other) const {
|
||||
return this->max_v == other.max_v && this->max_i == other.max_i && this->type == other.type;
|
||||
}
|
||||
bool operator!=(const PdContract &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
// Type-C default power: 5 V (100 x 50 mV) at 500 mA (50 x 10 mA)
|
||||
static constexpr PdContract DEFAULT_CONTRACT{PD_PDO_TYPE_FIXED_SUPPLY, 100, 50};
|
||||
|
||||
/// A contract packed into one word so it can be shared between tasks as an atomic.
|
||||
/// 0 means there is no contract (nothing attached).
|
||||
struct PackedContract {
|
||||
uint32_t value{0};
|
||||
|
||||
static PackedContract from(const PdContract &contract) {
|
||||
return {(static_cast<uint32_t>(contract.max_v) << 16) | contract.max_i};
|
||||
}
|
||||
bool is_set() const { return this->value != 0; }
|
||||
float voltage() const { return (this->value >> 16) * 0.05f; }
|
||||
float current() const { return (this->value & 0xFFFF) * 0.01f; }
|
||||
};
|
||||
|
||||
struct PdMsg {
|
||||
uint8_t type{0};
|
||||
PdSpecRevision spec_rev{PD_SPEC_REV_2};
|
||||
uint8_t id{0};
|
||||
uint8_t num_of_obj{0};
|
||||
bool extended{false};
|
||||
uint32_t data_objects[PD_MAX_NUM_DATA_OBJECTS]{};
|
||||
|
||||
void set_header(uint16_t header);
|
||||
uint16_t get_coded_header() const;
|
||||
};
|
||||
|
||||
/// USB-PD sink protocol logic, independent of the PHY chip.
|
||||
/// Everything except the getters and set_request_voltage() runs in the PHY task, which must also call
|
||||
/// check_transition_timeout_() and check_request_retry_() regularly; the protocol has no timers of its own.
|
||||
class PowerDelivery {
|
||||
public:
|
||||
void set_request_voltage(uint8_t voltage) {
|
||||
this->request_voltage_ = std::clamp(voltage, MIN_REQUEST_VOLTAGE, MAX_REQUEST_VOLTAGE);
|
||||
}
|
||||
uint8_t get_request_voltage() const { return this->request_voltage_; }
|
||||
|
||||
PdState get_state() const { return this->state_; }
|
||||
bool is_connected() const { return is_connected_state(this->state_); }
|
||||
|
||||
protected:
|
||||
/// Returns false when the message could not be handed to the chip; the PHY logs the failure.
|
||||
virtual bool send_message(const PdMsg &msg) = 0;
|
||||
/// Called after state_ or contract_ changed, to let the main loop publish it.
|
||||
virtual void on_state_changed() = 0;
|
||||
/// Called after the source soft reset the protocol; it sends new source capabilities next.
|
||||
virtual void on_soft_reset_received() = 0;
|
||||
/// Called when source capabilities arrive, before the request for them is sent.
|
||||
virtual void on_source_caps_received() = 0;
|
||||
/// Called after an accepted request was not followed by PS_RDY in time and the state fell back to default power;
|
||||
/// the PHY sends a soft reset so the source offers its capabilities again.
|
||||
virtual void on_transition_timeout() = 0;
|
||||
|
||||
void handle_message_(const PdMsg &msg);
|
||||
/// Called by the PHY when the chip gave up on a message after its retries without receiving a GoodCRC.
|
||||
void handle_transmit_failed_();
|
||||
void reset_protocol_();
|
||||
void set_state_(PdState state);
|
||||
void set_contract_(const PdContract &contract);
|
||||
void clear_contract_();
|
||||
/// Stores both before calling on_state_changed() once, so the main loop never sees the new state with the old
|
||||
/// contract.
|
||||
void set_state_and_contract_(PdState state, const PdContract &contract);
|
||||
void set_ams_(bool active);
|
||||
/// Returns true while an atomic message sequence is in progress; ends it after a timeout.
|
||||
bool check_ams_();
|
||||
/// Falls back to default power when the source accepted a request but did not report the new supply ready in time.
|
||||
void check_transition_timeout_();
|
||||
/// Sends the last request again once tSinkRequest has passed after the source answered Wait.
|
||||
void check_request_retry_();
|
||||
|
||||
PdMsg make_control_msg_(PdControlMsgType type) const;
|
||||
PdMsg make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const;
|
||||
|
||||
// Shared with the main loop
|
||||
std::atomic<PdState> state_{PdState::PD_STATE_DISCONNECTED};
|
||||
std::atomic<uint32_t> contract_{0};
|
||||
std::atomic<uint8_t> request_voltage_{5};
|
||||
|
||||
// PHY task only
|
||||
uint32_t ams_start_{0};
|
||||
uint32_t wait_received_ms_{0};
|
||||
bool retry_request_pending_{false};
|
||||
|
||||
private:
|
||||
void handle_data_message_(const PdMsg &msg);
|
||||
void handle_control_message_(const PdMsg &msg);
|
||||
void handle_soft_reset_(const PdMsg &msg);
|
||||
void respond_to_source_caps_(const PdMsg &msg);
|
||||
bool send_request_();
|
||||
void request_failed_();
|
||||
|
||||
PdContract requested_contract_{};
|
||||
PdContract accepted_contract_{};
|
||||
uint32_t last_rdo_{0};
|
||||
bool active_ams_{false};
|
||||
// Set by Accept; PS_RDY is only valid after the source accepted the request of the current sequence
|
||||
bool ps_rdy_expected_{false};
|
||||
uint8_t last_received_msg_id_{255};
|
||||
uint8_t msg_counter_{0};
|
||||
uint8_t wait_retries_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
@@ -2,7 +2,10 @@
|
||||
#include <string>
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "hon_climate.h"
|
||||
#include "hon_packet.h"
|
||||
|
||||
@@ -11,6 +14,26 @@ using namespace esphome::uart;
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
static bool vertical_axis_is_auto(climate::ClimateSwingMode swing_mode) {
|
||||
return (swing_mode == climate::CLIMATE_SWING_VERTICAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
|
||||
}
|
||||
|
||||
static bool horizontal_axis_is_auto(climate::ClimateSwingMode swing_mode) {
|
||||
return (swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
|
||||
}
|
||||
|
||||
static bool is_auto(hon_protocol::VerticalSwingMode mode) {
|
||||
return (mode == hon_protocol::VerticalSwingMode::AUTO) || (mode == hon_protocol::VerticalSwingMode::AUTO_SPECIAL);
|
||||
}
|
||||
|
||||
static bool is_auto(hon_protocol::HorizontalSwingMode mode) { return mode == hon_protocol::HorizontalSwingMode::AUTO; }
|
||||
|
||||
// A position the user just set wins over the appliance's echo of the old one for this long
|
||||
constexpr uint32_t AIRFLOW_SELECT_DEBOUNCE_MS = 10000;
|
||||
static bool in_debounce(uint32_t set_time, uint32_t now) {
|
||||
return set_time != 0 && now - set_time < AIRFLOW_SELECT_DEBOUNCE_MS;
|
||||
}
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "haier.climate");
|
||||
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
|
||||
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
|
||||
@@ -20,6 +43,27 @@ constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
|
||||
const uint8_t ONE_BUF[] = {0x00, 0x01};
|
||||
const uint8_t ZERO_BUF[] = {0x00, 0x00};
|
||||
|
||||
// Alarm texts indexed by alarm code; the last entry is the fallback for unknown codes
|
||||
PROGMEM_STRING_TABLE(
|
||||
HonAlarmMessages, "Outdoor module failure", "Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure", "Outdoor EEPROM abnormality", "Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure", "Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit", "Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality", "Full water protection", "Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure", "CBD and module communication failure", "Indoor DC fan failure",
|
||||
"Outdoor DC fan failure", "Door switch failure", "Dust filter needs cleaning reminder", "Water shortage protection",
|
||||
"Humidity sensor failure", "Indoor temperature sensor failure", "Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure", "Outdoor PM2.5 sensor failure", "Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection", "Outdoor compressor operation abnormality", "Outdoor DC current protection",
|
||||
"Outdoor no-load failure", "CT current abnormality", "Indoor cooling freeze protection",
|
||||
"High and low pressure protection", "Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure", "Outdoor cooling overload", "Water pump drainage failure",
|
||||
"Three-phase power supply failure", "Four-way valve failure", "External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm", "Different mode operation failure", "Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure", "Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure", "50Hz zero crossing failure", "Outdoor unit failure",
|
||||
"Formaldehyde sensor failure", "VOC sensor failure", "CO2 sensor failure", "Firewall failure", "Unknown");
|
||||
|
||||
HonClimate::HonClimate()
|
||||
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00,
|
||||
@@ -67,21 +111,60 @@ bool HonClimate::get_quiet_mode_state() const {
|
||||
}
|
||||
|
||||
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
|
||||
if (!this->current_vertical_swing_.has_value()) {
|
||||
return this->current_vertical_swing_;
|
||||
}
|
||||
// While the appliance swings, or right after the user chose a position, report the chosen position
|
||||
if (is_auto(this->current_vertical_swing_.value()) ||
|
||||
in_debounce(this->vertical_direction_set_time_, App.get_loop_component_start_time())) {
|
||||
return this->settings_.last_vertiacal_swing;
|
||||
}
|
||||
return this->current_vertical_swing_;
|
||||
};
|
||||
}
|
||||
|
||||
void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) {
|
||||
if (is_auto(direction)) {
|
||||
ESP_LOGW(TAG, "Vertical AUTO mode is not allowed for airflow position selector");
|
||||
return;
|
||||
}
|
||||
if (this->settings_.last_vertiacal_swing != direction) {
|
||||
this->settings_.last_vertiacal_swing = direction;
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
this->pending_vertical_direction_ = direction;
|
||||
this->force_send_control_ = true;
|
||||
this->vertical_direction_set_time_ = App.get_loop_component_start_time();
|
||||
#ifdef USE_SELECT
|
||||
this->publish_airflow_selects_();
|
||||
#endif
|
||||
}
|
||||
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> HonClimate::get_horizontal_airflow() const {
|
||||
if (!this->current_horizontal_swing_.has_value()) {
|
||||
return this->current_horizontal_swing_;
|
||||
}
|
||||
if (is_auto(this->current_horizontal_swing_.value()) ||
|
||||
in_debounce(this->horizontal_direction_set_time_, App.get_loop_component_start_time())) {
|
||||
return this->settings_.last_horizontal_swing;
|
||||
}
|
||||
return this->current_horizontal_swing_;
|
||||
}
|
||||
|
||||
void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) {
|
||||
if (is_auto(direction)) {
|
||||
ESP_LOGW(TAG, "Horizontal AUTO mode is not allowed for airflow position selector");
|
||||
return;
|
||||
}
|
||||
if (this->settings_.last_horizontal_swing != direction) {
|
||||
this->settings_.last_horizontal_swing = direction;
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
this->pending_horizontal_direction_ = direction;
|
||||
this->force_send_control_ = true;
|
||||
this->horizontal_direction_set_time_ = App.get_loop_component_start_time();
|
||||
#ifdef USE_SELECT
|
||||
this->publish_airflow_selects_();
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *HonClimate::get_cleaning_status_text() const {
|
||||
@@ -511,6 +594,18 @@ void HonClimate::initialization() {
|
||||
} else {
|
||||
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false};
|
||||
}
|
||||
bool settings_fixed = false;
|
||||
if (is_auto(this->settings_.last_vertiacal_swing)) {
|
||||
this->settings_.last_vertiacal_swing = hon_protocol::VerticalSwingMode::CENTER;
|
||||
settings_fixed = true;
|
||||
}
|
||||
if (is_auto(this->settings_.last_horizontal_swing)) {
|
||||
this->settings_.last_horizontal_swing = hon_protocol::HorizontalSwingMode::CENTER;
|
||||
settings_fixed = true;
|
||||
}
|
||||
if (settings_fixed) {
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
|
||||
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
|
||||
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
@@ -646,12 +741,21 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||
}
|
||||
}
|
||||
}
|
||||
const ClimateSwingMode target_swing_mode = this->current_hvac_settings_.swing_mode.value_or(this->swing_mode);
|
||||
if (this->pending_vertical_direction_.has_value()) {
|
||||
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
|
||||
if (!vertical_axis_is_auto(target_swing_mode)) {
|
||||
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
|
||||
}
|
||||
this->pending_vertical_direction_.reset();
|
||||
}
|
||||
if (this->pending_horizontal_direction_.has_value()) {
|
||||
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
|
||||
if (!horizontal_axis_is_auto(target_swing_mode)) {
|
||||
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
|
||||
}
|
||||
this->pending_horizontal_direction_.reset();
|
||||
}
|
||||
{
|
||||
@@ -687,9 +791,17 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
|
||||
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
|
||||
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
|
||||
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
|
||||
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
|
||||
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
|
||||
: "Unknown";
|
||||
#ifdef USE_ESP8266
|
||||
// Static so a delayed on_alarm automation can still read it, but only until the next
|
||||
// alarm change, including a later one in this packet; `code` stays correct
|
||||
static char alarm_message[HonAlarmMessages::MAX_LENGTH + 1];
|
||||
ESPHOME_strncpy_P(alarm_message,
|
||||
reinterpret_cast<const char *>(
|
||||
HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX)),
|
||||
sizeof(alarm_message));
|
||||
#else
|
||||
const char *alarm_message = HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX);
|
||||
#endif
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
|
||||
alarm_code, alarm_message);
|
||||
if (alarm_status) {
|
||||
@@ -810,6 +922,41 @@ void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) {
|
||||
}
|
||||
#endif // USE_SWITCH
|
||||
|
||||
#ifdef USE_SELECT
|
||||
template<typename Mode, size_t N>
|
||||
static void publish_airflow_index(select::Select *sel, const std::array<Mode, N> &order, Mode mode) {
|
||||
if (sel == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Saved settings never hold AUTO after initialization(); any other value outside the table shows as Center
|
||||
size_t index = N;
|
||||
size_t center = 0;
|
||||
for (size_t i = 0; i < N; i++) {
|
||||
const Mode entry = airflow_order_at(order, i);
|
||||
if (entry == mode) {
|
||||
index = i;
|
||||
} else if (entry == Mode::CENTER) {
|
||||
center = i;
|
||||
}
|
||||
}
|
||||
if (index == N) {
|
||||
index = center;
|
||||
}
|
||||
const auto active = sel->active_index();
|
||||
if (active.has_value() && *active == index) {
|
||||
return;
|
||||
}
|
||||
sel->publish_state(index);
|
||||
}
|
||||
|
||||
void HonClimate::publish_airflow_selects_() {
|
||||
publish_airflow_index(this->vertical_airflow_select_, VERTICAL_SWING_MODES_ORDER,
|
||||
this->settings_.last_vertiacal_swing);
|
||||
publish_airflow_index(this->horizontal_airflow_select_, HORIZONTAL_SWING_MODES_ORDER,
|
||||
this->settings_.last_horizontal_swing);
|
||||
}
|
||||
#endif // USE_SELECT
|
||||
|
||||
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||
size_t expected_size =
|
||||
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
|
||||
@@ -1044,16 +1191,25 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
// Saving last known non auto mode for vertical and horizontal swing
|
||||
this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode;
|
||||
this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode;
|
||||
bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) &&
|
||||
(this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) &&
|
||||
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) ||
|
||||
((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) &&
|
||||
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing));
|
||||
if (save_settings) {
|
||||
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
|
||||
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
|
||||
const uint32_t now_ms = App.get_loop_component_start_time();
|
||||
bool save_vertical = !in_debounce(this->vertical_direction_set_time_, now_ms) &&
|
||||
!is_auto(this->current_vertical_swing_.value()) &&
|
||||
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing);
|
||||
bool save_horizontal = !in_debounce(this->horizontal_direction_set_time_, now_ms) &&
|
||||
!is_auto(this->current_horizontal_swing_.value()) &&
|
||||
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing);
|
||||
if (save_vertical || save_horizontal) {
|
||||
if (save_vertical) {
|
||||
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
|
||||
}
|
||||
if (save_horizontal) {
|
||||
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
|
||||
}
|
||||
this->hon_rtc_.save(&this->settings_);
|
||||
}
|
||||
#ifdef USE_SELECT
|
||||
this->publish_airflow_selects_();
|
||||
#endif
|
||||
should_publish = should_publish || (old_swing_mode != this->swing_mode);
|
||||
}
|
||||
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||
@@ -1229,8 +1385,10 @@ void HonClimate::fill_control_messages_queue_() {
|
||||
(uint8_t) hon_protocol::DataParameters::SET_POINT,
|
||||
buffer, 2);
|
||||
}
|
||||
ClimateSwingMode target_swing_mode = this->swing_mode;
|
||||
// Vertical swing mode
|
||||
if (climate_control.swing_mode.has_value()) {
|
||||
target_swing_mode = climate_control.swing_mode.value();
|
||||
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO};
|
||||
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO};
|
||||
switch (climate_control.swing_mode.value()) {
|
||||
@@ -1256,6 +1414,32 @@ void HonClimate::fill_control_messages_queue_() {
|
||||
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
|
||||
vertical_swing_buf, 2);
|
||||
}
|
||||
// A swing change above already sent both axes from settings_.last_*, which set_*_airflow() updated
|
||||
const bool swing_sent = climate_control.swing_mode.has_value();
|
||||
if (this->pending_vertical_direction_.has_value()) {
|
||||
if (vertical_axis_is_auto(target_swing_mode)) {
|
||||
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
|
||||
} else if (!swing_sent) {
|
||||
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) this->pending_vertical_direction_.value()};
|
||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
|
||||
vertical_swing_buf, 2);
|
||||
}
|
||||
this->pending_vertical_direction_.reset();
|
||||
}
|
||||
if (this->pending_horizontal_direction_.has_value()) {
|
||||
if (horizontal_axis_is_auto(target_swing_mode)) {
|
||||
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
|
||||
} else if (!swing_sent) {
|
||||
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) this->pending_horizontal_direction_.value()};
|
||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
|
||||
horizontal_swing_buf, 2);
|
||||
}
|
||||
this->pending_horizontal_direction_.reset();
|
||||
}
|
||||
// Fan mode
|
||||
if (climate_control.fan_mode.has_value()) {
|
||||
switch (climate_control.fan_mode.value()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <queue>
|
||||
#ifdef USE_SENSOR
|
||||
@@ -14,12 +15,36 @@
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "haier_base.h"
|
||||
#include "hon_packet.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
#ifdef USE_SELECT
|
||||
// Positions in the order the select platform lists them; in flash on ESP8266, so read with airflow_order_at()
|
||||
inline constexpr std::array<hon_protocol::VerticalSwingMode, 7> VERTICAL_SWING_MODES_ORDER PROGMEM{
|
||||
hon_protocol::VerticalSwingMode::HEALTH_UP, hon_protocol::VerticalSwingMode::MAX_UP,
|
||||
hon_protocol::VerticalSwingMode::UP, hon_protocol::VerticalSwingMode::CENTER,
|
||||
hon_protocol::VerticalSwingMode::DOWN, hon_protocol::VerticalSwingMode::MAX_DOWN,
|
||||
hon_protocol::VerticalSwingMode::HEALTH_DOWN,
|
||||
};
|
||||
inline constexpr std::array<hon_protocol::HorizontalSwingMode, 5> HORIZONTAL_SWING_MODES_ORDER PROGMEM{
|
||||
hon_protocol::HorizontalSwingMode::MAX_LEFT, hon_protocol::HorizontalSwingMode::LEFT,
|
||||
hon_protocol::HorizontalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::RIGHT,
|
||||
hon_protocol::HorizontalSwingMode::MAX_RIGHT,
|
||||
};
|
||||
|
||||
template<typename Mode, size_t N> Mode airflow_order_at(const std::array<Mode, N> &order, size_t index) {
|
||||
static_assert(sizeof(Mode) == 1);
|
||||
return static_cast<Mode>(progmem_read_byte(reinterpret_cast<const uint8_t *>(order.data() + index)));
|
||||
}
|
||||
#endif
|
||||
|
||||
enum class CleaningState : uint8_t {
|
||||
NO_CLEANING = 0,
|
||||
SELF_CLEAN = 1,
|
||||
@@ -100,6 +125,16 @@ class HonClimate final : public HaierClimateBase {
|
||||
protected:
|
||||
switch_::Switch *beeper_switch_{nullptr};
|
||||
switch_::Switch *quiet_mode_switch_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
public:
|
||||
void set_vertical_airflow_select(select::Select *sel) { this->vertical_airflow_select_ = sel; }
|
||||
void set_horizontal_airflow_select(select::Select *sel) { this->horizontal_airflow_select_ = sel; }
|
||||
|
||||
protected:
|
||||
void publish_airflow_selects_();
|
||||
select::Select *vertical_airflow_select_{nullptr};
|
||||
select::Select *horizontal_airflow_select_{nullptr};
|
||||
#endif
|
||||
public:
|
||||
HonClimate();
|
||||
@@ -195,6 +230,9 @@ class HonClimate final : public HaierClimateBase {
|
||||
uint8_t big_data_counter_{0};
|
||||
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
|
||||
// millis() of the last user-set position per axis, 0 when none
|
||||
uint32_t vertical_direction_set_time_{0};
|
||||
uint32_t horizontal_direction_set_time_{0};
|
||||
HonSettings settings_{};
|
||||
ESPPreferenceObject hon_rtc_;
|
||||
SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF};
|
||||
|
||||
@@ -197,60 +197,4 @@ enum class SubcommandsControl : uint16_t {
|
||||
// content: all values like in status packet)
|
||||
};
|
||||
|
||||
const std::string HON_ALARM_MESSAGES[] = {
|
||||
"Outdoor module failure",
|
||||
"Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure",
|
||||
"Outdoor EEPROM abnormality",
|
||||
"Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure",
|
||||
"Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit",
|
||||
"Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality",
|
||||
"Full water protection",
|
||||
"Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure",
|
||||
"CBD and module communication failure",
|
||||
"Indoor DC fan failure",
|
||||
"Outdoor DC fan failure",
|
||||
"Door switch failure",
|
||||
"Dust filter needs cleaning reminder",
|
||||
"Water shortage protection",
|
||||
"Humidity sensor failure",
|
||||
"Indoor temperature sensor failure",
|
||||
"Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure",
|
||||
"Outdoor PM2.5 sensor failure",
|
||||
"Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection",
|
||||
"Outdoor compressor operation abnormality",
|
||||
"Outdoor DC current protection",
|
||||
"Outdoor no-load failure",
|
||||
"CT current abnormality",
|
||||
"Indoor cooling freeze protection",
|
||||
"High and low pressure protection",
|
||||
"Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure",
|
||||
"Outdoor cooling overload",
|
||||
"Water pump drainage failure",
|
||||
"Three-phase power supply failure",
|
||||
"Four-way valve failure",
|
||||
"External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm",
|
||||
"Different mode operation failure",
|
||||
"Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure",
|
||||
"Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure",
|
||||
"50Hz zero crossing failure",
|
||||
"Outdoor unit failure",
|
||||
"Formaldehyde sensor failure",
|
||||
"VOC sensor failure",
|
||||
"CO2 sensor failure",
|
||||
"Firewall failure",
|
||||
};
|
||||
|
||||
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
|
||||
|
||||
} // namespace esphome::haier::hon_protocol
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..climate import (
|
||||
CONF_HAIER_ID,
|
||||
CONF_HORIZONTAL_AIRFLOW,
|
||||
CONF_VERTICAL_AIRFLOW,
|
||||
HonClimate,
|
||||
haier_ns,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@paveldn"]
|
||||
VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select)
|
||||
HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select)
|
||||
|
||||
# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h
|
||||
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [
|
||||
"Health Up",
|
||||
"Max Up",
|
||||
"Up",
|
||||
"Center",
|
||||
"Down",
|
||||
"Max Down",
|
||||
"Health Down",
|
||||
]
|
||||
|
||||
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = [
|
||||
"Max Left",
|
||||
"Left",
|
||||
"Center",
|
||||
"Right",
|
||||
"Max Right",
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
|
||||
cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema(
|
||||
VerticalAirflowSelect,
|
||||
icon="mdi:arrow-expand-vertical",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema(
|
||||
HorizontalAirflowSelect,
|
||||
icon="mdi:arrow-expand-horizontal",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
await selects(
|
||||
CONF_VERTICAL_AIRFLOW,
|
||||
hub.set_vertical_airflow_select,
|
||||
options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS,
|
||||
)
|
||||
await selects(
|
||||
CONF_HORIZONTAL_AIRFLOW,
|
||||
hub.set_horizontal_airflow_select,
|
||||
options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class HorizontalAirflowSelect : public select::Select, public Parented<HonClimate> {
|
||||
protected:
|
||||
void control(size_t index) override {
|
||||
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class VerticalAirflowSelect : public select::Select, public Parented<HonClimate> {
|
||||
protected:
|
||||
void control(size_t index) override {
|
||||
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
|
||||
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
|
||||
var = await climate_ir.new_climate_ir(config, tag())
|
||||
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
|
||||
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <IRSender.h>
|
||||
#include <HeatpumpIRFactory.h>
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
@@ -45,71 +44,76 @@ class IRSenderESPHome : public IRSender {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
|
||||
|
||||
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
|
||||
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
|
||||
};
|
||||
template<Protocol P> struct ProtocolClass;
|
||||
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
|
||||
return new typename ProtocolClass<P>::type(); // NOLINT
|
||||
}
|
||||
|
||||
// Codegen calls only the configured protocols, so the linker drops the other classes
|
||||
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
|
||||
template<> struct ProtocolClass<protocol> { using type = cls; }; \
|
||||
template HeatpumpIR *create_heatpump_ir<protocol>();
|
||||
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
|
||||
|
||||
#undef HEATPUMPIR_PROTOCOL
|
||||
|
||||
void HeatpumpIRClimate::setup() {
|
||||
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
|
||||
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
|
||||
ESP_LOGE(TAG, "Invalid protocol");
|
||||
return;
|
||||
}
|
||||
this->heatpump_ir_ = protocol_constructor->second();
|
||||
climate_ir::ClimateIR::setup();
|
||||
if (this->sensor_) {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
|
||||
@@ -89,25 +89,21 @@ enum VerticalDirection {
|
||||
VERTICAL_DIRECTION_DOWN = 5,
|
||||
};
|
||||
|
||||
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir();
|
||||
|
||||
/// Selects the protocol at compile time so the enum and the library object always match.
|
||||
template<Protocol P> struct ProtocolTag {};
|
||||
|
||||
// Temperature
|
||||
const float TEMP_MIN = 0; // Celsius
|
||||
const float TEMP_MAX = 100; // Celsius
|
||||
|
||||
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
HeatpumpIRClimate()
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
template<Protocol P>
|
||||
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
|
||||
void setup() override;
|
||||
void set_protocol(Protocol protocol) {
|
||||
this->protocol_ = protocol;
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
void set_horizontal_default(HorizontalDirection horizontal_direction) {
|
||||
this->default_horizontal_direction_ = horizontal_direction;
|
||||
}
|
||||
@@ -119,6 +115,18 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
|
||||
|
||||
protected:
|
||||
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
|
||||
heatpump_ir_(heatpump_ir),
|
||||
protocol_(protocol) {
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
HeatpumpIR *heatpump_ir_;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
|
||||
@@ -59,8 +59,8 @@ namespace esphome::host {
|
||||
char **get_argv() { return s_argv; }
|
||||
|
||||
const std::string &get_exe_path() {
|
||||
static const std::string empty;
|
||||
return s_exe_path != nullptr ? *s_exe_path : empty;
|
||||
static const std::string EMPTY;
|
||||
return s_exe_path != nullptr ? *s_exe_path : EMPTY;
|
||||
}
|
||||
|
||||
void arm_reexec(const std::string &path) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "core.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <ctime>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -39,8 +39,8 @@ bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock();
|
||||
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
static const uint8_t esphome_host_mac_address[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
|
||||
memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
|
||||
static constexpr uint8_t HOST_MAC_ADDRESS[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
|
||||
memcpy(mac, HOST_MAC_ADDRESS, sizeof(HOST_MAC_ADDRESS));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
#include "preferences.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -45,11 +46,10 @@ void HostPreferences::setup_() {
|
||||
break;
|
||||
if (fread(&len, sizeof(len), 1, fp) != 1)
|
||||
break;
|
||||
uint8_t data[len];
|
||||
if (fread(data, sizeof(uint8_t), len, fp) != len)
|
||||
std::vector<uint8_t> data(len);
|
||||
if (fread(data.data(), sizeof(uint8_t), len, fp) != len)
|
||||
break;
|
||||
std::vector vec(data, data + len);
|
||||
this->data_[key] = vec;
|
||||
this->data_[key] = std::move(data);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
for (const auto &[name, value] : request_headers) {
|
||||
h_headers.emplace(name, value);
|
||||
}
|
||||
httplib::Client client(scheme_host.c_str());
|
||||
httplib::Client client(scheme_host);
|
||||
if (!client.is_valid()) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
|
||||
return nullptr;
|
||||
@@ -114,7 +114,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
|
||||
}
|
||||
|
||||
container->content_length = container->response_body_.size();
|
||||
for (auto header : response.headers) {
|
||||
for (const auto &header : response.headers) {
|
||||
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
|
||||
auto lower_name = str_lower_case(header.first); // NOLINT
|
||||
if (should_collect_header(lower_case_collect_headers, lower_name)) {
|
||||
|
||||
@@ -37,7 +37,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["split_buffer"]
|
||||
@@ -348,11 +347,8 @@ def _final_validate(config: ConfigType) -> None:
|
||||
config
|
||||
)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if display.has_external_writer(config[CONF_ID]):
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
|
||||
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE
|
||||
from esphome.types import ConfigType
|
||||
@@ -16,6 +17,8 @@ LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDev
|
||||
|
||||
|
||||
CONF_LD2450_ID = "ld2450_id"
|
||||
CONF_POLYGON_ZONE_ID = "polygon_zone_id"
|
||||
CONF_POLYGON_ZONES = "polygon_zones"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
@@ -50,6 +53,14 @@ _CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_DATA, "add_on_data_callback"),
|
||||
)
|
||||
|
||||
# Polygon zone sources build only when zones are configured
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{
|
||||
"polygon.cpp": "LD2450_POLYGON_ZONE_COUNT",
|
||||
"polygon_zone.cpp": "LD2450_POLYGON_ZONE_COUNT",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILTERS,
|
||||
CONF_HAS_MOVING_TARGET,
|
||||
CONF_HAS_STILL_TARGET,
|
||||
CONF_HAS_TARGET,
|
||||
@@ -11,7 +12,14 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2450_ID, LD2450Component
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_LD2450_ID,
|
||||
CONF_POLYGON_ZONE_ID,
|
||||
CONF_POLYGON_ZONES,
|
||||
FILTER_SOURCE_FILES,
|
||||
LD2450Component,
|
||||
)
|
||||
from .text import PolygonZone
|
||||
|
||||
DEPENDENCIES = ["ld2450"]
|
||||
|
||||
@@ -19,6 +27,18 @@ ICON_MEDITATION = "mdi:meditation"
|
||||
ICON_SHIELD_ACCOUNT = "mdi:shield-account"
|
||||
ICON_TARGET_ACCOUNT = "mdi:target-account"
|
||||
|
||||
|
||||
def _reject_filters(config: ConfigType) -> ConfigType:
|
||||
# Clearing a polygon makes presence unknown without going through filters, so they would get out of step
|
||||
if CONF_FILTERS in config:
|
||||
raise cv.Invalid(
|
||||
"Polygon zone presence binary sensors can't have filters; use the ld2450 number platform's "
|
||||
"'presence_timeout' to keep presence on after targets leave",
|
||||
path=[CONF_FILTERS],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
|
||||
@@ -37,6 +57,16 @@ CONFIG_SCHEMA = {
|
||||
filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_MEDITATION,
|
||||
),
|
||||
cv.Optional(CONF_POLYGON_ZONES): cv.ensure_list(
|
||||
binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_POLYGON_ZONE_ID): cv.use_id(PolygonZone),
|
||||
}
|
||||
),
|
||||
_reject_filters,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -46,3 +76,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
|
||||
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
|
||||
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
|
||||
for presence_conf in config.get(CONF_POLYGON_ZONES, []):
|
||||
presence = await binary_sensor.new_binary_sensor(presence_conf)
|
||||
zone = await cg.get_variable(presence_conf[CONF_POLYGON_ZONE_ID])
|
||||
cg.add(zone.set_presence_binary_sensor(presence))
|
||||
|
||||
@@ -179,6 +179,11 @@ void LD2450Component::setup() {
|
||||
this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
|
||||
this->set_presence_timeout();
|
||||
}
|
||||
#endif
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
for (PolygonZone *zone : this->polygon_zones_) {
|
||||
zone->setup();
|
||||
}
|
||||
#endif
|
||||
this->restart_and_read_all_info();
|
||||
}
|
||||
@@ -265,6 +270,12 @@ void LD2450Component::dump_config() {
|
||||
LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
|
||||
LOG_BUTTON(" ", "Restart", this->restart_button_);
|
||||
#endif
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
ESP_LOGCONFIG(TAG, "Polygon Zones:");
|
||||
for (PolygonZone *zone : this->polygon_zones_) {
|
||||
zone->dump_config();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD2450Component::loop() {
|
||||
@@ -465,15 +476,15 @@ void LD2450Component::handle_periodic_data_() {
|
||||
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
|
||||
// Loop thru targets
|
||||
for (index = 0; index < MAX_TARGETS; index++) {
|
||||
#ifdef USE_SENSOR
|
||||
// X
|
||||
start = TARGET_X + index * 8;
|
||||
is_moving = false;
|
||||
// tx is used for further calculations, so always needs to be populated
|
||||
// tx and ty are used for further calculations, so always need to be populated
|
||||
tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
// Y
|
||||
start = TARGET_Y + index * 8;
|
||||
ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
#ifdef USE_SENSOR
|
||||
// RESOLUTION
|
||||
start = TARGET_RESOLUTION + index * 8;
|
||||
res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
|
||||
@@ -542,6 +553,23 @@ void LD2450Component::handle_periodic_data_() {
|
||||
still_target_count = target_count - moving_target_count;
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
for (PolygonZone *zone : this->polygon_zones_) {
|
||||
if (!zone->has_polygon())
|
||||
continue;
|
||||
bool target_inside = false;
|
||||
for (const Target &target : this->target_info_) {
|
||||
// Untracked targets are stored as (0, 0)
|
||||
if ((target.x != 0 || target.y != 0) && zone->contains(target.x, target.y)) {
|
||||
target_inside = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
zone->update(target_inside, now, this->get_timeout_status_(zone->get_last_inside_ms()));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Loop thru zones
|
||||
uint8_t zone_still_targets = 0;
|
||||
@@ -602,7 +630,7 @@ void LD2450Component::handle_periodic_data_() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// For presence timeout check
|
||||
if (target_count > 0) {
|
||||
this->presence_millis_ = App.get_loop_component_start_time();
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
#include "polygon_zone.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/components/ld24xx/ld24xx.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -137,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s);
|
||||
void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s);
|
||||
void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s);
|
||||
#endif
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
void register_polygon_zone(PolygonZone *zone) { this->polygon_zones_.push_back(zone); }
|
||||
#endif
|
||||
void reset_radar_zone();
|
||||
void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2,
|
||||
@@ -167,7 +174,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
uint32_t presence_millis_ = 0;
|
||||
uint32_t still_presence_millis_ = 0;
|
||||
uint32_t moving_presence_millis_ = 0;
|
||||
uint32_t timeout_ = 5;
|
||||
uint32_t timeout_ = DEFAULT_PRESENCE_TIMEOUT * 1000; // presence_timeout, in ms
|
||||
uint8_t buffer_data_[MAX_LINE_LENGTH];
|
||||
uint8_t mac_address_[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
|
||||
uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
|
||||
@@ -200,6 +207,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
StaticVector<PolygonZone *, LD2450_POLYGON_ZONE_COUNT> polygon_zones_;
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> data_callback_;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "polygon.h"
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace esphome::ld2450 {
|
||||
|
||||
namespace {
|
||||
|
||||
class Scanner {
|
||||
public:
|
||||
Scanner(const char *str, size_t len) : pos_(str), end_(str + len) {}
|
||||
|
||||
bool at_end() const { return this->pos_ == this->end_; }
|
||||
|
||||
void skip_spaces() {
|
||||
while (this->pos_ != this->end_ && *this->pos_ == ' ')
|
||||
this->pos_++;
|
||||
}
|
||||
|
||||
bool consume(char c) {
|
||||
this->skip_spaces();
|
||||
if (this->pos_ != this->end_ && *this->pos_ == c) {
|
||||
this->pos_++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Read an integer in [min, max]; returns false if there is none or it is out of range.
|
||||
bool read_int(int16_t min, int16_t max, int16_t &out) {
|
||||
bool negative = this->consume('-');
|
||||
int32_t value = 0;
|
||||
uint8_t digits = 0;
|
||||
while (this->pos_ != this->end_ && *this->pos_ >= '0' && *this->pos_ <= '9') {
|
||||
// Six digits is already beyond any valid coordinate; stop before value can overflow
|
||||
if (++digits > 6)
|
||||
return false;
|
||||
value = value * 10 + (*this->pos_ - '0');
|
||||
this->pos_++;
|
||||
}
|
||||
if (digits == 0)
|
||||
return false;
|
||||
if (negative)
|
||||
value = -value;
|
||||
if (value < min || value > max)
|
||||
return false;
|
||||
out = static_cast<int16_t>(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
const char *pos_;
|
||||
const char *end_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Polygon::is_valid() const {
|
||||
if (this->count == 0)
|
||||
return true;
|
||||
if (this->count < MIN_POLYGON_POINTS || this->count > MAX_POLYGON_POINTS)
|
||||
return false;
|
||||
for (uint8_t i = 0; i < this->count; i++) {
|
||||
const Point &p = this->points[i];
|
||||
if (p.x < POLYGON_MIN_X || p.x > POLYGON_MAX_X || p.y < POLYGON_MIN_Y || p.y > POLYGON_MAX_Y)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Polygon::contains(int16_t x, int16_t y) const {
|
||||
bool inside = false;
|
||||
for (uint8_t i = 0, j = this->count - 1; i < this->count; j = i++) {
|
||||
const Point &a = this->points[i];
|
||||
const Point &b = this->points[j];
|
||||
if ((a.y > y) == (b.y > y))
|
||||
continue;
|
||||
// Does the edge a-b cross the ray going from (x, y) towards +x? Compare
|
||||
// x - a.x < (y - a.y) * (b.x - a.x) / (b.y - a.y) without division; values fit in 32 bits.
|
||||
const int32_t dy = static_cast<int32_t>(b.y) - a.y;
|
||||
const int32_t lhs = (static_cast<int32_t>(x) - a.x) * dy;
|
||||
const int32_t rhs = (static_cast<int32_t>(y) - a.y) * (static_cast<int32_t>(b.x) - a.x);
|
||||
if (dy > 0 ? lhs < rhs : lhs > rhs)
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
bool Polygon::parse(const char *str, size_t len) {
|
||||
Polygon result;
|
||||
Scanner scanner(str, len);
|
||||
scanner.skip_spaces();
|
||||
while (!scanner.at_end()) {
|
||||
if (result.count == MAX_POLYGON_POINTS)
|
||||
return false;
|
||||
Point &p = result.points[result.count];
|
||||
if (!scanner.read_int(POLYGON_MIN_X, POLYGON_MAX_X, p.x) || !scanner.consume(',') ||
|
||||
!scanner.read_int(POLYGON_MIN_Y, POLYGON_MAX_Y, p.y))
|
||||
return false;
|
||||
result.count++;
|
||||
bool separator = scanner.consume(';');
|
||||
scanner.skip_spaces();
|
||||
if (!separator && !scanner.at_end())
|
||||
return false;
|
||||
}
|
||||
if (!result.is_valid())
|
||||
return false;
|
||||
*this = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Polygon::format(char *buf) const {
|
||||
size_t pos = 0;
|
||||
buf[0] = '\0';
|
||||
for (uint8_t i = 0; i < this->count; i++) {
|
||||
pos = buf_append_printf(buf, POLYGON_STR_SIZE, pos, "%s%d,%d", i == 0 ? "" : ";", this->points[i].x,
|
||||
this->points[i].y);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::ld2450 {
|
||||
|
||||
static constexpr uint8_t MIN_POLYGON_POINTS = 3;
|
||||
// The most points whose text always fits Home Assistant's 255-character limit: 23 x "-4860,7560" + 22 x ';' = 252
|
||||
static constexpr uint8_t MAX_POLYGON_POINTS = 23;
|
||||
// Coordinate limits of the radar detection area, in mm
|
||||
static constexpr int16_t POLYGON_MIN_X = -4860;
|
||||
static constexpr int16_t POLYGON_MAX_X = 4860;
|
||||
static constexpr int16_t POLYGON_MIN_Y = 0;
|
||||
static constexpr int16_t POLYGON_MAX_Y = 7560;
|
||||
// Longest point is "-4860,7560;" (11 characters), plus the null terminator
|
||||
static constexpr size_t POLYGON_STR_SIZE = MAX_POLYGON_POINTS * 11 + 1;
|
||||
|
||||
struct Point {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
};
|
||||
|
||||
/// A polygon in radar coordinates. Trivially copyable so it can be saved to preferences as-is.
|
||||
struct Polygon {
|
||||
std::array<Point, MAX_POLYGON_POINTS> points{};
|
||||
uint8_t count{0};
|
||||
|
||||
/// True when the polygon has no points, which disables the zone.
|
||||
bool empty() const { return this->count == 0; }
|
||||
|
||||
/// True when the polygon is empty, or has a valid number of points that are all inside the radar area.
|
||||
bool is_valid() const;
|
||||
|
||||
/// Return whether the point is inside the polygon (even-odd rule). Always false for an empty polygon.
|
||||
bool contains(int16_t x, int16_t y) const;
|
||||
|
||||
/** Parse "x,y;x,y;..." (mm) into this polygon.
|
||||
*
|
||||
* Spaces around numbers and a trailing ';' are allowed. An empty or all-space string gives an empty polygon.
|
||||
* @return false if the text is invalid; the polygon is then left unchanged.
|
||||
*/
|
||||
bool parse(const char *str, size_t len);
|
||||
|
||||
/// Write the polygon as "x,y;x,y;..." into buf, which must hold at least POLYGON_STR_SIZE bytes.
|
||||
void format(char *buf) const;
|
||||
};
|
||||
|
||||
// text.py counts on this size
|
||||
static_assert(sizeof(Polygon) == 94, "Polygon layout changed; update POLYGON_SAVED_BYTES in text.py");
|
||||
|
||||
} // namespace esphome::ld2450
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "polygon_zone.h"
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ld2450 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ld2450.polygon_zone");
|
||||
|
||||
void PolygonZone::setup() {
|
||||
// Bump the version when Polygon's layout changes
|
||||
this->pref_ = this->make_entity_preference<Polygon>(1);
|
||||
Polygon saved;
|
||||
if (this->pref_.load(&saved)) {
|
||||
if (saved.is_valid()) {
|
||||
this->polygon_ = saved;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': saved polygon is invalid; zone disabled", this->get_name().c_str());
|
||||
}
|
||||
}
|
||||
// Presence stays unknown until the first radar frame
|
||||
this->publish_polygon_();
|
||||
}
|
||||
|
||||
void PolygonZone::dump_config() {
|
||||
char polygon_s[POLYGON_STR_SIZE];
|
||||
this->polygon_.format(polygon_s);
|
||||
LOG_TEXT(" ", "PolygonZone", this);
|
||||
ESP_LOGCONFIG(TAG, " Polygon: '%s'", polygon_s);
|
||||
LOG_BINARY_SENSOR(" ", "Presence", this->presence_);
|
||||
}
|
||||
|
||||
void PolygonZone::control(const std::string &value) {
|
||||
Polygon polygon;
|
||||
if (!polygon.parse(value.c_str(), value.size())) {
|
||||
ESP_LOGW(TAG, "'%s': invalid polygon '%s'; expected 'x,y;x,y;...' with %u to %u points", this->get_name().c_str(),
|
||||
value.c_str(), MIN_POLYGON_POINTS, MAX_POLYGON_POINTS);
|
||||
} else if (!this->pref_.save(&polygon)) {
|
||||
// On ESP8266, the space for saved settings is small and can run out with several zones
|
||||
ESP_LOGW(TAG, "'%s': could not save the polygon; keeping the previous one", this->get_name().c_str());
|
||||
} else {
|
||||
this->polygon_ = polygon;
|
||||
// A target seen inside the previous polygon must not keep the new one on
|
||||
this->last_inside_ms_ = 0;
|
||||
if (this->polygon_.empty())
|
||||
this->presence_->invalidate_state();
|
||||
}
|
||||
// On error this publishes the previous polygon again, so the frontend shows the value in use
|
||||
this->publish_polygon_();
|
||||
}
|
||||
|
||||
void PolygonZone::publish_polygon_() {
|
||||
char polygon_s[POLYGON_STR_SIZE];
|
||||
this->polygon_.format(polygon_s);
|
||||
this->publish_state(polygon_s);
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450
|
||||
|
||||
#endif // LD2450_POLYGON_ZONE_COUNT
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/text/text.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "polygon.h"
|
||||
|
||||
namespace esphome::ld2450 {
|
||||
|
||||
/** A zone with any polygon shape. The text state holds the polygon, so it can be edited and read back from HA.
|
||||
*
|
||||
* The polygon is in the radar's raw coordinates (mm), and targets are checked against it as decoded from each
|
||||
* radar frame, before any sensor filter. Filters on the target X/Y sensors change only what they publish, so a
|
||||
* filter that changes their values (multiply, offset, lambda, ...) puts them in a different frame from the polygon.
|
||||
*
|
||||
* Like the hub's has_target binary sensors, presence stays on until presence_timeout has passed since a target was
|
||||
* last inside the zone.
|
||||
*/
|
||||
class PolygonZone : public text::Text {
|
||||
public:
|
||||
/// Config validation guarantees every zone gets exactly one presence binary sensor.
|
||||
void set_presence_binary_sensor(binary_sensor::BinarySensor *presence) { this->presence_ = presence; }
|
||||
|
||||
/// Load the saved polygon and publish it. Called from the hub's setup().
|
||||
void setup();
|
||||
void dump_config();
|
||||
/** Publish presence for one radar frame. A zone without a polygon keeps presence unknown.
|
||||
*
|
||||
* @param target_inside whether any target is inside the zone in this frame.
|
||||
* @param now the frame's time, in ms.
|
||||
* @param timed_out whether presence_timeout has passed since get_last_inside_ms().
|
||||
*/
|
||||
void update(bool target_inside, uint32_t now, bool timed_out) {
|
||||
if (this->polygon_.empty())
|
||||
return;
|
||||
if (target_inside) {
|
||||
this->last_inside_ms_ = now;
|
||||
this->presence_->publish_state(true);
|
||||
} else if (timed_out) {
|
||||
this->presence_->publish_state(false);
|
||||
}
|
||||
}
|
||||
/// When a target was last inside the zone, in ms; 0 if never since the polygon was set.
|
||||
uint32_t get_last_inside_ms() const { return this->last_inside_ms_; }
|
||||
bool contains(int16_t x, int16_t y) const { return this->polygon_.contains(x, y); }
|
||||
bool has_polygon() const { return !this->polygon_.empty(); }
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void publish_polygon_();
|
||||
|
||||
binary_sensor::BinarySensor *presence_{nullptr};
|
||||
Polygon polygon_;
|
||||
ESPPreferenceObject pref_;
|
||||
uint32_t last_inside_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2450
|
||||
|
||||
#endif // LD2450_POLYGON_ZONE_COUNT
|
||||
@@ -17,7 +17,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2450_ID, LD2450Component
|
||||
from . import CONF_LD2450_ID, FILTER_SOURCE_FILES, LD2450Component # noqa: F401 pylint: disable=unused-import
|
||||
|
||||
DEPENDENCIES = ["ld2450"]
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
from esphome import preferences
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PLATFORM, ENTITY_CATEGORY_CONFIG
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_LD2450_ID,
|
||||
CONF_POLYGON_ZONE_ID,
|
||||
CONF_POLYGON_ZONES,
|
||||
DOMAIN,
|
||||
FILTER_SOURCE_FILES,
|
||||
LD2450Component,
|
||||
ld2450_ns,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["ld2450"]
|
||||
|
||||
ICON_VECTOR_POLYGON = "mdi:vector-polygon"
|
||||
|
||||
# Must match MIN_POLYGON_POINTS and MAX_POLYGON_POINTS in polygon.h
|
||||
MIN_POLYGON_POINTS = 3
|
||||
MAX_POLYGON_POINTS = 23
|
||||
# Matches what Polygon::parse() accepts, apart from coordinate ranges, which only the device checks:
|
||||
# empty, or MIN_POLYGON_POINTS to MAX_POLYGON_POINTS "x,y" points separated by ';'. HA validates input with it.
|
||||
# No character classes: the HA frontend compiles it with the JS "v" flag, HA core with Python re.
|
||||
POLYGON_PATTERN = (
|
||||
r"^ *$|^ *-?\d{1,4} *, *\d{1,4} *"
|
||||
rf"(; *-?\d{{1,4}} *, *\d{{1,4}} *){{{MIN_POLYGON_POINTS - 1},{MAX_POLYGON_POINTS - 1}}};? *$"
|
||||
)
|
||||
# Home Assistant's limit for a state; the longest polygon the device publishes is 252 characters
|
||||
POLYGON_MAX_LENGTH = 255
|
||||
|
||||
# sizeof(Polygon), pinned by a static_assert in polygon.h
|
||||
POLYGON_SAVED_BYTES = 94
|
||||
|
||||
PolygonZone = ld2450_ns.class_("PolygonZone", text.Text)
|
||||
|
||||
_request_polygon_zone_slot = cg.slot_counter("LD2450_POLYGON_ZONE_COUNT")
|
||||
|
||||
|
||||
def _require_manual_id(config: ConfigType) -> ConfigType:
|
||||
if not config[CONF_ID].is_manual:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_ID}' is required, so the zone's presence binary sensor can refer to it",
|
||||
path=[CONF_ID],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
|
||||
# Polygons are in raw radar mm, checked before sensor filters (polygon_zone.h)
|
||||
cv.Required(CONF_POLYGON_ZONES): cv.ensure_list(
|
||||
text.text_schema(
|
||||
PolygonZone,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_VECTOR_POLYGON,
|
||||
mode="TEXT",
|
||||
),
|
||||
_require_manual_id,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
"""Each polygon zone must have exactly one presence binary sensor, on the same LD2450."""
|
||||
full_config = fv.full_config.get()
|
||||
# Polygon zone id -> the LD2450 id of each presence binary sensor that uses it
|
||||
references: dict[str, list[str]] = {}
|
||||
for binary_sensor_conf in full_config.get("binary_sensor", []):
|
||||
if binary_sensor_conf.get(CONF_PLATFORM) != "ld2450":
|
||||
continue
|
||||
hub_id = binary_sensor_conf[CONF_LD2450_ID].id
|
||||
for presence_conf in binary_sensor_conf.get(CONF_POLYGON_ZONES, []):
|
||||
zone_id = presence_conf[CONF_POLYGON_ZONE_ID].id
|
||||
references.setdefault(zone_id, []).append(hub_id)
|
||||
|
||||
zone_hub_id = config[CONF_LD2450_ID].id
|
||||
for index, zone_conf in enumerate(config[CONF_POLYGON_ZONES]):
|
||||
zone_id = zone_conf[CONF_ID].id
|
||||
hub_ids = references.get(zone_id, [])
|
||||
if len(hub_ids) != 1:
|
||||
problem = (
|
||||
"has no presence binary sensor"
|
||||
if not hub_ids
|
||||
else "is used by more than one presence binary sensor"
|
||||
)
|
||||
raise cv.Invalid(
|
||||
f"Polygon zone '{zone_id}' {problem}; each zone needs exactly one, under "
|
||||
f"'binary_sensor: - platform: ld2450' -> '{CONF_POLYGON_ZONES}'",
|
||||
path=[CONF_POLYGON_ZONES, index],
|
||||
)
|
||||
if hub_ids[0] != zone_hub_id:
|
||||
raise cv.Invalid(
|
||||
f"Polygon zone '{zone_id}' belongs to LD2450 '{zone_hub_id}', but its presence "
|
||||
f"binary sensor is under LD2450 '{hub_ids[0]}'; use the same '{CONF_LD2450_ID}'",
|
||||
path=[CONF_POLYGON_ZONES, index],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_LD2450_ID])
|
||||
# Each zone saves its polygon; the ESP8266 checks that every component's saved values fit
|
||||
preferences.reserve_preferences(
|
||||
len(config[CONF_POLYGON_ZONES]), POLYGON_SAVED_BYTES
|
||||
)
|
||||
for zone_conf in config[CONF_POLYGON_ZONES]:
|
||||
zone = await text.new_text(
|
||||
zone_conf, max_length=POLYGON_MAX_LENGTH, pattern=POLYGON_PATTERN
|
||||
)
|
||||
_request_polygon_zone_slot(str(hub))
|
||||
cg.add(hub.register_polygon_zone(zone))
|
||||
@@ -14,7 +14,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2450_ID, LD2450Component
|
||||
from . import CONF_LD2450_ID, FILTER_SOURCE_FILES, LD2450Component # noqa: F401 pylint: disable=unused-import
|
||||
|
||||
DEPENDENCIES = ["ld2450"]
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ static void ble_scan_callback(void *param) {
|
||||
memcpy(slot->data, info->data, slot->data_len);
|
||||
|
||||
s_ble->push_scan_report(slot);
|
||||
}
|
||||
} // NOLINT(clang-analyzer-unix.Malloc) the slot is a pool entry handed to push_scan_report(), not a leak
|
||||
|
||||
BLEScanReport *LN882HBLE::allocate_scan_report() {
|
||||
BLEScanReport *slot = this->report_pool_.allocate();
|
||||
@@ -287,6 +287,7 @@ void LN882HBLE::enable() {
|
||||
return;
|
||||
this->state_ = BLEComponentState::ENABLING;
|
||||
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr) - MMIO register at a fixed address
|
||||
*reinterpret_cast<volatile uint32_t *>(BLE_COEX_PTI_REG_ADDR) = BLE_COEX_PTI_ENABLE_ALL;
|
||||
soc_module_clk_gate_enable(CLK_G_BLE);
|
||||
|
||||
|
||||
@@ -142,6 +142,8 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
}
|
||||
#endif // USE_ESP8266
|
||||
|
||||
uint8_t Logger::get_log_level(const char *tag) { return this->level_for(tag); }
|
||||
|
||||
inline uint8_t Logger::level_for(const char *tag) {
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
// Skip the lookup when no per-tag levels are set
|
||||
|
||||
@@ -178,6 +178,8 @@ class Logger final : public Component {
|
||||
void set_log_level(const char *tag, uint8_t log_level);
|
||||
#endif
|
||||
uint8_t get_log_level() { return this->current_level_; }
|
||||
/// The level in force for a tag: its own when one is set, else the default
|
||||
uint8_t get_log_level(const char *tag);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
@@ -511,6 +513,14 @@ class LoggerMessageTrigger final : public Trigger<uint8_t, const char *, const c
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
/// Whether a crash record from before this boot is waiting to be reported
|
||||
bool crash_log_pending();
|
||||
/// Log the previous crash for a client that just subscribed to the logs. On the ESP32 the record
|
||||
/// is kept until a client has seen it, so this call also clears it there.
|
||||
void log_crash_to_client();
|
||||
#endif
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
// Platform-specific inline implementations of write_msg_()
|
||||
|
||||
@@ -162,5 +162,16 @@ const LogString *Logger::get_uart_selection_() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
bool crash_log_pending() { return esp32::crash_handler_has_data(); }
|
||||
void log_crash_to_client() {
|
||||
// A level that drops the lines would throw the record away unseen; keep it for a client that can see it
|
||||
if (global_logger->get_log_level(esp32::CRASH_LOG_TAG) < ESPHOME_LOG_LEVEL_ERROR)
|
||||
return;
|
||||
esp32::crash_handler_log();
|
||||
esp32::crash_handler_clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif
|
||||
|
||||
@@ -48,6 +48,11 @@ const LogString *Logger::get_uart_selection_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
bool crash_log_pending() { return esp8266::crash_handler_has_data(); }
|
||||
void log_crash_to_client() { esp8266::crash_handler_log(); }
|
||||
#endif
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#if !defined(USE_ESP8266_LOGGER_SERIAL) && !defined(USE_ESP8266_LOGGER_SERIAL1)
|
||||
|
||||
@@ -49,5 +49,10 @@ const LogString *Logger::get_uart_selection_() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
bool crash_log_pending() { return rp2::crash_handler_has_data(); }
|
||||
void log_crash_to_client() { rp2::crash_handler_log(); }
|
||||
#endif
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif // USE_RP2
|
||||
|
||||
@@ -335,6 +335,16 @@ def final_validation(config_list):
|
||||
widget_type.final_validate(name, conf, widget_conf, path[1:])
|
||||
|
||||
|
||||
def lv_conf_path_flag(lv_conf_h_file: Path) -> str:
|
||||
"""The ``-DLV_CONF_PATH`` build flag, decoded identically by the shell
|
||||
lexers (PlatformIO, host, ESP8266) and the quoted CMake arguments
|
||||
(ESP-IDF, Zephyr) the build flags pass through."""
|
||||
# forward slashes for Windows, then escape what shlex or a quoted CMake
|
||||
# argument would not pass through literally
|
||||
path = re.sub(r"[^\w/.:+=,-]", r"\\\g<0>", Path(lv_conf_h_file).as_posix())
|
||||
return f'-DLV_CONF_PATH=\\"{path}\\"'
|
||||
|
||||
|
||||
async def to_code(configs):
|
||||
config_0 = configs[0]
|
||||
# Global configuration
|
||||
@@ -563,9 +573,7 @@ async def to_code(configs):
|
||||
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
|
||||
clean_build(clear_pio_cache=False)
|
||||
cg.add_build_flag("-DLV_CONF_H=1")
|
||||
# handle windows paths in a way that doesn't break the generated C++
|
||||
lv_conf_h_path = Path(lv_conf_h_file).as_posix()
|
||||
cg.add_build_flag(f'-DLV_CONF_PATH=\\"{lv_conf_h_path}\\"')
|
||||
cg.add_build_flag(lv_conf_path_flag(lv_conf_h_file))
|
||||
cg.add_build_flag("-DLV_KCONFIG_IGNORE")
|
||||
|
||||
for prop in df.get_remapped_uses():
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
#include "microphone_source.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <gain.h> // esp-audio-libs
|
||||
#include <pcm_convert.h> // esp-audio-libs
|
||||
|
||||
namespace esphome::microphone {
|
||||
|
||||
static const int32_t Q25_MAX_VALUE = (1 << 25) - 1;
|
||||
static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE;
|
||||
// Builds the exact LinearGain for an integer factor without the float dB conversion: the factor is
|
||||
// mantissa / 2^31 * 2^exponent, with the exponent set to the factor's bit width.
|
||||
static esp_audio_libs::gain::LinearGain factor_to_linear_gain(uint32_t factor) {
|
||||
esp_audio_libs::gain::LinearGain gain;
|
||||
gain.exponent = 32 - __builtin_clz(factor);
|
||||
gain.mantissa_q31 = static_cast<int32_t>(factor << (31 - gain.exponent));
|
||||
return gain;
|
||||
}
|
||||
|
||||
audio::AudioStreamInfo MicrophoneSource::get_audio_stream_info() {
|
||||
return audio::AudioStreamInfo(this->bits_per_sample_, this->channels_.count(),
|
||||
@@ -26,50 +37,26 @@ void MicrophoneSource::stop() {
|
||||
}
|
||||
|
||||
void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data) {
|
||||
// - Bit depth conversions are obtained by truncating bits or padding with zeros - no dithering is applied.
|
||||
// - In the comments, Qxx refers to a fixed point number with xx bits of precision for representing fractional values.
|
||||
// For example, audio with a bit depth of 16 can store a sample in a int16, which can be considered a Q15 number.
|
||||
// - All samples are converted to Q25 before applying the gain factor - this results in a small precision loss for
|
||||
// data with 32 bits per sample. Since the maximum gain factor is 64 = (1<<6), this ensures that applying the gain
|
||||
// will never overflow a 32 bit signed integer. This still retains more bit depth than what is audibly noticeable.
|
||||
// - Loops for reading/writing data buffers are unrolled, assuming little endian, for a small performance increase.
|
||||
const audio::AudioStreamInfo source_info = this->mic_->get_audio_stream_info();
|
||||
const uint32_t total_frames = source_info.bytes_to_frames(data.size());
|
||||
const uint8_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8;
|
||||
|
||||
const size_t source_bytes_per_sample = this->mic_->get_audio_stream_info().samples_to_bytes(1);
|
||||
const uint32_t source_channels = this->mic_->get_audio_stream_info().get_channels();
|
||||
|
||||
const size_t source_bytes_per_frame = this->mic_->get_audio_stream_info().frames_to_bytes(1);
|
||||
|
||||
const uint32_t total_frames = this->mic_->get_audio_stream_info().bytes_to_frames(data.size());
|
||||
const size_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8;
|
||||
const size_t target_bytes_per_frame = target_bytes_per_sample * this->channels_.count();
|
||||
|
||||
filtered_data.resize(target_bytes_per_frame * total_frames);
|
||||
|
||||
uint8_t *current_data = filtered_data.data();
|
||||
|
||||
for (uint32_t frame_index = 0; frame_index < total_frames; ++frame_index) {
|
||||
for (uint32_t channel_index = 0; channel_index < source_channels; ++channel_index) {
|
||||
if (this->channels_.test(channel_index)) {
|
||||
// Channel's current sample is included in the target mask. Convert bits per sample, if necessary.
|
||||
|
||||
const uint32_t sample_index = frame_index * source_bytes_per_frame + channel_index * source_bytes_per_sample;
|
||||
|
||||
int32_t sample = audio::unpack_audio_sample_to_q31(&data[sample_index], source_bytes_per_sample); // Q31
|
||||
sample >>= 6; // Q31 -> Q25
|
||||
|
||||
// Apply gain using multiplication
|
||||
sample *= this->gain_factor_; // Q25
|
||||
|
||||
// Clamp ``sample`` in case gain multiplication overflows 25 bits
|
||||
sample = clamp<int32_t>(sample, Q25_MIN_VALUE, Q25_MAX_VALUE); // Q25
|
||||
|
||||
sample *= (1 << 6); // Q25 -> Q31
|
||||
|
||||
audio::pack_q31_as_audio_sample(sample, current_data, target_bytes_per_sample);
|
||||
current_data = current_data + target_bytes_per_sample;
|
||||
}
|
||||
// Enabled channels in ascending order, which is their order in each output frame
|
||||
std::array<uint8_t, 8> channel_map;
|
||||
uint8_t target_channels = 0;
|
||||
for (size_t channel = 0; channel < this->channels_.size(); ++channel) {
|
||||
if (this->channels_.test(channel)) {
|
||||
channel_map[target_channels++] = static_cast<uint8_t>(channel);
|
||||
}
|
||||
}
|
||||
|
||||
filtered_data.resize(static_cast<size_t>(total_frames) * target_bytes_per_sample * target_channels);
|
||||
|
||||
// Applies the gain at full precision before narrowing; samples saturate at full scale, and narrowing rounds
|
||||
esp_audio_libs::pcm_convert::copy_frames_with_gain(data.data(), filtered_data.data(), source_info.samples_to_bytes(1),
|
||||
source_info.get_channels(), target_bytes_per_sample,
|
||||
channel_map.data(), target_channels,
|
||||
factor_to_linear_gain(this->gain_factor_), total_frames);
|
||||
}
|
||||
|
||||
} // namespace esphome::microphone
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "midea_portasplit"
|
||||
@@ -0,0 +1,30 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
CODEOWNERS = ["@Fexiven"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
midea_portasplit_ns = cg.esphome_ns.namespace("midea_portasplit")
|
||||
PortaSplitClimate = midea_portasplit_ns.class_(
|
||||
"PortaSplitClimate", climate.Climate, uart.UARTDevice, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
climate.climate_schema(PortaSplitClimate)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
DOMAIN, baud_rate=9600, require_tx=True, require_rx=True
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate.new_climate(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -0,0 +1,337 @@
|
||||
#include "midea_portasplit.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::midea_portasplit {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "midea_portasplit");
|
||||
|
||||
static constexpr uint32_t FOLLOWUP_TIMEOUT_ID = 0;
|
||||
|
||||
static constexpr uint32_t STATUS_INTERVAL_MS = 30000;
|
||||
static constexpr uint32_t NETWORK_INTERVAL_MS = 10000;
|
||||
static constexpr uint32_t FOLLOWUP_DELAY_MS = 1500;
|
||||
// At 9600 baud bytes within a frame arrive about 1 ms apart.
|
||||
static constexpr uint32_t RX_TIMEOUT_MS = 100;
|
||||
|
||||
// Ten header bytes plus CRC8 and checksum.
|
||||
static constexpr size_t FRAME_OVERHEAD = 12;
|
||||
static constexpr size_t MAX_TX_PAYLOAD = 24;
|
||||
|
||||
static constexpr float MIN_TARGET_TEMP = 16.0f;
|
||||
static constexpr float MAX_TARGET_TEMP = 30.0f;
|
||||
|
||||
// The AC expects its Wi-Fi module to answer the 0x63 keepalive poll and to send periodic 0x0D network status
|
||||
// frames. These frames use the XOR sync byte (0x1E ^ 0xAC = 0xB2). The AC does not use the IP address; the
|
||||
// fixed frame below was captured from the original module.
|
||||
static constexpr uint8_t MODULE_FRAME[31] PROGMEM = {0xAA, 0x1E, 0xAC, 0xB2, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x01,
|
||||
0x01, 0x04, 0xC0, 0xA8, 0xB2, 0x72, 0x00, 0x00, 0x00, 0x01, 0x01,
|
||||
0x01, 0x28, 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr uint8_t STATUS_QUERY[12] PROGMEM = {0x41, 0x21, 0x00, 0xFF, 0x03, 0xFF,
|
||||
0x00, 0x02, 0x00, 0x00, 0x00, 0x00};
|
||||
// Same capability queries the original module sends at boot.
|
||||
static constexpr uint8_t CAPS_0[] PROGMEM = {0xB5, 0x01, 0x00};
|
||||
static constexpr uint8_t CAPS_1[] PROGMEM = {0xB5, 0x01, 0x01, 0x01};
|
||||
|
||||
// Two's complement of the sum of all bytes after the 0xAA start marker.
|
||||
static uint8_t checksum(const uint8_t *frame, size_t len) { return (uint8_t) -sum_bytes(frame + 1, len - 1); }
|
||||
|
||||
static uint8_t climate_mode_to_midea(climate::ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
return 2;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
return 3;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
return 4;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
return 5;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static climate::ClimateMode midea_mode_to_climate(uint8_t mode) {
|
||||
switch (mode) {
|
||||
case 2:
|
||||
return climate::CLIMATE_MODE_COOL;
|
||||
case 3:
|
||||
return climate::CLIMATE_MODE_DRY;
|
||||
case 4:
|
||||
return climate::CLIMATE_MODE_HEAT;
|
||||
case 5:
|
||||
return climate::CLIMATE_MODE_FAN_ONLY;
|
||||
case 1:
|
||||
default:
|
||||
return climate::CLIMATE_MODE_HEAT_COOL;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t fan_mode_to_midea(climate::ClimateFanMode mode) {
|
||||
switch (mode) {
|
||||
case climate::CLIMATE_FAN_QUIET:
|
||||
return 20;
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return 40;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
return 60;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return 80;
|
||||
default:
|
||||
return FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
// The AC reports its own fan steps (30 for low, 50 for medium), so classify by range.
|
||||
static climate::ClimateFanMode midea_fan_to_climate(uint8_t fan) {
|
||||
if (fan > 100)
|
||||
return climate::CLIMATE_FAN_AUTO;
|
||||
if (fan <= 30)
|
||||
return climate::CLIMATE_FAN_QUIET;
|
||||
if (fan <= 50)
|
||||
return climate::CLIMATE_FAN_LOW;
|
||||
if (fan <= 70)
|
||||
return climate::CLIMATE_FAN_MEDIUM;
|
||||
return climate::CLIMATE_FAN_HIGH;
|
||||
}
|
||||
|
||||
void PortaSplitClimate::setup() {
|
||||
// The status interval starts with a random offset of up to 5 s, which also serves as the boot query.
|
||||
this->set_timeout(10000, [this]() { this->send_capability_queries_(); });
|
||||
this->set_interval(STATUS_INTERVAL_MS, [this]() { this->send_status_query_(); });
|
||||
this->set_interval(NETWORK_INTERVAL_MS, [this]() { this->send_module_frame_(0x0D, this->net_counter_++, 0x08); });
|
||||
}
|
||||
|
||||
void PortaSplitClimate::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
uint8_t buf[32];
|
||||
size_t n;
|
||||
while ((n = std::min((size_t) this->available(), sizeof(buf))) != 0 && this->read_array(buf, n)) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
this->rx_feed_(buf[i]);
|
||||
this->rx_last_byte_ = now;
|
||||
}
|
||||
// The line went quiet mid-frame; drop it so its bytes cannot swallow the next one.
|
||||
if (this->rx_len_ != 0 && now - this->rx_last_byte_ > RX_TIMEOUT_MS)
|
||||
this->rx_len_ = 0;
|
||||
}
|
||||
|
||||
void PortaSplitClimate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Midea PortaSplit:");
|
||||
this->dump_traits_(TAG);
|
||||
}
|
||||
|
||||
climate::ClimateTraits PortaSplitClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_visual_min_temperature(MIN_TARGET_TEMP);
|
||||
traits.set_visual_max_temperature(MAX_TARGET_TEMP);
|
||||
traits.set_visual_temperature_step(0.5f);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT_COOL,
|
||||
climate::CLIMATE_MODE_COOL,
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
});
|
||||
traits.set_supported_fan_modes({
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_FAN_QUIET,
|
||||
climate::CLIMATE_FAN_LOW,
|
||||
climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH,
|
||||
});
|
||||
traits.set_supported_swing_modes({
|
||||
climate::CLIMATE_SWING_OFF,
|
||||
climate::CLIMATE_SWING_VERTICAL,
|
||||
});
|
||||
traits.set_supported_presets({
|
||||
climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_BOOST,
|
||||
climate::CLIMATE_PRESET_ECO,
|
||||
climate::CLIMATE_PRESET_AWAY, // Frost protection (8 degC heating)
|
||||
});
|
||||
return traits;
|
||||
}
|
||||
|
||||
void PortaSplitClimate::control(const climate::ClimateCall &call) {
|
||||
if (call.get_mode().has_value()) {
|
||||
const climate::ClimateMode mode = *call.get_mode();
|
||||
this->st_power_ = mode != climate::CLIMATE_MODE_OFF;
|
||||
if (this->st_power_)
|
||||
this->st_mode_ = climate_mode_to_midea(mode);
|
||||
// Frost protection only runs while heating.
|
||||
if (mode != climate::CLIMATE_MODE_HEAT)
|
||||
this->st_frost_ = false;
|
||||
}
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
const float target = clamp(*call.get_target_temperature(), MIN_TARGET_TEMP, MAX_TARGET_TEMP);
|
||||
this->st_target_half_ = (uint8_t) lroundf(target * 2.0f);
|
||||
}
|
||||
if (call.get_fan_mode().has_value())
|
||||
this->st_fan_ = fan_mode_to_midea(*call.get_fan_mode());
|
||||
if (call.get_swing_mode().has_value())
|
||||
this->st_swing_ = *call.get_swing_mode() == climate::CLIMATE_SWING_VERTICAL;
|
||||
if (call.get_preset().has_value()) {
|
||||
const climate::ClimatePreset preset = *call.get_preset();
|
||||
this->st_turbo_ = preset == climate::CLIMATE_PRESET_BOOST;
|
||||
this->st_sleep_ = preset == climate::CLIMATE_PRESET_SLEEP;
|
||||
this->st_eco_ = preset == climate::CLIMATE_PRESET_ECO;
|
||||
this->st_frost_ = preset == climate::CLIMATE_PRESET_AWAY;
|
||||
if (this->st_frost_) {
|
||||
this->st_mode_ = climate_mode_to_midea(climate::CLIMATE_MODE_HEAT);
|
||||
this->st_power_ = true;
|
||||
}
|
||||
}
|
||||
this->send_set_command_();
|
||||
}
|
||||
|
||||
void PortaSplitClimate::send_frame_(uint8_t proto, uint8_t type, const uint8_t *payload, size_t len) {
|
||||
uint8_t f[FRAME_OVERHEAD + MAX_TX_PAYLOAD] = {0};
|
||||
const size_t total = FRAME_OVERHEAD + len;
|
||||
f[0] = 0xAA;
|
||||
f[1] = (uint8_t) (total - 1);
|
||||
f[2] = 0xAC;
|
||||
// f[3] is the sync byte and must stay 0x00 on the PortaSplit; f[4..7] are reserved.
|
||||
f[8] = proto;
|
||||
f[9] = type;
|
||||
progmem_memcpy(&f[10], payload, len);
|
||||
f[10 + len] = crc8(&f[10], (uint8_t) len);
|
||||
f[11 + len] = checksum(f, 11 + len);
|
||||
this->write_array(f, total);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(sizeof(f))];
|
||||
ESP_LOGV(TAG, "TX: %s", format_hex_pretty_to(hex_buf, f, total));
|
||||
#endif
|
||||
}
|
||||
|
||||
void PortaSplitClimate::send_set_command_() {
|
||||
uint8_t p[MAX_TX_PAYLOAD] = {0};
|
||||
const uint8_t whole = this->st_target_half_ >> 1;
|
||||
const bool half = this->st_target_half_ & 1;
|
||||
p[0] = 0x40;
|
||||
p[1] = 0x42; // 0x02 is mandatory, 0x40 enables the beeper
|
||||
if (this->st_power_)
|
||||
p[1] |= 0x01;
|
||||
p[2] = (uint8_t) ((this->st_mode_ << 5) | (half ? 0x10 : 0x00) | ((whole - 16) & 0x0F));
|
||||
p[3] = this->st_fan_;
|
||||
p[4] = 0x7F;
|
||||
p[5] = 0x7F;
|
||||
p[6] = 0xF0;
|
||||
p[7] = this->st_swing_ ? 0x3C : 0x30;
|
||||
p[8] = this->st_turbo_ ? 0x20 : 0x00;
|
||||
p[9] = (uint8_t) ((this->st_ion_ ? 0x20 : 0x00) | (this->st_eco_ ? 0x80 : 0x00));
|
||||
p[10] = this->st_sleep_ ? 0x01 : 0x00;
|
||||
p[18] = (uint8_t) ((whole - 12) & 0x1F);
|
||||
p[19] = 0x80;
|
||||
p[21] = 0x0A;
|
||||
p[22] = this->st_frost_ ? 0x80 : 0x00;
|
||||
p[23] = this->msg_id_++;
|
||||
this->send_frame_(0x02, 0x02, p, sizeof(p));
|
||||
// Read back the state the AC actually applied.
|
||||
this->set_timeout(FOLLOWUP_TIMEOUT_ID, FOLLOWUP_DELAY_MS, [this]() { this->send_status_query_(); });
|
||||
}
|
||||
|
||||
void PortaSplitClimate::send_status_query_() {
|
||||
uint8_t p[sizeof(STATUS_QUERY)];
|
||||
progmem_memcpy(p, STATUS_QUERY, sizeof(p));
|
||||
p[11] = this->msg_id_++;
|
||||
this->send_frame_(0x00, 0x03, p, sizeof(p));
|
||||
}
|
||||
|
||||
void PortaSplitClimate::send_capability_queries_() {
|
||||
this->send_frame_(0x03, 0x03, CAPS_0, sizeof(CAPS_0));
|
||||
this->set_timeout(1000, [this]() { this->send_frame_(0x03, 0x03, CAPS_1, sizeof(CAPS_1)); });
|
||||
}
|
||||
|
||||
void PortaSplitClimate::send_module_frame_(uint8_t type, uint8_t counter, uint8_t wifi_flags) {
|
||||
uint8_t f[sizeof(MODULE_FRAME)];
|
||||
progmem_memcpy(f, MODULE_FRAME, sizeof(f));
|
||||
f[6] = counter;
|
||||
f[9] = type;
|
||||
f[25] = wifi_flags;
|
||||
f[30] = checksum(f, 30);
|
||||
this->write_array(f, sizeof(f));
|
||||
}
|
||||
|
||||
void PortaSplitClimate::rx_feed_(uint8_t b) {
|
||||
if (this->rx_len_ == 0 && b != 0xAA)
|
||||
return;
|
||||
this->rx_[this->rx_len_++] = b;
|
||||
if (this->rx_len_ < 3)
|
||||
return;
|
||||
const size_t total = (size_t) this->rx_[1] + 1;
|
||||
// Every frame carries the appliance type 0xAC; anything else is a false start.
|
||||
if (this->rx_len_ == 3 && (total < FRAME_OVERHEAD || total > MAX_FRAME || this->rx_[2] != 0xAC)) {
|
||||
this->rx_len_ = 0;
|
||||
return;
|
||||
}
|
||||
if (this->rx_len_ < total)
|
||||
return;
|
||||
this->rx_len_ = 0;
|
||||
if (checksum(this->rx_.data(), total - 1) != this->rx_[total - 1]) {
|
||||
ESP_LOGW(TAG, "RX checksum mismatch, dropping frame");
|
||||
return;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MAX_FRAME)];
|
||||
ESP_LOGV(TAG, "RX: %s", format_hex_pretty_to(hex_buf, this->rx_.data(), total));
|
||||
#endif
|
||||
this->handle_frame_(this->rx_.data(), total);
|
||||
}
|
||||
|
||||
void PortaSplitClimate::handle_frame_(const uint8_t *f, size_t len) {
|
||||
if (f[9] == 0x63) {
|
||||
this->send_module_frame_(0x63, 0, 0x00);
|
||||
} else if (f[9] == 0x05 && f[10] == 0xB5) {
|
||||
// The original module acknowledges telemetry pushes by echoing them back.
|
||||
this->write_array(f, len);
|
||||
} else if (f[10] == 0xC0) {
|
||||
this->handle_c0_(f, len);
|
||||
}
|
||||
}
|
||||
|
||||
void PortaSplitClimate::handle_c0_(const uint8_t *f, size_t len) {
|
||||
if (len < 26)
|
||||
return;
|
||||
const uint8_t mode = (f[12] >> 5) & 0x07;
|
||||
const uint8_t target_half = (uint8_t) ((((f[12] & 0x0F) + 16) << 1) | ((f[12] & 0x10) ? 1 : 0));
|
||||
const climate::ClimateFanMode fan = midea_fan_to_climate(f[13]);
|
||||
|
||||
this->st_power_ = f[11] & 0x01;
|
||||
if (mode >= 1 && mode <= 5)
|
||||
this->st_mode_ = mode;
|
||||
this->st_target_half_ = target_half;
|
||||
this->st_fan_ = fan_mode_to_midea(fan);
|
||||
this->st_swing_ = (f[17] & 0x0F) == 0x0C;
|
||||
this->st_turbo_ = f[18] & 0x20;
|
||||
this->st_ion_ = f[19] & 0x20;
|
||||
this->st_eco_ = f[19] & 0x10; // Set command uses bit 7, readback uses bit 4.
|
||||
this->st_sleep_ = f[20] & 0x01;
|
||||
this->st_frost_ = len > 31 && (f[31] & 0x80);
|
||||
|
||||
this->mode = this->st_power_ ? midea_mode_to_climate(this->st_mode_) : climate::CLIMATE_MODE_OFF;
|
||||
this->target_temperature = target_half / 2.0f;
|
||||
// 0x00 and 0xFF mean the indoor probe gave no reading.
|
||||
const uint8_t raw_temp = f[21];
|
||||
this->current_temperature = (raw_temp == 0x00 || raw_temp == 0xFF) ? NAN : ((float) raw_temp - 50.0f) / 2.0f;
|
||||
this->fan_mode = fan;
|
||||
this->swing_mode = this->st_swing_ ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
|
||||
if (this->st_frost_) {
|
||||
this->preset = climate::CLIMATE_PRESET_AWAY;
|
||||
} else if (this->st_turbo_) {
|
||||
this->preset = climate::CLIMATE_PRESET_BOOST;
|
||||
} else if (this->st_sleep_) {
|
||||
this->preset = climate::CLIMATE_PRESET_SLEEP;
|
||||
} else if (this->st_eco_) {
|
||||
this->preset = climate::CLIMATE_PRESET_ECO;
|
||||
} else {
|
||||
this->preset = climate::CLIMATE_PRESET_NONE;
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
} // namespace esphome::midea_portasplit
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
// Midea PortaSplit climate component.
|
||||
//
|
||||
// The PortaSplit uses sync byte 0x00 for climate frames and silently drops the
|
||||
// XOR sync byte (LEN ^ 0xAC) frames that the `midea` component sends, which is
|
||||
// why the stock `midea` platform cannot drive this unit.
|
||||
//
|
||||
// Frame: AA | LEN | AC | SYNC | 00 00 00 00 | PROTO | TYPE | PAYLOAD | CRC8 | CS
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::midea_portasplit {
|
||||
|
||||
static constexpr uint8_t FAN_AUTO = 102;
|
||||
|
||||
class PortaSplitClimate : public climate::Climate, public uart::UARTDevice, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
climate::ClimateTraits traits() override;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
protected:
|
||||
static constexpr size_t MAX_FRAME = 128;
|
||||
|
||||
// The payload may live in PROGMEM.
|
||||
void send_frame_(uint8_t proto, uint8_t type, const uint8_t *payload, size_t len);
|
||||
void send_set_command_();
|
||||
void send_status_query_();
|
||||
void send_module_frame_(uint8_t type, uint8_t counter, uint8_t wifi_flags);
|
||||
void send_capability_queries_();
|
||||
|
||||
void rx_feed_(uint8_t b);
|
||||
void handle_frame_(const uint8_t *f, size_t len);
|
||||
void handle_c0_(const uint8_t *f, size_t len);
|
||||
|
||||
uint32_t rx_last_byte_{0};
|
||||
// Last known device state, mirrored from C0 so a set command does not clobber it.
|
||||
uint8_t st_target_half_{48}; // Target temperature in half degrees
|
||||
uint8_t st_mode_{1};
|
||||
uint8_t st_fan_{FAN_AUTO};
|
||||
bool st_power_ : 1 {false};
|
||||
bool st_swing_ : 1 {false};
|
||||
bool st_turbo_ : 1 {false};
|
||||
bool st_ion_ : 1 {false};
|
||||
bool st_sleep_ : 1 {false};
|
||||
bool st_eco_ : 1 {false};
|
||||
bool st_frost_ : 1 {false};
|
||||
|
||||
uint8_t msg_id_{1};
|
||||
uint8_t net_counter_{0};
|
||||
uint8_t rx_len_{0};
|
||||
std::array<uint8_t, MAX_FRAME> rx_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::midea_portasplit
|
||||
@@ -130,19 +130,8 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
int get_width() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return HEIGHT;
|
||||
return WIDTH;
|
||||
}
|
||||
|
||||
int get_height() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return WIDTH;
|
||||
return HEIGHT;
|
||||
}
|
||||
int get_width() override { return display::rotation_swaps_axes(this->rotation_) ? HEIGHT : WIDTH; }
|
||||
int get_height() override { return display::rotation_swaps_axes(this->rotation_) ? WIDTH : HEIGHT; }
|
||||
|
||||
void set_init_sequence(const uint8_t *sequence, size_t len) {
|
||||
this->init_sequence_ = sequence;
|
||||
@@ -621,6 +610,9 @@ class MipiSpiBuffer
|
||||
#endif
|
||||
}
|
||||
|
||||
// With hardware rotation the buffer is written in drawn coordinates.
|
||||
bool pixel_axes_swapped() const override { return !HAS_HARDWARE_ROTATION && display::Display::pixel_axes_swapped(); }
|
||||
|
||||
// Draw a pixel at the given coordinates.
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (this->is_point_clipped(x, y))
|
||||
|
||||
@@ -53,7 +53,6 @@ from esphome.const import (
|
||||
CONF_SUBSCRIBE_QOS,
|
||||
CONF_TOPIC,
|
||||
CONF_TOPIC_PREFIX,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_USE_ABBREVIATIONS,
|
||||
CONF_USERNAME,
|
||||
CONF_WILL_MESSAGE,
|
||||
@@ -119,18 +118,6 @@ MQTTMessage = mqtt_ns.struct("MQTTMessage")
|
||||
MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason")
|
||||
MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component)
|
||||
MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action)
|
||||
MQTTMessageTrigger = mqtt_ns.class_(
|
||||
"MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component
|
||||
)
|
||||
MQTTJsonMessageTrigger = mqtt_ns.class_(
|
||||
"MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst)
|
||||
)
|
||||
MQTTConnectTrigger = mqtt_ns.class_(
|
||||
"MQTTConnectTrigger", automation.Trigger.template(cg.bool_)
|
||||
)
|
||||
MQTTDisconnectTrigger = mqtt_ns.class_(
|
||||
"MQTTDisconnectTrigger", automation.Trigger.template(MQTTClientDisconnectReason)
|
||||
)
|
||||
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
|
||||
|
||||
MQTTAlarmControlPanelComponent = mqtt_ns.class_(
|
||||
@@ -229,7 +216,6 @@ JSON_MESSAGE_AUTOMATION_SCHEMA = automation.validate_synchronous_automation(
|
||||
"on_json_message",
|
||||
"the parsed JSON document",
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTJsonMessageTrigger),
|
||||
cv.Required(CONF_TOPIC): cv.subscribe_topic,
|
||||
cv.Optional(CONF_QOS, default=0): cv.mqtt_qos,
|
||||
},
|
||||
@@ -295,21 +281,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="15min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTConnectTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
MQTTDisconnectTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(),
|
||||
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger),
|
||||
cv.Required(CONF_TOPIC): cv.subscribe_topic,
|
||||
cv.Optional(CONF_QOS, default=0): cv.mqtt_qos,
|
||||
cv.Optional(CONF_PAYLOAD): cv.string_strict,
|
||||
@@ -346,6 +321,18 @@ def exp_mqtt_message(config):
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CONNECT, "set_on_connect", [(cg.bool_, "session_present")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DISCONNECT,
|
||||
"set_on_disconnect",
|
||||
[(MQTTClientDisconnectReason, "reason")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WEB)
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -460,29 +447,37 @@ async def to_code(config):
|
||||
cg.add_define("USE_MQTT_IDF_ENQUEUE")
|
||||
# end esp-idf
|
||||
|
||||
# The client queues subscriptions until it connects, so they can be made at construction.
|
||||
for conf in config.get(CONF_ON_MESSAGE, []):
|
||||
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC])
|
||||
cg.add(trig.set_qos(conf[CONF_QOS]))
|
||||
if CONF_PAYLOAD in conf:
|
||||
cg.add(trig.set_payload(conf[CONF_PAYLOAD]))
|
||||
await cg.register_component(trig, conf)
|
||||
await automation.build_automation(trig, [(cg.std_string, "x")], conf)
|
||||
callback = await automation.build_trigger_callback(
|
||||
[(cg.std_string, "x")],
|
||||
conf,
|
||||
params=[(cg.std_string, "topic"), (cg.std_string, "payload")],
|
||||
forward=["payload"],
|
||||
# The length is compared first; the literal stays in flash on ESP8266.
|
||||
when=automation.ApplyCall(
|
||||
"StringRef(payload) == {}",
|
||||
((CONF_PAYLOAD, cg.std_string, automation.string_ref_literal),),
|
||||
),
|
||||
)
|
||||
cg.add(
|
||||
var.subscribe(cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS])
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_JSON_MESSAGE, []):
|
||||
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS])
|
||||
await automation.build_automation(trig, [(cg.JsonObjectConst, "x")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_CONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.bool_, "session_present")], conf
|
||||
callback = await automation.build_trigger_callback(
|
||||
[(cg.JsonObjectConst, "x")],
|
||||
conf,
|
||||
params=[(cg.std_string, "topic"), (cg.JsonObject, "root")],
|
||||
forward=["root"],
|
||||
)
|
||||
cg.add(
|
||||
var.subscribe_json(
|
||||
cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS]
|
||||
)
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_DISCONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(MQTTClientDisconnectReason, "reason")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE]))
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@ MQTTClientComponent::MQTTClientComponent() {
|
||||
}
|
||||
|
||||
// Connection
|
||||
// Room for "esphome/<kind>/<name>" with the longest kind, "discover"
|
||||
static constexpr size_t ESPHOME_TOPIC_BUFFER_SIZE = sizeof("esphome/discover/") + ESPHOME_DEVICE_NAME_MAX_LEN;
|
||||
|
||||
static void format_esphome_topic(char (&topic)[ESPHOME_TOPIC_BUFFER_SIZE], const char *kind) {
|
||||
buf_append_printf(topic, sizeof(topic), 0, "esphome/%s/%s", kind, App.get_name().c_str());
|
||||
}
|
||||
|
||||
void MQTTClientComponent::setup() {
|
||||
this->mqtt_backend_.set_on_message(
|
||||
[this](const char *topic, const char *payload, size_t len, size_t index, size_t total) {
|
||||
@@ -78,6 +85,13 @@ void MQTTClientComponent::setup() {
|
||||
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
static_cast<MQTTClientComponent *>(self)->on_log(level, tag, message, message_len);
|
||||
});
|
||||
#ifdef USE_MQTT_CRASH_LOG
|
||||
// `esphome logs` announces itself here, like an API client's log subscription, and the record stays
|
||||
// loggable until the next boot, so the offer does too. A log topic that drops errors would swallow the
|
||||
// replay, so it is not offered. The topic is rebuilt from the device name when needed, so nothing is
|
||||
// stored for it.
|
||||
this->crash_log_offered_ = logger::crash_log_pending() && this->log_level_ >= ESPHOME_LOG_LEVEL_ERROR;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -87,10 +101,8 @@ void MQTTClientComponent::setup() {
|
||||
2);
|
||||
|
||||
// Format topic on stack - subscribe() copies it
|
||||
// "esphome/ping/" (13) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
|
||||
constexpr size_t ping_topic_buffer_size = 13 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
|
||||
char ping_topic[ping_topic_buffer_size];
|
||||
buf_append_printf(ping_topic, sizeof(ping_topic), 0, "esphome/ping/%s", App.get_name().c_str());
|
||||
char ping_topic[ESPHOME_TOPIC_BUFFER_SIZE];
|
||||
format_esphome_topic(ping_topic, "ping");
|
||||
this->subscribe(
|
||||
ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||
}
|
||||
@@ -105,10 +117,8 @@ void MQTTClientComponent::send_device_info_() {
|
||||
return;
|
||||
}
|
||||
// Format topic on stack to avoid heap allocation
|
||||
// "esphome/discover/" (17) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
|
||||
constexpr size_t topic_buffer_size = 17 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
|
||||
char topic[topic_buffer_size];
|
||||
buf_append_printf(topic, sizeof(topic), 0, "esphome/discover/%s", App.get_name().c_str());
|
||||
char topic[ESPHOME_TOPIC_BUFFER_SIZE];
|
||||
format_esphome_topic(topic, "discover");
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
this->publish_json(
|
||||
@@ -354,6 +364,14 @@ void MQTTClientComponent::check_connected() {
|
||||
delay(100); // NOLINT
|
||||
|
||||
this->resubscribe_subscriptions_();
|
||||
#ifdef USE_MQTT_CRASH_LOG
|
||||
// Best effort: a refused subscription is tried again at the next connection
|
||||
if (this->crash_log_offered_) {
|
||||
char topic[ESPHOME_TOPIC_BUFFER_SIZE];
|
||||
format_esphome_topic(topic, "logs");
|
||||
this->subscribe_(topic, 0);
|
||||
}
|
||||
#endif
|
||||
this->send_device_info_();
|
||||
|
||||
for (MQTTComponent *component : this->children_)
|
||||
@@ -461,34 +479,31 @@ void MQTTClientComponent::resubscribe_subscriptions_() {
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
|
||||
MQTTSubscription subscription{
|
||||
.topic = topic,
|
||||
.qos = qos,
|
||||
.callback = std::move(callback),
|
||||
.subscribed = false,
|
||||
.resubscribe_timeout = 0,
|
||||
};
|
||||
this->resubscribe_subscription_(&subscription);
|
||||
this->subscriptions_.push_back(subscription);
|
||||
#ifdef USE_MQTT_CRASH_LOG
|
||||
void MQTTClientComponent::handle_crash_log_request_(const std::string &topic, const std::string &payload) {
|
||||
// Only the CLI's empty request counts: a log_topic pointing at this topic would otherwise replay the crash
|
||||
// log for every line it logs
|
||||
if (!this->crash_log_offered_ || !payload.empty())
|
||||
return;
|
||||
char logs_topic[ESPHOME_TOPIC_BUFFER_SIZE];
|
||||
format_esphome_topic(logs_topic, "logs");
|
||||
if (topic == logs_topic)
|
||||
logger::log_crash_to_client();
|
||||
}
|
||||
#endif
|
||||
|
||||
void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) {
|
||||
auto f = [callback](const std::string &topic, const std::string &payload) {
|
||||
json::parse_json(payload, [topic, callback](JsonObject root) -> bool {
|
||||
callback(topic, root);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
MQTTSubscription subscription{
|
||||
.topic = topic,
|
||||
void MQTTClientComponent::add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed,
|
||||
uint8_t qos) {
|
||||
auto &subscription = this->subscriptions_.emplace_back(MQTTSubscription{
|
||||
.topic = std::move(topic),
|
||||
.qos = qos,
|
||||
.callback = f,
|
||||
.subscribed = false,
|
||||
.boxed = boxed,
|
||||
.removed = false,
|
||||
.callback = callback,
|
||||
.resubscribe_timeout = 0,
|
||||
};
|
||||
});
|
||||
this->resubscribe_subscription_(&subscription);
|
||||
this->subscriptions_.push_back(subscription);
|
||||
}
|
||||
|
||||
void MQTTClientComponent::unsubscribe(const std::string &topic) {
|
||||
@@ -502,10 +517,25 @@ void MQTTClientComponent::unsubscribe(const std::string &topic) {
|
||||
this->status_momentary_warning(1000);
|
||||
}
|
||||
|
||||
auto it = subscriptions_.begin();
|
||||
while (it != subscriptions_.end()) {
|
||||
if (it->topic == topic) {
|
||||
it = subscriptions_.erase(it);
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
if (subscription.topic == topic)
|
||||
subscription.removed = true;
|
||||
}
|
||||
// A callback may be running; the outermost dispatch erases them once it returns.
|
||||
if (this->dispatching_) {
|
||||
this->removed_while_dispatching_ = true;
|
||||
} else {
|
||||
this->erase_removed_subscriptions_();
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTClientComponent::erase_removed_subscriptions_() {
|
||||
auto it = this->subscriptions_.begin();
|
||||
while (it != this->subscriptions_.end()) {
|
||||
if (it->removed) {
|
||||
if (it->boxed)
|
||||
it->callback.free_boxed();
|
||||
it = this->subscriptions_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
@@ -660,9 +690,24 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string
|
||||
// in simple tests but will cause crashes with complex automations.
|
||||
this->defer([this, topic, payload]() {
|
||||
#endif
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
if (topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||
subscription.callback(topic, payload);
|
||||
#ifdef USE_MQTT_CRASH_LOG
|
||||
this->handle_crash_log_request_(topic, payload);
|
||||
#endif
|
||||
// Indexed with the count taken up front: a callback may subscribe, which can reallocate the
|
||||
// vector, and the new entry waits for the next message.
|
||||
// Restored rather than cleared, so only the outermost dispatch erases what was unsubscribed meanwhile.
|
||||
const bool was_dispatching = this->dispatching_;
|
||||
this->dispatching_ = true;
|
||||
const size_t count = this->subscriptions_.size();
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &subscription = this->subscriptions_[i];
|
||||
if (!subscription.removed && topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||
subscription.callback.call(topic, payload);
|
||||
}
|
||||
this->dispatching_ = was_dispatching;
|
||||
if (!was_dispatching && this->removed_while_dispatching_) {
|
||||
this->removed_while_dispatching_ = false;
|
||||
this->erase_removed_subscriptions_();
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
});
|
||||
@@ -762,29 +807,6 @@ void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&call
|
||||
|
||||
MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// MQTTMessageTrigger
|
||||
MQTTMessageTrigger::MQTTMessageTrigger(std::string topic) : topic_(std::move(topic)) {}
|
||||
void MQTTMessageTrigger::setup() {
|
||||
global_mqtt_client->subscribe(
|
||||
this->topic_,
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
if (this->payload_.has_value() && payload != *this->payload_) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->trigger(payload);
|
||||
},
|
||||
this->qos_);
|
||||
}
|
||||
void MQTTMessageTrigger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"MQTT Message Trigger:\n"
|
||||
" Topic: '%s'\n"
|
||||
" QoS: %u",
|
||||
this->topic_.c_str(), this->qos_);
|
||||
}
|
||||
float MQTTMessageTrigger::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
#endif // USE_MQTT
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
// The crash log replay needs the logger, which mqtt does not depend on
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
#define USE_MQTT_CRASH_LOG
|
||||
#endif
|
||||
|
||||
namespace esphome::mqtt {
|
||||
|
||||
/** Callback for MQTT events.
|
||||
@@ -31,19 +36,24 @@ namespace esphome::mqtt {
|
||||
using mqtt_on_connect_callback_t = std::function<MQTTBackend::on_connect_callback_t>;
|
||||
using mqtt_on_disconnect_callback_t = std::function<MQTTBackend::on_disconnect_callback_t>;
|
||||
|
||||
/** Callback for MQTT subscriptions.
|
||||
*
|
||||
* First parameter is the topic, the second one is the payload.
|
||||
*/
|
||||
using mqtt_callback_t = std::function<void(const std::string &, const std::string &)>;
|
||||
using mqtt_json_callback_t = std::function<void(const std::string &, JsonObject)>;
|
||||
/// Stored callback of an MQTT subscription: the topic, then the payload.
|
||||
using mqtt_subscription_callback_t = Callback<void(const std::string &, const std::string &)>;
|
||||
|
||||
// Remove before 2027.5.0
|
||||
using mqtt_callback_t ESPDEPRECATED("subscribe() takes any callable; pass the lambda directly. Removed in 2027.5.0",
|
||||
"2026.11.0") = std::function<void(const std::string &, const std::string &)>;
|
||||
using mqtt_json_callback_t ESPDEPRECATED(
|
||||
"subscribe_json() takes any callable; pass the lambda directly. Removed in 2027.5.0",
|
||||
"2026.11.0") = std::function<void(const std::string &, JsonObject)>;
|
||||
|
||||
/// internal struct for MQTT subscriptions.
|
||||
struct MQTTSubscription {
|
||||
std::string topic;
|
||||
uint8_t qos;
|
||||
mqtt_callback_t callback;
|
||||
bool subscribed;
|
||||
bool boxed; // made by create_boxed(), freed on unsubscribe
|
||||
bool removed; // unsubscribed while a callback ran, erased once dispatch returns
|
||||
mqtt_subscription_callback_t callback;
|
||||
uint32_t resubscribe_timeout;
|
||||
};
|
||||
|
||||
@@ -165,12 +175,21 @@ class MQTTClientComponent final : public Component {
|
||||
bool is_log_message_enabled() const;
|
||||
|
||||
/** Subscribe to an MQTT topic and call callback when a message is received.
|
||||
*
|
||||
* A callback that fits in a pointer (capture at most `this`) is stored inline; a larger one is
|
||||
* boxed on the heap and freed by unsubscribe().
|
||||
*
|
||||
* @param topic The topic. Wildcards are currently not supported.
|
||||
* @param callback The callback function.
|
||||
* @param qos The QoS of this subscription.
|
||||
*/
|
||||
void subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos = 0);
|
||||
template<typename F> void subscribe(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
constexpr bool boxed = !mqtt_subscription_callback_t::fits_inline<F>();
|
||||
this->add_subscription_(std::move(topic),
|
||||
boxed ? mqtt_subscription_callback_t::create_boxed(std::forward<F>(callback))
|
||||
: mqtt_subscription_callback_t::create(std::forward<F>(callback)),
|
||||
boxed, qos);
|
||||
}
|
||||
|
||||
/** Subscribe to a MQTT topic and automatically parse JSON payload.
|
||||
*
|
||||
@@ -181,11 +200,21 @@ class MQTTClientComponent final : public Component {
|
||||
* received.
|
||||
* @param qos The QoS of this subscription.
|
||||
*/
|
||||
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
|
||||
template<typename F> void subscribe_json(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
static_assert(std::is_invocable_v<const std::decay_t<F> &, const std::string &, JsonObject>,
|
||||
"subscribe_json() needs a callback that does not mutate its captures");
|
||||
this->subscribe(
|
||||
std::move(topic),
|
||||
[cb = std::forward<F>(callback)](const std::string &topic, const std::string &payload) {
|
||||
call_json(cb, topic, payload);
|
||||
},
|
||||
qos);
|
||||
}
|
||||
|
||||
/** Unsubscribe from an MQTT topic.
|
||||
*
|
||||
* If multiple existing subscriptions to the same topic exist, all of them will be removed.
|
||||
* From inside a subscription callback they stop receiving messages at once and are erased once it returns.
|
||||
*
|
||||
* @param topic The topic to unsubscribe from.
|
||||
* Must match the topic in the original subscribe or subscribe_json call exactly.
|
||||
@@ -285,6 +314,17 @@ class MQTTClientComponent final : public Component {
|
||||
|
||||
bool subscribe_(const char *topic, uint8_t qos);
|
||||
void resubscribe_subscription_(MQTTSubscription *sub);
|
||||
template<typename F> static void call_json(const F &callback, const std::string &topic, const std::string &payload) {
|
||||
JsonDocument doc = json::parse_json(payload);
|
||||
if (doc.overflowed() || doc.isNull())
|
||||
return;
|
||||
callback(topic, doc.as<JsonObject>());
|
||||
}
|
||||
void erase_removed_subscriptions_();
|
||||
#ifdef USE_MQTT_CRASH_LOG
|
||||
void handle_crash_log_request_(const std::string &topic, const std::string &payload);
|
||||
#endif
|
||||
void add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos);
|
||||
void resubscribe_subscriptions_();
|
||||
|
||||
MQTTCredentials credentials_;
|
||||
@@ -327,6 +367,9 @@ class MQTTClientComponent final : public Component {
|
||||
bool dns_resolved_{false};
|
||||
bool dns_resolve_error_{false};
|
||||
bool enable_on_boot_{true};
|
||||
#ifdef USE_MQTT_CRASH_LOG
|
||||
bool crash_log_offered_{false};
|
||||
#endif
|
||||
std::vector<MQTTComponent *> children_;
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t connect_begin_;
|
||||
@@ -336,48 +379,12 @@ class MQTTClientComponent final : public Component {
|
||||
|
||||
bool publish_nan_as_none_{false};
|
||||
bool wait_for_connection_{false};
|
||||
bool dispatching_{false};
|
||||
bool removed_while_dispatching_{false};
|
||||
};
|
||||
|
||||
extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
class MQTTMessageTrigger final : public Trigger<std::string>, public Component {
|
||||
public:
|
||||
explicit MQTTMessageTrigger(std::string topic);
|
||||
|
||||
void set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
void set_payload(const std::string &payload) { this->payload_ = payload; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
std::string topic_;
|
||||
uint8_t qos_{0};
|
||||
optional<std::string> payload_;
|
||||
};
|
||||
|
||||
class MQTTJsonMessageTrigger final : public Trigger<JsonObjectConst> {
|
||||
public:
|
||||
explicit MQTTJsonMessageTrigger(const std::string &topic, uint8_t qos) {
|
||||
global_mqtt_client->subscribe_json(
|
||||
topic, [this](const std::string &topic, JsonObject root) { this->trigger(root); }, qos);
|
||||
}
|
||||
};
|
||||
|
||||
class MQTTConnectTrigger final : public Trigger<bool> {
|
||||
public:
|
||||
explicit MQTTConnectTrigger(MQTTClientComponent *client) {
|
||||
client->set_on_connect([this](bool session_present) { this->trigger(session_present); });
|
||||
}
|
||||
};
|
||||
|
||||
class MQTTDisconnectTrigger final : public Trigger<MQTTClientDisconnectReason> {
|
||||
public:
|
||||
explicit MQTTDisconnectTrigger(MQTTClientComponent *client) {
|
||||
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class MQTTPublishJsonAction final : public Action<Ts...> {
|
||||
public:
|
||||
MQTTPublishJsonAction(MQTTClientComponent *parent) : parent_(parent) {}
|
||||
|
||||
@@ -342,14 +342,6 @@ bool MQTTComponent::is_discovery_enabled() const {
|
||||
return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled();
|
||||
}
|
||||
|
||||
void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
|
||||
global_mqtt_client->subscribe(topic, std::move(callback), qos);
|
||||
}
|
||||
|
||||
void MQTTComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) {
|
||||
global_mqtt_client->subscribe_json(topic, callback, qos);
|
||||
}
|
||||
|
||||
MQTTComponent::MQTTComponent() = default;
|
||||
|
||||
float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
@@ -259,7 +259,9 @@ class MQTTComponent : public Component {
|
||||
* @param callback The callback that will be called when a message with matching topic is received.
|
||||
* @param qos The MQTT quality of service. Defaults to 0.
|
||||
*/
|
||||
void subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos = 0);
|
||||
template<typename F> void subscribe(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
global_mqtt_client->subscribe(std::move(topic), std::forward<F>(callback), qos);
|
||||
}
|
||||
|
||||
/** Subscribe to a MQTT topic and automatically parse JSON payload.
|
||||
*
|
||||
@@ -270,7 +272,9 @@ class MQTTComponent : public Component {
|
||||
* received.
|
||||
* @param qos The MQTT quality of service. Defaults to 0.
|
||||
*/
|
||||
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
|
||||
template<typename F> void subscribe_json(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
global_mqtt_client->subscribe_json(std::move(topic), std::forward<F>(callback), qos);
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr uint32_t SEND_DEFER_ID = 0;
|
||||
|
||||
@@ -32,7 +32,7 @@ ESPHOME_LOG_TAG(TAG, "ota");
|
||||
constexpr size_t MAX_OTA_SIZE = 256u * 1024u * 1024u; // 256 MiB
|
||||
constexpr size_t HEADER_PEEK_SIZE = 64;
|
||||
|
||||
ssize_t read_header_(const char *path, uint8_t *buf, size_t len) {
|
||||
ssize_t read_header(const char *path, uint8_t *buf, size_t len) {
|
||||
int fd = ::open(path, O_RDONLY);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
@@ -50,7 +50,7 @@ struct ElfIdent {
|
||||
uint16_t e_type;
|
||||
};
|
||||
|
||||
ElfIdent parse_elf_(const uint8_t *buf, size_t len) {
|
||||
ElfIdent parse_elf(const uint8_t *buf, size_t len) {
|
||||
ElfIdent out{};
|
||||
if (len < EI_NIDENT + 4)
|
||||
return out;
|
||||
@@ -76,17 +76,17 @@ ElfIdent parse_elf_(const uint8_t *buf, size_t len) {
|
||||
return out;
|
||||
}
|
||||
|
||||
bool validate_elf_(const char *staging_path, const std::string &exe_path) {
|
||||
bool validate_elf(const char *staging_path, const std::string &exe_path) {
|
||||
uint8_t new_buf[HEADER_PEEK_SIZE];
|
||||
uint8_t cur_buf[HEADER_PEEK_SIZE];
|
||||
ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
|
||||
ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
|
||||
ssize_t new_n = read_header(staging_path, new_buf, sizeof(new_buf));
|
||||
ssize_t cur_n = read_header(exe_path.c_str(), cur_buf, sizeof(cur_buf));
|
||||
if (new_n < static_cast<ssize_t>(EI_NIDENT + 4) || cur_n < static_cast<ssize_t>(EI_NIDENT + 4)) {
|
||||
ESP_LOGE(TAG, "ELF header read failed");
|
||||
return false;
|
||||
}
|
||||
ElfIdent new_id = parse_elf_(new_buf, new_n);
|
||||
ElfIdent cur_id = parse_elf_(cur_buf, cur_n);
|
||||
ElfIdent new_id = parse_elf(new_buf, new_n);
|
||||
ElfIdent cur_id = parse_elf(cur_buf, cur_n);
|
||||
if (!new_id.valid) {
|
||||
ESP_LOGE(TAG, "Uploaded payload is not a valid ELF");
|
||||
return false;
|
||||
@@ -122,7 +122,7 @@ struct MachOIdent {
|
||||
uint32_t cpusubtype;
|
||||
};
|
||||
|
||||
MachOIdent parse_macho_(const uint8_t *buf, size_t len) {
|
||||
MachOIdent parse_macho(const uint8_t *buf, size_t len) {
|
||||
MachOIdent out{};
|
||||
// mach_header is the common prefix of mach_header and mach_header_64;
|
||||
// cputype/cpusubtype/filetype have identical offsets in both.
|
||||
@@ -153,18 +153,18 @@ MachOIdent parse_macho_(const uint8_t *buf, size_t len) {
|
||||
return out;
|
||||
}
|
||||
|
||||
bool validate_macho_(const char *staging_path, const std::string &exe_path) {
|
||||
bool validate_macho(const char *staging_path, const std::string &exe_path) {
|
||||
uint8_t new_buf[HEADER_PEEK_SIZE];
|
||||
uint8_t cur_buf[HEADER_PEEK_SIZE];
|
||||
ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
|
||||
ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
|
||||
ssize_t new_n = read_header(staging_path, new_buf, sizeof(new_buf));
|
||||
ssize_t cur_n = read_header(exe_path.c_str(), cur_buf, sizeof(cur_buf));
|
||||
if (new_n < static_cast<ssize_t>(sizeof(struct mach_header)) ||
|
||||
cur_n < static_cast<ssize_t>(sizeof(struct mach_header))) {
|
||||
ESP_LOGE(TAG, "Mach-O header read failed");
|
||||
return false;
|
||||
}
|
||||
MachOIdent new_id = parse_macho_(new_buf, new_n);
|
||||
MachOIdent cur_id = parse_macho_(cur_buf, cur_n);
|
||||
MachOIdent new_id = parse_macho(new_buf, new_n);
|
||||
MachOIdent cur_id = parse_macho(cur_buf, cur_n);
|
||||
if (!new_id.valid) {
|
||||
ESP_LOGE(TAG, "Uploaded payload is not a valid thin Mach-O executable");
|
||||
return false;
|
||||
@@ -182,11 +182,11 @@ bool validate_macho_(const char *staging_path, const std::string &exe_path) {
|
||||
}
|
||||
#endif // __APPLE__
|
||||
|
||||
bool validate_executable_(const char *staging_path, const std::string &exe_path) {
|
||||
bool validate_executable(const char *staging_path, const std::string &exe_path) {
|
||||
#ifdef __linux__
|
||||
return validate_elf_(staging_path, exe_path);
|
||||
return validate_elf(staging_path, exe_path);
|
||||
#elif defined(__APPLE__)
|
||||
return validate_macho_(staging_path, exe_path);
|
||||
return validate_macho(staging_path, exe_path);
|
||||
#else
|
||||
(void) staging_path;
|
||||
(void) exe_path;
|
||||
@@ -298,7 +298,7 @@ OTAResponseTypes HostOTABackend::end() {
|
||||
::close(this->fd_);
|
||||
this->fd_ = -1;
|
||||
|
||||
if (!validate_executable_(this->staging_path_.c_str(), this->final_path_)) {
|
||||
if (!validate_executable(this->staging_path_.c_str(), this->final_path_)) {
|
||||
::unlink(this->staging_path_.c_str());
|
||||
this->staging_path_.clear();
|
||||
return OTA_RESPONSE_ERROR_UPDATE_END;
|
||||
|
||||
@@ -37,7 +37,8 @@ void PCF85063Component::read_time() {
|
||||
.second = uint8_t(pcf85063_.reg.second + 10 * pcf85063_.reg.second_10),
|
||||
.minute = uint8_t(pcf85063_.reg.minute + 10u * pcf85063_.reg.minute_10),
|
||||
.hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10),
|
||||
.day_of_week = uint8_t(pcf85063_.reg.weekday),
|
||||
// The chip counts weekdays 0-6 from Sunday, ESPTime counts 1-7
|
||||
.day_of_week = uint8_t(this->pcf85063_.reg.weekday + 1),
|
||||
.day_of_month = uint8_t(pcf85063_.reg.day + 10u * pcf85063_.reg.day_10),
|
||||
.month = uint8_t(pcf85063_.reg.month + 10u * pcf85063_.reg.month_10),
|
||||
.year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000),
|
||||
@@ -62,7 +63,7 @@ void PCF85063Component::write_time() {
|
||||
pcf85063_.reg.month_10 = now.month / 10;
|
||||
pcf85063_.reg.day = now.day_of_month % 10;
|
||||
pcf85063_.reg.day_10 = now.day_of_month / 10;
|
||||
pcf85063_.reg.weekday = now.day_of_week;
|
||||
this->pcf85063_.reg.weekday = now.day_of_week - 1;
|
||||
pcf85063_.reg.hour = now.hour % 10;
|
||||
pcf85063_.reg.hour_10 = now.hour / 10;
|
||||
pcf85063_.reg.minute = now.minute % 10;
|
||||
|
||||
@@ -50,13 +50,15 @@ static constexpr size_t PN532_FRAME_MAX_DATA_SIZE = 254;
|
||||
static constexpr size_t PN532_FRAME_MAX_SIZE = 1 + 6 + PN532_FRAME_MAX_DATA_SIZE + 2;
|
||||
/// Holds one frame in either direction so bus traffic never allocates
|
||||
using PN532Frame = StaticVector<uint8_t, PN532_FRAME_MAX_SIZE>;
|
||||
/// Holds pages 3 to 6 (16 bytes) plus an NDEF message of up to 255 bytes and its TLV header, rounded up to whole reads
|
||||
using UltralightReadBuffer = StaticVector<uint8_t, 272>;
|
||||
/// Longest NDEF message accepted from a MIFARE Ultralight/NTAG tag (the user memory of an NTAG216)
|
||||
static constexpr uint32_t MIFARE_ULTRALIGHT_MAX_NDEF_SIZE = 888;
|
||||
/// Longest NDEF message accepted from a MIFARE Classic tag (the capacity of a 4K tag)
|
||||
static constexpr uint32_t MIFARE_CLASSIC_MAX_NDEF_SIZE = 3440;
|
||||
/// A MIFARE READ answers with 16 bytes: one Classic block or four Ultralight pages
|
||||
static constexpr size_t MIFARE_READ_SIZE = 16;
|
||||
using MifareReadData = std::array<uint8_t, MIFARE_READ_SIZE>;
|
||||
/// Pages 3 to 6, read in one go: the capability container and the start of the NDEF TLV
|
||||
using UltralightHeader = std::array<uint8_t, MIFARE_READ_SIZE>;
|
||||
|
||||
class PN532BinarySensor;
|
||||
|
||||
@@ -134,10 +136,11 @@ class PN532 : public PollingComponent {
|
||||
bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
|
||||
|
||||
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(nfc::NfcTagUid &uid);
|
||||
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, UltralightReadBuffer &data);
|
||||
/// Fills `data` with the bytes from `start_page` onward
|
||||
bool read_mifare_ultralight_bytes_(uint8_t start_page, std::span<uint8_t> data);
|
||||
bool is_mifare_ultralight_formatted_(std::span<const uint8_t> page_3_to_6);
|
||||
uint16_t read_mifare_ultralight_capacity_();
|
||||
bool find_mifare_ultralight_ndef_(std::span<const uint8_t> page_3_to_6, uint8_t &message_length,
|
||||
bool find_mifare_ultralight_ndef_(std::span<const uint8_t> page_3_to_6, uint32_t &message_length,
|
||||
uint8_t &message_start_index);
|
||||
bool write_mifare_ultralight_page_(uint8_t page_num, std::span<const uint8_t> write_data);
|
||||
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
|
||||
#include "pn532.h"
|
||||
@@ -10,10 +11,9 @@ namespace esphome::pn532 {
|
||||
ESPHOME_LOG_TAG(TAG, "pn532.mifare_ultralight");
|
||||
|
||||
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
|
||||
UltralightReadBuffer data;
|
||||
UltralightHeader data;
|
||||
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
|
||||
if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
|
||||
data)) {
|
||||
if (!this->read_mifare_ultralight_bytes_(3, data)) {
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
|
||||
@@ -22,49 +22,68 @@ std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
|
||||
uint8_t message_length;
|
||||
uint32_t message_length;
|
||||
uint8_t message_start_index;
|
||||
if (!this->find_mifare_ultralight_ndef_(data, message_length, message_start_index)) {
|
||||
ESP_LOGW(TAG, "Couldn't find NDEF message");
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index);
|
||||
ESP_LOGVV(TAG, "NDEF message length: %" PRIu32 ", start: %u", message_length, message_start_index);
|
||||
|
||||
if (message_length == 0) {
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
// we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages
|
||||
const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0;
|
||||
|
||||
// data[2] is the CC byte read earlier as part of pages 3-6, same value used by read_mifare_ultralight_capacity_()
|
||||
const uint32_t capacity = data[2] * 8U;
|
||||
const uint32_t total_length = message_length + message_start_index;
|
||||
if (total_length > capacity) {
|
||||
ESP_LOGW(TAG, "NDEF message of %" PRIu32 " bytes at offset %u exceeds tag capacity %" PRIu32, message_length,
|
||||
message_start_index, capacity);
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
if (message_length > MIFARE_ULTRALIGHT_MAX_NDEF_SIZE) {
|
||||
ESP_LOGE(TAG, "NDEF message too long: %" PRIu32 " bytes", message_length);
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
|
||||
// we already read pages 3-6 earlier, pick up where we left off so we're not re-reading pages
|
||||
constexpr size_t header_message_bytes = MIFARE_READ_SIZE - nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // pages 4 to 6
|
||||
const uint16_t read_length =
|
||||
total_length > header_message_bytes ? static_cast<uint16_t>(total_length - header_message_bytes) : 0;
|
||||
// A message up to the old 255 byte limit stays on the stack, a longer one borrows the heap for the read
|
||||
SmallBufferWithHeapFallback<MIFARE_READ_SIZE + 256> buffer(data.size() + read_length);
|
||||
std::span<uint8_t> message(buffer.get(), data.size() + read_length);
|
||||
std::copy(data.begin(), data.end(), message.begin());
|
||||
if (read_length) {
|
||||
if (!read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data)) {
|
||||
if (!this->read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3,
|
||||
message.subspan(data.size()))) {
|
||||
ESP_LOGE(TAG, "Error reading tag data");
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
}
|
||||
// skip page 3 as well as any bytes ahead of message_start_index
|
||||
const size_t skip = message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||
if (skip >= data.size()) {
|
||||
if (skip >= message.size()) {
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
|
||||
}
|
||||
|
||||
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2,
|
||||
make_unique<nfc::NdefMessage>(std::span<const uint8_t>(data).subspan(skip)));
|
||||
make_unique<nfc::NdefMessage>(std::span<const uint8_t>(message).subspan(skip)));
|
||||
}
|
||||
|
||||
bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, UltralightReadBuffer &data) {
|
||||
bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, std::span<uint8_t> data) {
|
||||
MifareReadData chunk;
|
||||
|
||||
for (uint8_t i = 0; i * MIFARE_READ_SIZE < num_bytes; i++) {
|
||||
for (uint8_t i = 0; i * MIFARE_READ_SIZE < data.size(); i++) {
|
||||
// a READ returns 4 pages (16 bytes)
|
||||
if (!this->mifare_read_(uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page), chunk)) {
|
||||
return false;
|
||||
}
|
||||
// keep only the bytes still wanted from this read
|
||||
const uint16_t remaining = num_bytes - i * MIFARE_READ_SIZE;
|
||||
const size_t count = std::min<size_t>(MIFARE_READ_SIZE, remaining);
|
||||
for (const uint8_t byte : std::span<const uint8_t>(chunk).subspan(0, count)) {
|
||||
data.push_back(byte);
|
||||
}
|
||||
const size_t offset = i * MIFARE_READ_SIZE;
|
||||
const size_t count = std::min(MIFARE_READ_SIZE, data.size() - offset);
|
||||
std::copy_n(chunk.begin(), count, data.begin() + offset);
|
||||
}
|
||||
|
||||
char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
@@ -82,15 +101,15 @@ bool PN532::is_mifare_ultralight_formatted_(const std::span<const uint8_t> page_
|
||||
}
|
||||
|
||||
uint16_t PN532::read_mifare_ultralight_capacity_() {
|
||||
UltralightReadBuffer data;
|
||||
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) && data.size() > 2) {
|
||||
std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> data;
|
||||
if (this->read_mifare_ultralight_bytes_(3, data)) {
|
||||
ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U);
|
||||
return data[2] * 8U;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_to_6, uint8_t &message_length,
|
||||
bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_to_6, uint32_t &message_length,
|
||||
uint8_t &message_start_index) {
|
||||
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
|
||||
|
||||
@@ -98,16 +117,31 @@ bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_t
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t tlv_offset;
|
||||
if (page_3_to_6[p4_offset + 0] == 0x03) {
|
||||
message_length = page_3_to_6[p4_offset + 1];
|
||||
message_start_index = 2;
|
||||
return true;
|
||||
tlv_offset = 0;
|
||||
} else if (page_3_to_6[p4_offset + 5] == 0x03) {
|
||||
message_length = page_3_to_6[p4_offset + 6];
|
||||
message_start_index = 7;
|
||||
return true;
|
||||
tlv_offset = 5;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
// Long-form TLV length: 0x03 0xFF LEN_HI LEN_LO, used whenever the message is >= 255 bytes.
|
||||
// Short-form is just 0x03 LEN. See NFC Forum Type 2 Tag Operation spec section 2.3.
|
||||
if (page_3_to_6[p4_offset + tlv_offset + 1] == 0xFF) {
|
||||
if (page_3_to_6.size() <= p4_offset + tlv_offset + 3) {
|
||||
// long-form marker present but the length bytes weren't read -- fail explicitly instead of
|
||||
// misreporting a 255-byte short-form message
|
||||
return false;
|
||||
}
|
||||
message_length =
|
||||
(static_cast<uint32_t>(page_3_to_6[p4_offset + tlv_offset + 2]) << 8) | page_3_to_6[p4_offset + tlv_offset + 3];
|
||||
message_start_index = tlv_offset + 4;
|
||||
} else {
|
||||
message_length = page_3_to_6[p4_offset + tlv_offset + 1];
|
||||
message_start_index = tlv_offset + 2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "prometheus_handler.h"
|
||||
#ifdef USE_NETWORK
|
||||
#if defined(USE_NETWORK) && !defined(USE_HOST)
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::prometheus {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
|
||||
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
|
||||
@@ -392,6 +392,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
"USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT]
|
||||
) # back-compat alias
|
||||
cg.add_define("USE_RP2_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
cg.add_define("USE_RP2040_CRASH_HANDLER") # back-compat alias
|
||||
|
||||
_configure_lwip()
|
||||
|
||||
@@ -157,7 +157,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
} else {
|
||||
auto stride = x_offset + w + x_pad;
|
||||
auto data = ptr + (stride * y_offset + x_offset) * 2;
|
||||
const auto *data = ptr + (stride * y_offset + x_offset) * 2;
|
||||
SDL_UpdateTexture(this->texture_, &rect, data, stride * 2);
|
||||
}
|
||||
this->redraw_(rect);
|
||||
@@ -199,7 +199,7 @@ void Sdl::process_key(uint32_t keycode, bool down) {
|
||||
callback->second(down);
|
||||
}
|
||||
|
||||
Sdl *Sdl::instance_for_window_(uint32_t window_id) {
|
||||
Sdl *Sdl::instance_for_window(uint32_t window_id) {
|
||||
SDL_Window *window = SDL_GetWindowFromID(window_id);
|
||||
if (window == nullptr)
|
||||
return nullptr;
|
||||
@@ -293,7 +293,7 @@ void Sdl::loop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
Sdl *target = instance_for_window_(window_id);
|
||||
Sdl *target = instance_for_window(window_id);
|
||||
if (target == nullptr) {
|
||||
// Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
|
||||
// input that stops working leaves no trace at all.
|
||||
|
||||
@@ -42,7 +42,7 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
void dump_config() override { LOG_DISPLAY("", "SDL", this); }
|
||||
template<typename F> void add_key_listener(int32_t keycode, F &&callback) {
|
||||
if (!this->key_callbacks_.count(keycode)) {
|
||||
if (!this->key_callbacks_.contains(keycode)) {
|
||||
this->key_callbacks_[keycode] = CallbackManager<void(bool)>();
|
||||
}
|
||||
this->key_callbacks_[keycode].add(std::forward<F>(callback));
|
||||
@@ -66,7 +66,7 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
|
||||
void handle_event_(const SDL_Event &event);
|
||||
/// The display owning the given window, or nullptr if it is not one of ours.
|
||||
static Sdl *instance_for_window_(uint32_t window_id);
|
||||
static Sdl *instance_for_window(uint32_t window_id);
|
||||
SDL_Renderer *renderer_{};
|
||||
SDL_Window *window_{};
|
||||
SDL_Texture *texture_{};
|
||||
|
||||
@@ -162,6 +162,7 @@ bool MR60BHA2Component::validate_message_() {
|
||||
void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, const uint8_t *data, size_t length) {
|
||||
switch (frame_type) {
|
||||
case BREATH_RATE_TYPE_BUFFER:
|
||||
#ifdef USE_SENSOR
|
||||
if (this->breath_rate_sensor_ != nullptr && length >= 4) {
|
||||
uint32_t current_breath_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
|
||||
if (current_breath_rate_int != 0) {
|
||||
@@ -170,11 +171,14 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
|
||||
this->breath_rate_sensor_->publish_state(breath_rate_float);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case PEOPLE_EXIST_TYPE_BUFFER:
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->has_target_binary_sensor_ != nullptr && length >= 2) {
|
||||
uint16_t has_target_int = encode_uint16(data[1], data[0]);
|
||||
this->has_target_binary_sensor_->publish_state(has_target_int);
|
||||
#ifdef USE_SENSOR
|
||||
if (has_target_int == 0) {
|
||||
if (this->breath_rate_sensor_ != nullptr)
|
||||
this->breath_rate_sensor_->publish_state(0.0);
|
||||
@@ -185,9 +189,12 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
|
||||
if (this->num_targets_sensor_ != nullptr)
|
||||
this->num_targets_sensor_->publish_state(0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case HEART_RATE_TYPE_BUFFER:
|
||||
#ifdef USE_SENSOR
|
||||
if (this->heart_rate_sensor_ != nullptr && length >= 4) {
|
||||
uint32_t current_heart_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
|
||||
if (current_heart_rate_int != 0) {
|
||||
@@ -196,8 +203,10 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
|
||||
this->heart_rate_sensor_->publish_state(heart_rate_float);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case DISTANCE_TYPE_BUFFER:
|
||||
#ifdef USE_SENSOR
|
||||
if (length >= 1 && data[0] != 0) {
|
||||
if (this->distance_sensor_ != nullptr && length >= 8) {
|
||||
uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]);
|
||||
@@ -206,12 +215,15 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
|
||||
this->distance_sensor_->publish_state(distance_float);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case PRINT_CLOUD_BUFFER:
|
||||
#ifdef USE_SENSOR
|
||||
if (this->num_targets_sensor_ != nullptr && length >= 4) {
|
||||
uint32_t current_num_targets_int = encode_uint32(data[3], data[2], data[1], data[0]);
|
||||
this->num_targets_sensor_->publish_state(current_num_targets_int);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -96,6 +96,7 @@ static bool validate_checksum(const uint8_t *data, size_t len, uint8_t expected_
|
||||
return calculate_checksum(data, len) == expected_checksum;
|
||||
}
|
||||
|
||||
#ifdef USE_SELECT
|
||||
static uint8_t find_nearest_index(float value, const float *arr, int size) {
|
||||
int nearest_index = 0;
|
||||
float min_diff = std::abs(value - arr[0]);
|
||||
@@ -108,6 +109,7 @@ static uint8_t find_nearest_index(float value, const float *arr, int size) {
|
||||
}
|
||||
return nearest_index;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Convert a float value to a byte array.
|
||||
@@ -250,15 +252,19 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) {
|
||||
void MR60FDA2Component::process_frame_() {
|
||||
switch (this->current_frame_type_) {
|
||||
case IS_FALL_TYPE_BUFFER:
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->fall_detected_binary_sensor_ != nullptr) {
|
||||
this->fall_detected_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
|
||||
}
|
||||
#endif
|
||||
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
|
||||
break;
|
||||
|
||||
case PEOPLE_EXIST_TYPE_BUFFER:
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->people_exist_binary_sensor_ != nullptr)
|
||||
this->people_exist_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
|
||||
#endif
|
||||
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
|
||||
break;
|
||||
|
||||
@@ -293,6 +299,7 @@ void MR60FDA2Component::process_frame_() {
|
||||
float install_height_float = 0;
|
||||
float height_threshold_float = 0;
|
||||
uint32_t current_sensitivity = 0;
|
||||
#ifdef USE_SELECT
|
||||
if (this->install_height_select_ != nullptr) {
|
||||
uint32_t current_install_height_int =
|
||||
encode_uint32(current_data_buf_[3], current_data_buf_[2], current_data_buf_[1], current_data_buf_[0]);
|
||||
@@ -318,6 +325,7 @@ void MR60FDA2Component::process_frame_() {
|
||||
uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3);
|
||||
this->sensitivity_select_->publish_state(select_index);
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float,
|
||||
height_threshold_float, current_sensitivity);
|
||||
|
||||
@@ -117,7 +117,7 @@ namespace esphome::shelly_dimmer {
|
||||
namespace {
|
||||
|
||||
int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) {
|
||||
if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end))
|
||||
if (addr < stm->dev->fl_start || addr > stm->dev->fl_end)
|
||||
return 0;
|
||||
|
||||
int page = 0;
|
||||
@@ -729,7 +729,7 @@ stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, co
|
||||
uint8_t buf[256 + 2];
|
||||
|
||||
buf[0] = aligned_len - 1;
|
||||
for (auto i = 0; i < len; i++) {
|
||||
for (unsigned int i = 0; i < len; i++) {
|
||||
cs ^= data[i];
|
||||
buf[i + 1] = data[i];
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@ struct BitWriter {
|
||||
uint32_t buffer{0};
|
||||
unsigned count{0};
|
||||
|
||||
void put(uint32_t code, unsigned size) {
|
||||
this->buffer |= code << this->count;
|
||||
this->count += size;
|
||||
void put(uint32_t value, unsigned bits) {
|
||||
this->buffer |= value << this->count;
|
||||
this->count += bits;
|
||||
while (this->count >= 8) {
|
||||
this->out.push_back(static_cast<uint8_t>(this->buffer & 0xFF));
|
||||
this->buffer >>= 8;
|
||||
|
||||
@@ -55,7 +55,7 @@ std::string sanitise_filename(const char *const name, const char *extension, boo
|
||||
break;
|
||||
}
|
||||
char c = *p;
|
||||
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
|
||||
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '_' && c != '-') {
|
||||
c = '_';
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -81,14 +81,13 @@ bool sockaddr_to_lwip(const struct sockaddr *addr, socklen_t addrlen, ip_addr_t
|
||||
|
||||
bool sockaddr_to_lwip_bind(sa_family_t family, const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip,
|
||||
uint16_t *port) {
|
||||
if (!sockaddr_to_lwip(addr, addrlen, ip, port))
|
||||
return false;
|
||||
const bool ok = sockaddr_to_lwip(addr, addrlen, ip, port);
|
||||
#if LWIP_IPV6
|
||||
// Only promote wildcard binds — a specific address must keep filtering
|
||||
if (family == AF_INET6 && ip_addr_isany_val(*ip))
|
||||
if (ok && family == AF_INET6 && ip_addr_isany_val(*ip))
|
||||
IP_SET_TYPE_VAL(*ip, IPADDR_TYPE_ANY);
|
||||
#endif
|
||||
return true;
|
||||
return ok;
|
||||
}
|
||||
|
||||
int lwip_err_to_errno(err_t err) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user