Compare commits

...
Author SHA1 Message Date
J. Nick Koston b4d016e0dd [waveshare_epaper] Set removal of the deprecated component for 2027.5.0 2026-10-13 13:42:57 -10:00
Panoplian 3cdcf0417a [lvgl] Make the list_outside_block tests work offline (#20676) 2026-10-13 13:31:08 -10:00
J. Nick Koston c6e90de262 [ci] Catch out-of-class member log tags and keep midea's in flash (#20674) 2026-10-14 12:21:24 +13:00
J. Nick Koston 92f6ab8367 [ci] Lint log tags in headers and core (#20668) 2026-10-14 12:21:24 +13:00
J. Nick Koston f61955baa2 [core] Declare the remaining log tags with ESPHOME_LOG_TAG (#20666) 2026-10-14 12:21:23 +13:00
J. Nick Koston 36e1d831d3 [core] Add a decimal fast path to parse_number<float> (#20639) 2026-10-14 12:18:35 +13:00
J. Nick Koston 63616bf642 [rtttl] Play a configured song from static storage without copying it (#19569) 2026-10-14 12:06:27 +13:00
d275926f65 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:55:22 -05:00
Andrej Friesen f265c46c21 [PCA9505] Add PCA9505/PCA9506 40-channel GPIO expander (#20667) 2026-10-13 12:50:43 -10:00
Frédéric MetrichandClaude Opus 5.5 7d735f0677 [emontx] Lock the emonTx on reboot when its firmware supports it (#20621)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:37:09 -10:00
Remco van Essen 5c23a827b6 [tmc2209] Add current and diagnostic sensors (#20638) 2026-10-13 12:35:55 -10:00
Jonathan Swoboda e265c0c0fc [core] Sync the static analysis dependency pins with the code (#20681) 2026-10-13 12:30:42 -10:00
J. Nick Koston a0c7d754cb [syslog] Keep the log level to severity table in flash (#20662) 2026-10-13 12:22:35 -10:00
Jesse Hills a6dbd72c53 Merge branch 'beta' into dev 2026-10-14 11:15:09 +13:00
Jesse Hills 9d530f5c56 Merge pull request #20680 from esphome/bump-2026.10.0b4
2026.10.0b4
2026-10-14 11:14:49 +13:00
Panoplian 6562c49686 [thermostat] Allow a dry-only thermostat without cool_action (#20682) 2026-10-13 12:08:13 -10:00
guillempages a97ce3a201 [image] Fix pillow deprecation warning (#20679) 2026-10-13 11:58:21 -10:00
Jesse Hills 5a78f636b9 Bump version to 2026.10.0b4 2026-10-14 10:43:50 +13:00
tomaszduda23andClaude 53d504137b [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 10:43:49 +13:00
bb0546fc0b [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 10:43:49 +13:00
emes30andJ. Nick Koston b43630cf8e [rp2_pio] Use pioasm 4.1.0 so PIO programs run on RP2350 (#19260)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:43:49 +13:00
Firat HajiyevandFirat17aze c9664d4e42 [wifi] Reject unsupported WPA3 on ESP8266 (#20592)
Co-authored-by: Firat17aze <187820330+Firat17aze@users.noreply.github.com>
2026-10-14 10:43:49 +13:00
Kevin Ahrendt 39ea6a3220 [audio] Bump microFLAC to v0.3.0 (#20591) 2026-10-14 10:43:49 +13:00
J. Nick Koston f4a6d6f18b [core] Create the PlatformIO HTTP cache dir before the prefetch resolve threads run (#20579) 2026-10-14 10:43:49 +13:00
esphome[bot] 6c24d4aa43 Bump aioesphomeapi from 46.9.0 to 46.10.0 (#20563) 2026-10-14 10:43:49 +13:00
J. Nick Koston 0dc1d3b5a1 [toshiba] Move the lookup tables to PROGMEM (#20636) 2026-10-13 11:25:18 -10:00
tomaszduda23andClaude 42341d05f9 [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 11:12:08 -10:00
J. Nick Koston f941fee5ed [homeassistant] Add sensor platform benchmarks (#20637) 2026-10-13 11:10:14 -10:00
Bonne Eggleston 8721976066 [epaper_spi] Abandon an update when the busy pin never clears (#20555) 2026-10-14 07:47:09 +11:00
Bonne Eggleston 05bf7d3440 [epaper_spi] Write pixel blocks straight into mono buffers (#20552) 2026-10-14 07:40:56 +11:00
918b7d2553 [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 09:31:19 -10:00
rwrozelleandClaude Opus 5.5 417ab4d2e9 [openthread] Add core_options for OpenThread compile-time options (#20595)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:00:45 -04:00
Jonathan Swoboda 0821c84b7a [core] Let a nested repeat shadow the outer iteration variable (#20634) 2026-10-13 14:35:24 -04:00
Clyde Stubbs a827058928 [lvgl] Change deprecated calls to current version (#20620) 2026-10-13 14:27:50 -04:00
J. Nick Koston 3063b76210 [haier_tundra] Build the IR frame on the stack (#20635) 2026-10-13 14:25:36 -04:00
Remco van EssenandClaude Opus 5.5 55261ae7e5 [tmc2209] Add driver diagnostics and stall detection (#20618)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:17:25 -10:00
cociwebandJ. Nick Koston 1d8423edd8 [es8388] Add initial option configuration for DAC output and ADC input mic (#16342)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:14:43 -10:00
n-IA-haneandn-IA-hane d27ec8ec49 [voice_assistant] Make TTS playback start timeout configurable (#18707)
Co-authored-by: n-IA-hane <n-IA-hane@users.noreply.github.com>
2026-10-13 08:13:26 -10:00
Ivan KruglovandJ. Nick Koston 5acbd44067 [pcf8574] Add reset option to preserve outputs across MCU reboots (#20589)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:12:28 -10:00
40271fb597 [haier_tundra] New Climate IR component (#20469)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:10:36 -10:00
95766ed3ad [icm20689] Add ICM-20689 6-axis accelerometer/gyroscope (SPI) (#19234)
Co-authored-by: Ludovic BOUÉ <938089+lboue@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:07:32 -10:00
4a80d785a8 [filter_lifetime] Add component with sensor/lambda support for is_on … (#13914)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 07:52:46 -10:00
182 changed files with 6139 additions and 294 deletions
+6
View File
@@ -212,6 +212,7 @@ esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/file/* @esphome/core
esphome/components/filter_lifetime/* @tuct
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/friedrich/* @rwrozelle
@@ -242,6 +243,9 @@ esphome/components/haier/button/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/haier_tundra/* @fauxpark
esphome/components/haier_tundra/button/* @fauxpark
esphome/components/haier_tundra/switch/* @fauxpark
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
@@ -286,6 +290,7 @@ esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/i2s_clockless_led_strip/* @j9brown
esphome/components/iaqcore/* @yozik04
esphome/components/icm20689/* @lboue
esphome/components/icnt86/* @danepowell
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
@@ -434,6 +439,7 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
+16 -1
View File
@@ -906,6 +906,21 @@ async def while_action_to_code(
return var
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
names = {name for _, name in args}
renamed: TemplateArgsType = []
for type_, name in args:
if name == "iteration":
depth = 1
while f"iteration_{depth}" in names:
depth += 1
name = f"iteration_{depth}"
names.add(name)
renamed.append((type_, name))
return renamed
@register_action(
"repeat",
RepeatAction,
@@ -929,7 +944,7 @@ async def repeat_action_to_code(
actions = await build_action_list(
config[CONF_THEN],
cg.TemplateArguments(cg.uint32, *template_arg.args),
[(cg.uint32, "iteration"), *args],
[(cg.uint32, "iteration"), *_shadow_iteration(args)],
)
for action in actions:
cg.add(var.add_then_action(action))
+2 -1
View File
@@ -1,10 +1,11 @@
#pragma once
#include <set>
#include "esphome/core/log.h"
namespace esphome::bedjet {
static const char *const TAG = "bedjet";
ESPHOME_LOG_TAG(TAG, "bedjet");
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::ble_client {
const char *const Automation::TAG = "ble_client.automation";
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
} // namespace esphome::ble_client
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bme280_base/bme280_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bme280_i2c {
static const char *const TAG = "bme280_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bme280_i2c.sensor");
class BME280I2CComponent final : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
bool read_byte(uint8_t a_register, uint8_t *data) override;
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bmp280_base/bmp280_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bmp280_i2c {
static const char *const TAG = "bmp280_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bmp280_i2c.sensor");
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
class BMP280I2CComponent final : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bmp581_i2c.sensor");
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent final : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
@@ -8,7 +8,7 @@
namespace esphome::chsc6x {
static const char *const TAG = "chsc6x.touchscreen";
ESPHOME_LOG_TAG(TAG, "chsc6x.touchscreen");
static const uint8_t CHSC6X_REG_STATUS = 0x00;
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
@@ -8,7 +8,7 @@
namespace esphome::cst816 {
static const char *const TAG = "cst816.touchscreen";
ESPHOME_LOG_TAG(TAG, "cst816.touchscreen");
static const uint8_t REG_STATUS = 0x00;
static const uint8_t REG_TOUCH_NUM = 0x02;
@@ -8,7 +8,7 @@
namespace esphome::cst9220 {
static const char *const TAG = "cst9220.touchscreen";
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
// The CST92xx family uses 16-bit (big-endian) register addresses.
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
@@ -1,4 +1,6 @@
#pragma once
#include <array>
#include <cstdint>
#include "esphome/core/color.h"
namespace esphome::display {
@@ -154,4 +156,54 @@ class ColorUtil {
return color;
}
};
/** Rec.601 luma weights (0.299/0.587/0.114), scaled by 256 */
static constexpr uint16_t REC601_LUMA_R = 77;
static constexpr uint16_t REC601_LUMA_G = 150;
static constexpr uint16_t REC601_LUMA_B = 29;
/** Rec.601 luma of an RGB color, for optimum perceptual brightness */
constexpr uint8_t rec601_luma(Color color) {
return (uint8_t) ((REC601_LUMA_R * color.r + REC601_LUMA_G * color.g + REC601_LUMA_B * color.b + 128u) >> 8);
}
/** Map an RGB color to a single monochrome bit: 1 = white, 0 = black */
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
/** color_to_mono() of an RGB565 value as ColorUtil::to_color() reads it, from table lookups instead of a division
* per channel.
*/
class Rgb565ToMono {
public:
explicit Rgb565ToMono(ColorOrder order) {
// Rec.601 weight of the 5-bit, 6-bit and 5-bit fields, by the channel each one holds in this order
uint16_t high = REC601_LUMA_R, middle = REC601_LUMA_G, low = REC601_LUMA_B;
if (order == COLOR_ORDER_BGR) {
high = REC601_LUMA_B;
low = REC601_LUMA_R;
} else if (order == COLOR_ORDER_GRB) {
high = REC601_LUMA_G;
middle = REC601_LUMA_R;
}
// Each field value scaled to 0..255, then weighted
for (unsigned i = 0; i != 32; i++) {
this->high_[i] = high * esp_scale(i, 31);
this->low_[i] = low * esp_scale(i, 31);
}
for (unsigned i = 0; i != 64; i++)
this->middle_[i] = middle * esp_scale(i, 63);
}
uint8_t operator()(uint16_t value) const {
const uint32_t weighted = this->high_[value >> 11] + this->middle_[(value >> 5) & 0x3F] + this->low_[value & 0x1F];
return ((weighted + 128u) >> 8) >= 128 ? 1 : 0;
}
protected:
// Weighted contribution of each value of each field
std::array<uint16_t, 32> high_{};
std::array<uint16_t, 64> middle_{};
std::array<uint16_t, 32> low_{};
};
} // namespace esphome::display
+15 -3
View File
@@ -23,11 +23,18 @@ DEPENDENCIES = ["uart"]
emontx_ns = cg.esphome_ns.namespace("emontx")
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
LockOnReboot = emontx_ns.enum("LockOnReboot", is_class=True)
CONF_EMONTX_ID = "emontx_id"
CONF_TAG_NAME = "tag_name"
CONF_ON_JSON = "on_json"
CONF_LOCK_ON_REBOOT = "lock_on_reboot"
LOCK_ON_REBOOT_AUTO = "auto"
LOCK_ON_REBOOT_MODES = {
LOCK_ON_REBOOT_AUTO: LockOnReboot.LOCK_ON_REBOOT_AUTO,
True: LockOnReboot.LOCK_ON_REBOOT_ALWAYS,
False: LockOnReboot.LOCK_ON_REBOOT_NEVER,
}
DOMAIN = "emontx"
@@ -59,7 +66,9 @@ CONFIG_SCHEMA = (
cv.GenerateID(): cv.declare_id(EmonTx),
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
cv.Optional(CONF_LOCK_ON_REBOOT, default=False): cv.boolean,
cv.Optional(CONF_LOCK_ON_REBOOT, default=LOCK_ON_REBOOT_AUTO): cv.Any(
cv.boolean, cv.one_of(LOCK_ON_REBOOT_AUTO, lower=True)
),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -135,8 +144,11 @@ async def to_code(config: ConfigType) -> None:
await uart.register_uart_device(var, config)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if config[CONF_LOCK_ON_REBOOT]:
cg.add(var.set_lock_on_reboot(True))
lock = config[CONF_LOCK_ON_REBOOT]
cg.add(var.set_lock_on_reboot(LOCK_ON_REBOOT_MODES[lock]))
if lock == LOCK_ON_REBOOT_AUTO:
# Locking depends on the firmware version the emonTx reports
cg.add_define("USE_EMONTX_VERSION")
# Action: emontx.send_command
+129 -27
View File
@@ -7,30 +7,85 @@ namespace esphome::emontx {
ESPHOME_LOG_TAG(TAG, "emontx");
static constexpr uint32_t UNLOCK_ID = 0;
// The emonTx joins bytes received while a command is pending into that command, so the next one waits
static constexpr uint32_t COMMAND_GAP_MS = 250;
#ifdef USE_EMONTX_VERSION
static constexpr uint32_t VERSION_REQUEST_ID = 0;
// The emonTx joins bytes received while a command is pending into that command, so 'v' waits
static constexpr uint32_t VERSION_REQUEST_DELAY_MS = 250;
static constexpr uint32_t VERSION_REQUEST_ID = 1;
// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
// Every data line starts with the message counter, in JSON or text format
static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
static constexpr uint32_t LOCK_MIN_MAJOR = 1;
static constexpr uint32_t LOCK_MIN_MINOR = 2;
// True if line starts with the PROGMEM prefix and has more after it
static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
}
// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
static bool read_number(const char *&pos, const char *end, uint32_t &value) {
const char *start = pos;
value = 0;
while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
value = value * 10 + (*pos++ - '0');
return pos != start;
}
// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
static bool supports_lock(const char *version, const char *end) {
if (version < end && (*version == 'v' || *version == 'V'))
version++;
uint32_t major;
if (!read_number(version, end, major) || version == end || *version != '.')
return false;
version++;
uint32_t minor;
if (!read_number(version, end, minor))
return false;
return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
}
#endif
void EmonTx::setup() {
this->buffer_pos_ = 0;
if (this->lock_on_reboot_) {
// The line feed ends any boot noise first
ESP_LOGI(TAG, "Unlocking emonTx after boot");
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
// The line feed ends any boot noise
this->write_byte('\n');
this->send_command("emonunlock");
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
this->unlock_();
#ifdef USE_EMONTX_VERSION
if (this->wants_version_())
this->request_version_();
#endif
});
return;
}
#ifdef USE_EMONTX_VERSION
if (this->version_text_sensor_ != nullptr)
// With AUTO, the reply decides whether to unlock
if (this->wants_version_())
this->request_version_();
#endif
}
void EmonTx::unlock_() {
ESP_LOGI(TAG, "Unlocking emonTx");
this->send_command("emonunlock");
}
void EmonTx::on_shutdown() {
if (!this->lock_on_reboot_)
bool lock = this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS;
#ifdef USE_EMONTX_VERSION
lock |= this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && this->lock_supported_;
#endif
if (!lock)
return;
if (this->is_paused()) {
ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
@@ -63,7 +118,7 @@ void EmonTx::loop() {
this->discarding_line_ = dropped != '\n';
#ifdef USE_EMONTX_VERSION
// The emonTx may have been flashed, or a request dropped, during the pause
if (this->version_text_sensor_ != nullptr)
if (this->wants_version_())
this->request_version_();
#endif
}
@@ -96,8 +151,10 @@ void EmonTx::loop() {
this->parse_json_(this->buffer_.data(), len);
}
#ifdef USE_EMONTX_VERSION
if (this->version_text_sensor_ != nullptr)
if (this->wants_version_()) {
this->parse_msg_number_(this->buffer_.data(), len);
this->parse_version_line_(this->buffer_.data(), len);
}
#endif
}
} else if (received == '\r' || this->discarding_line_) {
@@ -124,16 +181,6 @@ void EmonTx::parse_json_(const char *data, size_t len) {
}
}
#endif
#ifdef USE_EMONTX_VERSION
auto msg = root["MSG"];
if (this->version_text_sensor_ != nullptr && msg.is<uint32_t>()) {
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
uint32_t msg_number = msg;
if (msg_number <= this->last_msg_number_)
this->request_version_();
this->last_msg_number_ = msg_number;
}
#endif
this->json_callbacks_.call(root, StringRef(data, len));
return true;
@@ -145,17 +192,50 @@ void EmonTx::parse_json_(const char *data, size_t len) {
}
#ifdef USE_EMONTX_VERSION
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
void EmonTx::request_version_() {
// The firmware may have changed; until the reply, an unknown version gets no lock commands
this->lock_supported_ = false;
this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
this->send_version_request_();
}
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
void EmonTx::send_version_request_() {
if (this->is_paused())
return;
this->write_byte('\n');
this->set_timeout(VERSION_REQUEST_ID, VERSION_REQUEST_DELAY_MS, [this]() { this->send_command("v"); });
this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
}
void EmonTx::parse_msg_number_(const char *line, size_t len) {
const char *pos;
if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
pos = line + sizeof(MSG_JSON_PREFIX) - 1;
} else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
} else {
return;
}
uint32_t msg_number;
if (!read_number(pos, line + len, msg_number))
return;
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
if (msg_number <= this->last_msg_number_) {
this->request_version_();
} else if (this->version_frames_left_ > 0) {
if (--this->version_frames_left_ == 0) {
ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
} else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
this->send_version_request_();
}
}
this->last_msg_number_ = msg_number;
}
void EmonTx::parse_version_line_(const char *line, size_t len) {
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
if (len <= prefix_len || ESPHOME_strncmp_P(line, VERSION_PREFIX, prefix_len) != 0)
if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
return;
const char *version = line + prefix_len;
const char *end = line + len;
@@ -164,7 +244,23 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
if (version == end)
return;
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
this->version_text_sensor_->publish_state(version, end - version);
this->version_frames_left_ = 0;
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr)
this->version_text_sensor_->publish_state(version, end - version);
#endif
bool supported = supports_lock(version, end);
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
// Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
if (this->is_paused())
return;
this->unlock_();
this->unlock_sent_ = true;
});
}
this->lock_supported_ = supported;
}
#endif
@@ -173,7 +269,13 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
*/
void EmonTx::dump_config() {
ESP_LOGCONFIG(TAG, "EmonTx:");
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", YESNO(this->lock_on_reboot_));
const char *lock = LOG_STR_LITERAL("no");
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
lock = LOG_STR_LITERAL("yes");
} else if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO) {
lock = LOG_STR_LITERAL("auto");
}
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
#ifdef USE_SENSOR
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
@@ -183,7 +285,7 @@ void EmonTx::dump_config() {
#else
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
#endif
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
#endif
}
+24 -5
View File
@@ -13,7 +13,7 @@
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
@@ -22,6 +22,12 @@ namespace esphome::emontx {
/// Maximum line length in bytes (plus one byte reserved for null terminator)
static constexpr size_t MAX_LINE_LENGTH = 1024;
enum class LockOnReboot : uint8_t {
LOCK_ON_REBOOT_NEVER,
LOCK_ON_REBOOT_ALWAYS,
LOCK_ON_REBOOT_AUTO, // Only when the emonTx reports firmware v1.2.0 or later
};
/**
* @class EmonTx
* @brief Main class for the EmonTx component.
@@ -30,7 +36,7 @@ static constexpr size_t MAX_LINE_LENGTH = 1024;
* extracts tags and values, and publishes them to registered sensors.
*/
class EmonTx final : public Component, public uart::UARTDevice {
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
#endif
public:
@@ -56,8 +62,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
/// Lock the emonTx ('emonlock') before a reboot, so serial garbage during the reboot is not taken
/// as a command, and unlock it in setup(). Needs emonTx firmware v1.2.0 or later.
void set_lock_on_reboot(bool lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
/// as a command, and unlock it after boot. Needs emonTx firmware v1.2.0 or later, which AUTO checks.
void set_lock_on_reboot(LockOnReboot lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
#ifdef USE_SENSOR
void init_sensors(size_t count) { this->sensors_.init(count); }
@@ -66,8 +72,18 @@ class EmonTx final : public Component, public uart::UARTDevice {
protected:
void parse_json_(const char *data, size_t len);
void unlock_();
#ifdef USE_EMONTX_VERSION
bool wants_version_() const {
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr)
return true;
#endif
return this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO;
}
void request_version_();
void send_version_request_();
void parse_msg_number_(const char *line, size_t len);
void parse_version_line_(const char *line, size_t len);
#endif
@@ -79,9 +95,12 @@ class EmonTx final : public Component, public uart::UARTDevice {
uint16_t buffer_pos_{0};
std::atomic<bool> paused_{false};
bool was_paused_{false};
bool lock_on_reboot_{false};
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
#ifdef USE_EMONTX_VERSION
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
uint32_t last_msg_number_{0};
#endif
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
@@ -21,3 +21,4 @@ async def to_code(config: ConfigType) -> None:
text_sensors = text_sensor.sub_text_sensors(config)
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
cg.add_define("USE_EMONTX_VERSION")
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
+7 -12
View File
@@ -2,6 +2,7 @@
#include <cstdint>
#include <algorithm>
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/color.h"
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
@@ -13,21 +14,15 @@
namespace esphome::epaper_spi {
// The Rec.601 luma helpers and the RGB565 mono lookup live in the shared display utilities, so other
// drivers (e.g. it8951 in its 1 bit per pixel mode) can use them too.
using display::color_to_mono;
using display::rec601_luma;
using display::Rgb565ToMono;
/** Delta for when to regard as gray */
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
/** Rec.601 luma (0.299/0.587/0.114 weights, scaled by 256) for optimum perceptual brightness */
constexpr uint8_t rec601_luma(Color color) {
return (uint8_t) ((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
}
/** Map RGB color to a single monochrome bit
*
* @param color RGB color to convert from
* @return 1 = white, 0 = black
*/
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
/** Map RGB color to one of 4 discrete gray levels (2 bits per pixel)
*
* @param color RGB color to convert from
+133 -2
View File
@@ -131,9 +131,7 @@ void EPaperBase::update() {
}
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
this->waiting_for_idle_start_ = millis();
#endif
this->waiting_for_idle_ = should_wait;
}
@@ -156,6 +154,10 @@ void EPaperBase::loop() {
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
#endif
} else {
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
this->abandon_update_();
return;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
@@ -168,6 +170,20 @@ void EPaperBase::loop() {
this->process_state_();
}
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
void EPaperBase::abandon_update_() {
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
this->epaper_state_to_string_());
this->status_set_warning(LOG_STR("Busy pin timeout"));
if (this->state_ == EPaperState::TRANSFER_DATA)
this->abort_transfer();
// Leave the panel powered down rather than with its high-voltage drivers enabled
this->power_off();
this->full_update_requested_ = true;
this->current_data_index_ = 0;
this->set_state_(EPaperState::IDLE);
}
/**
* Process the state machine.
* Typical state sequence:
@@ -242,6 +258,7 @@ void EPaperBase::process_state_() {
break;
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
this->status_clear_warning();
this->set_state_(EPaperState::IDLE);
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
break;
@@ -345,6 +362,120 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
}
}
namespace {
// One source row of an RGB565 block, written into a contiguous 1 bit per pixel buffer. Without SwapAxes the row lies
// along a buffer row and the bit moves by step; with it the row lies along a bit column and the byte moves by step
// rows.
template<bool SwapAxes, bool BigEndianSource>
void HOT write_rgb565_row(uint8_t *byte, uint8_t mask, int step, size_t row_width, const uint8_t *source, int count,
const Rgb565ToMono &to_mono) {
const ptrdiff_t byte_step = SwapAxes ? step * static_cast<ptrdiff_t>(row_width) : 0;
for (; count != 0; count--, source += 2) {
const uint16_t value = BigEndianSource ? (source[0] << 8) | source[1] : source[0] | (source[1] << 8);
if (to_mono(value)) {
*byte |= mask;
} else {
*byte &= ~mask;
}
if constexpr (SwapAxes) {
byte += byte_step;
} else if (step > 0) {
mask >>= 1;
if (mask == 0) {
mask = 0x80;
byte++;
}
} else {
mask <<= 1;
if (mask == 0) {
mask = 0x01;
byte--;
}
}
}
}
} // namespace
// Each source column maps to one buffer coordinate and each source row to the other, each moving by 1 per pixel, so
// the visible part of a block and its dirty area are worked out once for the whole block.
void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, 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 bool swap = this->effective_transform_ & SWAP_XY;
const bool mirror_x = this->effective_transform_ & MIRROR_X;
const bool mirror_y = this->effective_transform_ & MIRROR_Y;
// The buffer axis a source column moves along, and the one a source row moves along
const int column_limit = swap ? this->height_ : this->width_;
const int row_limit = swap ? this->width_ : this->height_;
const bool column_mirrored = swap ? mirror_y : mirror_x;
const bool row_mirrored = swap ? mirror_x : mirror_y;
const int first_column = std::max(0, -x_start);
const int end_column = std::min(w, column_limit - x_start);
const int first_row = std::max(0, -y_start);
const int end_row = std::min(h, row_limit - y_start);
if (first_column >= end_column || first_row >= end_row)
return;
auto column_to_buffer = [&](int column) {
const int u = x_start + column;
return column_mirrored ? column_limit - 1 - u : u;
};
auto row_to_buffer = [&](int row) {
const int v = y_start + row;
return row_mirrored ? row_limit - 1 - v : v;
};
const int column_low = std::min(column_to_buffer(first_column), column_to_buffer(end_column - 1));
const int row_low = std::min(row_to_buffer(first_row), row_to_buffer(end_row - 1));
const int columns = end_column - first_column;
const int rows = end_row - first_row;
const int x_low = swap ? row_low : column_low;
const int y_low = swap ? column_low : row_low;
const int x_high = x_low + (swap ? rows : columns);
const int y_high = y_low + (swap ? columns : rows);
const Rgb565ToMono to_mono(order);
const size_t line_stride = x_offset + w + x_pad;
const size_t row_width = this->row_width_;
const int step = column_mirrored ? -1 : 1;
for (int row = first_row; row != end_row; row++) {
const uint8_t *source = ptr + ((y_offset + row) * line_stride + x_offset + first_column) * 2;
const int along = column_to_buffer(first_column);
const int across = row_to_buffer(row);
const int x = swap ? across : along;
const int y = swap ? along : across;
uint8_t *byte = buffer + y * row_width + x / 8;
const uint8_t mask = 0x80u >> (x % 8);
if (swap) {
if (big_endian) {
write_rgb565_row<true, true>(byte, mask, step, row_width, source, columns, to_mono);
} else {
write_rgb565_row<true, false>(byte, mask, step, row_width, source, columns, to_mono);
}
} else if (big_endian) {
write_rgb565_row<false, true>(byte, mask, step, row_width, source, columns, to_mono);
} else {
write_rgb565_row<false, false>(byte, mask, step, row_width, source, columns, to_mono);
}
}
this->x_low_ = clamp_at_most(this->x_low_, x_low);
this->x_high_ = clamp_at_least(this->x_high_, x_high);
this->y_low_ = clamp_at_most(this->y_low_, y_low);
this->y_high_ = clamp_at_least(this->y_high_, y_high);
}
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
// usual case): the same result as draw_pixel_at() for each pixel. Anything else goes through draw_pixel_at().
void HOT EPaperBase::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 contiguous_length;
uint8_t *contiguous = this->buffer_.get_span(0, contiguous_length);
if (!this->buffer_is_mono() || this->get_clipping().is_set() || bitness != COLOR_BITNESS_565 ||
contiguous_length != this->buffer_length_) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
this->draw_rgb565_block_(contiguous, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset, x_pad);
}
void EPaperBase::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG,
+13 -2
View File
@@ -107,6 +107,8 @@ 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_; }
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;
// Make the next update a full one. Applied when that update starts, so one in progress is not affected.
void request_full_update() { this->full_update_requested_ = true; }
// True from the start of an update until the display has refreshed and gone back to sleep.
@@ -125,9 +127,14 @@ class EPaperBase : public Display,
virtual bool initialise(bool partial);
void send_init_sequence_(const uint8_t *sequence, size_t length);
void wait_for_idle_(bool should_wait);
void abandon_update_();
bool init_buffer_(size_t buffer_length);
void update_effective_transform_();
bool rotate_coordinates_(int &x, int &y);
void draw_rgb565_block_(uint8_t *buffer, 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);
// Whether the buffer holds 1 bit per pixel as draw_pixel_at() writes it, so draw_pixels_at() can write it directly
virtual bool buffer_is_mono() const { return false; }
/**
* Methods that must be implemented by concrete classes to control the display
@@ -137,6 +144,8 @@ class EPaperBase : public Display,
* @return true if done, false if it should be called next loop
*/
virtual bool transfer_data() = 0;
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
virtual void abort_transfer() {}
/**
* Refresh the screen after data transfer
*/
@@ -187,18 +196,20 @@ class EPaperBase : public Display,
// point to the pixel past the last one updated, i.e. may range up to width/height.
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
uint32_t waiting_for_idle_start_{};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t waiting_for_idle_last_print_{};
uint32_t waiting_for_idle_start_{};
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
uint32_t update_start_time_{};
#endif
// properties with specific initialisers go last
EPaperState state_{EPaperState::IDLE};
uint32_t reset_duration_{10};
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
uint16_t busy_timeout_seconds_{120};
uint8_t full_update_every_{1};
EPaperState state_{EPaperState::IDLE};
};
} // namespace esphome::epaper_spi
@@ -15,6 +15,7 @@ class EPaperMono : public EPaperBase {
}
protected:
bool buffer_is_mono() const override { return true; }
void refresh_screen(bool partial) override;
void power_on() override {}
void power_off() override{};
@@ -58,7 +58,9 @@ bool HOT EPaperSSD1677::transfer_data() {
// A full update ignores 0x26, and the copy may not hold a real frame yet (first update after
// boot): send the new frame to both planes.
const bool send_copy = this->plane_ == 0 && this->update_count_ != 0;
const bool direct = send_copy || this->buffer_is_plane();
// A mono buffer already holds the frame as a RAM plane does, so it can be sent as it is; a deeper
// buffer (the four-level model) returns false and packs the plane in plane_row().
const bool direct = send_copy || this->buffer_is_mono();
const auto &source = send_copy ? this->sent_ : this->buffer_;
const size_t row_length = this->plane_row_length_();
const size_t plane_length = row_length * this->height_;
@@ -34,9 +34,6 @@ class EPaperSSD1677 : public EPaperMono {
void init_comparison_frame_();
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
// Whether the buffer already holds the frame as a RAM plane does, so it can be sent as it is.
// A subclass with a deeper buffer returns false and overrides plane_row().
virtual bool buffer_is_plane() const { return true; }
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
virtual void plane_row(size_t y, uint8_t *out);
bool reset() override;
@@ -38,9 +38,9 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 {
void draw_pixel_at(int x, int y, Color color) override;
protected:
bool buffer_is_mono() const override { return false; }
// A partial update if partial updates are enabled and this is not a full one.
bool is_partial_push_() const { return this->update_count_ != 0 && this->sent_.is_valid(); }
bool buffer_is_plane() const override { return false; }
void plane_row(size_t y, uint8_t *out) override;
void refresh_screen(bool partial) override;
bool transfer_data() override;
@@ -364,6 +364,15 @@ bool HOT EPaperT133A01::transfer_data() {
return true;
}
void EPaperT133A01::abort_transfer() {
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
if (this->current_data_index_ == 0)
return;
this->disable();
this->cs_pin_->digital_write(true);
this->cs1_pin_->digital_write(true);
}
void EPaperT133A01::dump_config() {
EPaperBase::dump_config();
LOG_PIN(" CS Pin: ", this->cs_pin_);
@@ -46,6 +46,7 @@ class EPaperT133A01 : public EPaperBase {
void deep_sleep() override;
bool transfer_data() override;
void abort_transfer() override;
/**
* Send a command (and optional data) selecting one or both controllers.
@@ -37,6 +37,7 @@ class EPaperUC8179 final : public EPaperBase {
}
protected:
bool buffer_is_mono() const override { return true; }
bool initialise(bool partial) override;
bool transfer_data() override;
void refresh_screen(bool partial) override;
+26 -14
View File
@@ -111,24 +111,36 @@ void ES8388::setup() {
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0x09));
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
if (this->dac_output_select_ != nullptr) {
auto dac_power = this->get_dac_power();
if (dac_power.has_value()) {
if (this->dac_output_select_->has_index(dac_power.value())) {
this->dac_output_select_->publish_state(dac_power.value());
} else {
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
if (this->dac_output_initial_.has_value()) {
const DacOutputLine line = this->dac_output_initial_.value();
ES8388_ERROR_FAILED(this->set_dac_output(line));
this->dac_output_select_->publish_state(static_cast<size_t>(line));
} else {
auto dac_power = this->get_dac_power();
if (dac_power.has_value()) {
if (this->dac_output_select_->has_index(dac_power.value())) {
this->dac_output_select_->publish_state(dac_power.value());
} else {
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
}
}
}
}
if (this->adc_input_mic_select_ != nullptr) {
auto mic_input = this->get_mic_input();
if (mic_input.has_value()) {
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
this->adc_input_mic_select_->publish_state(mic_input.value());
} else {
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
if (this->adc_input_mic_initial_.has_value()) {
const AdcInputMicLine line = this->adc_input_mic_initial_.value();
ES8388_ERROR_FAILED(this->set_adc_input_mic(line));
this->adc_input_mic_select_->publish_state(static_cast<size_t>(line));
} else {
auto mic_input = this->get_mic_input();
if (mic_input.has_value()) {
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
this->adc_input_mic_select_->publish_state(mic_input.value());
} else {
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
}
}
}
}
@@ -138,7 +150,7 @@ void ES8388::setup() {
void ES8388::dump_config() {
ESP_LOGCONFIG(TAG, "ES8388 Audio Codec:");
LOG_I2C_DEVICE(this);
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
LOG_SELECT(" ", "DacOutputSelect", this->dac_output_select_);
LOG_SELECT(" ", "ADCInputMicSelect", this->adc_input_mic_select_);
#endif
+13 -2
View File
@@ -5,7 +5,7 @@
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
#include "esphome/components/select/select.h"
#endif
@@ -26,7 +26,7 @@ enum AdcInputMicLine : uint8_t {
};
class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
SUB_SELECT(dac_output)
SUB_SELECT(adc_input_mic)
#endif
@@ -68,11 +68,22 @@ class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I
bool set_dac_output(DacOutputLine line);
bool set_adc_input_mic(AdcInputMicLine line);
#ifdef USE_ES8388_SELECT
/// Line to apply in setup() instead of keeping what the codec reports.
void set_dac_output_initial(DacOutputLine line) { this->dac_output_initial_ = line; }
void set_adc_input_mic_initial(AdcInputMicLine line) { this->adc_input_mic_initial_ = line; }
#endif
protected:
/// @brief Mutes or unmutes the DAC audio out
/// @param mute_state True to mute, false to unmute
/// @return True if successful and false otherwise
bool set_mute_state_(bool mute_state);
#ifdef USE_ES8388_SELECT
optional<DacOutputLine> dac_output_initial_{};
optional<AdcInputMicLine> adc_input_mic_initial_{};
#endif
};
} // namespace esphome::es8388
+43 -3
View File
@@ -1,7 +1,11 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_CHIP # noqa: F401
from esphome.const import (
CONF_INITIAL_OPTION,
ENTITY_CATEGORY_CONFIG,
ICON_CHIP, # noqa: F401
)
from esphome.types import ConfigType
from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
@@ -9,6 +13,21 @@ from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
CONF_DAC_OUTPUT = "dac_output"
CONF_ADC_INPUT_MIC = "adc_input_mic"
DacOutputLine = es8388_ns.enum("DacOutputLine")
DAC_OUTPUT_OPTIONS = {
"LINE1": DacOutputLine.DAC_OUTPUT_LINE1,
"LINE2": DacOutputLine.DAC_OUTPUT_LINE2,
"BOTH": DacOutputLine.DAC_OUTPUT_BOTH,
}
DAC_OUTPUT_OPTION_NAMES = list(DAC_OUTPUT_OPTIONS)
AdcInputMicLine = es8388_ns.enum("AdcInputMicLine")
ADC_INPUT_MIC_OPTIONS = {
"LINE1": AdcInputMicLine.ADC_INPUT_MIC_LINE1,
"LINE2": AdcInputMicLine.ADC_INPUT_MIC_LINE2,
"DIFFERENCE": AdcInputMicLine.ADC_INPUT_MIC_DIFFERENCE,
}
ADC_INPUT_MIC_OPTION_NAMES = list(ADC_INPUT_MIC_OPTIONS)
DacOutputSelect = es8388_ns.class_("DacOutputSelect", select.Select)
ADCInputMicSelect = es8388_ns.class_("ADCInputMicSelect", select.Select)
@@ -19,24 +38,45 @@ CONFIG_SCHEMA = cv.All(
DacOutputSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
{
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
DAC_OUTPUT_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_ADC_INPUT_MIC): select.select_schema(
ADCInputMicSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
{
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
ADC_INPUT_MIC_OPTIONS, upper=True
),
}
),
}
)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ES8388_SELECT")
hub = await cg.get_variable(config[CONF_ES8388_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=["LINE1", "LINE2", "BOTH"]
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=DAC_OUTPUT_OPTION_NAMES
)
await selects(
CONF_ADC_INPUT_MIC,
hub.set_adc_input_mic_select,
options=["LINE1", "LINE2", "DIFFERENCE"],
options=ADC_INPUT_MIC_OPTION_NAMES,
)
if (
initial := config.get(CONF_DAC_OUTPUT, {}).get(CONF_INITIAL_OPTION)
) is not None:
cg.add(hub.set_dac_output_initial(initial))
if (
initial := config.get(CONF_ADC_INPUT_MIC, {}).get(CONF_INITIAL_OPTION)
) is not None:
cg.add(hub.set_adc_input_mic_initial(initial))
+2 -1
View File
@@ -3,11 +3,12 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
#include <deque>
namespace esphome::ezo {
static const char *const TAG = "ezo.sensor";
ESPHOME_LOG_TAG(TAG, "ezo.sensor");
enum EzoCommandType : uint8_t {
EZO_READ = 0,
+3 -1
View File
@@ -330,7 +330,9 @@ async def write_image(
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
pixels = encoder.convert(
image.resize((width, height)), path
).get_flattened_data()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
@@ -0,0 +1,2 @@
CODEOWNERS = ["@tuct"]
DOMAIN = "filter_lifetime"
@@ -0,0 +1,112 @@
#include "filter_lifetime.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
namespace esphome::filter_lifetime {
ESPHOME_LOG_TAG(TAG, "filter_lifetime");
static constexpr uint32_t SECONDS_PER_HOUR = 3600;
static constexpr float MINUTES_PER_DAY = 24.0f * 60.0f;
void FilterLifetime::setup() {
this->pref_ = this->make_entity_preference<uint32_t>();
this->pref_.load(&this->runtime_seconds_);
this->last_update_ = millis();
this->publish_values_();
}
void FilterLifetime::update() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t elapsed_ms = now - this->last_update_;
this->last_update_ = now;
if (this->get_is_on_()) {
float speed = this->get_current_speed_();
if (std::isnan(speed))
speed = 0.0f;
speed = std::clamp(speed, 0.0f, 100.0f);
const uint32_t prev_hours = this->runtime_seconds_ / SECONDS_PER_HOUR;
this->carry_ms_ += static_cast<uint32_t>(elapsed_ms * (speed / 100.0f));
this->runtime_seconds_ += this->carry_ms_ / 1000;
this->carry_ms_ %= 1000;
// Save once per hour of runtime (and in on_shutdown): every save erases a flash sector on ESP8266.
if (this->runtime_seconds_ / SECONDS_PER_HOUR != prev_hours)
this->pref_.save(&this->runtime_seconds_);
ESP_LOGV(TAG, "Elapsed %" PRIu32 " ms at %.0f%%, runtime %" PRIu32 " s", elapsed_ms, speed, this->runtime_seconds_);
}
this->publish_values_();
}
void FilterLifetime::publish_values_() {
const float runtime_minutes = this->runtime_seconds_ / 60.0f;
const float max_minutes = this->max_lifetime_minutes_;
const float remaining_minutes = std::max(0.0f, max_minutes - runtime_minutes);
this->publish_state(100.0f * remaining_minutes / max_minutes);
if (this->runtime_hours_sensor_ != nullptr)
this->runtime_hours_sensor_->publish_state(runtime_minutes / 60.0f);
if (this->remaining_days_sensor_ != nullptr)
this->remaining_days_sensor_->publish_state(remaining_minutes / MINUTES_PER_DAY);
}
void FilterLifetime::on_shutdown() { this->pref_.save(&this->runtime_seconds_); }
void FilterLifetime::reset_filter() {
this->runtime_seconds_ = 0;
this->carry_ms_ = 0;
this->pref_.save(&this->runtime_seconds_);
this->publish_values_();
ESP_LOGI(TAG, "Filter lifetime reset");
}
bool FilterLifetime::get_is_on_() {
#ifdef USE_BINARY_SENSOR
// Reads false until the sensor first publishes, so nothing accumulates before then.
if (this->is_on_sensor_ != nullptr)
return this->is_on_sensor_->state;
#endif
if (this->is_on_lambda_ != nullptr)
return this->is_on_lambda_();
return true;
}
float FilterLifetime::get_current_speed_() {
if (this->current_speed_sensor_ != nullptr)
return this->current_speed_sensor_->state; // NaN before the first publish, treated as 0
if (this->current_speed_lambda_ != nullptr)
return this->current_speed_lambda_();
return 100.0f;
}
void FilterLifetime::dump_config() {
LOG_SENSOR("", "Filter Lifetime", this);
ESP_LOGCONFIG(TAG,
" Max lifetime: %" PRIu32 " min (%.1f days)\n"
" Runtime: %.1f h",
this->max_lifetime_minutes_, this->max_lifetime_minutes_ / MINUTES_PER_DAY,
this->runtime_seconds_ / 3600.0f);
#ifdef USE_BINARY_SENSOR
if (this->is_on_sensor_ != nullptr) {
LOG_BINARY_SENSOR(" ", "Is-on sensor", this->is_on_sensor_);
} else
#endif
{
ESP_LOGCONFIG(TAG, " Is-on: %s",
this->is_on_lambda_ != nullptr ? LOG_STR_LITERAL("lambda") : LOG_STR_LITERAL("always"));
}
if (this->current_speed_sensor_ != nullptr) {
LOG_SENSOR(" ", "Speed sensor", this->current_speed_sensor_);
} else {
ESP_LOGCONFIG(TAG, " Speed: %s",
this->current_speed_lambda_ != nullptr ? LOG_STR_LITERAL("lambda") : LOG_STR_LITERAL("100%"));
}
LOG_SENSOR(" ", "Runtime hours", this->runtime_hours_sensor_);
LOG_SENSOR(" ", "Remaining days", this->remaining_days_sensor_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::filter_lifetime
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
namespace esphome::filter_lifetime {
class FilterLifetime : public sensor::Sensor, public PollingComponent {
public:
void setup() override;
void update() override;
void dump_config() override;
void on_shutdown() override;
void reset_filter();
void set_max_lifetime(uint32_t minutes) { this->max_lifetime_minutes_ = minutes; }
void set_is_on_lambda(bool (*is_on)()) { this->is_on_lambda_ = is_on; }
#ifdef USE_BINARY_SENSOR
void set_is_on_sensor(binary_sensor::BinarySensor *sensor) { this->is_on_sensor_ = sensor; }
#endif
void set_current_speed_lambda(float (*speed)()) { this->current_speed_lambda_ = speed; }
void set_current_speed_sensor(sensor::Sensor *sensor) { this->current_speed_sensor_ = sensor; }
void set_runtime_hours_sensor(sensor::Sensor *sensor) { this->runtime_hours_sensor_ = sensor; }
void set_remaining_days_sensor(sensor::Sensor *sensor) { this->remaining_days_sensor_ = sensor; }
protected:
bool get_is_on_();
float get_current_speed_();
void publish_values_();
bool (*is_on_lambda_)(){nullptr};
float (*current_speed_lambda_)(){nullptr};
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *is_on_sensor_{nullptr};
#endif
sensor::Sensor *current_speed_sensor_{nullptr};
sensor::Sensor *runtime_hours_sensor_{nullptr};
sensor::Sensor *remaining_days_sensor_{nullptr};
ESPPreferenceObject pref_;
uint32_t max_lifetime_minutes_{0};
uint32_t last_update_{0};
uint32_t runtime_seconds_{0}; // Speed-weighted running time, persisted
uint32_t carry_ms_{0}; // Weighted milliseconds not yet counted as a second
};
} // namespace esphome::filter_lifetime
@@ -0,0 +1,94 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor, sensor
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_DURATION,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_HOUR,
UNIT_PERCENT,
)
from esphome.types import ConfigType
CONF_MAX_LIFETIME = "max_lifetime"
CONF_IS_ON = "is_on"
CONF_IS_ON_SENSOR = "is_on_sensor"
CONF_CURRENT_SPEED = "current_speed"
CONF_CURRENT_SPEED_SENSOR = "current_speed_sensor"
CONF_RUNTIME_HOURS = "runtime_hours"
CONF_REMAINING_DAYS = "remaining_days"
UNIT_DAY = "d"
filter_lifetime_ns = cg.esphome_ns.namespace("filter_lifetime")
FilterLifetime = filter_lifetime_ns.class_(
"FilterLifetime", sensor.Sensor, cg.PollingComponent
)
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
FilterLifetime,
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=2,
icon="mdi:air-filter",
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Required(CONF_MAX_LIFETIME): cv.All(
cv.positive_time_period_minutes,
cv.Range(min=cv.TimePeriod(minutes=1)),
),
cv.Optional(CONF_IS_ON): cv.returning_lambda,
cv.Optional(CONF_IS_ON_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_CURRENT_SPEED): cv.returning_lambda,
cv.Optional(CONF_CURRENT_SPEED_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_RUNTIME_HOURS): sensor.sensor_schema(
unit_of_measurement=UNIT_HOUR,
accuracy_decimals=1,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_TOTAL_INCREASING,
icon="mdi:timer-outline",
),
cv.Optional(CONF_REMAINING_DAYS): sensor.sensor_schema(
unit_of_measurement=UNIT_DAY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
icon="mdi:calendar-clock",
),
}
)
.extend(cv.polling_component_schema("60s")),
cv.has_at_most_one_key(CONF_IS_ON, CONF_IS_ON_SENSOR),
cv.has_at_most_one_key(CONF_CURRENT_SPEED, CONF_CURRENT_SPEED_SENSOR),
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_max_lifetime(config[CONF_MAX_LIFETIME]))
if (is_on := config.get(CONF_IS_ON)) is not None:
cg.add(var.set_is_on_lambda(await cg.templatable(is_on, [], bool)))
elif (is_on_sensor := config.get(CONF_IS_ON_SENSOR)) is not None:
cg.add(var.set_is_on_sensor(await cg.get_variable(is_on_sensor)))
if (current_speed := config.get(CONF_CURRENT_SPEED)) is not None:
cg.add(
var.set_current_speed_lambda(await cg.templatable(current_speed, [], float))
)
elif (speed_sensor := config.get(CONF_CURRENT_SPEED_SENSOR)) is not None:
cg.add(var.set_current_speed_sensor(await cg.get_variable(speed_sensor)))
sensors = sensor.sub_sensors(config)
await sensors(CONF_RUNTIME_HOURS, var.set_runtime_hours_sensor)
await sensors(CONF_REMAINING_DAYS, var.set_remaining_days_sensor)
automation.register_apply_action(
"filter_lifetime.reset_filter",
cv.Schema({cv.GenerateID(): cv.use_id(FilterLifetime)}),
automation.ApplyCall("reset_filter()"),
)
@@ -0,0 +1,2 @@
DOMAIN = "haier_tundra"
CODEOWNERS = ["@fauxpark"]
@@ -0,0 +1,43 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import CONF_LIGHT
from esphome.types import ConfigType
from ..climate import HaierTundra, haier_tundra_ns
CODEOWNERS = ["@fauxpark"]
HaierTundraButton = haier_tundra_ns.class_(
"HaierTundraButton", button.Button, cg.Component
)
ButtonKind = haier_tundra_ns.enum("ButtonKind", is_class=True)
ICON_LED_OUTLINE = "mdi:led-outline"
ICON_SPRAY_BOTTLE = "mdi:spray-bottle"
CONF_HAIER_TUNDRA_ID = "haier_tundra_id"
CONF_SELF_CLEAN = "self_clean"
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_TUNDRA_ID): cv.use_id(HaierTundra),
cv.Optional(CONF_LIGHT): button.button_schema(
HaierTundraButton, icon=ICON_LED_OUTLINE
),
cv.Optional(CONF_SELF_CLEAN): button.button_schema(
HaierTundraButton, icon=ICON_SPRAY_BOTTLE
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_TUNDRA_ID])
for key, kind in (
(CONF_LIGHT, ButtonKind.BUTTON_KIND_LIGHT),
(CONF_SELF_CLEAN, ButtonKind.BUTTON_KIND_SELF_CLEAN),
):
if (conf := config.get(key)) is not None:
var = await button.new_button(conf, hub, kind)
await cg.register_component(var, conf)
@@ -0,0 +1,18 @@
#include "haier_tundra_button.h"
#include "esphome/core/log.h"
namespace esphome::haier_tundra {
ESPHOME_LOG_TAG(TAG, "haier_tundra.button");
void HaierTundraButton::dump_config() { LOG_BUTTON("", "Haier Tundra Button", this); }
void HaierTundraButton::press_action() {
if (this->kind_ == ButtonKind::BUTTON_KIND_LIGHT) {
this->parent_->toggle_light();
} else {
this->parent_->start_self_clean();
}
}
} // namespace esphome::haier_tundra
@@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/button/button.h"
#include "esphome/components/haier_tundra/haier_tundra.h"
namespace esphome::haier_tundra {
enum class ButtonKind : uint8_t {
BUTTON_KIND_LIGHT,
BUTTON_KIND_SELF_CLEAN,
};
class HaierTundraButton final : public button::Button, public Component, public Parented<HaierTundra> {
public:
HaierTundraButton(HaierTundra *parent, ButtonKind kind) : Parented(parent), kind_(kind) {}
void dump_config() override;
protected:
void press_action() override;
ButtonKind kind_;
};
} // namespace esphome::haier_tundra
@@ -0,0 +1,51 @@
import esphome.codegen as cg
from esphome.components import climate_ir, remote_base
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PLATFORM
import esphome.final_validate as fv
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
haier_tundra_ns = cg.esphome_ns.namespace("haier_tundra")
HaierTundra = haier_tundra_ns.class_("HaierTundra", climate_ir.ClimateIR)
CONF_UNIT = "unit"
Unit = haier_tundra_ns.enum("Unit")
UNITS = {
"a": Unit.HAIER_HEADER_UNIT_A,
"b": Unit.HAIER_HEADER_UNIT_B,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HaierTundra).extend(
{
cv.Optional(CONF_UNIT, default="a"): cv.enum(UNITS, lower=True),
}
)
def _final_validate_unit(config: ConfigType) -> None:
# Each transmitter may drive at most one unit A and one unit B
climates = fv.full_config.get()["climate"]
seen = set()
for climate in climates:
if CONF_PLATFORM in climate and climate[CONF_PLATFORM] == "haier_tundra":
unit_tx_pair = (
climate[CONF_UNIT],
climate[remote_base.CONF_TRANSMITTER_ID],
)
if unit_tx_pair in seen and climate[CONF_ID] == config[CONF_ID]:
raise cv.Invalid(
"Only one of each unit is allowed per transmitter", path=[CONF_UNIT]
)
seen.add(unit_tx_pair)
FINAL_VALIDATE_SCHEMA = _final_validate_unit
async def to_code(config: ConfigType) -> None:
remote_base.request_protocol("haier")
await climate_ir.new_climate_ir(config, config[CONF_UNIT])
@@ -0,0 +1,260 @@
#include "haier_tundra.h"
namespace esphome::haier_tundra {
// Number of bytes in decoded packet sans checksum
static constexpr uint8_t HAIER_TUNDRA_FRAME_SIZE = 13;
void HaierTundra::transmit_state() {
// Button value does not seem to matter too much except
// when toggling light, as it has no bit flag of its own
// So in the default case we can simply set it to POWER
this->transmit_(HAIER_BUTTON_POWER);
}
void HaierTundra::transmit_(uint8_t button) {
// Force Quiet and Turbo off if we're not in Cool or Heat mode
if (this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT) {
#ifdef USE_SWITCH
if (this->quiet_mode_switch_ != nullptr) {
this->quiet_mode_switch_->publish_state(false);
}
if (this->turbo_mode_switch_ != nullptr) {
this->turbo_mode_switch_->publish_state(false);
}
#endif
this->quiet_mode_ = false;
this->turbo_mode_ = false;
}
const uint8_t frame[HAIER_TUNDRA_FRAME_SIZE] = {
this->unit_,
static_cast<uint8_t>(this->temperature_() << 4 | this->vertical_swing_()),
static_cast<uint8_t>(this->horizontal_swing_() << 5),
static_cast<uint8_t>(this->health_() << 1),
static_cast<uint8_t>(this->power_() << 6),
static_cast<uint8_t>(this->fan_speed_() << 5),
static_cast<uint8_t>(this->quiet_() << 7 | this->turbo_() << 6),
static_cast<uint8_t>(this->operation_mode_() << 5),
0x00,
0x00,
static_cast<uint8_t>((button == HAIER_BUTTON_SELF_CLEAN ? 1 : 0) << 4),
0x00,
button,
};
auto transmit = this->transmitter_->transmit();
remote_base::HaierProtocol().encode(transmit.get_data(), frame, sizeof(frame));
transmit.perform();
}
uint8_t HaierTundra::temperature_() {
return (uint8_t) roundf(clamp<float>(this->target_temperature, HAIER_TEMP_MIN, HAIER_TEMP_MAX)) - HAIER_TEMP_MIN;
}
uint8_t HaierTundra::vertical_swing_() {
if (this->mode == climate::CLIMATE_MODE_OFF) {
return HAIER_SWING_V_OFF;
}
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
case climate::CLIMATE_SWING_BOTH:
return HAIER_SWING_V_AUTO;
default:
if (this->mode == climate::CLIMATE_MODE_HEAT) {
return HAIER_SWING_V_1BOT;
}
return HAIER_SWING_V_1TOP; // Cool/Dry/Auto
}
}
uint8_t HaierTundra::horizontal_swing_() {
switch (this->swing_mode) {
case climate::CLIMATE_SWING_HORIZONTAL:
case climate::CLIMATE_SWING_BOTH:
return HAIER_SWING_H_AUTO;
default:
return HAIER_SWING_H_MIDDLE;
}
}
uint8_t HaierTundra::health_() { return this->health_mode_ ? 1 : 0; }
uint8_t HaierTundra::power_() { return this->mode == climate::CLIMATE_MODE_OFF ? 0 : 1; }
uint8_t HaierTundra::fan_speed_() {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_LOW:
return HAIER_FAN_LOW;
case climate::CLIMATE_FAN_MEDIUM:
return HAIER_FAN_MED;
case climate::CLIMATE_FAN_HIGH:
return HAIER_FAN_HIGH;
case climate::CLIMATE_FAN_AUTO:
default:
return HAIER_FAN_AUTO;
}
}
uint8_t HaierTundra::quiet_() { return this->quiet_mode_ ? 1 : 0; }
uint8_t HaierTundra::turbo_() { return this->turbo_mode_ ? 1 : 0; }
uint8_t HaierTundra::operation_mode_() {
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
return HAIER_MODE_COOL;
case climate::CLIMATE_MODE_DRY:
return HAIER_MODE_DRY;
case climate::CLIMATE_MODE_HEAT:
return HAIER_MODE_HEAT;
case climate::CLIMATE_MODE_FAN_ONLY:
return HAIER_MODE_FAN;
case climate::CLIMATE_MODE_HEAT_COOL:
default:
return HAIER_MODE_AUTO;
}
}
bool HaierTundra::on_receive(remote_base::RemoteReceiveData data) {
auto haier = remote_base::HaierProtocol().decode(data);
if (haier.has_value()) {
return this->on_haier_(*haier);
}
return false;
}
bool HaierTundra::on_haier_(const remote_base::HaierData &haier) {
// Verify frame length and header
if (haier.data.size() != HAIER_TUNDRA_FRAME_SIZE || haier.data[0] != this->unit_) {
return false;
}
this->target_temperature = this->get_temperature_(haier.data[1] >> 4);
this->swing_mode = this->get_swing_mode_(haier.data[1] & 0xF, haier.data[2] >> 5);
this->health_mode_ = haier.data[3] & 0x2;
this->mode = this->get_operation_mode_(haier.data[4] & 0x40, haier.data[7] >> 5);
this->fan_mode = this->get_fan_speed_(haier.data[5] >> 5);
this->quiet_mode_ = haier.data[6] & 0x80;
this->turbo_mode_ = haier.data[6] & 0x40;
#ifdef USE_SWITCH
if (this->health_mode_switch_ != nullptr) {
this->health_mode_switch_->publish_state(this->health_mode_);
}
if (this->quiet_mode_switch_ != nullptr) {
this->quiet_mode_switch_->publish_state(this->quiet_mode_);
}
if (this->turbo_mode_switch_ != nullptr) {
this->turbo_mode_switch_->publish_state(this->turbo_mode_);
}
#endif
this->publish_state();
return true;
}
float HaierTundra::get_temperature_(uint8_t temp) { return temp + HAIER_TEMP_MIN; }
climate::ClimateSwingMode HaierTundra::get_swing_mode_(uint8_t swing_v, uint8_t swing_h) {
if (swing_v == HAIER_SWING_V_AUTO) {
if (swing_h == HAIER_SWING_H_AUTO) {
return climate::CLIMATE_SWING_BOTH;
}
return climate::CLIMATE_SWING_VERTICAL;
} else if (swing_h == HAIER_SWING_H_AUTO) {
return climate::CLIMATE_SWING_HORIZONTAL;
}
return climate::CLIMATE_SWING_OFF;
}
climate::ClimateMode HaierTundra::get_operation_mode_(uint8_t power, uint8_t mode) {
if (!power) {
return climate::CLIMATE_MODE_OFF;
}
switch (mode) {
case HAIER_MODE_COOL:
return climate::CLIMATE_MODE_COOL;
case HAIER_MODE_DRY:
return climate::CLIMATE_MODE_DRY;
case HAIER_MODE_HEAT:
return climate::CLIMATE_MODE_HEAT;
case HAIER_MODE_FAN:
return climate::CLIMATE_MODE_FAN_ONLY;
case HAIER_MODE_AUTO:
default:
return climate::CLIMATE_MODE_HEAT_COOL;
}
}
climate::ClimateFanMode HaierTundra::get_fan_speed_(uint8_t fan) {
switch (fan) {
case HAIER_FAN_LOW:
return climate::CLIMATE_FAN_LOW;
case HAIER_FAN_MED:
return climate::CLIMATE_FAN_MEDIUM;
case HAIER_FAN_HIGH:
return climate::CLIMATE_FAN_HIGH;
case HAIER_FAN_AUTO:
default:
return climate::CLIMATE_FAN_AUTO;
}
}
void HaierTundra::set_health_mode(bool health, bool transmit) {
this->health_mode_ = health;
if (transmit && this->mode != climate::CLIMATE_MODE_OFF) {
this->transmit_(HAIER_BUTTON_HEALTH);
}
}
void HaierTundra::set_quiet_mode(bool quiet, bool transmit) {
// Quiet & Turbo cannot be enabled at the same time
if (quiet) {
#ifdef USE_SWITCH
if (this->turbo_mode_switch_ != nullptr) {
this->turbo_mode_switch_->publish_state(false);
}
#endif
this->turbo_mode_ = false;
}
this->quiet_mode_ = quiet;
if (transmit)
this->transmit_(HAIER_BUTTON_TURBO_QUIET);
}
void HaierTundra::set_turbo_mode(bool turbo, bool transmit) {
// Quiet & Turbo cannot be enabled at the same time
if (turbo) {
#ifdef USE_SWITCH
if (this->quiet_mode_switch_ != nullptr) {
this->quiet_mode_switch_->publish_state(false);
}
#endif
this->quiet_mode_ = false;
}
this->turbo_mode_ = turbo;
if (transmit)
this->transmit_(HAIER_BUTTON_TURBO_QUIET);
}
void HaierTundra::toggle_light() {
if (this->mode != climate::CLIMATE_MODE_OFF) {
this->transmit_(HAIER_BUTTON_LIGHT);
}
}
void HaierTundra::start_self_clean() {
// Can be started when off
this->transmit_(HAIER_BUTTON_SELF_CLEAN);
}
} // namespace esphome::haier_tundra
@@ -0,0 +1,139 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
#include "esphome/components/remote_base/haier_protocol.h"
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
namespace esphome::haier_tundra {
// Byte 0 - Header value (Unit ID)
enum Unit : uint8_t {
HAIER_HEADER_UNIT_B = 0x59,
HAIER_HEADER_UNIT_A = 0xA6,
};
// Byte 1[7:4] - Temperature (˚C) 0x0..0xE
static constexpr uint8_t HAIER_TEMP_MIN = 16;
static constexpr uint8_t HAIER_TEMP_MAX = 30;
// Byte 1[3:0] - Vertical Swing
static constexpr uint8_t HAIER_SWING_V_OFF = 0x0; // Power off value; from Auto, stops at current position
static constexpr uint8_t HAIER_SWING_V_2TOP = 0x1; // [|/ ]
static constexpr uint8_t HAIER_SWING_V_1TOP = 0x2; // [| ] (Not available in Heat mode)
static constexpr uint8_t HAIER_SWING_V_2BOT = 0x3; // [ //]
static constexpr uint8_t HAIER_SWING_V_1BOT = 0xA; // [ /] (Heat mode only)
static constexpr uint8_t HAIER_SWING_V_AUTO = 0xC;
// Byte 2[7:5] - Horizontal Swing
static constexpr uint8_t HAIER_SWING_H_MIDDLE = 0x0; // [ || ]
static constexpr uint8_t HAIER_SWING_H_LEFT = 0x3; // [/ ]
static constexpr uint8_t HAIER_SWING_H_MIDDLE_LEFT = 0x4; // [ | ]
static constexpr uint8_t HAIER_SWING_H_MIDDLE_RIGHT = 0x5; // [ | ]
static constexpr uint8_t HAIER_SWING_H_RIGHT = 0x6; // [ \]
static constexpr uint8_t HAIER_SWING_H_AUTO = 0x7; // [/||\]
// Byte 3[7:5] - Timer Mode
static constexpr uint8_t HAIER_TIMER_MODE_DISABLED = 0x0;
static constexpr uint8_t HAIER_TIMER_MODE_OFF = 0x1; // OFF
static constexpr uint8_t HAIER_TIMER_MODE_ON = 0x2; // ON
static constexpr uint8_t HAIER_TIMER_MODE_ON_OFF = 0x4; // ON → OFF
static constexpr uint8_t HAIER_TIMER_MODE_OFF_ON = 0x5; // ON ← OFF
// Byte 3[1] - Health
// Byte 4[6] - Power
// Byte 5[7:5] - Fan Speed
static constexpr uint8_t HAIER_FAN_HIGH = 0x1;
static constexpr uint8_t HAIER_FAN_MED = 0x2;
static constexpr uint8_t HAIER_FAN_LOW = 0x3;
static constexpr uint8_t HAIER_FAN_AUTO = 0x5;
// Byte 5[4:0] - Timer Off Hours (0-23) 0x00..0x17
// Byte 6[7] - Quiet Mode
// Byte 6[6] - Turbo Mode
// Byte 6[5:0] - Timer Off Minutes (0-59) 0x00..0x3B
// Byte 7[7:5] - Mode
static constexpr uint8_t HAIER_MODE_AUTO = 0x0;
static constexpr uint8_t HAIER_MODE_COOL = 0x1;
static constexpr uint8_t HAIER_MODE_DRY = 0x2;
static constexpr uint8_t HAIER_MODE_HEAT = 0x4;
static constexpr uint8_t HAIER_MODE_FAN = 0x6;
// Byte 7[4:0] - Timer On Hours (0-23) 0x00..0x17
// Byte 8[7] - Sleep Mode
// Byte 8[5:0] - Timer On Minutes (0-59) 0x00..0x3B
// Byte 10[4] - Self Clean
// Byte 12 - Button
static constexpr uint8_t HAIER_BUTTON_TEMP_UP = 0x00;
static constexpr uint8_t HAIER_BUTTON_TEMP_DOWN = 0x01;
static constexpr uint8_t HAIER_BUTTON_SWING_V = 0x02;
static constexpr uint8_t HAIER_BUTTON_SWING_H = 0x03;
static constexpr uint8_t HAIER_BUTTON_FAN_SPEED = 0x04;
static constexpr uint8_t HAIER_BUTTON_POWER = 0x05;
static constexpr uint8_t HAIER_BUTTON_MODE = 0x06;
static constexpr uint8_t HAIER_BUTTON_HEALTH = 0x07;
static constexpr uint8_t HAIER_BUTTON_TURBO_QUIET = 0x08;
static constexpr uint8_t HAIER_BUTTON_SLEEP = 0x0B;
static constexpr uint8_t HAIER_BUTTON_TIMER = 0x10;
static constexpr uint8_t HAIER_BUTTON_LIGHT = 0x15;
static constexpr uint8_t HAIER_BUTTON_SELF_CLEAN = 0x19;
class HaierTundra final : public climate_ir::ClimateIR {
public:
explicit HaierTundra(Unit unit)
: climate_ir::ClimateIR(HAIER_TEMP_MIN, HAIER_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}),
unit_(unit) {}
#ifdef USE_SWITCH
SUB_SWITCH(health_mode);
SUB_SWITCH(quiet_mode);
SUB_SWITCH(turbo_mode);
#endif
void set_health_mode(bool health, bool transmit = true);
void set_quiet_mode(bool quiet, bool transmit = true);
void set_turbo_mode(bool turbo, bool transmit = true);
void toggle_light();
void start_self_clean();
protected:
// Transmit via IR the state of this climate controller.
void transmit_state() override;
void transmit_(uint8_t button);
uint8_t temperature_();
uint8_t vertical_swing_();
uint8_t horizontal_swing_();
uint8_t health_();
uint8_t power_();
uint8_t fan_speed_();
uint8_t quiet_();
uint8_t turbo_();
uint8_t operation_mode_();
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool on_haier_(const remote_base::HaierData &haier);
float get_temperature_(uint8_t temp);
climate::ClimateSwingMode get_swing_mode_(uint8_t swing_v, uint8_t swing_h);
climate::ClimateMode get_operation_mode_(uint8_t power, uint8_t mode);
climate::ClimateFanMode get_fan_speed_(uint8_t fan);
Unit unit_;
bool health_mode_{false};
bool quiet_mode_{false};
bool turbo_mode_{false};
};
} // namespace esphome::haier_tundra
@@ -0,0 +1,60 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from ..climate import HaierTundra, haier_tundra_ns
CODEOWNERS = ["@fauxpark"]
HaierTundraSwitch = haier_tundra_ns.class_(
"HaierTundraSwitch", switch.Switch, cg.Component
)
SwitchKind = haier_tundra_ns.enum("SwitchKind", is_class=True)
ICON_LEAF = "mdi:leaf"
ICON_ARROW_DOWN_RIGHT = "mdi:arrow-down-right"
ICON_ARROW_UP_RIGHT_BOLD = "mdi:arrow-up-right-bold"
CONF_HAIER_TUNDRA_ID = "haier_tundra_id"
CONF_HEALTH_MODE = "health_mode"
CONF_QUIET_MODE = "quiet_mode"
CONF_TURBO_MODE = "turbo_mode"
def _switch_schema(icon: str) -> cv.Schema:
return switch.switch_schema(
HaierTundraSwitch,
icon=icon,
default_restore_mode="RESTORE_DEFAULT_OFF",
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
)
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_TUNDRA_ID): cv.use_id(HaierTundra),
cv.Optional(CONF_HEALTH_MODE): _switch_schema(ICON_LEAF),
cv.Optional(CONF_QUIET_MODE): _switch_schema(ICON_ARROW_DOWN_RIGHT),
cv.Optional(CONF_TURBO_MODE): _switch_schema(ICON_ARROW_UP_RIGHT_BOLD),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_TUNDRA_ID])
switches = switch.sub_switches(config)
if sw := await switches(
CONF_HEALTH_MODE, hub.set_health_mode_switch, hub, SwitchKind.SWITCH_KIND_HEALTH
):
await cg.register_component(sw, config[CONF_HEALTH_MODE])
if sw := await switches(
CONF_QUIET_MODE, hub.set_quiet_mode_switch, hub, SwitchKind.SWITCH_KIND_QUIET
):
await cg.register_component(sw, config[CONF_QUIET_MODE])
if sw := await switches(
CONF_TURBO_MODE, hub.set_turbo_mode_switch, hub, SwitchKind.SWITCH_KIND_TURBO
):
await cg.register_component(sw, config[CONF_TURBO_MODE])
@@ -0,0 +1,43 @@
#include "haier_tundra_switch.h"
#include "esphome/core/log.h"
namespace esphome::haier_tundra {
ESPHOME_LOG_TAG(TAG, "haier_tundra.switch");
void HaierTundraSwitch::setup() {
auto initial = this->get_initial_state_with_restore_mode();
if (initial.has_value()) {
const bool state = initial.value();
this->apply_(state, false);
this->publish_state(state);
}
}
void HaierTundraSwitch::dump_config() { LOG_SWITCH("", "Haier Tundra Switch", this); }
void HaierTundraSwitch::write_state(bool state) {
// Quiet and Turbo can only be turned on in Cool or Heat mode
if (state && this->kind_ != SwitchKind::SWITCH_KIND_HEALTH && this->parent_->mode != climate::CLIMATE_MODE_COOL &&
this->parent_->mode != climate::CLIMATE_MODE_HEAT) {
return;
}
this->apply_(state, true);
this->publish_state(state);
}
void HaierTundraSwitch::apply_(bool state, bool transmit) {
switch (this->kind_) {
case SwitchKind::SWITCH_KIND_HEALTH:
this->parent_->set_health_mode(state, transmit);
break;
case SwitchKind::SWITCH_KIND_QUIET:
this->parent_->set_quiet_mode(state, transmit);
break;
case SwitchKind::SWITCH_KIND_TURBO:
this->parent_->set_turbo_mode(state, transmit);
break;
}
}
} // namespace esphome::haier_tundra
@@ -0,0 +1,28 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
#include "esphome/components/haier_tundra/haier_tundra.h"
namespace esphome::haier_tundra {
enum class SwitchKind : uint8_t {
SWITCH_KIND_HEALTH,
SWITCH_KIND_QUIET,
SWITCH_KIND_TURBO,
};
class HaierTundraSwitch final : public switch_::Switch, public Component, public Parented<HaierTundra> {
public:
HaierTundraSwitch(HaierTundra *parent, SwitchKind kind) : Parented(parent), kind_(kind) {}
void setup() override;
void dump_config() override;
protected:
void write_state(bool state) override;
void apply_(bool state, bool transmit);
SwitchKind kind_;
};
} // namespace esphome::haier_tundra
+13
View File
@@ -0,0 +1,13 @@
import esphome.codegen as cg
from esphome.components import spi
from esphome.components.motion import MotionComponent
CODEOWNERS = ["@lboue"]
DOMAIN = "icm20689"
CONF_ICM20689_ID = "icm20689_id"
# C++ namespace / class
icm20689_ns = cg.esphome_ns.namespace("icm20689")
ICM20689Component = icm20689_ns.class_(
"ICM20689Component", MotionComponent, spi.SPIDevice
)
+126
View File
@@ -0,0 +1,126 @@
#include "icm20689.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cstddef>
#include <cstdint>
namespace esphome::icm20689 {
ESPHOME_LOG_TAG(TAG, "icm20689");
namespace {
// Range enum values 0..3 map to full-scale settings of 2x/4x/8x/16x the base range.
constexpr float accel_scale(uint8_t range) { return (2.0f * (1U << range)) / 32768.0f; }
constexpr float gyro_scale(uint8_t range) { return (250.0f * (1U << range)) / 32768.0f; }
} // namespace
void ICM20689Component::write_register_(uint8_t reg, uint8_t value) {
this->enable();
this->write_byte(reg & 0x7F);
this->write_byte(value);
this->disable();
}
void ICM20689Component::read_registers_(uint8_t reg, uint8_t *data, size_t len) {
this->enable();
this->write_byte(reg | ICM20689_READ_BIT);
this->read_array(data, len);
this->disable();
}
void ICM20689Component::setup() {
MotionComponent::setup();
this->spi_setup();
if (!this->spi_is_ready()) {
ESP_LOGE(TAG, "SPI device is not ready");
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
// Datasheet 4.20: a soft reset must be the first operation after power-up,
// before any register initialization. WHO_AM_I is only valid afterwards.
this->write_register_(ICM20689_REG_PWR_MGMT_1, ICM20689_SOFT_RESET);
delay(100); // NOLINT
// The chip comes out of reset in sleep mode: wake it and select the
// auto-selected best clock source (required for full gyro performance).
this->write_register_(ICM20689_REG_PWR_MGMT_1, ICM20689_CLKSEL_AUTO);
// USER_CTRL is not writable in sleep mode, so I2C_IF_DIS is set once awake.
// Datasheet 6.1: disable the I2C interface when using SPI.
this->write_register_(ICM20689_REG_USER_CTRL, ICM20689_I2C_IF_DIS);
uint8_t who_am_i = 0;
this->read_registers_(ICM20689_REG_WHO_AM_I, &who_am_i, 1);
if (who_am_i != ICM20689_WHO_AM_I_VALUE) {
ESP_LOGE(TAG, "Unexpected WHO_AM_I 0x%02X (expected 0x%02X) -- check wiring/CS", who_am_i, ICM20689_WHO_AM_I_VALUE);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
// Allow the sensor to settle after waking from sleep.
delay(50); // NOLINT
// Configure the accelerometer and gyroscope full-scale ranges.
this->write_register_(ICM20689_REG_ACCEL_CONFIG, static_cast<uint8_t>(this->accel_range_) << 3);
this->write_register_(ICM20689_REG_GYRO_CONFIG, static_cast<uint8_t>(this->gyro_range_) << 3);
}
void ICM20689Component::dump_config() {
ESP_LOGCONFIG(TAG, "ICM20689 IMU:");
LOG_PIN(" CS Pin: ", this->cs_);
if (this->is_failed()) {
ESP_LOGE(TAG, " " ESP_LOG_MSG_COMM_FAIL);
return;
}
ESP_LOGCONFIG(TAG, " Accel range: ±%dg", 2 << static_cast<uint8_t>(this->accel_range_));
ESP_LOGCONFIG(TAG, " Gyro range: ±%d°/s", 250 << static_cast<uint8_t>(this->gyro_range_));
MotionComponent::dump_config();
}
bool ICM20689Component::update_data(motion::MotionData &data) {
if (this->is_failed())
return false;
const uint8_t accel_range = static_cast<uint8_t>(this->accel_range_);
const uint8_t gyro_range = static_cast<uint8_t>(this->gyro_range_);
// Read one contiguous sample:
// 0..5 accelerometer X/Y/Z
// 6..7 temperature
// 8..13 gyroscope X/Y/Z
uint8_t raw_data[14] = {};
this->read_registers_(ICM20689_REG_ACCEL_XOUT_H, raw_data, sizeof(raw_data));
const float accel_factor = accel_scale(accel_range);
data.acceleration[motion::X_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[0], raw_data[1])) * accel_factor;
data.acceleration[motion::Y_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[2], raw_data[3])) * accel_factor;
data.acceleration[motion::Z_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[4], raw_data[5])) * accel_factor;
const float gyro_factor = gyro_scale(gyro_range);
data.angular_rate[motion::X_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[8], raw_data[9])) * gyro_factor;
data.angular_rate[motion::Y_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[10], raw_data[11])) * gyro_factor;
data.angular_rate[motion::Z_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[12], raw_data[13])) * gyro_factor;
if (!this->temperature_callback_.empty()) {
// Datasheet formula: temperature (°C) = raw / 326.8 + 25.
const float temperature = static_cast<int16_t>(encode_uint16(raw_data[6], raw_data[7])) / 326.8f + 25.0f;
this->temperature_callback_.call(temperature);
}
return true;
}
} // namespace esphome::icm20689
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include "esphome/components/motion/motion_component.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::icm20689 {
// Register map
static constexpr uint8_t ICM20689_REG_WHO_AM_I = 0x75;
static constexpr uint8_t ICM20689_REG_PWR_MGMT_1 = 0x6B;
static constexpr uint8_t ICM20689_REG_USER_CTRL = 0x6A;
static constexpr uint8_t ICM20689_REG_GYRO_CONFIG = 0x1B;
static constexpr uint8_t ICM20689_REG_ACCEL_CONFIG = 0x1C;
static constexpr uint8_t ICM20689_REG_ACCEL_XOUT_H = 0x3B;
static constexpr uint8_t ICM20689_READ_BIT = 0x80;
static constexpr uint8_t ICM20689_WHO_AM_I_VALUE = 0x98;
static constexpr uint8_t ICM20689_SOFT_RESET = 0x81; // PWR_MGMT_1: DEVICE_RESET | CLKSEL=1 (datasheet 4.20)
static constexpr uint8_t ICM20689_CLKSEL_AUTO = 0x01; // PWR_MGMT_1: SLEEP=0, CLKSEL=1 (auto-select best clock)
static constexpr uint8_t ICM20689_I2C_IF_DIS = 0x10; // USER_CTRL bit 4
// Accelerometer range options: FS_SEL bits [4:3] of ACCEL_CONFIG
enum ICM20689AccelRange : uint8_t {
ICM20689_ACCEL_RANGE_2G = 0x00,
ICM20689_ACCEL_RANGE_4G = 0x01,
ICM20689_ACCEL_RANGE_8G = 0x02,
ICM20689_ACCEL_RANGE_16G = 0x03,
};
// Gyroscope range options: FS_SEL bits [4:3] of GYRO_CONFIG
enum ICM20689GyroRange : uint8_t {
ICM20689_GYRO_RANGE_250 = 0x00,
ICM20689_GYRO_RANGE_500 = 0x01,
ICM20689_GYRO_RANGE_1000 = 0x02,
ICM20689_GYRO_RANGE_2000 = 0x03,
};
class ICM20689Component final : public motion::MotionComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
public:
void setup() override;
void dump_config() override;
void set_accel_range(ICM20689AccelRange range) { this->accel_range_ = range; }
void set_gyro_range(ICM20689GyroRange range) { this->gyro_range_ = range; }
template<typename F> void add_temperature_listener(F &&callback) {
this->temperature_callback_.add(std::forward<F>(callback));
}
protected:
bool update_data(motion::MotionData &data) override;
void write_register_(uint8_t reg, uint8_t value);
// Reads len bytes starting at reg in one CS-low transaction.
void read_registers_(uint8_t reg, uint8_t *data, size_t len);
ICM20689AccelRange accel_range_{ICM20689_ACCEL_RANGE_2G};
ICM20689GyroRange gyro_range_{ICM20689_GYRO_RANGE_250};
LazyCallbackManager<void(float)> temperature_callback_{};
};
} // namespace esphome::icm20689
+56
View File
@@ -0,0 +1,56 @@
import esphome.codegen as cg
from esphome.components import spi
from esphome.components.const import CONF_ACCELEROMETER_RANGE, CONF_GYROSCOPE_RANGE
from esphome.components.motion import motion_schema, new_motion_component
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import ICM20689Component, icm20689_ns
DEPENDENCIES = ["spi"]
# Enum proxies (must match the C++ enum values exactly)
ICM20689AccelRange = icm20689_ns.enum("ICM20689AccelRange")
ACCEL_RANGE_OPTIONS = {
"2G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_2G,
"4G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_4G,
"8G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_8G,
"16G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_16G,
}
ICM20689GyroRange = icm20689_ns.enum("ICM20689GyroRange")
GYRO_RANGE_OPTIONS = {
"250DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_250,
"500DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_500,
"1000DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_1000,
"2000DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_2000,
}
# Top-level CONFIG_SCHEMA
CONFIG_SCHEMA = (
motion_schema(ICM20689Component, has_accel=True, has_gyro=True)
.extend(
{
cv.Optional(CONF_ACCELEROMETER_RANGE, default="2G"): cv.enum(
ACCEL_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_RANGE, default="250DPS"): cv.enum(
GYRO_RANGE_OPTIONS, upper=True
),
}
)
.extend(
spi.spi_device_schema(
cs_pin_required=True, default_mode="MODE0", default_data_rate="1MHz"
)
)
)
# Code generation
async def to_code(config: ConfigType) -> None:
var = await new_motion_component(config)
await spi.register_spi_device(var, config)
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
+42
View File
@@ -0,0 +1,42 @@
# YAML config keys
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_TEMPERATURE,
CONF_TYPE,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
from . import CONF_ICM20689_ID, ICM20689Component
AUTO_LOAD = ["icm20689"]
CONFIG_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_TEMPERATURE,
).extend(
{
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
cv.GenerateID(CONF_ICM20689_ID): cv.use_id(ICM20689Component),
}
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
parent = await cg.get_variable(config[CONF_ICM20689_ID])
data = MockObj("data")
value_lambda = await cg.process_lambda(
var.publish_state(data),
[(cg.float_, str(data))],
)
cg.add(parent.add_temperature_listener(value_lambda))
+2 -1
View File
@@ -2,12 +2,13 @@
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include "ili9xxx_defines.h"
#include "ili9xxx_init.h"
namespace esphome::ili9xxx {
static const char *const TAG = "ili9xxx";
ESPHOME_LOG_TAG(TAG, "ili9xxx");
const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6
enum ILI9XXXColorMode {
+4 -6
View File
@@ -891,12 +891,10 @@ static uint8_t color_to_nibble(const Color &color) {
if (color.raw_32 == 0xFFFFFFFF)
return 0x0F; // white
// Derive luma from RGB using Rec.601 weights (0.299/0.587/0.114, scaled by
// 256). Rec.601 is the standard for converting SDR images to grayscale and
// spreads saturated colours across the mid-range; Rec.709 instead crams them
// against white/black where the 16 panel levels are hard to tell apart.
auto luma = static_cast<uint8_t>((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
return quantize_8bit_to_nibble(luma);
// Rec.601 luma is the standard for converting SDR images to grayscale and spreads saturated colours
// across the mid-range; Rec.709 instead crams them against white/black where the 16 panel levels are
// hard to tell apart.
return quantize_8bit_to_nibble(display::rec601_luma(color));
}
// 4x4 ordered (Bayer) dither threshold over the weighted-luma range (0..65535).
+8 -8
View File
@@ -85,7 +85,7 @@ class LvColorPickerType : public LvCompound {
if (left != nullptr) {
lv_obj_set_style_align(left, LV_ALIGN_LEFT_MID, LV_PART_MAIN);
// A narrow hue column may be slimmer than its label.
lv_obj_add_flag(left, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
lv_obj_set_flag(left, LV_OBJ_FLAG_OVERFLOW_VISIBLE, true);
}
// The text rows keep their natural height and the bars between them take what is left,
@@ -233,7 +233,7 @@ class LvColorPickerType : public LvCompound {
static lv_obj_t *create_box(lv_obj_t *parent) {
auto *obj = lv_obj_create(parent);
lv_obj_add_style(obj, &plain_box, LV_PART_MAIN);
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flag(obj, LV_OBJ_FLAG_SCROLLABLE, false);
return obj;
}
@@ -326,7 +326,7 @@ class LvColorPickerType : public LvCompound {
lv_obj_set_style_radius(disc, LV_RADIUS_CIRCLE, LV_PART_MAIN);
lv_obj_set_style_bg_opa(disc, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_bg_grad(disc, grad, LV_PART_MAIN);
lv_obj_remove_flag(disc, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flag(disc, LV_OBJ_FLAG_SCROLLABLE, false);
return disc;
}
@@ -336,7 +336,7 @@ class LvColorPickerType : public LvCompound {
this->wheel_ = create_disc(container, &wheel_hue_grad);
// Not clickable, so touches fall through to the wheel underneath.
auto *overlay = create_disc(this->wheel_, &wheel_white_grad);
lv_obj_remove_flag(overlay, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_flag(overlay, LV_OBJ_FLAG_CLICKABLE, false);
this->wheel_handle_ = lv_obj_create(this->wheel_);
lv_obj_set_size(this->wheel_handle_, WHEEL_HANDLE_SIZE, WHEEL_HANDLE_SIZE);
@@ -345,8 +345,8 @@ class LvColorPickerType : public LvCompound {
lv_obj_set_style_bg_opa(this->wheel_handle_, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(this->wheel_handle_, 2, LV_PART_MAIN);
lv_obj_set_style_border_color(this->wheel_handle_, lv_color_white(), LV_PART_MAIN);
lv_obj_remove_flag(this->wheel_handle_, LV_OBJ_FLAG_CLICKABLE);
lv_obj_remove_flag(this->wheel_handle_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flag(this->wheel_handle_, LV_OBJ_FLAG_CLICKABLE, false);
lv_obj_set_flag(this->wheel_handle_, LV_OBJ_FLAG_SCROLLABLE, false);
// PRESSED as well, so a tap takes effect without the finger moving.
lv_obj_add_event_cb(this->wheel_, wheel_touch_cb, LV_EVENT_PRESSED, this);
@@ -359,7 +359,7 @@ class LvColorPickerType : public LvCompound {
void add_value_label_(SliderIndex index, lv_color_t color) {
auto *slider = this->sliders_[index];
lv_obj_add_flag(slider, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
lv_obj_set_flag(slider, LV_OBJ_FLAG_OVERFLOW_VISIBLE, true);
auto *label = lv_label_create(slider);
this->values_[index] = label;
lv_obj_set_style_align(label, LV_ALIGN_CENTER, LV_PART_MAIN);
@@ -509,7 +509,7 @@ class LvColorPickerType : public LvCompound {
static void bubble_events(lv_obj_t *obj) {
for (uint32_t i = 0; i < lv_obj_get_child_count(obj); i++) {
auto *child = lv_obj_get_child(obj, i);
lv_obj_add_flag(child, LV_OBJ_FLAG_EVENT_BUBBLE);
lv_obj_set_flag(child, LV_OBJ_FLAG_EVENT_BUBBLE, true);
bubble_events(child);
}
}
+1 -1
View File
@@ -214,7 +214,7 @@ enum class Orientation : uint8_t {
};
class LvglComponent final : public PollingComponent {
constexpr static const char *const TAG = "lvgl";
constexpr static const char *const TAG = "lvgl"; // NOLINT: lvgl does not build for ESP8266
public:
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
+1 -1
View File
@@ -19,7 +19,7 @@ using alloc_string_t = std::basic_string<char, std::char_traits<char>, RAMAlloca
* @tparam V The type of the value in the mapping. Should be a basic type or pointer.
*/
static const char *const TAG = "mapping";
ESPHOME_LOG_TAG(TAG, "mapping");
template<typename K, typename V> class Mapping {
public:
+52 -1
View File
@@ -20,6 +20,10 @@
#ifdef USE_DASHBOARD_IMPORT
#include "esphome/components/dashboard_import/dashboard_import.h"
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
#include "esphome/components/ethernet/ethernet_component.h"
#include "esphome/components/wifi/wifi_component.h"
#endif
namespace esphome::mdns {
@@ -39,6 +43,45 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
// Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
// Same order as the default route in NetworkComponent::loop()
const char *MDNSComponent::connected_network_() const {
const bool ethernet = ethernet::global_eth_component->is_connected();
const bool wifi = wifi::global_wifi_component->is_connected();
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
if (wifi)
return NETWORK_WIFI;
#endif
if (ethernet)
return NETWORK_ETHERNET;
if (wifi)
return NETWORK_WIFI;
return nullptr;
}
void MDNSComponent::update_network_txt_() {
if (this->network_txt_ == nullptr)
return;
// With no interface connected, keep the last value instead of flapping to wifi
const char *network = this->connected_network_();
if (network == nullptr || network == this->network_txt_)
return;
if (!this->publish_network_txt_(network)) {
// The _esphomelib service is missing, so a retry would fail too
this->network_txt_ = nullptr;
return;
}
ESP_LOGD(TAG, "Network TXT: %s", network);
this->network_txt_ = network;
#ifdef USE_MDNS_STORE_SERVICES
this->network_record_->value = MDNS_STR(network);
#endif
}
#endif
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
#ifdef USE_MDNS_STORE_SERVICES
auto &services = this->services_;
@@ -160,7 +203,15 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
#if defined(USE_WIFI)
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
const char *network = this->connected_network_();
this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
#ifdef USE_MDNS_STORE_SERVICES
// services is services_ here, which never reallocates
this->network_record_ = &txt_records.back();
#endif
#elif defined(USE_WIFI)
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET)
+22
View File
@@ -32,6 +32,12 @@
#define USE_MDNS_DEVICE_INFO_TXT
#endif
// With both wifi and ethernet, the _esphomelib "network" TXT value follows the connected interface.
// OpenThread hands its services to the SRP client once, so it keeps the fixed value.
#if defined(USE_API) && defined(USE_WIFI) && defined(USE_ETHERNET) && !defined(USE_OPENTHREAD)
#define USE_MDNS_DYNAMIC_NETWORK_TXT
#endif
namespace esphome::mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
@@ -150,6 +156,22 @@ class MDNSComponent final : public Component
void setup_buffers_and_register_(PlatformRegisterFn platform_register);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
static constexpr uint32_t NETWORK_TXT_CHECK_INTERVAL_MS = 1000;
/// The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
const char *connected_network_() const;
/// Check the connected interface and publish a changed "network" TXT value.
void update_network_txt_();
/// Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
bool publish_network_txt_(const char *network);
/// Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
const char *network_txt_{nullptr};
#ifdef USE_MDNS_STORE_SERVICES
/// The "network" record in services_, kept in step with network_txt_.
MDNSTXTRecord *network_record_{nullptr};
#endif
#endif
#ifdef USE_MDNS_DYNAMIC_TXT
/// Storage for runtime-generated TXT values from user lambdas
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
+18 -1
View File
@@ -89,7 +89,24 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
}
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
bool MDNSComponent::publish_network_txt_(const char *network) {
// ESP-IDF copies the value and announces the changed record
esp_err_t err = mdns_service_txt_item_set("_esphomelib", "_tcp", "network", network);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to update network TXT: %s", esp_err_to_name(err));
return false;
}
return true;
}
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp32);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
#endif
}
void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD
+19
View File
@@ -46,7 +46,26 @@ void MDNSComponent::start_polling_window_() {
}
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
ESPHOME_LOG_TAG(TAG, "mdns");
bool MDNSComponent::publish_network_txt_(const char *network) {
// LEAmDNS replaces the value of an existing key; announce() sends the change
if (!MDNS.addServiceTxt("esphomelib", "tcp", "network", network)) {
ESP_LOGW(TAG, "Failed to update network TXT");
return false;
}
MDNS.announce();
this->start_polling_window_();
return true;
}
#endif
void MDNSComponent::setup() {
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
// No-op until the first IP-up event registers the services
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
#endif
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
+1 -1
View File
@@ -5,7 +5,7 @@
namespace esphome::midea::ac {
const char *const Constants::TAG = "midea";
const char Constants::TAG[] PROGMEM = "midea";
const char *const Constants::FREEZE_PROTECTION = "Freeze Protection";
const char *const Constants::SILENT = "Silent";
const char *const Constants::TURBO = "Turbo";
+1 -1
View File
@@ -17,7 +17,7 @@ using MideaPreset = dudanov::midea::ac::Preset;
class Constants {
public:
static const char *const TAG;
static const char TAG[]; // PROGMEM, like ESPHOME_LOG_TAG
static const char *const FREEZE_PROTECTION;
static const char *const SILENT;
static const char *const TURBO;
+1 -1
View File
@@ -18,7 +18,7 @@
namespace esphome::mipi_dsi {
constexpr static const char *const TAG = "display.mipi_dsi";
ESPHOME_LOG_TAG(TAG, "display.mipi_dsi");
const uint8_t SW_RESET_CMD = 0x01;
const uint8_t SLEEP_OUT = 0x11;
const uint8_t SDIR_CMD = 0xC7;
+2 -1
View File
@@ -2,6 +2,7 @@
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
#include <esp_lcd_panel_rgb.h>
#ifdef USE_SPI
@@ -10,7 +11,7 @@
namespace esphome::mipi_rgb {
constexpr static const char *const TAG = "display.mipi_rgb";
ESPHOME_LOG_TAG(TAG, "display.mipi_rgb");
const uint8_t SW_RESET_CMD = 0x01;
const uint8_t SLEEP_OUT = 0x11;
const uint8_t SDIR_CMD = 0xC7;
+2 -1
View File
@@ -7,10 +7,11 @@
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::mipi_spi {
constexpr static const char *const TAG = "display.mipi_spi";
ESPHOME_LOG_TAG(TAG, "display.mipi_spi");
// Maximum bytes to log for commands (truncated if larger)
static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64;
+4 -2
View File
@@ -1040,11 +1040,13 @@ def run_compile(args, config: ConfigType) -> bool:
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
bootloader = zephyr_data()[KEY_BOOTLOADER]
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes.
# The bootloader resets on a package whose sd_req does not list its SoftDevice,
# so v7 accepts S140 7.0.1, 7.2.0 and 7.3.0 (e.g. Seeed XIAO nRF52840).
_GENPKG_PARAMS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
}
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
_UF2_FAMILY_IDS = {
+68
View File
@@ -1,3 +1,4 @@
import re
from typing import Any
from esphome import automation
@@ -44,9 +45,12 @@ from esphome.core import (
coroutine_with_priority,
)
import esphome.final_validate as fv
from esphome.helpers import write_file_if_changed
from esphome.types import ConfigType
from esphome.writer import clean_build
from .const import (
CONF_CORE_OPTIONS,
CONF_DEVICE_TYPE,
CONF_EXT_PAN_ID,
CONF_FORCE_DATASET,
@@ -59,6 +63,7 @@ from .const import (
CONF_PSKC,
CONF_SRP_ID,
CONF_TLV,
OPENTHREAD_CONFIG_PREFIX,
)
CODEOWNERS = ["@mrene"]
@@ -84,6 +89,10 @@ CONF_DEVICE_TYPES = [
"MTD",
]
OT_HEADER_DIR = "openthread"
OT_HEADER_FILE = "esphome_ot_config.h"
OT_CORE_OPTION_NAME = re.compile(r"[A-Z0-9_]+")
def _validate_txpower(value: Any) -> int | float:
if CORE.is_esp32:
@@ -158,6 +167,31 @@ def set_sdkconfig_options(config: ConfigType) -> None:
add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True)
def _write_custom_header(config: ConfigType) -> None:
"""Write core_options to the OpenThread custom header."""
if not (options := config.get(CONF_CORE_OPTIONS)):
return
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_HEADER_CUSTOM", True)
# IDF resolves the path relative to the project (build) directory.
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CUSTOM_HEADER_PATH", OT_HEADER_DIR)
add_idf_sdkconfig_option(
"CONFIG_OPENTHREAD_CUSTOM_HEADER_FILE_NAME", OT_HEADER_FILE
)
content = (
"// Auto-generated by ESPHome from openthread core_options - do not edit\n"
"#pragma once\n\n"
+ "".join(
f"#define {name} {value}\n" for name, value in sorted(options.items())
)
)
# PlatformIO does not track this header as an OpenThread input; the esp-idf
# toolchain does.
header_path = CORE.relative_build_path(OT_HEADER_DIR, OT_HEADER_FILE)
if write_file_if_changed(header_path, content) and not CORE.using_toolchain_esp_idf:
clean_build(clear_pio_cache=False)
openthread_ns = cg.esphome_ns.namespace("openthread")
OpenThreadComponent = openthread_ns.class_("OpenThreadComponent", cg.Component)
OpenThreadSrpComponent = openthread_ns.class_("OpenThreadSrpComponent", cg.Component)
@@ -220,6 +254,34 @@ def _validate_platform(config: ConfigType) -> ConfigType:
)(config)
def _validate_core_option_names(options: dict[str, str]) -> dict[str, str]:
for name in options:
if (
not name.startswith(OPENTHREAD_CONFIG_PREFIX)
or name == OPENTHREAD_CONFIG_PREFIX
):
raise cv.Invalid(
f"'{name}' is not an OpenThread core option; names must be "
f"{OPENTHREAD_CONFIG_PREFIX} followed by the option name",
path=[name],
)
if not OT_CORE_OPTION_NAME.fullmatch(name):
raise cv.Invalid(f"'{name}' may only contain A-Z, 0-9 and _", path=[name])
return options
def _validate_core_option_value(value: Any) -> str:
value = cv.string_strict(value)
if not value.strip():
raise cv.Invalid("OpenThread core option value must not be empty")
if "\n" in value or "\r" in value:
raise cv.Invalid("OpenThread core option value must be a single line")
# GCC treats a backslash followed by spaces as a line continuation too.
if value.rstrip().endswith("\\"):
raise cv.Invalid("OpenThread core option value must not end with \\")
return value
def _validate_tlv_hex(value: Any) -> str:
s = cv.string_strict(value)
if len(s) % 2 != 0:
@@ -252,6 +314,11 @@ CONFIG_SCHEMA = cv.All(
_validate_txpower,
),
cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CORE_OPTIONS): cv.All(
cv.only_on_esp32,
{cv.string_strict: _validate_core_option_value},
_validate_core_option_names,
),
}
).extend(_CONNECTION_SCHEMA),
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
@@ -325,6 +392,7 @@ async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
set_sdkconfig_options(config)
_write_custom_header(config)
elif CORE.using_zephyr:
# Crypto through PSA: mbedtls, plus Oberon from SDK 2.7
include_west_project("mbedtls")
+3
View File
@@ -1,3 +1,4 @@
CONF_CORE_OPTIONS = "core_options"
CONF_DEVICE_TYPE = "device_type"
CONF_EXT_PAN_ID = "ext_pan_id"
CONF_FORCE_DATASET = "force_dataset"
@@ -10,3 +11,5 @@ CONF_POLL_PERIOD = "poll_period"
CONF_PSKC = "pskc"
CONF_SRP_ID = "srp_id"
CONF_TLV = "tlv"
OPENTHREAD_CONFIG_PREFIX = "OPENTHREAD_CONFIG_"
+79
View File
@@ -0,0 +1,79 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
AUTO_LOAD = ["gpio_expander"]
CODEOWNERS = ["@ssieb", "@ajfriesen"]
DEPENDENCIES = ["i2c"]
DOMAIN = "pca9505"
MULTI_CONF = True
pca9505_ns = cg.esphome_ns.namespace("pca9505")
PCA9505Component = pca9505_ns.class_("PCA9505Component", cg.Component, i2c.I2CDevice)
PCA9505GPIOPin = pca9505_ns.class_(
"PCA9505GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9505Component)
)
CONF_PCA9505 = "pca9505"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9505Component),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
def validate_mode(value: ConfigType) -> ConfigType:
if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
raise cv.Invalid("Mode must be either input or output")
if value[CONF_INPUT] and value[CONF_OUTPUT]:
raise cv.Invalid("Mode must be either input or output")
return value
PCA9505_PIN_SCHEMA = pins.gpio_base_schema(
PCA9505GPIOPin,
cv.int_range(min=0, max=39),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
).extend(
{
cv.Required(CONF_PCA9505): cv.use_id(PCA9505Component),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_PCA9505, PCA9505_PIN_SCHEMA)
async def pca9505_pin_to_code(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_PCA9505])
cg.add(var.set_pin(config[CONF_NUMBER]))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var
+165
View File
@@ -0,0 +1,165 @@
#include "pca9505.h"
#include "esphome/core/log.h"
namespace esphome::pca9505 {
// Each register block holds one byte per port. Add the port number to reach a single
// port, or set the auto-increment bit to transfer the whole block in one go.
static constexpr uint8_t INPUT_REG = 0x00;
static constexpr uint8_t OUTPUT_REG = 0x08;
static constexpr uint8_t INVERT_REG = 0x10;
static constexpr uint8_t CONFIG_REG = 0x18;
static constexpr uint8_t MASK_REG = 0x20;
static constexpr uint8_t AUTO_INCREMENT = 0x80;
ESPHOME_LOG_TAG(TAG, "pca9505");
void PCA9505Component::setup() {
// Test to see if device exists
if (!this->read_bank_(0)) {
ESP_LOGE(TAG, "PCA9505 not detected at 0x%02X", this->address_);
this->mark_failed();
return;
}
// No polarity inversion
this->write_all_banks_(INVERT_REG, 0x00);
// All outputs low, before anything is switched to being an output
this->write_all_banks_(OUTPUT_REG, 0x00);
// All inputs at initialization. The part sees a 1 as an input, so this is the
// inverse of config_, which is all zeroes here.
this->write_all_banks_(CONFIG_REG, 0xFF);
// Mask every interrupt. This must be written, not assumed: the chip keeps its
// registers through an MCU reboot, so a previous run may have left pins unmasked.
for (uint8_t &bank : this->mask_) {
bank = 0xFF;
}
this->write_all_banks_(MASK_REG, 0xFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA9505Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
// Don't invalidate cache on read - only invalidate when interrupt fires
this->set_invalidate_on_read_(false);
}
// Nothing to poll until pin_mode() registers an input pin.
this->disable_loop();
}
void IRAM_ATTR PCA9505Component::gpio_intr(PCA9505Component *arg) { arg->enable_loop_soon_any_context(); }
void PCA9505Component::loop() {
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
}
void PCA9505Component::dump_config() {
ESP_LOGCONFIG(TAG, "PCA9505:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
bool PCA9505Component::digital_read_hw(uint8_t pin) { return this->read_bank_(pin / 8); }
bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin / 8] & (1 << (pin % 8)); }
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
if (value) {
this->output_[bank] |= mask;
} else {
this->output_[bank] &= ~mask;
}
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
}
void PCA9505Component::pin_mode(uint8_t pin, gpio::Flags flags) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
if (flags & gpio::FLAG_INPUT) {
this->config_[bank] &= ~mask;
// An input may raise INT; the polling loop is only needed when there is no interrupt pin.
this->mask_[bank] &= ~mask;
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags & gpio::FLAG_OUTPUT) {
this->config_[bank] |= mask;
// An output must never raise INT.
this->mask_[bank] |= mask;
}
// Invert as the part sees a 1 as an input
this->write_bank_(CONFIG_REG, bank, ~this->config_[bank]);
// Without an interrupt pin INT is unconnected, so the masks written at setup can stay
if (this->interrupt_pin_ != nullptr) {
this->write_bank_(MASK_REG, bank, this->mask_[bank]);
}
}
bool PCA9505Component::read_bank_(uint8_t bank) {
if (this->is_failed())
return false;
if (this->read_register(INPUT_REG + bank, &this->input_[bank], 1) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to read input register"));
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9505Component::write_bank_(uint8_t reg, uint8_t bank, uint8_t value) {
if (this->is_failed())
return false;
if (this->write_register(reg + bank, &value, 1) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to write register"));
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9505Component::write_all_banks_(uint8_t reg, uint8_t value) {
if (this->is_failed())
return false;
uint8_t values[BANK_COUNT];
for (uint8_t &bank : values) {
bank = value;
}
if (this->write_register(reg | AUTO_INCREMENT, values, sizeof(values)) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to write register"));
return false;
}
this->status_clear_warning();
return true;
}
float PCA9505Component::get_setup_priority() const { return setup_priority::IO; }
void PCA9505GPIOPin::setup() { this->pin_mode(this->flags_); }
void PCA9505GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool PCA9505GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA9505GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCA9505GPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "%u via PCA9505", this->pin_);
}
} // namespace esphome::pca9505
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::pca9505 {
/// Number of I/O pins on the PCA9505 and PCA9506.
static constexpr uint8_t PIN_COUNT = 40;
/// The pins are grouped into five ports of eight, each with its own set of registers.
static constexpr uint8_t BANK_COUNT = PIN_COUNT / 8;
class PCA9505Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, PIN_COUNT> {
public:
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void setup() override;
void loop() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
protected:
static void IRAM_ATTR gpio_intr(PCA9505Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool read_bank_(uint8_t bank);
bool write_bank_(uint8_t reg, uint8_t bank, uint8_t value);
bool write_all_banks_(uint8_t reg, uint8_t value);
/// The pin config per bank - 1 means OUTPUT, 0 means INPUT
uint8_t config_[BANK_COUNT]{};
/// The output state per bank - 1 means HIGH, 0 means LOW
uint8_t output_[BANK_COUNT]{};
/// The input pin states per bank - 1 means HIGH, 0 means LOW
uint8_t input_[BANK_COUNT]{};
/// Which pins may pull INT low - 1 means masked, matching the chip
uint8_t mask_[BANK_COUNT]{};
InternalGPIOPin *interrupt_pin_{nullptr};
};
class PCA9505GPIOPin final : public GPIOPin, public Parented<PCA9505Component> {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
gpio::Flags get_flags() const override { return this->flags_; }
protected:
uint8_t pin_{0};
bool inverted_{false};
gpio::Flags flags_{};
};
} // namespace esphome::pca9505
+6
View File
@@ -10,6 +10,7 @@ from esphome.const import (
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_RESET,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
@@ -26,11 +27,14 @@ PCF8574GPIOPin = pcf8574_ns.class_("PCF8574GPIOPin", cg.GPIOPin)
CONF_PCF8574 = "pcf8574"
CONF_PCF8575 = "pcf8575"
# Keep in sync with PCF8574Component::reset_ in pcf8574.h.
DEFAULT_RESET = True
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCF8574Component),
cv.Optional(CONF_PCF8575, default=False): cv.boolean,
cv.Optional(CONF_RESET, default=DEFAULT_RESET): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
@@ -44,6 +48,8 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_pcf8575(config[CONF_PCF8575]))
if (reset := config[CONF_RESET]) != DEFAULT_RESET:
cg.add(var.set_reset(reset))
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
+18 -15
View File
@@ -12,8 +12,13 @@ void PCF8574Component::setup() {
return;
}
this->write_gpio_();
this->read_gpio_();
if (this->reset_) {
this->write_gpio_();
this->read_gpio_();
} else {
this->output_mask_ = this->input_mask_;
this->mode_mask_ = ~this->configured_input_pins_;
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
@@ -40,8 +45,9 @@ void PCF8574Component::loop() {
void PCF8574Component::dump_config() {
ESP_LOGCONFIG(TAG,
"PCF8574:\n"
" Is PCF8575: %s",
YESNO(this->pcf8575_));
" Is PCF8575: %s\n"
" Reset: %s",
YESNO(this->pcf8575_), YESNO(this->reset_));
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
@@ -82,20 +88,12 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
bool PCF8574Component::read_gpio_() {
if (this->is_failed())
return false;
bool success;
uint8_t data[2];
if (this->pcf8575_) {
success = this->read_bytes_raw(data, 2);
this->input_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0);
} else {
success = this->read_bytes_raw(data, 1);
this->input_mask_ = data[0];
}
if (!success) {
uint8_t data[2]{}; // Keep the bytes zero for the PCF8574's one-byte read.
if (!this->read_bytes_raw(data, this->pcf8575_ ? 2 : 1)) {
this->status_set_warning();
return false;
}
this->input_mask_ = (uint16_t(data[1]) << 8) | data[0];
this->status_clear_warning();
return true;
}
@@ -123,6 +121,11 @@ bool PCF8574Component::write_gpio_() {
float PCF8574Component::get_setup_priority() const { return setup_priority::IO; }
void PCF8574GPIOPin::setup() { pin_mode(flags_); }
void PCF8574GPIOPin::set_flags(gpio::Flags flags) {
this->flags_ = flags;
if (flags & gpio::FLAG_INPUT)
this->parent_->register_input_pin(this->pin_);
}
void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
+10 -1
View File
@@ -16,6 +16,9 @@ class PCF8574Component final : public Component,
PCF8574Component() = default;
void set_pcf8575(bool pcf8575) { pcf8575_ = pcf8575; }
void set_reset(bool reset) { this->reset_ = reset; }
// Called by pin setters before any component's setup or GPIO writes.
void register_input_pin(uint8_t pin) { this->configured_input_pins_ |= 1 << pin; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
/// Check i2c availability and setup masks
@@ -44,6 +47,11 @@ class PCF8574Component final : public Component,
uint16_t output_mask_{0x00};
/// The state read in read_gpio_ - 1 means HIGH, 0 means LOW
uint16_t input_mask_{0x00};
// Pins configured as inputs before setup; 1 means input. With reset disabled, all other pins
// (including undeclared pins) are treated as outputs seeded from their observed levels.
uint16_t configured_input_pins_{0x00};
// Keep in sync with DEFAULT_RESET in __init__.py; codegen omits the setter for the default.
bool reset_{true};
bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574
InternalGPIOPin *interrupt_pin_{nullptr};
};
@@ -63,7 +71,8 @@ class PCF8574GPIOPin final : public GPIOPin {
void set_parent(PCF8574Component *parent) { parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
// Call after set_parent() and set_pin(): input flags register the pin with the parent.
void set_flags(gpio::Flags flags);
gpio::Flags get_flags() const override { return this->flags_; }
+1 -1
View File
@@ -35,7 +35,7 @@ CONFIG_SCHEMA = cv.ensure_list(
async def to_code(config: list[ConfigType]) -> None:
cg.add_library("wjtje/qr-code-generator-library", "^1.7.0")
cg.add_library("wjtje/qr-code-generator-library", "1.7.0")
for entry in config:
var = cg.new_Pvariable(entry[CONF_ID])
+2 -1
View File
@@ -8,12 +8,13 @@
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include "esp_lcd_panel_rgb.h"
namespace esphome::qspi_dbi {
constexpr static const char *const TAG = "display.qspi_dbi";
ESPHOME_LOG_TAG(TAG, "display.qspi_dbi");
static const uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11;
static const uint8_t NORON = 0x13;
+1 -1
View File
@@ -16,7 +16,7 @@
namespace esphome::rpi_dpi_rgb {
constexpr static const char *const TAG = "rpi_dpi_rgb";
ESPHOME_LOG_TAG(TAG, "rpi_dpi_rgb");
class RpiDpiRgb final : public display::Display {
public:
+11 -2
View File
@@ -14,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@glmnet", "@ximex"]
DOMAIN = "rtttl"
CONF_RTTTL = "rtttl"
# The player keeps the song length in 16 bits
MAX_SONG_LENGTH = 65535
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
rtttl_ns = cg.esphome_ns.namespace("rtttl")
@@ -97,16 +99,23 @@ async def to_code(config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
def _static_song(config: ConfigType, value: str) -> str:
# A constant song plays straight from static storage, flash on ESP8266, with no copy per play
return f"esphome::rtttl::StaticSong{{{cg.FlashStringLiteral(value)}}}"
automation.register_apply_action(
"rtttl.play",
cv.maybe_simple_value(
{
cv.GenerateID(CONF_ID): cv.use_id(Rtttl),
cv.Required(CONF_RTTTL): cv.templatable(cv.string),
cv.Required(CONF_RTTTL): cv.templatable(
cv.All(cv.string, cv.ByteLength(max=MAX_SONG_LENGTH))
),
},
key=CONF_RTTTL,
),
automation.ApplyField(CONF_RTTTL, "play", cg.std_string),
automation.ApplyField(CONF_RTTTL, "play", cg.std_string, const_fn=_static_song),
)
automation.register_apply_action(
+82 -24
View File
@@ -1,5 +1,8 @@
#include "rtttl.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -147,14 +150,14 @@ void Rtttl::loop() {
#endif // USE_SPEAKER
// Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
while (this->position_ < this->rtttl_.length()) {
char c = this->rtttl_[this->position_];
while (this->position_ < this->song_len_) {
char c = this->song_at_(this->position_);
if (c != ',' && c != ' ')
break;
this->position_++;
}
if (this->position_ >= this->rtttl_.length()) {
if (this->position_ >= this->song_len_) {
this->finish_();
return;
}
@@ -169,12 +172,12 @@ void Rtttl::loop() {
this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_;
}
uint8_t note_index_in_octave = note_index_from_char(this->rtttl_[this->position_]);
uint8_t note_index_in_octave = note_index_from_char(this->song_at_(this->position_));
this->position_++;
// Now, get optional '#' sharp
if (this->rtttl_[this->position_] == '#') {
if (this->song_at_(this->position_) == '#') {
note_index_in_octave++;
this->position_++;
}
@@ -192,7 +195,7 @@ void Rtttl::loop() {
}
// Now, get optional '.' dotted note
if (this->rtttl_[this->position_] == '.') {
if (this->song_at_(this->position_) == '.') {
this->note_duration_ += this->note_duration_ / 2; // Duration +50%
this->position_++;
}
@@ -264,15 +267,68 @@ void Rtttl::loop() {
}
void Rtttl::play(std::string rtttl) {
if (this->state_ != State::STOPPED && this->state_ != State::STOPPING) {
size_t pos = this->rtttl_.find(':');
size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length();
ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str());
if (this->is_busy_()) {
return;
}
this->owned_song_ = std::move(rtttl);
this->set_song_(this->owned_song_.data(), this->owned_song_.size());
this->start_();
}
this->rtttl_ = std::move(rtttl);
void Rtttl::play(StaticSong song) {
if (this->is_busy_()) {
return;
}
// Release a previously owned song; the static one needs no storage.
std::string().swap(this->owned_song_);
const char *data = reinterpret_cast<const char *>(song.rtttl);
this->set_song_(data, ESPHOME_strlen_P(data));
this->start_();
}
void Rtttl::set_song_(const char *song, size_t len) {
this->song_ = song;
// Anything past what the 16 bit length holds is dropped; the validator keeps configured songs shorter.
this->song_len_ = static_cast<uint16_t>(std::min<size_t>(len, std::numeric_limits<uint16_t>::max()));
}
bool Rtttl::is_busy_() const {
if (this->state_ == State::STOPPED || this->state_ == State::STOPPING) {
return false;
}
char name[SONG_NAME_LENGTH_LIMIT + 1];
this->song_name_(name, sizeof(name), this->song_find_(':', 0, this->song_len_));
ESP_LOGW(TAG, "Already playing: %s", name);
return true;
}
size_t Rtttl::song_find_(char c, size_t from, size_t end) const {
for (size_t pos = from; pos < end; pos++) {
if (this->song_at_(pos) == c)
return pos;
}
return end;
}
size_t Rtttl::song_find_control_(char key, size_t from, size_t end) const {
size_t pos = from;
while ((pos = this->song_find_(key, pos, end)) < end) {
if (this->song_at_(pos + 1) == '=')
return pos;
pos++;
}
return end;
}
void Rtttl::song_name_(char *buf, size_t size, size_t name_len) const {
size_t len = std::min(name_len, size - 1);
for (size_t pos = 0; pos < len; pos++) {
buf[pos] = this->song_at_(pos);
}
buf[len] = '\0';
}
void Rtttl::start_() {
this->default_note_denominator_ = DEFAULT_NOTE_DENOMINATOR;
this->default_octave_ = DEFAULT_OCTAVE;
this->note_duration_ = 0;
@@ -281,9 +337,9 @@ void Rtttl::play(std::string rtttl) {
uint16_t num; // Used for: default note-denominator, default octave, BPM
// Get name
this->position_ = this->rtttl_.find(':');
this->position_ = this->song_find_(':', 0, this->song_len_);
if (this->position_ == std::string::npos) {
if (this->position_ == this->song_len_) {
ESP_LOGE(TAG, "Unable to determine name; missing ':'");
return;
}
@@ -292,18 +348,20 @@ void Rtttl::play(std::string rtttl) {
static_cast<unsigned>(SONG_NAME_LENGTH_LIMIT));
return;
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
char name[SONG_NAME_LENGTH_LIMIT + 1];
this->song_name_(name, sizeof(name), this->position_);
ESP_LOGD(TAG, "Playing song %s", name);
size_t name_end_position = this->position_;
size_t control_end = this->rtttl_.find(':', name_end_position + 1);
if (control_end == std::string::npos) {
size_t control_end = this->song_find_(':', name_end_position + 1, this->song_len_);
if (control_end == this->song_len_) {
ESP_LOGE(TAG, "Missing second ':'");
return;
}
// Get default duration
size_t pos = this->rtttl_.find("d=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
size_t pos = this->song_find_control_('d', name_end_position, control_end);
if (pos == control_end) {
ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
} else {
this->position_ = pos + 2;
@@ -317,8 +375,8 @@ void Rtttl::play(std::string rtttl) {
}
// Get default octave
pos = this->rtttl_.find("o=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
pos = this->song_find_control_('o', name_end_position, control_end);
if (pos == control_end) {
ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
} else {
this->position_ = pos + 2;
@@ -332,8 +390,8 @@ void Rtttl::play(std::string rtttl) {
}
// Get BPM
pos = this->rtttl_.find("b=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
pos = this->song_find_control_('b', name_end_position, control_end);
if (pos == control_end) {
ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
} else {
this->position_ = pos + 2;
@@ -387,7 +445,7 @@ void Rtttl::stop() {
}
#endif // USE_SPEAKER
this->position_ = this->rtttl_.length();
this->position_ = this->song_len_;
this->note_duration_ = 0;
}
@@ -411,7 +469,7 @@ void Rtttl::finish_() {
#endif // USE_SPEAKER
// Ensure no more notes are played in case finish_() is called for an error.
this->position_ = this->rtttl_.length();
this->position_ = this->song_len_;
this->note_duration_ = 0;
}
+53 -16
View File
@@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#ifdef USE_OUTPUT
#include "esphome/components/output/float_output.h"
@@ -14,6 +15,10 @@
namespace esphome::rtttl {
namespace testing {
class RtttlParserAccess;
} // namespace testing
inline constexpr uint8_t DEFAULT_NOTE_DENOMINATOR = 4; // Default note-denominator (quarter note)
inline constexpr uint8_t DEFAULT_OCTAVE =
6; // Default octave for a note (see: `MIN_OCTAVE`, `MAX_OCTAVE` in `rtttl.cpp`)
@@ -26,6 +31,11 @@ enum class State : uint8_t {
STOPPING,
};
/// A song in storage that outlives playback, such as a literal from generated code; PROGMEM on ESP8266.
struct StaticSong {
ProgmemStr rtttl;
};
class Rtttl final : public Component {
public:
#ifdef USE_OUTPUT
@@ -38,7 +48,10 @@ class Rtttl final : public Component {
void dump_config() override;
void loop() override;
/// Play a song; the string is copied so it may come from anywhere.
void play(std::string rtttl);
/// Play a song from static storage without copying it.
void play(StaticSong song);
void stop();
float get_gain() { return this->gain_; }
@@ -55,11 +68,29 @@ class Rtttl final : public Component {
protected:
inline uint16_t get_integer_() {
uint16_t ret = 0;
while (isdigit(this->rtttl_[this->position_])) {
ret = (ret * 10) + (this->rtttl_[this->position_++] - '0');
while (isdigit(this->song_at_(this->position_))) {
ret = (ret * 10) + (this->song_at_(this->position_++) - '0');
}
return ret;
}
/// The byte of the song at pos, or NUL past its end; the read is flash safe for a PROGMEM song on ESP8266.
char song_at_(size_t pos) const {
if (pos >= this->song_len_)
return '\0';
return static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(this->song_) + pos));
}
/// Index of the first `c` in [from, end), or `end` when absent.
size_t song_find_(char c, size_t from, size_t end) const;
/// Index of `key` followed by '=' in [from, end), or `end` when absent.
size_t song_find_control_(char key, size_t from, size_t end) const;
/// Copies the first name_len bytes of the song, its name, into buf for logging.
void song_name_(char *buf, size_t size, size_t name_len) const;
/// Points the view at a song, clamped to what song_len_ can hold.
void set_song_(const char *song, size_t len);
/// Parses the header and starts playback of the song the view points at.
void start_();
/// Logs and returns true when a song is still playing, so a new one must wait.
bool is_busy_() const;
/**
* @brief Finalizes the playback of the RTTTL string.
*
@@ -70,26 +101,30 @@ class Rtttl final : public Component {
void finish_();
void set_state_(State state);
/// The RTTTL string to play.
std::string rtttl_;
/// The current position in the RTTTL string.
size_t position_{0};
/// The default duration of a note (e.g. 4 for a quarter note).
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
/// The default octave for a note.
uint8_t default_octave_{DEFAULT_OCTAVE};
/// The duration of the current note in milliseconds.
uint16_t note_duration_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_duration_;
/// The current state of the RTTTL player.
State state_{State::STOPPED};
/// The song being played; points at owned_song_ or at static storage.
const char *song_{nullptr};
/// Backing store for play(std::string).
std::string owned_song_;
/// The time in milliseconds since microcontroller boot when the last note was started.
uint32_t last_note_start_time_;
/// The frequency of the current note in Hz.
uint32_t output_freq_{0};
/// The gain of the output.
float gain_{0.6f};
/// Length of the song in bytes; songs are a few hundred bytes, so 16 bits is ample.
uint16_t song_len_{0};
/// The current position in the song.
uint16_t position_{0};
/// The duration of the current note in milliseconds.
uint16_t note_duration_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_duration_;
/// The default duration of a note (e.g. 4 for a quarter note).
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
/// The default octave for a note.
uint8_t default_octave_{DEFAULT_OCTAVE};
/// The current state of the RTTTL player.
State state_{State::STOPPED};
#ifdef USE_OUTPUT
/// The output to write the sound to.
@@ -113,6 +148,8 @@ class Rtttl final : public Component {
/// The callback to call when playback is finished.
CallbackManager<void()> on_finished_playback_callback_;
#endif
friend class testing::RtttlParserAccess;
};
} // namespace esphome::rtttl
+1 -1
View File
@@ -115,7 +115,7 @@ class PNGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_PNG")
cg.add_library("pngle", "1.1.0")
cg.add_library("kikuchan98/pngle", "1.1.0")
class QOIFormat(Format):
@@ -15,7 +15,7 @@ class Component; // Forward declaration
namespace runtime_stats {
static const char *const TAG = "runtime_stats";
ESPHOME_LOG_TAG(TAG, "runtime_stats");
class RuntimeStatsCollector final {
public:
+7 -3
View File
@@ -295,8 +295,11 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
}
void loop() override {
if (this->num_running_ == 0)
if (this->num_running_ == 0) {
// Nothing left to wait for, e.g. once the last queued item has run
this->disable_loop();
return;
}
if (this->script_->is_running())
return;
@@ -305,9 +308,10 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
// Processing all items in a while loop causes infinite loops because
// play_next_() can trigger more items to be queued
if (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
std::apply([this](auto &...args) { this->play_next_(args...); }, params);
// Take the item out first: the chain may call stop(), which clears the queue
auto params = std::move(this->param_queue_.front());
this->param_queue_.pop_front();
std::apply([this](auto &...args) { this->play_next_(args...); }, params);
} else {
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
+1 -1
View File
@@ -13,7 +13,7 @@
namespace esphome::sdl {
constexpr static const char *const TAG = "sdl";
ESPHOME_LOG_TAG(TAG, "sdl");
class Sdl final : public display::Display, public snapshot::Snapshot {
public:
@@ -7,7 +7,7 @@
namespace esphome::spi_led_strip {
static const char *const TAG = "spi_led_strip";
ESPHOME_LOG_TAG(TAG, "spi_led_strip");
class SpiLedStrip final : public light::AddressableLight,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
+2 -1
View File
@@ -8,13 +8,14 @@
#include "esphome/core/component.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
#include "esphome/core/log.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
namespace esphome::st7701s {
constexpr static const char *const TAG = "display.st7701s";
ESPHOME_LOG_TAG(TAG, "display.st7701s");
const uint8_t SW_RESET_CMD = 0x01;
const uint8_t SLEEP_OUT = 0x11;
const uint8_t SDIR_CMD = 0xC7;
+3 -2
View File
@@ -2,12 +2,13 @@
#include "esphome/components/logger/logger.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/time.h"
namespace esphome::syslog {
// Map log levels to syslog severity using an array, indexed by ESPHome log level (1-7)
constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = {
static constexpr uint8_t LOG_LEVEL_TO_SYSLOG_SEVERITY[] PROGMEM = {
3, // NONE
3, // ERROR
4, // WARN
@@ -35,7 +36,7 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
// Syslog PRI calculation: facility * 8 + severity
int severity = 7;
if ((unsigned) level <= 7) {
severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level];
severity = progmem_read_byte(&LOG_LEVEL_TO_SYSLOG_SEVERITY[level]);
}
int pri = this->facility_ * 8 + severity;
+11 -6
View File
@@ -240,12 +240,6 @@ def validate_thermostat(config: ConfigType) -> ConfigType:
CONF_SUPPLEMENTAL_HEATING_ACTION,
CONF_SUPPLEMENTAL_HEATING_DELTA,
],
CONF_MIN_COOLING_OFF_TIME: [
CONF_COOL_ACTION,
],
CONF_MIN_COOLING_RUN_TIME: [
CONF_COOL_ACTION,
],
CONF_MIN_FANNING_OFF_TIME: [
CONF_FAN_ONLY_ACTION,
],
@@ -284,6 +278,17 @@ def validate_thermostat(config: ConfigType) -> ConfigType:
f"{req_trigger} must be defined to use {config_trigger}"
)
# Dry mode runs on the cooling timers, so a dehumidifier needs no cool_action.
for timer in (CONF_MIN_COOLING_OFF_TIME, CONF_MIN_COOLING_RUN_TIME):
if (
timer in config
and CONF_COOL_ACTION not in config
and CONF_DRY_ACTION not in config
):
raise cv.Invalid(
f"{CONF_COOL_ACTION} or {CONF_DRY_ACTION} must be defined to use {timer}"
)
if CONF_FAN_ONLY_ACTION in config:
# determine validation requirements based on fan_only_action_uses_fan_mode_timer setting
if config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] is True:
+44
View File
@@ -1,10 +1,54 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import tmc22xx
import esphome.config_validation as cv
from esphome.const import CONF_THRESHOLD
CODEOWNERS = ["@remcom"]
DOMAIN = "tmc2209"
CONF_SEDN = "sedn"
CONF_SEIMIN = "seimin"
CONF_SEMAX = "semax"
CONF_SEMIN = "semin"
CONF_SEUP = "seup"
CONF_TCOOL_THRESHOLD = "tcool_threshold"
tmc2209_ns = cg.esphome_ns.namespace("tmc2209")
TMC2209Stepper = tmc2209_ns.class_("TMC2209Stepper", tmc22xx.TMC22XXStepper)
tmc22xx.register_actions("tmc2209", TMC2209Stepper)
automation.register_apply_action(
"tmc2209.stallguard",
cv.Schema(
{
cv.GenerateID(): cv.use_id(TMC2209Stepper),
cv.Optional(CONF_THRESHOLD): cv.templatable(cv.uint8_t),
cv.Optional(CONF_TCOOL_THRESHOLD): cv.templatable(
cv.int_range(0, 2**20 - 1)
),
}
),
automation.ApplyField(CONF_THRESHOLD, "set_stallguard_threshold", cg.uint8),
automation.ApplyField(CONF_TCOOL_THRESHOLD, "set_tcool_threshold", cg.uint32),
)
automation.register_apply_action(
"tmc2209.coolconf",
cv.Schema(
{
cv.GenerateID(): cv.use_id(TMC2209Stepper),
cv.Optional(CONF_SEMIN): cv.templatable(cv.int_range(0, 15)),
cv.Optional(CONF_SEMAX): cv.templatable(cv.int_range(0, 15)),
cv.Optional(CONF_SEUP): cv.templatable(cv.int_range(0, 3)),
cv.Optional(CONF_SEDN): cv.templatable(cv.int_range(0, 3)),
cv.Optional(CONF_SEIMIN): cv.templatable(cv.boolean),
}
),
automation.ApplyField(CONF_SEMIN, "set_semin", cg.uint8),
automation.ApplyField(CONF_SEMAX, "set_semax", cg.uint8),
automation.ApplyField(CONF_SEUP, "set_seup", cg.uint8),
automation.ApplyField(CONF_SEDN, "set_sedn", cg.uint8),
automation.ApplyField(CONF_SEIMIN, "set_seimin", cg.bool_),
)
@@ -0,0 +1,87 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.tmc22xx import CONF_ANALOG_CURRENT_SCALE
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_CURRENT,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_AMPERE,
UNIT_PERCENT,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
from .. import TMC2209Stepper, tmc2209_ns
CONF_ACTUAL_CURRENT = "actual_current"
CONF_MOTOR_LOAD = "motor_load"
CONF_PWM_GRAD_AUTO = "pwm_grad_auto"
CONF_PWM_OFS_AUTO = "pwm_ofs_auto"
CONF_PWM_SCALE_AUTO = "pwm_scale_auto"
CONF_PWM_SCALE_SUM = "pwm_scale_sum"
CONF_STALLGUARD_RESULT = "stallguard_result"
CONF_TMC2209_ID = "tmc2209_id"
TMC2209Sensor = tmc2209_ns.class_("TMC2209Sensor", cg.PollingComponent)
_diagnostic_schema = sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(TMC2209Sensor),
cv.GenerateID(CONF_TMC2209_ID): cv.use_id(TMC2209Stepper),
cv.Optional(CONF_ACTUAL_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MOTOR_LOAD): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_STALLGUARD_RESULT): _diagnostic_schema,
cv.Optional(CONF_PWM_SCALE_SUM): _diagnostic_schema,
cv.Optional(CONF_PWM_SCALE_AUTO): _diagnostic_schema,
cv.Optional(CONF_PWM_OFS_AUTO): _diagnostic_schema,
cv.Optional(CONF_PWM_GRAD_AUTO): _diagnostic_schema,
}
).extend(cv.polling_component_schema("60s"))
def _final_validate(config: ConfigType) -> ConfigType:
if CONF_ACTUAL_CURRENT not in config:
return config
full_config = fv.full_config.get()
path = full_config.get_path_for_id(config[CONF_TMC2209_ID])[:-1]
if full_config.get_config_for_path(path).get(CONF_ANALOG_CURRENT_SCALE):
raise cv.Invalid(
f"'{CONF_ACTUAL_CURRENT}' can't be used with '{CONF_ANALOG_CURRENT_SCALE}', "
"the current then depends on VREF",
path=[CONF_ACTUAL_CURRENT],
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_TMC2209_ID])
var = cg.new_Pvariable(config[CONF_ID], hub)
await cg.register_component(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_ACTUAL_CURRENT, var.set_actual_current_sensor)
await sensors(CONF_STALLGUARD_RESULT, var.set_stallguard_result_sensor)
await sensors(CONF_MOTOR_LOAD, var.set_motor_load_sensor)
await sensors(CONF_PWM_SCALE_SUM, var.set_pwm_scale_sum_sensor)
await sensors(CONF_PWM_SCALE_AUTO, var.set_pwm_scale_auto_sensor)
await sensors(CONF_PWM_OFS_AUTO, var.set_pwm_ofs_auto_sensor)
await sensors(CONF_PWM_GRAD_AUTO, var.set_pwm_grad_auto_sensor)
@@ -0,0 +1,95 @@
#include "tmc2209_sensor.h"
#include "esphome/core/log.h"
#include <cmath>
namespace esphome::tmc2209 {
ESPHOME_LOG_TAG(TAG, "tmc2209.sensor");
using tmc22xx::TMC22XXStepper;
// Highest StallGuard result, reached with no load
static constexpr float STALLGUARD_RESULT_MAX = 510.0f;
static void publish_field(sensor::Sensor *sensor, const optional<uint32_t> &data, const RegisterField &field) {
if (sensor == nullptr)
return;
if (!data.has_value()) {
sensor->publish_state(NAN);
return;
}
uint32_t value = TMC22XXStepper::extract_field(*data, field);
sensor->publish_state(field.is_signed ? static_cast<float>(static_cast<int32_t>(value)) : static_cast<float>(value));
}
void TMC2209Sensor::update() {
if (this->parent_->is_failed())
return;
// Each failed read blocks the loop for the UART timeout. Leave the bus alone while the driver is known to be
// unreachable or after the first failed read; the remaining sensors then publish NaN.
bool reachable = !this->parent_->is_unreachable();
auto read = [this, &reachable](uint8_t reg) -> optional<uint32_t> {
if (!reachable)
return {};
auto data = this->parent_->read_register(reg);
reachable = data.has_value();
return data;
};
if (this->actual_current_sensor_ != nullptr) {
auto drv_status = read(tmc22xx::REG_DRV_STATUS);
float current = NAN;
if (drv_status.has_value()) {
auto scale = static_cast<uint8_t>(TMC22XXStepper::extract_field(*drv_status, tmc22xx::CS_ACTUAL));
current = this->parent_->scale_to_current(scale);
}
this->actual_current_sensor_->publish_state(current);
}
// StallGuard only measures the load while the motor turns
bool moving = !this->parent_->has_reached_target();
if (this->stallguard_result_sensor_ != nullptr || (this->motor_load_sensor_ != nullptr && moving)) {
auto sg_result = read(REG_SG_RESULT);
publish_field(this->stallguard_result_sensor_, sg_result, SG_RESULT);
if (this->motor_load_sensor_ != nullptr) {
// 100% is the load at which a stall is reported, where the result drops below twice the threshold
float stall_result = 2.0f * this->parent_->get_stallguard_threshold();
float load = NAN;
if (moving && sg_result.has_value() && stall_result < STALLGUARD_RESULT_MAX) {
float result = TMC22XXStepper::extract_field(*sg_result, SG_RESULT);
load = (STALLGUARD_RESULT_MAX - result) / (STALLGUARD_RESULT_MAX - stall_result) * 100.0f;
}
this->motor_load_sensor_->publish_state(load);
}
} else if (this->motor_load_sensor_ != nullptr) {
this->motor_load_sensor_->publish_state(NAN);
}
if (this->pwm_scale_sum_sensor_ != nullptr || this->pwm_scale_auto_sensor_ != nullptr) {
auto pwm_scale = read(tmc22xx::REG_PWM_SCALE);
publish_field(this->pwm_scale_sum_sensor_, pwm_scale, tmc22xx::PWM_SCALE_SUM);
publish_field(this->pwm_scale_auto_sensor_, pwm_scale, tmc22xx::PWM_SCALE_AUTO);
}
if (this->pwm_ofs_auto_sensor_ != nullptr || this->pwm_grad_auto_sensor_ != nullptr) {
auto pwm_auto = read(tmc22xx::REG_PWM_AUTO);
publish_field(this->pwm_ofs_auto_sensor_, pwm_auto, tmc22xx::PWM_OFS_AUTO);
publish_field(this->pwm_grad_auto_sensor_, pwm_auto, tmc22xx::PWM_GRAD_AUTO);
}
}
void TMC2209Sensor::dump_config() {
ESP_LOGCONFIG(TAG, "TMC2209 Sensor:");
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Actual Current", this->actual_current_sensor_);
LOG_SENSOR(" ", "StallGuard Result", this->stallguard_result_sensor_);
LOG_SENSOR(" ", "Motor Load", this->motor_load_sensor_);
LOG_SENSOR(" ", "PWM Scale Sum", this->pwm_scale_sum_sensor_);
LOG_SENSOR(" ", "PWM Scale Auto", this->pwm_scale_auto_sensor_);
LOG_SENSOR(" ", "PWM Offset Auto", this->pwm_ofs_auto_sensor_);
LOG_SENSOR(" ", "PWM Gradient Auto", this->pwm_grad_auto_sensor_);
}
} // namespace esphome::tmc2209
@@ -0,0 +1,35 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/tmc2209/tmc2209.h"
#include "esphome/core/component.h"
namespace esphome::tmc2209 {
class TMC2209Sensor : public PollingComponent {
public:
explicit TMC2209Sensor(TMC2209Stepper *parent) : parent_(parent) {}
void update() override;
void dump_config() override;
void set_actual_current_sensor(sensor::Sensor *sensor) { this->actual_current_sensor_ = sensor; }
void set_stallguard_result_sensor(sensor::Sensor *sensor) { this->stallguard_result_sensor_ = sensor; }
void set_motor_load_sensor(sensor::Sensor *sensor) { this->motor_load_sensor_ = sensor; }
void set_pwm_scale_sum_sensor(sensor::Sensor *sensor) { this->pwm_scale_sum_sensor_ = sensor; }
void set_pwm_scale_auto_sensor(sensor::Sensor *sensor) { this->pwm_scale_auto_sensor_ = sensor; }
void set_pwm_ofs_auto_sensor(sensor::Sensor *sensor) { this->pwm_ofs_auto_sensor_ = sensor; }
void set_pwm_grad_auto_sensor(sensor::Sensor *sensor) { this->pwm_grad_auto_sensor_ = sensor; }
protected:
TMC2209Stepper *parent_;
sensor::Sensor *actual_current_sensor_{nullptr};
sensor::Sensor *stallguard_result_sensor_{nullptr};
sensor::Sensor *motor_load_sensor_{nullptr};
sensor::Sensor *pwm_scale_sum_sensor_{nullptr};
sensor::Sensor *pwm_scale_auto_sensor_{nullptr};
sensor::Sensor *pwm_ofs_auto_sensor_{nullptr};
sensor::Sensor *pwm_grad_auto_sensor_{nullptr};
};
} // namespace esphome::tmc2209
+26 -2
View File
@@ -1,4 +1,6 @@
from esphome import automation
from esphome.components import tmc22xx
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import TMC2209Stepper
@@ -6,9 +8,31 @@ from . import TMC2209Stepper
AUTO_LOAD = ["tmc22xx"]
DEPENDENCIES = ["uart"]
CONFIG_SCHEMA = tmc22xx.tmc22xx_schema(TMC2209Stepper, max_address=3)
CONF_ON_STALL = "on_stall"
def _validate_stall_needs_diag(config: ConfigType) -> ConfigType:
if CONF_ON_STALL in config and tmc22xx.CONF_DIAG_PIN not in config:
raise cv.Invalid(
f"'{CONF_ON_STALL}' requires '{tmc22xx.CONF_DIAG_PIN}', stalls are only reported on DIAG"
)
return config
CONFIG_SCHEMA = cv.All(
tmc22xx.tmc22xx_schema(
TMC2209Stepper,
max_address=3,
extra={cv.Optional(CONF_ON_STALL): automation.validate_automation({})},
),
_validate_stall_needs_diag,
)
FINAL_VALIDATE_SCHEMA = tmc22xx.final_validate
async def to_code(config: ConfigType) -> None:
await tmc22xx.new_tmc22xx(config)
var = await tmc22xx.new_tmc22xx(config)
for conf in config.get(CONF_ON_STALL, []):
await automation.build_callback_automation(
var, "add_on_stall_callback", [], conf
)
+20
View File
@@ -10,4 +10,24 @@ void TMC2209Stepper::dump_config() {
TMC22XXStepper::dump_config();
}
uint32_t *TMC2209Stepper::shadow_register(uint8_t reg) {
switch (reg) {
case REG_TCOOLTHRS:
return &this->tcoolthrs_;
case REG_SGTHRS:
return &this->sgthrs_;
case REG_COOLCONF:
return &this->coolconf_;
default:
return TMC22XXStepper::shadow_register(reg);
}
}
void TMC2209Stepper::restore_registers() {
TMC22XXStepper::restore_registers();
this->write_register(REG_TCOOLTHRS, this->tcoolthrs_);
this->write_register(REG_SGTHRS, this->sgthrs_);
this->write_register(REG_COOLCONF, this->coolconf_);
}
} // namespace esphome::tmc2209
+42
View File
@@ -4,12 +4,54 @@
namespace esphome::tmc2209 {
using tmc22xx::RegisterField;
// StallGuard and CoolStep registers, which the TMC2208 does not have
static constexpr uint8_t REG_TCOOLTHRS = 0x14;
static constexpr uint8_t REG_SGTHRS = 0x40;
static constexpr uint8_t REG_SG_RESULT = 0x41;
static constexpr uint8_t REG_COOLCONF = 0x42;
static constexpr RegisterField TCOOLTHRS{REG_TCOOLTHRS, 0, 20, false};
static constexpr RegisterField SGTHRS{REG_SGTHRS, 0, 8, false};
static constexpr RegisterField SG_RESULT{REG_SG_RESULT, 0, 10, false};
static constexpr RegisterField SEMIN{REG_COOLCONF, 0, 4, false};
static constexpr RegisterField SEUP{REG_COOLCONF, 5, 2, false};
static constexpr RegisterField SEMAX{REG_COOLCONF, 8, 4, false};
static constexpr RegisterField SEDN{REG_COOLCONF, 13, 2, false};
static constexpr RegisterField SEIMIN{REG_COOLCONF, 15, 1, false};
class TMC2209Stepper : public tmc22xx::TMC22XXStepper {
public:
void dump_config() override;
/// Called when DIAG reports a stall. StallGuard only works in StealthChop, between TCOOLTHRS and TPWMTHRS.
template<typename F> void add_on_stall_callback(F &&callback) {
this->stall_callback_.add(std::forward<F>(callback));
}
/// A stall is reported when the StallGuard result drops below twice this value.
void set_stallguard_threshold(uint8_t threshold) { this->write_field(SGTHRS, threshold); }
uint8_t get_stallguard_threshold() const { return extract_field(this->sgthrs_, SGTHRS); }
/// Lower velocity limit, as TSTEP, for CoolStep and the StallGuard output on DIAG.
void set_tcool_threshold(uint32_t threshold) { this->write_field(TCOOLTHRS, threshold); }
void set_semin(uint8_t semin) { this->write_field(SEMIN, semin); }
void set_semax(uint8_t semax) { this->write_field(SEMAX, semax); }
void set_seup(uint8_t seup) { this->write_field(SEUP, seup); }
void set_sedn(uint8_t sedn) { this->write_field(SEDN, sedn); }
void set_seimin(bool seimin) { this->write_field(SEIMIN, seimin); }
protected:
uint8_t expected_version() const override { return 0x21; }
uint32_t *shadow_register(uint8_t reg) override;
void restore_registers() override;
void on_diag_without_error() override { this->stall_callback_.call(); }
LazyCallbackManager<void()> stall_callback_;
// Write-only registers with their power-on values
uint32_t tcoolthrs_{0};
uint32_t sgthrs_{0};
uint32_t coolconf_{0};
};
} // namespace esphome::tmc2209
+17 -1
View File
@@ -21,6 +21,7 @@ DEPENDENCIES = ["uart"]
CONF_ANALOG_CURRENT_SCALE = "analog_current_scale"
CONF_CLOCK_FREQUENCY = "clock_frequency"
CONF_DIAG_PIN = "diag_pin"
CONF_ENABLE_SPREADCYCLE = "enable_spreadcycle"
CONF_ENN_PIN = "enn_pin"
CONF_HOLD_CURRENT = "hold_current"
@@ -31,6 +32,7 @@ CONF_INTERPOLATION = "interpolation"
CONF_INVERSE_DIRECTION = "inverse_direction"
CONF_IRUN = "irun"
CONF_MICROSTEPS = "microsteps"
CONF_ON_STATUS = "on_status"
CONF_OTTRIM = "ottrim"
CONF_RSENSE = "rsense"
CONF_RUN_CURRENT = "run_current"
@@ -44,6 +46,7 @@ TMC22XXStepper = tmc22xx_ns.class_(
"TMC22XXStepper", stepper.Stepper, cg.Component, uart.UARTDevice
)
StandstillMode = tmc22xx_ns.enum("StandstillMode")
DriverEvent = tmc22xx_ns.enum("DriverEvent")
STANDSTILL_MODES = {
"normal": StandstillMode.STANDSTILL_MODE_NORMAL,
@@ -56,7 +59,9 @@ validate_microsteps = cv.one_of(1, 2, 4, 8, 16, 32, 64, 128, 256, int=True)
validate_current = cv.All(cv.current, cv.positive_float)
def tmc22xx_schema(cls: MockObjClass, max_address: int) -> cv.Schema:
def tmc22xx_schema(
cls: MockObjClass, max_address: int, extra: dict | None = None
) -> cv.Schema:
"""Return the stepper schema for a family member; `max_address` is 0 for chips without address pins."""
return cv.All(
stepper.STEPPER_SCHEMA.extend(
@@ -67,6 +72,7 @@ def tmc22xx_schema(cls: MockObjClass, max_address: int) -> cv.Schema:
cv.Optional(CONF_STEP_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_DIR_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_INDEX_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_DIAG_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_CLOCK_FREQUENCY, default="12MHz"): cv.All(
cv.frequency, cv.int_range(min=1)
),
@@ -79,6 +85,8 @@ def tmc22xx_schema(cls: MockObjClass, max_address: int) -> cv.Schema:
cv.Optional(CONF_RUN_CURRENT): validate_current,
cv.Optional(CONF_HOLD_CURRENT): validate_current,
cv.Optional(CONF_MICROSTEPS, default=16): validate_microsteps,
cv.Optional(CONF_ON_STATUS): automation.validate_automation({}),
**(extra or {}),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -133,6 +141,7 @@ async def new_tmc22xx(config: ConfigType) -> cg.Pvariable:
(CONF_STEP_PIN, var.set_step_pin),
(CONF_DIR_PIN, var.set_dir_pin),
(CONF_INDEX_PIN, var.set_index_pin),
(CONF_DIAG_PIN, var.set_diag_pin),
):
if (pin := config.get(key)) is not None:
cg.add(setter(await cg.gpio_pin_expression(pin)))
@@ -145,6 +154,13 @@ async def new_tmc22xx(config: ConfigType) -> cg.Pvariable:
):
if (value := config.get(key)) is not None:
cg.add(setter(value))
for conf in config.get(CONF_ON_STATUS, []):
await automation.build_callback_automation(
var,
"add_on_status_callback",
[(DriverEvent, "event"), (cg.bool_, "active")],
conf,
)
return var
+171 -15
View File
@@ -23,6 +23,49 @@ static constexpr float INTERNAL_RSENSE_OHM = 0.17f; // Equivalent sense resis
static constexpr float APPROACH_TIME_S = 0.02f;
// Skip VACTUAL writes that change the speed by less than 1/16, each write blocks the loop for the UART transfer
static constexpr int32_t VACTUAL_CHANGE_DIVISOR = 16;
static constexpr uint32_t STATUS_POLL_INTERVAL_MS = 1000;
// While the driver does not answer, for example without motor supply, only poll every 10 s; each failed read
// blocks the loop for the UART timeout
static constexpr uint8_t UNREACHABLE_POLL_SKIP = 9;
static constexpr uint32_t GSTAT_FLAGS = 0x07; // reset, drv_err, uv_cp
static constexpr uint32_t DRV_STATUS_FLAGS = 0x0FFF;
static const LogString *driver_event_to_string(DriverEvent event) {
switch (event) {
case DRIVER_EVENT_RESET:
return LOG_STR("Driver reset");
case DRIVER_EVENT_DRIVER_ERROR:
return LOG_STR("Driver error");
case DRIVER_EVENT_CHARGE_PUMP_UNDERVOLTAGE:
return LOG_STR("Charge pump undervoltage");
case DRIVER_EVENT_OVERTEMPERATURE_PREWARNING:
return LOG_STR("Overtemperature prewarning");
case DRIVER_EVENT_OVERTEMPERATURE:
return LOG_STR("Overtemperature shutdown");
case DRIVER_EVENT_SHORT_TO_GROUND_A:
return LOG_STR("Short to ground on phase A");
case DRIVER_EVENT_SHORT_TO_GROUND_B:
return LOG_STR("Short to ground on phase B");
case DRIVER_EVENT_LOW_SIDE_SHORT_A:
return LOG_STR("Low side short on phase A");
case DRIVER_EVENT_LOW_SIDE_SHORT_B:
return LOG_STR("Low side short on phase B");
case DRIVER_EVENT_OPEN_LOAD_A:
return LOG_STR("Open load on phase A");
case DRIVER_EVENT_OPEN_LOAD_B:
return LOG_STR("Open load on phase B");
case DRIVER_EVENT_TEMPERATURE_120C:
return LOG_STR("Temperature above 120C");
case DRIVER_EVENT_TEMPERATURE_143C:
return LOG_STR("Temperature above 143C");
case DRIVER_EVENT_TEMPERATURE_150C:
return LOG_STR("Temperature above 150C");
case DRIVER_EVENT_TEMPERATURE_157C:
return LOG_STR("Temperature above 157C");
default:
return LOG_STR("Unknown");
}
}
// CRC-8 with polynomial 0x07 over the datagram bits in the order they go out on the wire, LSB first
static uint8_t datagram_crc(const uint8_t *data, size_t len) {
@@ -34,8 +77,19 @@ static uint8_t datagram_crc(const uint8_t *data, size_t len) {
return crc;
}
// Index into RESTORED_REGISTERS, or -1 when the register is not restored after a driver reset
static int restored_index(uint8_t reg) {
for (size_t i = 0; i < RESTORED_REGISTERS.size(); i++) {
if (RESTORED_REGISTERS[i] == reg)
return static_cast<int>(i);
}
return -1;
}
void IRAM_ATTR IndexPulseStore::gpio_intr(IndexPulseStore *arg) { arg->pulses = arg->pulses + arg->direction; }
void IRAM_ATTR DiagPinStore::gpio_intr(DiagPinStore *arg) { arg->triggered = true; }
void TMC22XXStepper::setup() {
if (this->enn_pin_ != nullptr) {
this->enn_pin_->setup();
@@ -53,13 +107,54 @@ void TMC22XXStepper::setup() {
ESP_LOGW(TAG, "Unexpected IC version 0x%02X", this->version_);
}
// Bring write-only registers in line with their cached values and clear the reset flag
this->restore_registers();
this->write_register(REG_GSTAT, GSTAT_FLAGS);
this->apply_config_();
// Currents live in a write-only register, so later changes survive a driver reset through its cached value
if (!std::isnan(this->initial_run_current_))
this->set_run_current(this->initial_run_current_);
if (!std::isnan(this->initial_hold_current_))
this->set_hold_current(this->initial_hold_current_);
if (this->index_pin_ != nullptr) {
this->index_pin_->setup();
this->index_pin_->attach_interrupt(IndexPulseStore::gpio_intr, &this->index_store_, gpio::INTERRUPT_ANY_EDGE);
} else {
this->step_pin_->setup();
this->step_pin_->digital_write(false);
this->dir_pin_->setup();
this->dir_pin_->digital_write(false);
}
if (this->diag_pin_ != nullptr) {
this->diag_pin_->setup();
this->diag_pin_->attach_interrupt(DiagPinStore::gpio_intr, &this->diag_store_, gpio::INTERRUPT_RISING_EDGE);
}
// Polling notices a driver reset, and conditions that clear without a DIAG edge
this->set_interval(STATUS_POLL_INTERVAL_MS, [this]() {
if (this->status_poll_skip_ > 0) {
this->status_poll_skip_--;
return;
}
if (!this->update_status_())
this->status_poll_skip_ = UNREACHABLE_POLL_SKIP;
});
this->set_enabled(true);
}
void TMC22XXStepper::restore_registers() {
this->write_register(REG_IHOLD_IRUN, this->ihold_irun_);
this->write_register(REG_TPOWERDOWN, this->tpowerdown_);
this->write_register(REG_TPWMTHRS, this->tpwmthrs_);
this->write_register(REG_VACTUAL, 0);
this->write_register(REG_GSTAT, 0x07);
for (size_t i = 0; i < RESTORED_REGISTERS.size(); i++) {
if ((this->restored_written_ & (1u << i)) != 0)
this->write_register(RESTORED_REGISTERS[i], this->restored_values_[i]);
}
}
void TMC22XXStepper::apply_config_() {
this->write_field(PDN_DISABLE, true);
this->write_field(MSTEP_REG_SELECT, true);
this->write_field(INTERNAL_RSENSE, std::isnan(this->rsense_));
@@ -76,28 +171,59 @@ void TMC22XXStepper::setup() {
this->toff_ = *toff;
this->set_microsteps(this->initial_microsteps_);
if (!std::isnan(this->initial_run_current_))
this->set_run_current(this->initial_run_current_);
if (!std::isnan(this->initial_hold_current_))
this->set_hold_current(this->initial_hold_current_);
if (this->index_pin_ != nullptr) {
// INDEX toggles on every step of the internal pulse generator that VACTUAL drives
this->write_field(INDEX_OTPW, false);
this->write_field(INDEX_STEP, true);
this->index_pin_->setup();
this->index_pin_->attach_interrupt(IndexPulseStore::gpio_intr, &this->index_store_, gpio::INTERRUPT_ANY_EDGE);
} else {
// Step on both edges, so every step pin toggle is one step
this->write_field(DEDGE, true);
this->write_field(MULTISTEP_FILT, false);
this->step_pin_->setup();
this->step_pin_->digital_write(false);
this->dir_pin_->setup();
this->dir_pin_->digital_write(false);
}
}
this->set_enabled(true);
bool TMC22XXStepper::update_status_() {
auto gstat = this->read_register(REG_GSTAT);
if (!gstat.has_value())
return false;
uint32_t drv_status = 0;
// DRV_STATUS only feeds on_status and the DIAG check, skip the transfer when neither is used
if (this->diag_pin_ != nullptr || !this->status_callback_.empty()) {
auto value = this->read_register(REG_DRV_STATUS);
if (!value.has_value())
return false;
drv_status = *value;
}
// GSTAT flags stay set until written with 1; they are set again while the condition lasts
if ((*gstat & GSTAT_FLAGS) != 0)
this->write_register(REG_GSTAT, *gstat & GSTAT_FLAGS);
uint16_t status = (*gstat & GSTAT_FLAGS) | ((drv_status & DRV_STATUS_FLAGS) << 3);
// Restore before reporting, so an on_status automation for the reset sees and can change the restored settings
if ((status & (1u << DRIVER_EVENT_RESET)) != 0) {
ESP_LOGW(TAG, "Restoring settings after driver reset");
this->restore_registers();
// The motor stopped with the reset, ramp up again from standstill
this->current_speed_ = 0.0f;
this->set_enabled(this->enabled_);
}
uint16_t changed = status ^ this->status_;
this->status_ = status;
for (uint8_t event = 0; event < DRIVER_EVENT_COUNT; event++) {
if ((changed & (1u << event)) == 0)
continue;
bool active = (status & (1u << event)) != 0;
// Open load and temperature levels are informational, the driver keeps running
if (active && event < DRIVER_EVENT_OPEN_LOAD_A) {
ESP_LOGW(TAG, "%s", LOG_STR_ARG(driver_event_to_string(static_cast<DriverEvent>(event))));
} else {
ESP_LOGD(TAG, "%s: %s", LOG_STR_ARG(driver_event_to_string(static_cast<DriverEvent>(event))),
active ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("cleared"));
}
this->status_callback_.call(static_cast<DriverEvent>(event), active);
}
return true;
}
void TMC22XXStepper::dump_config() {
@@ -128,6 +254,7 @@ void TMC22XXStepper::dump_config() {
LOG_PIN(" STEP Pin: ", this->step_pin_);
LOG_PIN(" DIR Pin: ", this->dir_pin_);
LOG_PIN(" INDEX Pin: ", this->index_pin_);
LOG_PIN(" DIAG Pin: ", this->diag_pin_);
LOG_STEPPER(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
@@ -137,12 +264,29 @@ void TMC22XXStepper::dump_config() {
void TMC22XXStepper::loop() {
if (this->index_pin_ != nullptr)
this->add_index_pulses_();
if (this->diag_store_.triggered) {
this->diag_store_.triggered = false;
// DIAG rises on a driver error, any other rise is chip specific
if (!this->update_status_()) {
ESP_LOGW(TAG, "DIAG rose, but the driver status could not be read");
} else if ((this->status_ & GSTAT_FLAGS) == 0) {
this->on_diag_without_error();
}
}
if (this->has_reached_target()) {
this->high_freq_.stop();
this->moving_ = false;
} else {
this->high_freq_.start();
if (!this->enabled_)
this->set_enabled(true);
// An unreachable driver is polled only every 10 s. Read it before the first step, so settings lost when the
// motor supply dropped are restored before the move runs on the chip defaults.
if (!this->moving_ && this->status_poll_skip_ > 0) {
this->status_poll_skip_ = this->update_status_() ? 0 : UNREACHABLE_POLL_SKIP;
}
this->moving_ = true;
}
if (this->index_pin_ != nullptr) {
@@ -311,8 +455,12 @@ bool TMC22XXStepper::write_register(uint8_t reg, uint32_t value) {
}
this->status_clear_warning();
if (uint32_t *shadow = this->shadow_register(reg))
if (uint32_t *shadow = this->shadow_register(reg)) {
*shadow = value;
} else if (int index = restored_index(reg); index >= 0) {
this->restored_values_[index] = value;
this->restored_written_ |= 1u << index;
}
return true;
}
@@ -346,7 +494,15 @@ bool TMC22XXStepper::write_field(const RegisterField &field, uint32_t value) {
// Skip the UART timeout when the driver never answered during setup
if (this->is_failed())
return false;
auto data = this->read_register(field.reg);
optional<uint32_t> data;
int index = restored_index(field.reg);
if (index >= 0 && (this->restored_written_ & (1u << index)) != 0) {
// Modify the last written value, not the chip: after a driver reset that the poll has not seen yet, the chip
// holds its defaults, and those would replace the values to restore
data = this->restored_values_[index];
} else {
data = this->read_register(field.reg);
}
if (!data.has_value())
return false;
uint32_t mask = field.mask();
+64
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cmath>
#include "esphome/components/stepper/stepper.h"
@@ -30,7 +31,13 @@ static constexpr uint8_t REG_TPOWERDOWN = 0x11;
static constexpr uint8_t REG_TPWMTHRS = 0x13;
static constexpr uint8_t REG_VACTUAL = 0x22;
static constexpr uint8_t REG_CHOPCONF = 0x6C;
static constexpr uint8_t REG_DRV_STATUS = 0x6F;
static constexpr uint8_t REG_PWMCONF = 0x70;
static constexpr uint8_t REG_PWM_SCALE = 0x71;
static constexpr uint8_t REG_PWM_AUTO = 0x72;
// Readable registers that a driver reset sets back to their defaults; the last written values are restored
static constexpr std::array<uint8_t, 4> RESTORED_REGISTERS{REG_GCONF, REG_FACTORY_CONF, REG_CHOPCONF, REG_PWMCONF};
static constexpr RegisterField I_SCALE_ANALOG{REG_GCONF, 0, 1, false};
static constexpr RegisterField INTERNAL_RSENSE{REG_GCONF, 1, 1, false};
@@ -54,7 +61,12 @@ static constexpr RegisterField VSENSE{REG_CHOPCONF, 17, 1, false};
static constexpr RegisterField MRES{REG_CHOPCONF, 24, 4, false};
static constexpr RegisterField INTPOL{REG_CHOPCONF, 28, 1, false};
static constexpr RegisterField DEDGE{REG_CHOPCONF, 29, 1, false};
static constexpr RegisterField CS_ACTUAL{REG_DRV_STATUS, 16, 5, false};
static constexpr RegisterField FREEWHEEL{REG_PWMCONF, 20, 2, false};
static constexpr RegisterField PWM_SCALE_SUM{REG_PWM_SCALE, 0, 8, false};
static constexpr RegisterField PWM_SCALE_AUTO{REG_PWM_SCALE, 16, 9, true};
static constexpr RegisterField PWM_OFS_AUTO{REG_PWM_AUTO, 0, 8, false};
static constexpr RegisterField PWM_GRAD_AUTO{REG_PWM_AUTO, 16, 8, false};
enum StandstillMode : uint8_t {
STANDSTILL_MODE_NORMAL = 0,
@@ -63,6 +75,33 @@ enum StandstillMode : uint8_t {
STANDSTILL_MODE_COIL_SHORT_HS = 3,
};
/// Driver conditions reported to on_status, in the order of their status bits: GSTAT bits 0-2, then DRV_STATUS bits
/// 0-11.
enum DriverEvent : uint8_t {
DRIVER_EVENT_RESET = 0,
DRIVER_EVENT_DRIVER_ERROR,
DRIVER_EVENT_CHARGE_PUMP_UNDERVOLTAGE,
DRIVER_EVENT_OVERTEMPERATURE_PREWARNING,
DRIVER_EVENT_OVERTEMPERATURE,
DRIVER_EVENT_SHORT_TO_GROUND_A,
DRIVER_EVENT_SHORT_TO_GROUND_B,
DRIVER_EVENT_LOW_SIDE_SHORT_A,
DRIVER_EVENT_LOW_SIDE_SHORT_B,
DRIVER_EVENT_OPEN_LOAD_A,
DRIVER_EVENT_OPEN_LOAD_B,
DRIVER_EVENT_TEMPERATURE_120C,
DRIVER_EVENT_TEMPERATURE_143C,
DRIVER_EVENT_TEMPERATURE_150C,
DRIVER_EVENT_TEMPERATURE_157C,
DRIVER_EVENT_COUNT,
};
/// Set by a rising edge on DIAG, which signals a driver error or, on chips that support it, a stall.
struct DiagPinStore {
volatile bool triggered{false};
static void gpio_intr(DiagPinStore *arg);
};
/// Counts the step pulses the driver reports on INDEX while it runs from VACTUAL.
struct IndexPulseStore {
volatile int32_t pulses{0};
@@ -84,6 +123,7 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U
void set_step_pin(GPIOPin *pin) { this->step_pin_ = pin; }
void set_dir_pin(GPIOPin *pin) { this->dir_pin_ = pin; }
void set_index_pin(InternalGPIOPin *pin) { this->index_pin_ = pin; }
void set_diag_pin(InternalGPIOPin *pin) { this->diag_pin_ = pin; }
void set_clock_frequency(uint32_t frequency) { this->clock_frequency_ = frequency; }
void set_rsense(float rsense) { this->rsense_ = rsense; }
void set_vsense(bool vsense) { this->vsense_ = vsense; }
@@ -96,6 +136,13 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U
/// Enable or disable the motor outputs, using the ENN pin when configured and TOFF otherwise.
void set_enabled(bool enabled);
bool is_enabled() const { return this->enabled_; }
/// True while the driver does not answer, until the status poll reads it again (every 10 s).
bool is_unreachable() const { return this->status_poll_skip_ > 0; }
/// Called with the condition and whether it became active or cleared.
template<typename F> void add_on_status_callback(F &&callback) {
this->status_callback_.add(std::forward<F>(callback));
}
void set_microsteps(uint16_t microsteps);
uint16_t get_microsteps();
@@ -127,6 +174,15 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U
virtual uint8_t expected_version() const = 0;
/// Return the cached value of a write-only register, or nullptr when the register is readable.
virtual uint32_t *shadow_register(uint8_t reg);
/// Write the cached write-only registers and the last written configuration registers to the driver, at setup
/// and after a driver reset.
virtual void restore_registers();
/// Write the settings from the configuration to the readable registers.
void apply_config_();
/// Called when DIAG rises without a driver error.
virtual void on_diag_without_error() {}
/// Read GSTAT and DRV_STATUS and report changes. Returns false when the status could not be read.
bool update_status_();
optional<uint8_t> current_to_scale_(float current);
float full_scale_current_();
int32_t get_vactual_() const { return static_cast<int32_t>(extract_field(this->vactual_reg_, VACTUAL)); }
@@ -139,28 +195,36 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U
GPIOPin *step_pin_{nullptr};
GPIOPin *dir_pin_{nullptr};
InternalGPIOPin *index_pin_{nullptr};
InternalGPIOPin *diag_pin_{nullptr};
LazyCallbackManager<void(DriverEvent, bool)> status_callback_;
uint32_t clock_frequency_{12000000};
// Write-only registers shared by all family members, with their power-on values
uint32_t ihold_irun_{0x00071703};
uint32_t tpowerdown_{0x14};
uint32_t tpwmthrs_{0};
uint32_t vactual_reg_{0};
std::array<uint32_t, RESTORED_REGISTERS.size()> restored_values_{};
// NaN while not configured
float rsense_{NAN};
float initial_run_current_{NAN};
float initial_hold_current_{NAN};
IndexPulseStore index_store_{};
uint16_t initial_microsteps_{16};
uint16_t status_{0};
optional<bool> vsense_{};
optional<uint8_t> ottrim_{};
uint8_t address_{0};
uint8_t toff_{3};
uint8_t version_{0};
uint8_t restored_written_{0}; // Bit per RESTORED_REGISTERS entry that has been written
uint8_t status_poll_skip_{0};
int8_t direction_{0};
DiagPinStore diag_store_{};
bool analog_current_scale_{false};
bool enabled_{false};
bool vsense_active_{false};
bool step_state_{false};
bool moving_{false};
HighFrequencyLoopRequester high_freq_;
};
+22 -28
View File
@@ -3,8 +3,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <vector>
namespace esphome::toshiba {
struct RacPt1411hwruFanSpeed {
@@ -69,8 +67,8 @@ const uint8_t RAC_PT1411HWRU_CS_FOOTER_COOL = 0x72;
const uint8_t RAC_PT1411HWRU_CS_FOOTER_HEAT = 0x7E;
// RAC-PT1411HWRU Swing
const uint8_t RAC_PT1411HWRU_SWING_HEADER = 0xB9;
const std::vector<uint8_t> RAC_PT1411HWRU_SWING_VERTICAL{0xB9, 0x46, 0xF5, 0x0A, 0x04, 0xFB};
const std::vector<uint8_t> RAC_PT1411HWRU_SWING_OFF{0xB9, 0x46, 0xF5, 0x0A, 0x05, 0xFA};
static constexpr uint8_t RAC_PT1411HWRU_SWING_VERTICAL[] PROGMEM = {0xB9, 0x46, 0xF5, 0x0A, 0x04, 0xFB};
static constexpr uint8_t RAC_PT1411HWRU_SWING_OFF[] PROGMEM = {0xB9, 0x46, 0xF5, 0x0A, 0x05, 0xFA};
// RAC-PT1411HWRU Fan speeds
const uint8_t RAC_PT1411HWRU_FAN_OFF = 0x7B;
constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_AUTO{0xBF, 0x66};
@@ -92,12 +90,12 @@ const uint8_t RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY = 0x0E;
// Hence these look-up tables. In addition, the upper nibble is used here for additional
// "negative" and "fractional value" flags as required for some temperatures.
// RAC-PT1411HWRU °C Temperatures (short codes)
const std::vector<uint8_t> RAC_PT1411HWRU_TEMPERATURE_C{0x10, 0x00, 0x01, 0x03, 0x02, 0x06, 0x07, 0x05,
0x04, 0x0C, 0x0D, 0x09, 0x08, 0x0A, 0x0B};
static constexpr uint8_t RAC_PT1411HWRU_TEMPERATURE_C[] PROGMEM = {0x10, 0x00, 0x01, 0x03, 0x02, 0x06, 0x07, 0x05,
0x04, 0x0C, 0x0D, 0x09, 0x08, 0x0A, 0x0B};
// RAC-PT1411HWRU °F Temperatures (short codes)
const std::vector<uint8_t> RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20, 0x01, 0x21, 0x03, 0x23, 0x02,
0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C,
0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
static constexpr uint8_t RAC_PT1411HWRU_TEMPERATURE_F[] PROGMEM = {
0x10, 0x30, 0x00, 0x20, 0x01, 0x21, 0x03, 0x23, 0x02, 0x22, 0x06, 0x26, 0x07, 0x05,
0x25, 0x04, 0x24, 0x0C, 0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
// RAS-2819T protocol constants
const uint16_t RAS_2819T_HEADER1 = 0xC23D;
@@ -213,7 +211,7 @@ static constexpr uint8_t SEIYA_SWING_HORIZONTAL = 0x09;
static constexpr uint8_t SEIYA_SWING_NEUTRAL = 0x17; // no swing change in this frame
// RAS-2819T temperature codes for 18-30°C
static const uint8_t RAS_2819T_TEMP_CODES[] = {
static constexpr uint8_t RAS_2819T_TEMP_CODES[] PROGMEM = {
0x10, // 18°C
0x30, // 19°C
0x20, // 20°C
@@ -328,7 +326,7 @@ static uint8_t get_ras_2819t_temp_code(float temperature) {
return 0x40; // Default to 24°C
}
return RAS_2819T_TEMP_CODES[temp_index];
return progmem_read_byte(&RAS_2819T_TEMP_CODES[temp_index]);
}
/**
@@ -339,7 +337,7 @@ static float decode_ras_2819t_temperature(uint8_t temp_code) {
// Find the code in the temperature array
for (size_t temp_index = 0; temp_index < sizeof(RAS_2819T_TEMP_CODES); temp_index++) {
if (RAS_2819T_TEMP_CODES[temp_index] == base_temp_code) {
if (progmem_read_byte(&RAS_2819T_TEMP_CODES[temp_index]) == base_temp_code) {
return static_cast<float>(temp_index + 18); // 18°C is the minimum
}
}
@@ -680,10 +678,10 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
index = static_cast<uint8_t>(roundf(temp_adjd));
if (this->model_ == MODEL_RAC_PT1411HWRU_F) {
code = RAC_PT1411HWRU_TEMPERATURE_F[index];
code = progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_F[index]);
message[9] |= RAC_PT1411HWRU_FLAG_FAH;
} else {
code = RAC_PT1411HWRU_TEMPERATURE_C[index];
code = progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_C[index]);
}
if ((this->mode == climate::CLIMATE_MODE_FAN_ONLY) || (this->mode == climate::CLIMATE_MODE_OFF)) {
code = RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY;
@@ -752,15 +750,11 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
// Swing Mode
data->reset();
data->space(TOSHIBA_PACKET_SPACE);
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
this->encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
break;
case climate::CLIMATE_SWING_OFF:
default:
this->encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
}
const uint8_t *swing_frame =
this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? RAC_PT1411HWRU_SWING_VERTICAL : RAC_PT1411HWRU_SWING_OFF;
uint8_t swing[RAC_PT1411HWRU_MESSAGE_LENGTH];
progmem_memcpy(swing, swing_frame, sizeof(swing));
this->encode_(data, swing, sizeof(swing), 1);
data->space(TOSHIBA_PACKET_SPACE);
transmit.perform();
@@ -1300,14 +1294,14 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
temperature_code =
(message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG);
if (message[15] & RAC_PT1411HWRU_FLAG_FAH) {
for (size_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_F.size(); i++) {
if (RAC_PT1411HWRU_TEMPERATURE_F[i] == temperature_code) {
for (size_t i = 0; i < sizeof(RAC_PT1411HWRU_TEMPERATURE_F); i++) {
if (progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_F[i]) == temperature_code) {
this->target_temperature = static_cast<float>((i + TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN - 32) * 5) / 9;
}
}
} else {
for (size_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_C.size(); i++) {
if (RAC_PT1411HWRU_TEMPERATURE_C[i] == temperature_code) {
for (size_t i = 0; i < sizeof(RAC_PT1411HWRU_TEMPERATURE_C); i++) {
if (progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_C[i]) == temperature_code) {
this->target_temperature = i + TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
}
}
@@ -1333,7 +1327,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
break;
// Swing mode
case RAC_PT1411HWRU_SWING_HEADER:
if (message[4] == RAC_PT1411HWRU_SWING_VERTICAL[4]) {
if (message[4] == progmem_read_byte(&RAC_PT1411HWRU_SWING_VERTICAL[4])) {
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = climate::CLIMATE_SWING_OFF;
+2 -1
View File
@@ -12,6 +12,7 @@
#include <freertos/task.h>
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h"
#include "esphome/core/log.h"
#include <atomic>
#include <span>
@@ -39,7 +40,7 @@ namespace esphome::usb_host {
// - Main loop controls backpressure by deciding when to restart after consuming data
// The atomic bitmask ensures thread-safe allocation/deallocation without mutex blocking.
static const char *const TAG = "usb_host";
ESPHOME_LOG_TAG(TAG, "usb_host");
// Forward declarations
struct TransferRequest;
+2 -1
View File
@@ -9,6 +9,7 @@
#include "esphome/components/usb_host/usb_host.h"
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h"
#include "esphome/core/log.h"
#include <atomic>
#include <functional>
@@ -19,7 +20,7 @@ class USBUartComponent;
class USBUartChannelBase;
class USBUartTypePL2303;
static const char *const TAG = "usb_uart";
ESPHOME_LOG_TAG(TAG, "usb_uart");
static constexpr uint8_t USB_CDC_SUBCLASS_ACM = 0x02;
static constexpr uint8_t USB_SUBCLASS_COMMON = 0x02;
+15 -1
View File
@@ -1,4 +1,4 @@
from esphome import automation
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import media_player, micro_wake_word, microphone, speaker
import esphome.config_validation as cv
@@ -47,6 +47,9 @@ CONF_MICRO_WAKE_WORD = "micro_wake_word"
CONF_WAKE_WORD = "wake_word"
CONF_CONVERSATION_TIMEOUT = "conversation_timeout"
CONF_TTS_PLAYBACK_START_TIMEOUT = "tts_playback_start_timeout"
# Keep in sync with VoiceAssistant::tts_playback_start_timeout_ in voice_assistant.h.
TTS_PLAYBACK_START_TIMEOUT_DEFAULT = core.TimePeriod(milliseconds=2000)
CONF_ON_TIMER_STARTED = "on_timer_started"
CONF_ON_TIMER_UPDATED = "on_timer_updated"
@@ -110,6 +113,13 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_CONVERSATION_TIMEOUT, default="300s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_TTS_PLAYBACK_START_TIMEOUT,
default=TTS_PLAYBACK_START_TIMEOUT_DEFAULT,
): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=core.TimePeriod(milliseconds=1)),
),
cv.Optional(CONF_VOLUME_MULTIPLIER, default=1.0): cv.float_range(
min=0.0, min_included=False
),
@@ -218,6 +228,10 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_auto_gain(config[CONF_AUTO_GAIN]))
cg.add(var.set_volume_multiplier(config[CONF_VOLUME_MULTIPLIER]))
cg.add(var.set_conversation_timeout(config[CONF_CONVERSATION_TIMEOUT]))
if (
timeout := config[CONF_TTS_PLAYBACK_START_TIMEOUT]
) != TTS_PLAYBACK_START_TIMEOUT_DEFAULT:
cg.add(var.set_tts_playback_start_timeout(timeout))
if CONF_ON_LISTENING in config:
await automation.build_automation(

Some files were not shown because too many files have changed in this diff Show More