Merge remote-tracking branch 'origin/dev' into micronova-tables-progmem

This commit is contained in:
J. Nick Koston
2026-10-13 17:18:20 -10:00
170 changed files with 4635 additions and 291 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
@@ -97,7 +97,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: generated-proto-files
path: |
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
- name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
# The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name
@@ -210,7 +210,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: compile-test-image.tar.zst
- name: Load image
+11 -11
View File
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -438,7 +438,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -477,7 +477,7 @@ jobs:
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
@@ -488,7 +488,7 @@ jobs:
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
@@ -528,7 +528,7 @@ jobs:
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: import-time-waterfall
path: importtime.har
@@ -626,7 +626,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
with:
run: |
. venv/bin/activate
@@ -1448,7 +1448,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1647,7 +1647,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -1710,7 +1710,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -1740,13 +1740,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-pr
path: ./memory-analysis
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -162,7 +162,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -195,7 +195,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: digests-*
path: /tmp/digests
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+2 -2
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.10
rev: v0.17.0
hooks:
# Run the linter.
- id: ruff
@@ -36,7 +36,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
rev: v3.22.0
hooks:
- id: pyupgrade
args: [--py312-plus]
+1
View File
@@ -439,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
+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;
@@ -87,6 +87,38 @@ jd79660.extend(
),
)
# Waveshare 2.13-G (V2 hardware/init revision, PCB marked "rev 2.1" or later)
#
# Vendor init derived from vendor sample code
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g_V2.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
# Note: panel is 122px wide, but vendor code declares the TRES register as a fixed
# 124 on this revision. width=124 is declared here (rather than 122) purely so the
# transferred bytes-per-row matches what the vendor init tells the IC to expect; the
# extra 2 columns fall outside the visible glass. The model width must equal the
# TRES value in initsequence, so overriding dimensions: in YAML brings the shear back.
#
# fmt: off
jd79660.extend(
"Waveshare-2.13in-G",
width=124,
height=250,
initsequence=(
(0x61, 0x00, 0x7C, 0x00, 0xFA,), # TRES: 124x250 fixed (124 = TRES stride value)
(0xE9, 0x01,),
# Power On (0x04): Must be early part of init seq = Disabled later!
(0x04,),
),
fast_update=(
(0xE0, 0x02,),
(0xE6, 0x5A,),
(0xA5,),
),
)
# Waveshare 7.5-H
#
# Vendor init derived from vendor sample code
@@ -130,7 +162,7 @@ jd79660.extend(
# Note: busy pin uses LOW=busy, HIGH=idle.
#
# fmt: off
jd79660.extend(
gdey0213f51 = jd79660.extend(
"goodisplay-gdey0213f51-2.13",
width=128,
height=250,
@@ -154,3 +186,13 @@ jd79660.extend(
(0x04,),
),
)
# Waveshare 2.13-G (V1 hardware/init revision, older stock): the same panel and init as the Good
# Display entry above, byte for byte, so it only adds the name. Vendor sample:
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
# Note: width=128 is the TRES value the init sequence programs, not the 122 visible columns; overriding
# dimensions: in YAML shears the image. Not tested on V1 hardware.
gdey0213f51.extend("Waveshare-2.13in-G-V1")
@@ -24,3 +24,11 @@ WaveshareB(
data_rate="10MHz",
minimum_update_interval="1s",
)
WaveshareB(
"waveshare-2.66in-b",
width=152,
height=296,
data_rate="10MHz",
minimum_update_interval="15s",
)
+22
View File
@@ -6,6 +6,8 @@ import esphome.codegen as cg
from esphome.components import canbus
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
from esphome.components.esp32 import (
CONF_MINIMUM_CHIP_REVISION,
KEY_ESP32,
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -20,6 +22,8 @@ from esphome.components.esp32 import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
CONF_FRAMEWORK,
CONF_ID,
CONF_MODE,
CONF_RX_PIN,
@@ -27,6 +31,7 @@ from esphome.const import (
CONF_TX_PIN,
CONF_TX_QUEUE_LEN,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@Sympatron"]
@@ -49,6 +54,7 @@ CAN_MODES = {
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
CAN_SPEEDS_ESP32 = {
"20KBPS": CanSpeed.CAN_20KBPS,
"25KBPS": CanSpeed.CAN_25KBPS,
"50KBPS": CanSpeed.CAN_50KBPS,
"100KBPS": CanSpeed.CAN_100KBPS,
@@ -114,6 +120,22 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
)
def _final_validate(config: ConfigType) -> None:
if get_esp32_variant() == VARIANT_ESP32 and config[CONF_BIT_RATE] == "20KBPS":
esp32_config = fv.full_config.get()[KEY_ESP32]
min_revision = esp32_config[CONF_FRAMEWORK][CONF_ADVANCED].get(
CONF_MINIMUM_CHIP_REVISION
)
if min_revision is None or min_revision < "2.0":
raise cv.Invalid(
"20KBPS on ESP32 requires esp32.framework.advanced.minimum_chip_revision: '2.0' or higher",
path=[CONF_BIT_RATE],
)
FINAL_VALIDATE_SCHEMA = _final_validate
def get_default_tx_enqueue_timeout(bit_rate: str) -> int:
bit_rate_numeric = canbus.get_rate(bit_rate)
bits_per_packet = 140 # ~max CAN message length
@@ -33,6 +33,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
case canbus::CAN_16KBPS:
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS();
return true;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || (defined(USE_ESP32_VARIANT_ESP32) && CONFIG_ESP32_REV_MIN_FULL >= 200)
case canbus::CAN_20KBPS:
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS();
return true;
+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])
+1
View File
@@ -3,4 +3,5 @@ DOMAIN = "ili9xxx"
DEPRECATED_COMPONENT = """
The 'ili9xxx' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_spi' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -220,8 +220,10 @@ FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'ili9xxx' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
rhs = MODELS[config[CONF_MODEL]].new()
var = cg.Pvariable(config[CONF_ID], rhs)
+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).
+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:
+19
View File
@@ -3,7 +3,9 @@ from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
import esphome.config_validation as cv
from esphome.const import (
CONF_API,
CONF_DISABLED,
CONF_ETHERNET,
CONF_ID,
CONF_OPENTHREAD,
CONF_PORT,
@@ -285,6 +287,23 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# 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 (
CONF_API in CORE.config
and CONF_WIFI in CORE.config
and CONF_ETHERNET in CORE.config
and CONF_OPENTHREAD not in CORE.config
):
from esphome.components import network
cg.add_define("USE_MDNS_DYNAMIC_NETWORK_TXT")
# Where the network component tracks the default route (ESP32), follow
# it; otherwise mdns checks the connected interface every second.
if await network.register_active_interface_listener(var):
cg.add_define("USE_MDNS_ACTIVE_INTERFACE_LISTENER")
for service in config[CONF_SERVICES]:
txt_records = [
await _mdns_txt_record_templated(var, txt_key, txt_value)
+64 -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,57 @@ 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_(const char *network) {
// With no interface connected, keep the last value instead of flapping to wifi
if (this->network_txt_ == nullptr || 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
}
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
void MDNSComponent::on_active_interface(network::NetworkInterface interface) {
switch (interface) {
case network::NetworkInterface::NETWORK_INTERFACE_ETHERNET:
this->update_network_txt_(NETWORK_ETHERNET);
break;
case network::NetworkInterface::NETWORK_INTERFACE_WIFI:
this->update_network_txt_(NETWORK_WIFI);
break;
default:
break;
}
}
#endif
#endif
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
#ifdef USE_MDNS_STORE_SERVICES
auto &services = this->services_;
@@ -160,7 +215,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)
+31
View File
@@ -32,6 +32,13 @@
#define USE_MDNS_DEVICE_INFO_TXT
#endif
// USE_MDNS_DYNAMIC_NETWORK_TXT (wifi + ethernet + api): the _esphomelib "network" TXT value follows the
// connected interface. With USE_MDNS_ACTIVE_INTERFACE_LISTENER (ESP32) the network component reports
// default route changes; otherwise the value is checked every NETWORK_TXT_CHECK_INTERVAL_MS.
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
#include "esphome/components/network/network_component.h"
#endif
namespace esphome::mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
@@ -73,6 +80,10 @@ class MDNSComponent final : public Component
,
public ethernet::EthernetIPStateListener
#endif
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
,
public network::ActiveInterfaceListener
#endif
{
public:
void setup() override;
@@ -134,6 +145,10 @@ class MDNSComponent final : public Component
}
#endif
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
void on_active_interface(network::NetworkInterface interface) override;
#endif
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
@@ -150,6 +165,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;
/// Publish network as the "network" TXT value if it changed; nullptr keeps the current value.
void update_network_txt_(const char *network);
/// 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.
+19 -1
View File
@@ -89,7 +89,25 @@ 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);
#if defined(USE_MDNS_DYNAMIC_NETWORK_TXT) && !defined(USE_MDNS_ACTIVE_INTERFACE_LISTENER)
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS,
[this]() { this->update_network_txt_(this->connected_network_()); });
#endif
}
void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD
+20
View File
@@ -46,7 +46,27 @@ 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_(this->connected_network_()); });
#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;
+27 -7
View File
@@ -127,6 +127,19 @@ ClimateTraits AirConditioner::traits() {
return traits;
}
#ifdef USE_REMOTE_TRANSMITTER
static const LogString *display_control_str(DisplayControl control) {
switch (control) {
case DisplayControl::DISPLAY_CONTROL_UART:
return LOG_STR("UART");
case DisplayControl::DISPLAY_CONTROL_IR:
return LOG_STR("IR");
default:
return LOG_STR("AUTO");
}
}
#endif
void AirConditioner::dump_config() {
ESP_LOGCONFIG(Constants::TAG,
"MideaDongle:\n"
@@ -135,7 +148,10 @@ void AirConditioner::dump_config() {
" [x] Request attempts: %d",
this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts());
#ifdef USE_REMOTE_TRANSMITTER
ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter");
ESP_LOGCONFIG(Constants::TAG,
" [x] Using RemoteTransmitter\n"
" [x] Display control: %s",
LOG_STR_ARG(display_control_str(this->display_control_)));
#endif
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
this->base_.getCapabilities().dump();
@@ -183,16 +199,20 @@ void AirConditioner::do_swing_step() {
}
void AirConditioner::do_display_toggle() {
if (this->base_.getCapabilities().supportLightControl()) {
this->base_.displayToggle();
} else {
#ifdef USE_REMOTE_TRANSMITTER
// AUTO uses IR unless the appliance reports light control over UART. Without a transmitter
// built in, every setting sends the UART request: Mundoclima, Rotenso and others accept it
// without advertising it in the 0xB5 capabilities report.
const bool use_ir = this->display_control_ == DisplayControl::DISPLAY_CONTROL_IR ||
(this->display_control_ == DisplayControl::DISPLAY_CONTROL_AUTO &&
!this->base_.getCapabilities().supportLightControl());
if (use_ir) {
IrSpecialData data(0x08);
this->transmitter_.transmit(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
return;
}
#endif
this->base_.displayToggle();
}
} // namespace esphome::midea::ac
@@ -21,12 +21,20 @@ using climate::ClimateModeMask;
using climate::ClimateSwingModeMask;
using climate::ClimatePresetMask;
// How the display toggle reaches the appliance; the AUTO rules are in do_display_toggle()
enum class DisplayControl : uint8_t {
DISPLAY_CONTROL_UART, // always the UART request
DISPLAY_CONTROL_AUTO, // UART when the appliance reports it or no transmitter is built in, else IR
DISPLAY_CONTROL_IR, // always the IR remote, needs a transmitter
};
class AirConditioner final : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate {
public:
void dump_config() override;
void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
void set_display_control(DisplayControl control) { this->display_control_ = control; }
void on_status_change() override;
/* ############### */
@@ -54,6 +62,7 @@ class AirConditioner final : public ApplianceBase<dudanov::midea::ac::AirConditi
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
bool frost_protection_set_{false};
DisplayControl display_control_{DisplayControl::DISPLAY_CONTROL_AUTO};
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
+24
View File
@@ -33,15 +33,18 @@ from esphome.const import (
UNIT_WATT,
)
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@dudanov"]
DEPENDENCIES = ["climate", "uart"]
AUTO_LOAD = ["sensor"]
CONF_POWER_USAGE = "power_usage"
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
CONF_DISPLAY_CONTROL = "display_control"
midea_ac_ns = cg.esphome_ns.namespace("midea").namespace("ac")
AirConditioner = midea_ac_ns.class_("AirConditioner", climate.Climate, cg.Component)
Capabilities = midea_ac_ns.namespace("Constants")
DisplayControl = midea_ac_ns.enum("DisplayControl", is_class=True)
def templatize(value):
@@ -82,11 +85,29 @@ CUSTOM_PRESETS = {
"FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
}
# How midea_ac.display_toggle reaches the appliance; see do_display_toggle() for the AUTO rules
DISPLAY_CONTROLS = {
"UART": DisplayControl.DISPLAY_CONTROL_UART,
"AUTO": DisplayControl.DISPLAY_CONTROL_AUTO,
"IR": DisplayControl.DISPLAY_CONTROL_IR,
}
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
validate_display_control = cv.enum(DISPLAY_CONTROLS, upper=True)
def validate_display_control_with_transmitter(config: ConfigType) -> ConfigType:
"""`display_control: IR` needs a `remote_transmitter` to send through; UART and AUTO do not."""
if config.get(CONF_DISPLAY_CONTROL) == "IR" and CONF_TRANSMITTER_ID not in config:
raise cv.Invalid(
f"'{CONF_DISPLAY_CONTROL}: IR' requires '{CONF_TRANSMITTER_ID}' to be set"
)
return config
CONFIG_SCHEMA = cv.All(
climate.climate_schema(AirConditioner)
@@ -100,6 +121,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
cv.Optional(CONF_DISPLAY_CONTROL, default="AUTO"): validate_display_control,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
validate_swing_modes
@@ -134,6 +156,7 @@ CONFIG_SCHEMA = cv.All(
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_display_control_with_transmitter,
cv.only_on(
[
PLATFORM_ESP32,
@@ -204,6 +227,7 @@ async def to_code(config):
cg.add(var.set_transmitter(transmitter_))
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
cg.add(var.set_display_control(config[CONF_DISPLAY_CONTROL]))
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_SWING_MODES in config:
+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;
+32 -1
View File
@@ -100,6 +100,37 @@ if (
network_ns = cg.esphome_ns.namespace("network")
NetworkComponent = network_ns.class_("NetworkComponent", cg.Component)
_request_active_interface_listener_slot = cg.slot_counter(
"NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT"
)
def has_active_interface() -> bool:
"""Return True when NetworkComponent tracks the interface holding the default route.
That is ESP32 with more than one interface in 'network: priority:', the same
condition that emits USE_NETWORK_DEFAULT_ROUTE.
"""
return CORE.is_esp32 and len(CORE.config.get(DOMAIN, {}).get(CONF_PRIORITY, [])) > 1
async def register_active_interface_listener(listener: cg.MockObj) -> bool:
"""Register an ActiveInterfaceListener with the NetworkComponent.
The listener is notified from the main loop when the interface holding the
default route changes. Returns False, and registers nothing, when
has_active_interface() is False; the caller then needs another way to
follow interface changes.
"""
if not has_active_interface():
return False
_request_active_interface_listener_slot()
network = await cg.get_variable(CORE.config[DOMAIN][CONF_ID])
cg.add(network.add_active_interface_listener(listener))
return True
IPAddress = network_ns.class_("IPAddress")
@@ -402,7 +433,7 @@ async def to_code(config: ConfigType) -> None:
# frameworks build from source.
# The ethernet/wifi-only assumption behind the arbitration is enforced in
# _final_validate() so a future unsupported type fails as a config error.
if len(priority_list) > 1 and CORE.is_esp32:
if has_active_interface():
cg.add_define("USE_NETWORK_DEFAULT_ROUTE")
# Have lwIP switch to the DNS servers of the netif that owns the
# default route whenever the arbitration changes it.
@@ -78,16 +78,24 @@ void NetworkComponent::loop() {
// USE_NETWORK_PRIMARY_INTERFACE_WIFI is emitted for a wifi-first priority list;
// it selects the reported address in util.cpp and doubles as the route-order
// pivot here — the two uses must stay in sync.
esp_netif_t *best;
esp_netif_t *wifi = connected_wifi_netif();
esp_netif_t *ethernet = connected_ethernet_netif();
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
best = connected_wifi_netif();
if (best == nullptr)
best = connected_ethernet_netif();
esp_netif_t *best = wifi != nullptr ? wifi : ethernet;
#else
best = connected_ethernet_netif();
if (best == nullptr)
best = connected_wifi_netif();
esp_netif_t *best = ethernet != nullptr ? ethernet : wifi;
#endif
NetworkInterface active = NetworkInterface::NETWORK_INTERFACE_NONE;
if (best != nullptr) {
active = best == ethernet ? NetworkInterface::NETWORK_INTERFACE_ETHERNET : NetworkInterface::NETWORK_INTERFACE_WIFI;
}
if (active != this->active_interface_) {
this->active_interface_ = active;
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
for (auto *listener : this->active_interface_listeners_)
listener->on_active_interface(active);
#endif
}
if (best == nullptr) {
// Forget the last winner: stopping its netif cleared lwIP's default route and
// IDF's manual override suppresses re-election, so reconnect must re-assert it.
@@ -4,12 +4,32 @@
#include "esphome/core/component.h"
#ifdef USE_NETWORK_DEFAULT_ROUTE
#include "esphome/core/helpers.h"
// Forward declaration matching esp_netif's own typedef; avoids pulling esp_netif.h
// into this header.
using esp_netif_t = struct esp_netif_obj;
#endif
namespace esphome::network {
#ifdef USE_NETWORK_DEFAULT_ROUTE
/// The interface holding the default route. Lists only the interfaces loop() arbitrates
/// (ARBITRATED_NETWORK_TYPES in __init__.py); extend both together.
enum class NetworkInterface : uint8_t {
NETWORK_INTERFACE_NONE,
NETWORK_INTERFACE_ETHERNET,
NETWORK_INTERFACE_WIFI,
};
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
/// Notified from the main loop when the interface holding the default route changes.
class ActiveInterfaceListener {
public:
virtual void on_active_interface(NetworkInterface interface) = 0;
};
#endif
#endif
class NetworkComponent final : public Component {
public:
void setup() override;
@@ -20,6 +40,14 @@ class NetworkComponent final : public Component {
#ifdef USE_NETWORK_DEFAULT_ROUTE
void loop() override;
/// The first connected interface in network: priority order; NONE while none is connected.
NetworkInterface get_active_interface() const { return this->active_interface_; }
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
void add_active_interface_listener(ActiveInterfaceListener *listener) {
this->active_interface_listeners_.push_back(listener);
}
#endif
protected:
// Verify-lwIP-route interval for the settled state; keeps the global lwIP core
// mutex off the hot loop path.
@@ -27,6 +55,10 @@ class NetworkComponent final : public Component {
// Last netif this component made the default; avoids redundant esp_netif calls.
esp_netif_t *default_netif_{nullptr};
uint32_t last_route_check_{0};
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
StaticVector<ActiveInterfaceListener *, NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT> active_interface_listeners_;
#endif
NetworkInterface active_interface_{NetworkInterface::NETWORK_INTERFACE_NONE};
#endif
};
} // namespace esphome::network
+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 = {
+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
+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])
+1
View File
@@ -6,4 +6,5 @@ CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
DEPRECATED_COMPONENT = """
The 'qspi_dbi' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_spi' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -165,8 +165,10 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'qspi_dbi' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'qspi_dbi' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
+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;
@@ -4,4 +4,5 @@ DOMAIN = "rpi_dpi_rgb"
DEPRECATED_COMPONENT = """
The 'rpi_dpi_rgb' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_rgb' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -132,8 +132,10 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'rpi_dpi_rgb' component is deprecated, it is recommended to use 'mipi_rgb' instead."
"The 'rpi_dpi_rgb' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_rgb' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
+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:
+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:
+109 -2
View File
@@ -14,6 +14,15 @@ static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
// Numeric timeout IDs. Each chain is sequential, so only one timeout per ID is active at a time.
static constexpr uint32_t TIMEOUT_POLL = 1;
static constexpr uint32_t TIMEOUT_SETUP_STEP = 2;
static constexpr uint32_t TIMEOUT_STARTUP = 3;
// Post-command wait times from the datasheet: start execution (4.8.1), stop execution (4.8.2),
// fan cleaning (4.8.22), SHT heater (4.8.23), CO2 conditioning after start on SEN63C/SEN69C (4.8.1)
static constexpr uint32_t START_MEASUREMENT_DELAY = 50;
static constexpr uint32_t STOP_MEASUREMENT_DELAY = 1400;
static constexpr uint32_t FAN_CLEANING_DELAY = 10000;
static constexpr uint32_t SHT_HEATER_DELAY = 20000;
static constexpr uint32_t CO2_CONDITIONING_DELAY = 24000;
static constexpr uint32_t STARTUP_DELAY = 60000; // ms before published values are considered valid
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
@@ -27,6 +36,9 @@ static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467;
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
static constexpr uint16_t SEN6X_CMD_STOP_MEASUREMENTS = 0x0104;
static constexpr uint16_t SEN6X_CMD_START_FAN_CLEANING = 0x5607;
static constexpr uint16_t SEN6X_CMD_ACTIVATE_SHT_HEATER = 0x6765;
static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
static constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
@@ -191,7 +203,7 @@ void SEN6XComponent::finish_setup_() {
return;
}
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
this->on_measurement_started_();
this->initialized_ = true;
ESP_LOGD(TAG, "Initialized");
}
@@ -240,7 +252,7 @@ void SEN6XComponent::dump_config() {
}
void SEN6XComponent::update() {
if (!this->initialized_) {
if (!this->initialized_ || !this->measuring_) {
return;
}
@@ -442,6 +454,101 @@ void SEN6XComponent::parse_and_publish_measurements_() {
this->status_clear_warning();
}
// True while the previous command's execution or settling time has not elapsed
bool SEN6XComponent::command_blocked_() const { return millis() - this->command_started_at_ < this->command_wait_; }
void SEN6XComponent::set_command_wait_(uint32_t wait_ms) {
this->command_started_at_ = millis();
this->command_wait_ = wait_ms;
}
void SEN6XComponent::on_measurement_started_() {
this->measuring_ = true;
this->set_command_wait_(START_MEASUREMENT_DELAY);
// SEN63C/SEN69C condition the CO2 sensor for 24 s after a start; block restarts until then
if (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN69C) {
this->co2_started_at_ = millis();
this->co2_wait_ = CO2_CONDITIONING_DELAY;
}
// Values need the warm-up period again after every start
this->startup_complete_ = false;
this->set_timeout(TIMEOUT_STARTUP, STARTUP_DELAY, [this]() { this->startup_complete_ = true; });
}
void SEN6XComponent::start_measurement() {
if (!this->initialized_ || this->measuring_)
return;
// The CO2 conditioning window only blocks restarting, not other commands
if (this->command_blocked_() || millis() - this->co2_started_at_ < this->co2_wait_) {
ESP_LOGW(TAG, "Device busy");
return;
}
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
this->status_set_warning();
ESP_LOGW(TAG, "Start measurement failed (%d)", this->last_error_);
return;
}
this->on_measurement_started_();
}
void SEN6XComponent::stop_measurement() {
if (!this->initialized_ || !this->measuring_)
return;
if (this->command_blocked_()) {
ESP_LOGW(TAG, "Device busy");
return;
}
// Stop any in-flight polling before the device goes idle
this->cancel_timeout(TIMEOUT_POLL);
if (!this->write_command(SEN6X_CMD_STOP_MEASUREMENTS)) {
this->status_set_warning();
ESP_LOGW(TAG, "Stop measurement failed (%d)", this->last_error_);
return;
}
this->measuring_ = false;
this->set_command_wait_(STOP_MEASUREMENT_DELAY);
}
void SEN6XComponent::start_fan_cleaning() {
if (!this->initialized_)
return;
// The device only accepts this command in idle mode (datasheet section 4.8.22)
if (this->measuring_) {
ESP_LOGW(TAG, "Fan cleaning requires idle mode; stop the measurement first");
return;
}
if (this->command_blocked_()) {
ESP_LOGW(TAG, "Device busy");
return;
}
if (!this->write_command(SEN6X_CMD_START_FAN_CLEANING)) {
this->status_set_warning();
ESP_LOGW(TAG, "Fan cleaning failed (%d)", this->last_error_);
return;
}
this->set_command_wait_(FAN_CLEANING_DELAY);
}
void SEN6XComponent::activate_sht_heater() {
if (!this->initialized_)
return;
// The device only accepts this command in idle mode (datasheet section 4.8.23)
if (this->measuring_) {
ESP_LOGW(TAG, "SHT heater requires idle mode; stop the measurement first");
return;
}
if (this->command_blocked_()) {
ESP_LOGW(TAG, "Device busy");
return;
}
if (!this->write_command(SEN6X_CMD_ACTIVATE_SHT_HEATER)) {
this->status_set_warning();
ESP_LOGW(TAG, "SHT heater failed (%d)", this->last_error_);
return;
}
this->set_command_wait_(SHT_HEATER_DELAY);
}
SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
if (product_name == "SEN62")
return SEN62;
+13
View File
@@ -58,6 +58,10 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
NOX_STD_INITIAL,
gain_factor};
}
void start_measurement();
void stop_measurement();
void start_fan_cleaning();
void activate_sht_heater();
protected:
Sen6xType infer_type_from_product_name_(const std::string &product_name);
@@ -65,6 +69,9 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
void finish_setup_();
bool write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len);
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool command_blocked_() const;
void set_command_wait_(uint32_t wait_ms);
void on_measurement_started_();
void poll_data_ready_();
void read_measurements_();
void parse_and_publish_measurements_();
@@ -74,6 +81,11 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
optional<GasTuning> voc_tuning_params_;
optional<GasTuning> nox_tuning_params_;
Sen6xType sen6x_type_{UNKNOWN};
// Post-command wait windows, stored as start time plus duration so elapsed-time checks survive millis() wrap
uint32_t command_started_at_{0};
uint32_t command_wait_{0};
uint32_t co2_started_at_{0};
uint32_t co2_wait_{0};
uint16_t read_cmd_{0};
uint8_t setup_step_index_{0};
uint8_t firmware_version_major_{0};
@@ -81,6 +93,7 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
uint8_t poll_retries_remaining_{0};
uint8_t read_words_{0};
bool initialized_{false};
bool measuring_{false};
bool startup_complete_{false};
};
+20
View File
@@ -1,3 +1,5 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX
@@ -209,3 +211,21 @@ async def to_code(config: ConfigType) -> None:
args.append(std_initial)
args.append(tuning[CONF_GAIN_FACTOR])
cg.add(getattr(var, setter)(*args))
SEN6X_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(SEN6XComponent),
}
)
for _name, _call in (
("sen6x.start_measurement", "start_measurement()"),
("sen6x.stop_measurement", "stop_measurement()"),
("sen6x.start_fan_cleaning", "start_fan_cleaning()"),
("sen6x.activate_sht_heater", "activate_sht_heater()"),
):
automation.register_apply_action(
_name, SEN6X_ACTION_SCHEMA, automation.ApplyCall(_call)
)
@@ -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;
+1
View File
@@ -7,4 +7,5 @@ st7735_ns = cg.esphome_ns.namespace("st7735")
DEPRECATED_COMPONENT = """
The 'st7735' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_spi' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -92,8 +92,10 @@ async def setup_st7735(var: MockObj, config: ConfigType) -> None:
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'st7735' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'st7735' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
var = cg.new_Pvariable(
config[CONF_ID],
+1
View File
@@ -7,4 +7,5 @@ st7789v_ns = cg.esphome_ns.namespace("st7789v")
DEPRECATED_COMPONENT = """
The 'st7789v' component is deprecated and no new functionality will be added to it.
PRs should target the newer and more performant 'mipi_spi' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -183,8 +183,10 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'st7789v' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'st7789v' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
+14
View File
@@ -17,12 +17,22 @@ CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_ENABLE_ADC = "enable_adc"
CONF_I2C_WATCHDOG_TIMEOUT = "i2c_watchdog_timeout"
sy6970_ns = cg.esphome_ns.namespace("sy6970")
SY6970Component = sy6970_ns.class_(
"SY6970Component", cg.PollingComponent, i2c.I2CDevice
)
SY6970Listener = sy6970_ns.class_("SY6970Listener")
I2CWatchdogTimeout = sy6970_ns.enum("I2CWatchdogTimeout")
# REG07[5:4]; the component kicks the watchdog itself, so the chip's 40S default can stay on
I2C_WATCHDOG_TIMEOUTS = {
"DISABLED": I2CWatchdogTimeout.I2C_WATCHDOG_DISABLED,
"40S": I2CWatchdogTimeout.I2C_WATCHDOG_40S,
"80S": I2CWatchdogTimeout.I2C_WATCHDOG_80S,
"160S": I2CWatchdogTimeout.I2C_WATCHDOG_160S,
}
CONFIG_SCHEMA = (
cv.Schema(
@@ -43,6 +53,9 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean,
cv.Optional(CONF_I2C_WATCHDOG_TIMEOUT, default="40S"): cv.enum(
I2C_WATCHDOG_TIMEOUTS, upper=True
),
}
)
.extend(cv.polling_component_schema("5s"))
@@ -60,6 +73,7 @@ async def to_code(config: ConfigType) -> None:
config[CONF_PRECHARGE_CURRENT],
config[CONF_CHARGE_ENABLED],
config[CONF_ENABLE_ADC],
config[CONF_I2C_WATCHDOG_TIMEOUT],
)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+68 -2
View File
@@ -6,6 +6,25 @@ namespace esphome::sy6970 {
ESPHOME_LOG_TAG(TAG, "sy6970");
static const LogString *i2c_watchdog_timeout_to_string(I2CWatchdogTimeout timeout) {
switch (timeout) {
case I2C_WATCHDOG_DISABLED:
return LOG_STR("Disabled");
case I2C_WATCHDOG_40S:
return LOG_STR("40s");
case I2C_WATCHDOG_80S:
return LOG_STR("80s");
case I2C_WATCHDOG_160S:
return LOG_STR("160s");
default:
return LOG_STR("Unknown");
}
}
static constexpr uint32_t I2C_WATCHDOG_KICK_INTERVAL_ID = 0;
// Well under the 40 s default, so a failed kick or a timeout the chip did not accept still leaves margin
static constexpr uint32_t I2C_WATCHDOG_KICK_INTERVAL_MS = 10000;
bool SY6970Component::read_all_registers_() {
// Read all registers from 0x00 to 0x14 in one transaction (21 bytes)
// This includes unused registers 0x0F, 0x10 for performance
@@ -51,6 +70,11 @@ void SY6970Component::setup() {
ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x00)", chip_id);
}
// Service the watchdog first, so the settings below are not reverted while they are written
ESP_LOGV(TAG, "Setting I2C watchdog timeout to %s",
LOG_STR_ARG(i2c_watchdog_timeout_to_string(this->i2c_watchdog_timeout_)));
this->set_i2c_watchdog_timeout(this->i2c_watchdog_timeout_);
// Apply configuration options (all have defaults now)
ESP_LOGV(TAG, "Setting LED enabled to %s", ONOFF(this->led_enabled_));
this->set_led_enabled(this->led_enabled_);
@@ -85,9 +109,11 @@ void SY6970Component::dump_config() {
" Charge Current: %u mA\n"
" Precharge Current: %u mA\n"
" Charge Enabled: %s\n"
" ADC Enabled: %s",
" ADC Enabled: %s\n"
" I2C Watchdog Timeout: %s",
ONOFF(this->led_enabled_), this->input_current_limit_, this->charge_voltage_, this->charge_current_,
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_));
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_),
LOG_STR_ARG(i2c_watchdog_timeout_to_string(this->i2c_watchdog_timeout_)));
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
if (this->is_failed()) {
@@ -198,4 +224,44 @@ void SY6970Component::set_enable_adc_measure(bool enabled) {
this->update_register_(SY6970_REG_ADC_CONTROL, 0xC0, enabled ? 0xC0 : 0x00);
}
void SY6970Component::set_i2c_watchdog_timeout(I2CWatchdogTimeout timeout) {
if (this->is_failed())
return;
if (timeout != I2C_WATCHDOG_DISABLED) {
// Changing the timeout does not restart the chip's timer, which may be close to expiring
this->reset_i2c_watchdog();
}
// REG07 bits 5:4 (WATCHDOG[1:0])
bool written = this->update_register_(SY6970_REG_TIMER_CONTROL, 0x30, static_cast<uint8_t>(timeout) << 4);
if (!written) {
ESP_LOGW(TAG, "Failed to set I2C watchdog timeout to %s", LOG_STR_ARG(i2c_watchdog_timeout_to_string(timeout)));
}
if (timeout == I2C_WATCHDOG_DISABLED && written) {
this->i2c_watchdog_timeout_ = timeout;
this->cancel_interval(I2C_WATCHDOG_KICK_INTERVAL_ID);
return;
}
if (timeout != I2C_WATCHDOG_DISABLED) {
this->i2c_watchdog_timeout_ = timeout;
}
// Keeps kicking after a failed write too: the chip may still be on its 40 s default
this->set_interval(I2C_WATCHDOG_KICK_INTERVAL_ID, I2C_WATCHDOG_KICK_INTERVAL_MS,
[this]() { this->reset_i2c_watchdog(); });
}
bool SY6970Component::reset_i2c_watchdog() {
if (this->is_failed())
return false;
// WD_RST, self-clearing
if (!this->update_register_(SY6970_REG_SYS_CONTROL, 0x40, 0x40)) {
ESP_LOGW(TAG, "I2C watchdog reset failed, settings may revert");
return false;
}
return true;
}
} // namespace esphome::sy6970
+18 -2
View File
@@ -62,6 +62,15 @@ enum ChargeStatus {
CHARGE_STATUS_CHARGE_DONE = 3,
};
// REG07[5:4]. When the watchdog elapses the chip reverts CHG_CONFIG and STAT_DIS to their defaults,
// so an enabled watchdog is kicked on its own interval.
enum I2CWatchdogTimeout {
I2C_WATCHDOG_DISABLED = 0,
I2C_WATCHDOG_40S = 1,
I2C_WATCHDOG_80S = 2,
I2C_WATCHDOG_160S = 3,
};
// Structure to hold all register data read in one transaction
struct SY6970Data {
uint8_t registers[21]; // Registers 0x00-0x14 (includes unused 0x0F, 0x10)
@@ -76,14 +85,16 @@ class SY6970Listener {
class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
public:
SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current,
uint16_t precharge_current, bool charge_enabled, bool enable_adc)
uint16_t precharge_current, bool charge_enabled, bool enable_adc,
I2CWatchdogTimeout i2c_watchdog_timeout)
: led_enabled_(led_enabled),
input_current_limit_(input_current_limit),
charge_voltage_(charge_voltage),
charge_current_(charge_current),
precharge_current_(precharge_current),
charge_enabled_(charge_enabled),
enable_adc_(enable_adc) {}
enable_adc_(enable_adc),
i2c_watchdog_timeout_(i2c_watchdog_timeout) {}
void setup() override;
void dump_config() override;
void update() override;
@@ -99,6 +110,10 @@ class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
void set_charge_enabled(bool enabled);
void set_led_enabled(bool enabled);
void set_enable_adc_measure(bool enabled = true);
void set_i2c_watchdog_timeout(I2CWatchdogTimeout timeout);
/// Kick the I2C watchdog (REG03 bit 6); runs on its own interval while enabled. Returns false when not written.
bool reset_i2c_watchdog();
protected:
bool read_all_registers_();
@@ -116,6 +131,7 @@ class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
uint16_t precharge_current_;
bool charge_enabled_;
bool enable_adc_;
I2CWatchdogTimeout i2c_watchdog_timeout_;
};
} // namespace esphome::sy6970
+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:
@@ -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
+3
View File
@@ -9,10 +9,12 @@ 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};
@@ -30,6 +32,7 @@ class TMC2209Stepper : public tmc22xx::TMC22XXStepper {
/// 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); }
+9
View File
@@ -33,6 +33,8 @@ 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};
@@ -59,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,
@@ -129,6 +136,8 @@ 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) {
+31
View File
@@ -0,0 +1,31 @@
"""TMP102 threshold status entity."""
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from . import CONF_TMP102_ID, TMP102Component, validate_unique_child
CONFIG_SCHEMA = text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(
{
cv.GenerateID(CONF_TMP102_ID): cv.use_id(TMP102Component),
}
)
def _final_validate(config: ConfigType) -> ConfigType:
return validate_unique_child(config, "text_sensor")
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_TMP102_TEXT_SENSOR")
parent = await cg.get_variable(config[CONF_TMP102_ID])
var = await text_sensor.new_text_sensor(config)
cg.add(parent.set_threshold_status_text_sensor(var))
+98 -7
View File
@@ -1,8 +1,14 @@
#include "tmp102.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#ifdef USE_TMP102_TEXT_SENSOR
#include "esphome/core/progmem.h"
#endif
#include <cmath>
#ifdef USE_TMP102_TEXT_SENSOR
#include <cstdio>
#endif
namespace esphome::tmp102 {
@@ -12,6 +18,30 @@ static constexpr uint32_t READ_TEMP_TIMEOUT_ID = 0;
static constexpr uint8_t TMP102_REGISTER_TEMPERATURE = 0x00;
static constexpr float TMP102_CONVERSION_FACTOR = 0.0625f;
enum TMP102ThresholdStatus : uint8_t {
TMP102_THRESHOLD_STATUS_NOT_CONFIGURED,
TMP102_THRESHOLD_STATUS_SETUP_FAILED,
TMP102_THRESHOLD_STATUS_OK,
TMP102_THRESHOLD_STATUS_RESTORE_RESET,
TMP102_THRESHOLD_STATUS_RESTORE_RESET_SAVE_FAILED,
TMP102_THRESHOLD_STATUS_PREFERENCE_SAVE_FAILED,
TMP102_THRESHOLD_STATUS_RECOVERY_FAILED,
TMP102_THRESHOLD_STATUS_RECOVERED,
TMP102_THRESHOLD_STATUS_WRITE_HIGH_FAILED,
TMP102_THRESHOLD_STATUS_WRITE_LOW_FAILED,
TMP102_THRESHOLD_STATUS_SENSOR_SETUP_FAILED,
TMP102_THRESHOLD_STATUS_COUNT,
};
#ifdef USE_TMP102_TEXT_SENSOR
PROGMEM_STRING_TABLE(ThresholdStatusStrings, "Not configured", "Setup failed", "OK", "Restored thresholds reset",
"Restore reset; save failed", "Preference save failed", "Configuration recovery failed",
"Configuration restored", "Write failed: high threshold", "Write failed: low threshold",
"Rejected: sensor setup failed");
static_assert(ThresholdStatusStrings::COUNT == TMP102_THRESHOLD_STATUS_COUNT);
#endif
#if defined(USE_TMP102_CONFIGURE) || defined(USE_TMP102_BINARY_SENSOR)
static constexpr uint8_t TMP102_REGISTER_CONFIGURATION = 0x01;
static constexpr uint16_t TMP102_ALERT_BIT = 0x0020;
@@ -42,15 +72,30 @@ static uint16_t encode_temperature(float temperature, bool extended) {
return static_cast<uint16_t>(static_cast<uint32_t>(static_cast<uint16_t>(steps)) << (extended ? 3 : 4));
}
#endif
#endif
#if defined(USE_TMP102_CONFIGURE) || defined(USE_TMP102_TEXT_SENSOR)
void TMP102Component::setup() {
if (!this->configure_)
#ifdef USE_TMP102_CONFIGURE
if (this->configure_) {
this->setup_configured_();
return;
}
#endif
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_NOT_CONFIGURED);
}
#endif
#ifdef USE_TMP102_CONFIGURE
void TMP102Component::setup_configured_() {
bool corrected = false;
#ifdef USE_TMP102_NUMBER
float restored[2]{};
this->prepare_number_limits_(restored);
bool restore_corrected[2]{};
corrected = this->prepare_number_limits_(restored, restore_corrected);
#endif
if (!this->write_configuration_()) {
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_SETUP_FAILED);
this->mark_failed();
return;
}
@@ -61,14 +106,17 @@ void TMP102Component::setup() {
if (control == nullptr)
continue;
const float value = this->get_limit_temperature_(static_cast<TMP102LimitType>(limit));
if (value != restored[limit])
if (restore_corrected[limit] || value != restored[limit])
saved &= control->save_value_(value);
control->publish_state(value);
}
if (!saved) {
ESP_LOGW(TAG, "Failed to save threshold preferences");
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_RESTORE_RESET_SAVE_FAILED);
return;
}
#endif
this->publish_threshold_status_(corrected ? TMP102_THRESHOLD_STATUS_RESTORE_RESET : TMP102_THRESHOLD_STATUS_OK);
}
#endif
@@ -83,6 +131,9 @@ void TMP102Component::dump_config() {
#ifdef USE_TMP102_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Alert", this->alert_binary_sensor_);
#endif
#ifdef USE_TMP102_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Threshold Status", this->threshold_status_text_sensor_);
#endif
#ifdef USE_TMP102_CONFIGURE
if (!this->configure_)
return;
@@ -203,6 +254,9 @@ bool TMP102Component::check_configuration_() {
this->set_warning_and_invalidate_alert_();
if (!this->write_configuration_()) {
ESP_LOGW(TAG, "Failed to restore configuration and thresholds");
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_RECOVERY_FAILED);
} else {
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_RECOVERED);
}
return false;
}
@@ -228,22 +282,25 @@ void TMP102Component::set_limit_control(TMP102LimitNumber *number) {
this->limit_controls_[number->limit_type_] = number;
}
void TMP102Component::prepare_number_limits_(float *restored) {
bool TMP102Component::prepare_number_limits_(float *restored, bool *restore_corrected) {
const bool extended = this->config_ & TMP102_EXTENDED_MODE_BIT;
uint16_t raw[2] = {this->low_limit_, this->high_limit_};
bool corrected = false;
for (uint8_t limit = 0; limit < 2; ++limit) {
TMP102LimitNumber *control = this->limit_controls_[limit];
if (control == nullptr)
continue;
restored[limit] = control->load_value_(decode_temperature(raw[limit], extended));
restored[limit] = control->load_value_(decode_temperature(raw[limit], extended), restore_corrected[limit]);
corrected |= restore_corrected[limit];
raw[limit] = encode_temperature(restored[limit], extended);
}
if (static_cast<int16_t>(raw[TMP102_LIMIT_LOW]) > static_cast<int16_t>(raw[TMP102_LIMIT_HIGH])) {
ESP_LOGW(TAG, "Restored thresholds are inverted; using configured values");
return;
return true;
}
this->low_limit_ = raw[TMP102_LIMIT_LOW];
this->high_limit_ = raw[TMP102_LIMIT_HIGH];
return corrected;
}
float TMP102Component::get_limit_temperature_(TMP102LimitType limit) const {
@@ -261,10 +318,13 @@ bool TMP102Component::set_limit_temperature_(TMP102LimitType limit, float temper
ESP_LOGW(TAG, "Rejected %s threshold %.4f°C: crosses %s threshold %.4f°C",
high ? LOG_STR_LITERAL("high") : LOG_STR_LITERAL("low"), decode_temperature(raw, extended),
high ? LOG_STR_LITERAL("low") : LOG_STR_LITERAL("high"), decode_temperature(other, extended));
this->publish_threshold_rejection_(high, decode_temperature(raw, extended), decode_temperature(other, extended));
return false;
}
if (!this->write_register_(static_cast<uint8_t>(TMP102_REGISTER_LOW_LIMIT + limit), raw)) {
this->status_set_warning();
this->publish_threshold_status_(high ? TMP102_THRESHOLD_STATUS_WRITE_HIGH_FAILED
: TMP102_THRESHOLD_STATUS_WRITE_LOW_FAILED);
return false;
}
(high ? this->high_limit_ : this->low_limit_) = raw;
@@ -273,6 +333,32 @@ bool TMP102Component::set_limit_temperature_(TMP102LimitType limit, float temper
#endif
#endif
#ifdef USE_TMP102_TEXT_SENSOR
void TMP102Component::publish_threshold_status_(uint8_t status) {
if (this->threshold_status_text_sensor_ == nullptr)
return;
char text[40];
ESPHOME_strncpy_P(
text,
reinterpret_cast<ESPHOME_PGM_P>(ThresholdStatusStrings::get_progmem_str(status, TMP102_THRESHOLD_STATUS_OK)),
sizeof(text));
text[sizeof(text) - 1] = '\0';
this->threshold_status_text_sensor_->publish_state(text);
}
void TMP102Component::publish_threshold_rejection_(bool high, float value, float other) {
if (this->threshold_status_text_sensor_ == nullptr)
return;
char text[96];
if (high) {
ESPHOME_snprintf_P(text, sizeof(text), ESPHOME_PSTR("Rejected: high %.4f°C crosses low %.4f°C"), value, other);
} else {
ESPHOME_snprintf_P(text, sizeof(text), ESPHOME_PSTR("Rejected: low %.4f°C crosses high %.4f°C"), value, other);
}
this->threshold_status_text_sensor_->publish_state(text);
}
#endif
#ifdef USE_TMP102_BINARY_SENSOR
void TMP102Component::invalidate_alert_() {
if (this->alert_binary_sensor_ != nullptr)
@@ -294,7 +380,7 @@ bool TMP102Component::read_alert_state_() {
#endif
#ifdef USE_TMP102_NUMBER
float TMP102LimitNumber::load_value_(float fallback) {
float TMP102LimitNumber::load_value_(float fallback, bool &corrected) {
float value = fallback;
if (this->restore_value_) {
this->pref_ = this->make_entity_preference<float>();
@@ -303,6 +389,7 @@ float TMP102LimitNumber::load_value_(float fallback) {
if (!std::isfinite(value) || value < this->traits.get_min_value() || value > this->traits.get_max_value()) {
ESP_LOGW(TAG, "Invalid restored threshold; using configured value");
value = fallback;
corrected = true;
}
return value;
}
@@ -313,6 +400,7 @@ void TMP102LimitNumber::control(float value) {
// A chip that failed setup holds nothing the entity could show
if (this->parent_->is_failed()) {
ESP_LOGW(TAG, "Rejected threshold change: sensor setup failed");
this->parent_->publish_threshold_status_(TMP102_THRESHOLD_STATUS_SENSOR_SETUP_FAILED);
return;
}
const bool accepted = this->parent_->set_limit_temperature_(this->limit_type_, value);
@@ -320,6 +408,9 @@ void TMP102LimitNumber::control(float value) {
this->publish_state(value);
if (accepted && !this->save_value_(value)) {
ESP_LOGW(TAG, "Failed to save threshold preference");
this->parent_->publish_threshold_status_(TMP102_THRESHOLD_STATUS_PREFERENCE_SAVE_FAILED);
} else if (accepted) {
this->parent_->publish_threshold_status_(TMP102_THRESHOLD_STATUS_OK);
}
}
#endif
+28 -4
View File
@@ -12,11 +12,20 @@
#include "esphome/core/preferences.h"
#endif
#include "esphome/components/sensor/sensor.h"
#ifdef USE_TMP102_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include <cstdint>
namespace esphome::tmp102 {
#ifdef USE_HOST
namespace testing {
class TMP102Test;
}
#endif
#ifdef USE_TMP102_NUMBER
enum TMP102LimitType : uint8_t {
TMP102_LIMIT_LOW = 0,
@@ -31,8 +40,13 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
#ifdef USE_TMP102_BINARY_SENSOR
SUB_BINARY_SENSOR(alert)
#endif
#ifdef USE_TMP102_CONFIGURE
#ifdef USE_TMP102_TEXT_SENSOR
SUB_TEXT_SENSOR(threshold_status)
#endif
#if defined(USE_TMP102_CONFIGURE) || defined(USE_TMP102_TEXT_SENSOR)
void setup() override;
#endif
#ifdef USE_TMP102_CONFIGURE
void set_configuration(uint16_t config, uint16_t high_limit, uint16_t low_limit) {
this->config_ = config;
this->high_limit_ = high_limit;
@@ -47,6 +61,13 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
void update() override;
protected:
#ifdef USE_TMP102_TEXT_SENSOR
void publish_threshold_status_(uint8_t status);
void publish_threshold_rejection_(bool high, float value, float other);
#else
void publish_threshold_status_(uint8_t) {}
void publish_threshold_rejection_(bool, float, float) {}
#endif
void set_warning_and_invalidate_alert_() {
this->status_set_warning();
#ifdef USE_TMP102_BINARY_SENSOR
@@ -58,6 +79,7 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
void invalidate_alert_();
#endif
#ifdef USE_TMP102_CONFIGURE
void setup_configured_();
bool write_register_(uint8_t reg, uint16_t value);
bool write_configuration_();
bool check_configuration_();
@@ -67,11 +89,10 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
friend class TMP102LimitNumber;
bool set_limit_temperature_(TMP102LimitType limit, float temperature);
float get_limit_temperature_(TMP102LimitType limit) const;
void prepare_number_limits_(float *restored);
bool prepare_number_limits_(float *restored, bool *restore_corrected);
TMP102LimitNumber *limit_controls_[2]{};
#endif
uint16_t config_{0};
uint16_t high_limit_{0};
uint16_t low_limit_{0};
@@ -87,7 +108,10 @@ class TMP102LimitNumber final : public number::Number, public Parented<TMP102Com
protected:
friend class TMP102Component;
float load_value_(float fallback);
#ifdef USE_HOST
friend class testing::TMP102Test;
#endif
float load_value_(float fallback, bool &corrected);
bool save_value_(float value);
void control(float value) override;
+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;

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