mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 14:38:52 +00:00
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e426e33427 | ||
|
|
d5c6b01e19 | ||
|
|
57e14c1b54 | ||
|
|
11a228569b | ||
|
|
3b4deefd4c | ||
|
|
ddfcc0bc8b | ||
|
|
965b26dab6 | ||
|
|
8824610f82 | ||
|
|
69785f56f6 | ||
|
|
61b52c0a34 | ||
|
|
54c4cc81c5 | ||
|
|
02ea1b2696 | ||
|
|
16023df4fd | ||
|
|
0fd72f74f1 | ||
|
|
4254d76e5c | ||
|
|
7605958db5 | ||
|
|
35ea89859b | ||
|
|
fd7a0d3679 | ||
|
|
dc37e06143 | ||
|
|
9226e03496 | ||
|
|
a4fff7738b | ||
|
|
4242bcb084 | ||
|
|
8a88e97497 | ||
|
|
d586e20a31 | ||
|
|
e6161e64b1 | ||
|
|
431843c27e | ||
|
|
a1da3d6b90 | ||
|
|
930429061a | ||
|
|
afceb91d90 | ||
|
|
7d741361a3 | ||
|
|
9c70ffd826 | ||
|
|
dac6a9c8cc | ||
|
|
a3a4144e4f | ||
|
|
29e9ae008d | ||
|
|
c92c4d7f99 | ||
|
|
5bcc3f42e9 | ||
|
|
d6aa5ba14c | ||
|
|
59c7178910 | ||
|
|
e55741a26b | ||
|
|
0bdc14154b | ||
|
|
1d6f839ee7 | ||
|
|
2544518aca | ||
|
|
70e56df630 | ||
|
|
7d62aae996 | ||
|
|
c12baaa337 |
No files matched your search
@@ -286,7 +286,7 @@ jobs:
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@1382ad72dea72f9d53832f2bcb65b3e87a947204 # v3.0.1
|
||||
uses: j178/prek-action@af4d4fcb6922b8e6f65c0426947125fcc38794ce # v3.0.2
|
||||
with:
|
||||
prek-version: ${{ steps.prek.outputs.version }}
|
||||
# This job only runs on pull requests, so nothing ever populates
|
||||
|
||||
@@ -216,9 +216,10 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
|
||||
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
|
||||
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
|
||||
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
|
||||
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
|
||||
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
|
||||
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
|
||||
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
|
||||
end of `setup()` when the entities need a state at boot.
|
||||
@@ -446,8 +447,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
|
||||
```
|
||||
Restore an owned or standalone switch in `setup()` with `sw->restore_state()`, or
|
||||
`restore_state(default)` to still drive a state when the restore mode is disabled.
|
||||
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
|
||||
to still drive a state when the restore mode is disabled.
|
||||
|
||||
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
|
||||
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
|
||||
|
||||
@@ -147,6 +147,7 @@ esphome/components/cst816/* @clydebarrow
|
||||
esphome/components/cst9220/* @clydebarrow
|
||||
esphome/components/ct_clamp/* @jesserockz
|
||||
esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/cw2017/* @exciton
|
||||
esphome/components/d01/* @ch604
|
||||
esphome/components/dac7678/* @NickB1
|
||||
esphome/components/daikin_arc/* @MagicBear
|
||||
@@ -325,6 +326,7 @@ esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/ir_rf_base/* @bdraco @kbx81
|
||||
esphome/components/ir_rf_proxy/* @kbx81
|
||||
esphome/components/islandaire/* @ipopov
|
||||
esphome/components/it8951/* @koosoli @limengdu @Passific
|
||||
esphome/components/jsn_sr04t/* @Mafus1
|
||||
esphome/components/json/* @esphome/core
|
||||
@@ -356,6 +358,7 @@ esphome/components/lm75b/* @beormund
|
||||
esphome/components/ln882h_ble/* @Bl00d-B0b
|
||||
esphome/components/ln882h_ble_tracker/* @Bl00d-B0b
|
||||
esphome/components/ln882x/* @lamauny
|
||||
esphome/components/ln882x_spi_led_strip/* @sebo-b
|
||||
esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/logger/select/* @clydebarrow
|
||||
@@ -366,6 +369,7 @@ esphome/components/ltr390/* @latonita @sjtrny
|
||||
esphome/components/ltr501/* @latonita
|
||||
esphome/components/ltr_als_ps/* @latonita
|
||||
esphome/components/lvgl/* @clydebarrow
|
||||
esphome/components/m5stack_4relay/* @KoenBreeman
|
||||
esphome/components/m5stack_8angle/* @rnauber
|
||||
esphome/components/mapping/* @clydebarrow
|
||||
esphome/components/matrix_keypad/* @ssieb
|
||||
@@ -537,6 +541,7 @@ esphome/components/sdp3x/* @Azimath
|
||||
esphome/components/seeed_mr24hpc1/* @limengdu
|
||||
esphome/components/seeed_mr60bha2/* @limengdu
|
||||
esphome/components/seeed_mr60fda2/* @limengdu
|
||||
esphome/components/seeed_multi_channel_relay/* @KoenBreeman
|
||||
esphome/components/selec_meter/* @sourabhjaiswal
|
||||
esphome/components/select/* @esphome/core
|
||||
esphome/components/sen0321/* @notjj
|
||||
|
||||
@@ -8,10 +8,7 @@ ESPHOME_LOG_TAG(TAG, "copy.lock");
|
||||
void CopyLock::setup() {
|
||||
source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); });
|
||||
|
||||
traits.set_assumed_state(source_->traits.get_assumed_state());
|
||||
traits.set_requires_code(source_->traits.get_requires_code());
|
||||
traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
|
||||
traits.set_supports_open(source_->traits.get_supports_open());
|
||||
this->traits = this->source_->traits;
|
||||
|
||||
this->publish_state(source_->state);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::cover {
|
||||
|
||||
class CoverTraits {
|
||||
public:
|
||||
CoverTraits() = default;
|
||||
|
||||
bool get_is_assumed_state() const { return this->is_assumed_state_; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->is_assumed_state_ = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->supports_position_; }
|
||||
void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; }
|
||||
bool get_supports_tilt() const { return this->supports_tilt_; }
|
||||
void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; }
|
||||
bool get_supports_toggle() const { return this->supports_toggle_; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->supports_stop_; }
|
||||
void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; }
|
||||
bool get_supports_stop_tilt() const { return this->supports_stop_tilt_; }
|
||||
void set_supports_stop_tilt(bool supports_stop_tilt) { this->supports_stop_tilt_ = supports_stop_tilt; }
|
||||
bool get_is_assumed_state() const { return this->flags_.is_assumed_state; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->flags_.is_assumed_state = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->flags_.supports_position; }
|
||||
void set_supports_position(bool supports_position) { this->flags_.supports_position = supports_position; }
|
||||
bool get_supports_tilt() const { return this->flags_.supports_tilt; }
|
||||
void set_supports_tilt(bool supports_tilt) { this->flags_.supports_tilt = supports_tilt; }
|
||||
bool get_supports_toggle() const { return this->flags_.supports_toggle; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->flags_.supports_toggle = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->flags_.supports_stop; }
|
||||
void set_supports_stop(bool supports_stop) { this->flags_.supports_stop = supports_stop; }
|
||||
bool get_supports_stop_tilt() const { return this->flags_.supports_stop_tilt; }
|
||||
void set_supports_stop_tilt(bool supports_stop_tilt) { this->flags_.supports_stop_tilt = supports_stop_tilt; }
|
||||
|
||||
protected:
|
||||
bool is_assumed_state_{false};
|
||||
bool supports_position_{false};
|
||||
bool supports_tilt_{false};
|
||||
bool supports_toggle_{false};
|
||||
bool supports_stop_{false};
|
||||
bool supports_stop_tilt_{false};
|
||||
// Bit-packed flags to save memory (1 byte instead of 6)
|
||||
struct CoverTraitFlags {
|
||||
uint8_t is_assumed_state : 1;
|
||||
uint8_t supports_position : 1;
|
||||
uint8_t supports_tilt : 1;
|
||||
uint8_t supports_toggle : 1;
|
||||
uint8_t supports_stop : 1;
|
||||
uint8_t supports_stop_tilt : 1;
|
||||
uint8_t reserved : 2;
|
||||
} flags_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@exciton"]
|
||||
DOMAIN = "cw2017"
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "cw2017.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::cw2017 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cw2017");
|
||||
|
||||
static constexpr uint8_t REG_VERSION = 0x00;
|
||||
static constexpr uint8_t REG_VCELL = 0x02;
|
||||
static constexpr uint8_t REG_SOC = 0x04;
|
||||
static constexpr uint8_t REG_MODE = 0x08;
|
||||
static constexpr uint8_t REG_SOC_ALERT = 0x0B;
|
||||
static constexpr uint8_t REG_BATINFO = 0x10;
|
||||
|
||||
static constexpr uint8_t MODE_NORMAL = 0x00;
|
||||
static constexpr uint8_t MODE_SLEEP = 0x30;
|
||||
static constexpr uint8_t UPDATE_FLAG = 0x80;
|
||||
static constexpr uint8_t RESTART_MODES[] PROGMEM = {0xF0, MODE_SLEEP, MODE_NORMAL};
|
||||
static constexpr uint8_t RESTART_STEPS = sizeof(RESTART_MODES);
|
||||
|
||||
static constexpr uint16_t VCELL_MASK = 0x3FFF;
|
||||
static constexpr float VCELL_VOLTS_PER_LSB = 312.5e-6f;
|
||||
|
||||
static constexpr uint32_t STEP_DELAY_MS = 20;
|
||||
static constexpr uint32_t READY_POLL_MS = 100;
|
||||
static constexpr uint8_t READY_ATTEMPTS = 40;
|
||||
static constexpr uint32_t INIT_RETRY_MS = 1000;
|
||||
static constexpr uint8_t INIT_ATTEMPTS = 3;
|
||||
|
||||
static bool version_is_running(uint8_t version) { return (version & 0xFD) == 0x0D; }
|
||||
|
||||
// The configured profile only changes with new firmware, and flashing never ends in a deep-sleep wake.
|
||||
static bool woke_from_deep_sleep() {
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
return deep_sleep::woke_from_deep_sleep();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CW2017::read_profile_matches_(bool &matches) {
|
||||
uint8_t stored[PROFILE_SIZE];
|
||||
uint8_t expected[PROFILE_SIZE];
|
||||
if (!this->read_bytes(REG_BATINFO, stored, PROFILE_SIZE))
|
||||
return false;
|
||||
progmem_memcpy(expected, this->profile_, PROFILE_SIZE);
|
||||
matches = std::memcmp(stored, expected, PROFILE_SIZE) == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CW2017::upload_profile_() {
|
||||
// Built here with the register first, so the bus layer does not heap-allocate a buffer this size.
|
||||
uint8_t buffer[1 + PROFILE_SIZE] = {REG_BATINFO};
|
||||
progmem_memcpy(buffer + 1, this->profile_, PROFILE_SIZE);
|
||||
return this->write(buffer, sizeof(buffer)) == i2c::ERROR_OK && this->write_byte(REG_SOC_ALERT, UPDATE_FLAG);
|
||||
}
|
||||
|
||||
void CW2017::setup() { this->init_(); }
|
||||
|
||||
// Retried because another device resetting on the same bus can disturb an early transfer.
|
||||
void CW2017::init_failed_(const LogString *reason) {
|
||||
if (++this->init_attempts_ >= INIT_ATTEMPTS) {
|
||||
this->mark_failed(reason);
|
||||
return;
|
||||
}
|
||||
this->set_timeout(INIT_RETRY_MS, [this]() { this->init_(); });
|
||||
}
|
||||
|
||||
void CW2017::init_() {
|
||||
// A profile upload only takes effect after a restart, so a failed restart is retried, not skipped.
|
||||
if (this->restart_pending_) {
|
||||
this->restart_();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t mode = 0;
|
||||
uint8_t version = 0;
|
||||
uint8_t config = 0;
|
||||
if (!this->read_byte(REG_MODE, &mode) || !this->read_byte(REG_VERSION, &version) ||
|
||||
!this->read_byte(REG_SOC_ALERT, &config)) {
|
||||
this->init_failed_(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
const bool has_profile = (config & UPDATE_FLAG) != 0;
|
||||
bool matches = has_profile && woke_from_deep_sleep();
|
||||
if (has_profile && !matches && !this->read_profile_matches_(matches)) {
|
||||
this->init_failed_(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matches) {
|
||||
ESP_LOGI(TAG, "Uploading battery profile");
|
||||
if (!this->upload_profile_()) {
|
||||
this->init_failed_(LOG_STR("Battery profile upload failed"));
|
||||
return;
|
||||
}
|
||||
this->restart_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == MODE_SLEEP) {
|
||||
// The gauge keeps its profile and state through its own sleep, so a wake is a mode write, not a restart.
|
||||
if (!this->write_byte(REG_MODE, MODE_NORMAL)) {
|
||||
this->init_failed_(LOG_STR("Gauge wake failed"));
|
||||
return;
|
||||
}
|
||||
this->set_timeout(STEP_DELAY_MS, [this]() { this->wait_ready_(0); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode != MODE_NORMAL || !version_is_running(version)) {
|
||||
this->restart_();
|
||||
return;
|
||||
}
|
||||
|
||||
this->wait_ready_(0);
|
||||
}
|
||||
|
||||
void CW2017::restart_() {
|
||||
this->restart_pending_ = true;
|
||||
this->set_timeout(STEP_DELAY_MS, [this]() { this->restart_step_(0); });
|
||||
}
|
||||
|
||||
void CW2017::restart_step_(uint8_t step) {
|
||||
if (!this->write_byte(REG_MODE, progmem_read_byte(&RESTART_MODES[step]))) {
|
||||
this->init_failed_(LOG_STR("Restart failed"));
|
||||
return;
|
||||
}
|
||||
if (step + 1 < RESTART_STEPS) {
|
||||
this->set_timeout(STEP_DELAY_MS, [this, step]() { this->restart_step_(step + 1); });
|
||||
} else {
|
||||
this->restart_pending_ = false;
|
||||
this->set_timeout(STEP_DELAY_MS, [this]() { this->wait_ready_(0); });
|
||||
}
|
||||
}
|
||||
|
||||
void CW2017::wait_ready_(uint8_t attempt) {
|
||||
uint8_t version = 0;
|
||||
uint8_t soc = 0;
|
||||
if (this->read_byte(REG_VERSION, &version) && version_is_running(version) && this->read_byte(REG_SOC, &soc) &&
|
||||
soc <= 100) {
|
||||
ESP_LOGD(TAG, "Gauge ready %u ms after boot", (unsigned) millis());
|
||||
this->ready_ = true;
|
||||
this->update();
|
||||
return;
|
||||
}
|
||||
if (attempt + 1 >= READY_ATTEMPTS) {
|
||||
this->init_failed_(LOG_STR("Gauge did not become ready"));
|
||||
return;
|
||||
}
|
||||
this->set_timeout(READY_POLL_MS, [this, attempt]() { this->wait_ready_(attempt + 1); });
|
||||
}
|
||||
|
||||
void CW2017::update() {
|
||||
if (!this->ready_)
|
||||
return;
|
||||
|
||||
if (this->battery_level_sensor_ != nullptr) {
|
||||
uint8_t soc = 0;
|
||||
if (this->read_byte(REG_SOC, &soc) && soc <= 100) {
|
||||
this->battery_level_sensor_->publish_state(soc);
|
||||
} else {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->battery_voltage_sensor_ != nullptr) {
|
||||
uint16_t raw = 0;
|
||||
if (this->read_byte_16(REG_VCELL, &raw)) {
|
||||
this->battery_voltage_sensor_->publish_state((raw & VCELL_MASK) * VCELL_VOLTS_PER_LSB);
|
||||
} else {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void CW2017::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CW2017:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_sensor_);
|
||||
}
|
||||
|
||||
} // namespace esphome::cw2017
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::cw2017 {
|
||||
|
||||
static constexpr uint8_t PROFILE_SIZE = 80;
|
||||
|
||||
class CW2017 : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
explicit CW2017(const uint8_t *profile) : profile_(profile) {}
|
||||
|
||||
void set_battery_level_sensor(sensor::Sensor *sensor) { this->battery_level_sensor_ = sensor; }
|
||||
void set_battery_voltage_sensor(sensor::Sensor *sensor) { this->battery_voltage_sensor_ = sensor; }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void init_();
|
||||
void init_failed_(const LogString *reason);
|
||||
bool read_profile_matches_(bool &matches);
|
||||
bool upload_profile_();
|
||||
void restart_();
|
||||
void restart_step_(uint8_t step);
|
||||
void wait_ready_(uint8_t attempt);
|
||||
|
||||
const uint8_t *profile_;
|
||||
sensor::Sensor *battery_level_sensor_{nullptr};
|
||||
sensor::Sensor *battery_voltage_sensor_{nullptr};
|
||||
bool ready_{false};
|
||||
bool restart_pending_{false};
|
||||
uint8_t init_attempts_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::cw2017
|
||||
@@ -0,0 +1,61 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_RAW_DATA_ID,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_BATTERY_PROFILE = "battery_profile"
|
||||
PROFILE_SIZE = 80
|
||||
|
||||
cw2017_ns = cg.esphome_ns.namespace("cw2017")
|
||||
CW2017 = cw2017_ns.class_("CW2017", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CW2017),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.Required(CONF_BATTERY_PROFILE): cv.All(
|
||||
cv.ensure_list(cv.hex_uint8_t),
|
||||
cv.Length(min=PROFILE_SIZE, max=PROFILE_SIZE),
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x63))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
profile = cg.progmem_array(config[CONF_RAW_DATA_ID], config[CONF_BATTERY_PROFILE])
|
||||
var = await i2c.new_i2c_component(config, profile)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
|
||||
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage_sensor)
|
||||
@@ -7,7 +7,6 @@ namespace esphome::deep_sleep {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "deep_sleep");
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
switch (lt_get_reboot_reason()) {
|
||||
case REBOOT_REASON_SLEEP_GPIO:
|
||||
@@ -20,7 +19,6 @@ WakeupCause get_wakeup_cause() {
|
||||
return WAKEUP_CAUSE_NONE;
|
||||
}
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ struct WakeupCauseToRunDuration {
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
/// Why the device woke from deep sleep. Passed to on_wake automations.
|
||||
enum WakeupCause : uint8_t {
|
||||
/// The device did not wake from deep sleep (for example a cold boot, reset or OTA restart).
|
||||
@@ -79,6 +77,11 @@ enum WakeupCause : uint8_t {
|
||||
/// Return why the device woke from deep sleep. Implemented per platform.
|
||||
WakeupCause get_wakeup_cause();
|
||||
|
||||
/// Return true when this boot is a wake from deep sleep.
|
||||
inline bool woke_from_deep_sleep() { return get_wakeup_cause() != WAKEUP_CAUSE_NONE; }
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
/** Setup priority of on_wake triggers.
|
||||
*
|
||||
* Between restoring global variables (setup_priority::HARDWARE, 800) and on_boot automations at
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace esphome::deep_sleep {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "deep_sleep");
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
switch (esp_sleep_get_wakeup_cause()) {
|
||||
case ESP_SLEEP_WAKEUP_EXT0:
|
||||
@@ -47,7 +46,6 @@ WakeupCause get_wakeup_cause() {
|
||||
return WAKEUP_CAUSE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
|
||||
if (this->wakeup_cause_to_run_duration_.has_value()) {
|
||||
|
||||
@@ -3,17 +3,14 @@
|
||||
|
||||
#include <Esp.h>
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace esphome::deep_sleep {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "deep_sleep");
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
// The ESP8266 can only wake from deep sleep through the RTC timer (via GPIO16 -> RST).
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
@@ -22,7 +19,6 @@ WakeupCause get_wakeup_cause() {
|
||||
}
|
||||
return WAKEUP_CAUSE_NONE;
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ static bool wakeable_delay_feed_wdt(uint32_t ms) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A wake from System OFF is not told apart from a reset yet.
|
||||
WakeupCause get_wakeup_cause() { return WAKEUP_CAUSE_NONE; }
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
from esphome import automation
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FREQUENCY,
|
||||
CONF_HOUR,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_MINUTE,
|
||||
CONF_MODE,
|
||||
CONF_OFFSET,
|
||||
CONF_SECOND,
|
||||
CONF_STATE,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@linkedupbits"]
|
||||
@@ -10,23 +21,245 @@ MULTI_CONF = True
|
||||
DOMAIN = "ds3231"
|
||||
|
||||
CONF_DS3231_ID = "ds3231_id"
|
||||
CONF_ALARM = "alarm"
|
||||
CONF_ALARM_CHECK_INTERVAL = "alarm_check_interval"
|
||||
CONF_AGING_OFFSET = "aging_offset"
|
||||
CONF_AUTO_CLEAR_ALARMS = "auto_clear_alarms"
|
||||
CONF_BATTERY_BACKED_SQUARE_WAVE = "battery_backed_square_wave"
|
||||
CONF_DAY_OF_MONTH = "day_of_month"
|
||||
CONF_DAY_OF_WEEK = "day_of_week"
|
||||
CONF_ON_ALARM_1 = "on_alarm_1"
|
||||
CONF_ON_ALARM_2 = "on_alarm_2"
|
||||
CONF_OUTPUT_32KHZ = "output_32khz"
|
||||
CONF_SQUARE_WAVE_FREQUENCY = "square_wave_frequency"
|
||||
|
||||
ds3231_ns = cg.esphome_ns.namespace("ds3231")
|
||||
DS3231Component = ds3231_ns.class_("DS3231Component", cg.Component, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema({cv.GenerateID(): cv.declare_id(DS3231Component)})
|
||||
AlarmMode = ds3231_ns.enum("AlarmMode")
|
||||
ALARM_1_MODES = AlarmMode.options(
|
||||
"every_second",
|
||||
"every_minute",
|
||||
"hourly",
|
||||
"daily",
|
||||
"monthly",
|
||||
"weekly",
|
||||
prefix="ALARM_MODE_",
|
||||
)
|
||||
# Alarm 2 has no seconds register.
|
||||
ALARM_2_MODES = {k: v for k, v in ALARM_1_MODES.items() if k != "every_second"}
|
||||
|
||||
SquareWaveFrequency = ds3231_ns.enum("SquareWaveFrequency")
|
||||
# Keyed by the frequency in Hz; cv.frequency turns "1024Hz", "1.024kHz" or 1024 into it.
|
||||
SQUARE_WAVE_FREQUENCIES = {
|
||||
1: SquareWaveFrequency.SQUARE_WAVE_1HZ,
|
||||
1024: SquareWaveFrequency.SQUARE_WAVE_1024HZ,
|
||||
4096: SquareWaveFrequency.SQUARE_WAVE_4096HZ,
|
||||
8192: SquareWaveFrequency.SQUARE_WAVE_8192HZ,
|
||||
}
|
||||
validate_square_wave_frequency = cv.All(
|
||||
cv.frequency, cv.enum(SQUARE_WAVE_FREQUENCIES, int=True)
|
||||
)
|
||||
|
||||
validate_aging_offset = cv.int_range(min=-128, max=127)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DS3231Component),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_SQUARE_WAVE_FREQUENCY): validate_square_wave_frequency,
|
||||
cv.Optional(CONF_BATTERY_BACKED_SQUARE_WAVE): cv.boolean,
|
||||
cv.Optional(CONF_OUTPUT_32KHZ): cv.boolean,
|
||||
cv.Optional(CONF_AGING_OFFSET): validate_aging_offset,
|
||||
cv.Optional(CONF_AUTO_CLEAR_ALARMS, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_ALARM_CHECK_INTERVAL, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_ALARM_1): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_ALARM_2): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x68))
|
||||
.extend(i2c.i2c_device_schema(0x68)),
|
||||
cv.has_at_most_one_key(CONF_INTERRUPT_PIN, CONF_SQUARE_WAVE_FREQUENCY),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await i2c.new_i2c_component(config)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
cg.add_optional(
|
||||
var.set_square_wave_frequency, config.get(CONF_SQUARE_WAVE_FREQUENCY)
|
||||
)
|
||||
cg.add_optional(
|
||||
var.set_battery_backed_square_wave, config.get(CONF_BATTERY_BACKED_SQUARE_WAVE)
|
||||
)
|
||||
cg.add_optional(var.set_output_32khz_config, config.get(CONF_OUTPUT_32KHZ))
|
||||
cg.add_optional(var.set_aging_offset_config, config.get(CONF_AGING_OFFSET))
|
||||
cg.add(var.set_auto_clear_alarms(config[CONF_AUTO_CLEAR_ALARMS]))
|
||||
cg.add(var.set_alarm_check_interval(config[CONF_ALARM_CHECK_INTERVAL]))
|
||||
# The alarm flags are only read when something uses them; the binary sensor platform
|
||||
# turns this on as well. With an interrupt pin the check also releases INT/SQW.
|
||||
if any(
|
||||
key in config for key in (CONF_INTERRUPT_PIN, CONF_ON_ALARM_1, CONF_ON_ALARM_2)
|
||||
):
|
||||
cg.add(var.set_monitor_alarms(True))
|
||||
|
||||
# One callback for both alarms; each automation runs only for its own alarm number.
|
||||
for conf_key, alarm in ((CONF_ON_ALARM_1, 1), (CONF_ON_ALARM_2, 2)):
|
||||
if (conf := config.get(conf_key)) is not None:
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_alarm_callback",
|
||||
[],
|
||||
conf,
|
||||
params=[(cg.uint8, "alarm")],
|
||||
forward=[],
|
||||
when=f"alarm == {alarm}",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Actions
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
|
||||
DS3231_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(DS3231Component)}
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.force_temperature_conversion",
|
||||
automation.maybe_simple_id({cv.GenerateID(): cv.use_id(DS3231Component)}),
|
||||
DS3231_ACTION_SCHEMA,
|
||||
automation.ApplyCall("force_temperature_conversion()"),
|
||||
)
|
||||
|
||||
|
||||
def _validate_alarm_day(config: ConfigType) -> ConfigType:
|
||||
mode = config[CONF_MODE]
|
||||
if mode == "monthly" and CONF_DAY_OF_MONTH not in config:
|
||||
raise cv.Invalid(f"'{CONF_DAY_OF_MONTH}' is required in monthly mode")
|
||||
if mode == "weekly" and CONF_DAY_OF_WEEK not in config:
|
||||
raise cv.Invalid(f"'{CONF_DAY_OF_WEEK}' is required in weekly mode")
|
||||
# The unused day still has to be a valid register value.
|
||||
config.setdefault(CONF_DAY_OF_MONTH, 1)
|
||||
config.setdefault(CONF_DAY_OF_WEEK, 1)
|
||||
return config
|
||||
|
||||
|
||||
def _alarm_schema(modes: dict, with_second: bool) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_MODE): cv.enum(modes, lower=True),
|
||||
cv.Optional(CONF_MINUTE, default=0): cv.templatable(
|
||||
cv.int_range(min=0, max=59)
|
||||
),
|
||||
cv.Optional(CONF_HOUR, default=0): cv.templatable(cv.int_range(min=0, max=23)),
|
||||
cv.Optional(CONF_DAY_OF_MONTH): cv.templatable(cv.int_range(min=1, max=31)),
|
||||
# 1 = Sunday ... 7 = Saturday, as in ESPHome's time component.
|
||||
cv.Optional(CONF_DAY_OF_WEEK): cv.templatable(cv.int_range(min=1, max=7)),
|
||||
cv.Optional(CONF_ENABLED, default=True): cv.templatable(cv.boolean),
|
||||
}
|
||||
if with_second:
|
||||
schema[cv.Optional(CONF_SECOND, default=0)] = cv.templatable(
|
||||
cv.int_range(min=0, max=59)
|
||||
)
|
||||
return cv.All(cv.Schema(schema), _validate_alarm_day)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_alarm_1",
|
||||
_alarm_schema(ALARM_1_MODES, with_second=True),
|
||||
automation.ApplyCall(
|
||||
"set_alarm_1({}, {}, {}, {}, {}, {}, {})",
|
||||
(
|
||||
(CONF_MODE, AlarmMode),
|
||||
(CONF_SECOND, cg.uint8),
|
||||
(CONF_MINUTE, cg.uint8),
|
||||
(CONF_HOUR, cg.uint8),
|
||||
(CONF_DAY_OF_MONTH, cg.uint8),
|
||||
(CONF_DAY_OF_WEEK, cg.uint8),
|
||||
(CONF_ENABLED, cg.bool_),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_alarm_2",
|
||||
_alarm_schema(ALARM_2_MODES, with_second=False),
|
||||
automation.ApplyCall(
|
||||
"set_alarm_2({}, {}, {}, {}, {}, {})",
|
||||
(
|
||||
(CONF_MODE, AlarmMode),
|
||||
(CONF_MINUTE, cg.uint8),
|
||||
(CONF_HOUR, cg.uint8),
|
||||
(CONF_DAY_OF_MONTH, cg.uint8),
|
||||
(CONF_DAY_OF_WEEK, cg.uint8),
|
||||
(CONF_ENABLED, cg.bool_),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
DS3231_ALARM_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_ALARM): cv.templatable(cv.int_range(min=1, max=2)),
|
||||
}
|
||||
)
|
||||
|
||||
for _name, _method in (
|
||||
("ds3231.enable_alarm", "enable_alarm"),
|
||||
("ds3231.disable_alarm", "disable_alarm"),
|
||||
("ds3231.clear_alarm", "clear_alarm"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name,
|
||||
DS3231_ALARM_ACTION_SCHEMA,
|
||||
automation.ApplyField(CONF_ALARM, _method, cg.uint8),
|
||||
)
|
||||
|
||||
automation.register_apply_condition(
|
||||
"ds3231.is_alarm_triggered",
|
||||
DS3231_ALARM_ACTION_SCHEMA,
|
||||
automation.ApplyCall("is_alarm_triggered({})", ((CONF_ALARM, cg.uint8),)),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.enable_square_wave",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_FREQUENCY): validate_square_wave_frequency,
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_FREQUENCY, "enable_square_wave", SquareWaveFrequency),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.disable_square_wave",
|
||||
DS3231_ACTION_SCHEMA,
|
||||
automation.ApplyCall("disable_square_wave()"),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_32khz_output",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_STATE): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_STATE, "set_output_32khz", cg.bool_),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_aging_offset",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_OFFSET): cv.templatable(validate_aging_offset),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_OFFSET, "set_aging_offset", cg.int8),
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, DEVICE_CLASS_PROBLEM, ENTITY_CATEGORY_DIAGNOSTIC
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DS3231_ID, DS3231Component
|
||||
|
||||
DEPENDENCIES = ["ds3231"]
|
||||
|
||||
CONF_ALARM_1 = "alarm_1"
|
||||
CONF_ALARM_2 = "alarm_2"
|
||||
CONF_OSCILLATOR_STOPPED = "oscillator_stopped"
|
||||
|
||||
ICON_ALARM = "mdi:alarm"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DS3231_ID): cv.use_id(DS3231Component),
|
||||
cv.Optional(CONF_ALARM_1): binary_sensor.binary_sensor_schema(
|
||||
icon=ICON_ALARM
|
||||
),
|
||||
cv.Optional(CONF_ALARM_2): binary_sensor.binary_sensor_schema(
|
||||
icon=ICON_ALARM
|
||||
),
|
||||
cv.Optional(CONF_OSCILLATOR_STOPPED): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_ALARM_1, CONF_ALARM_2, CONF_OSCILLATOR_STOPPED),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DS3231_ID])
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_ALARM_1, hub.set_alarm_1_binary_sensor)
|
||||
await binary_sensors(CONF_ALARM_2, hub.set_alarm_2_binary_sensor)
|
||||
await binary_sensors(
|
||||
CONF_OSCILLATOR_STOPPED, hub.set_oscillator_stopped_binary_sensor
|
||||
)
|
||||
if CONF_ALARM_1 in config or CONF_ALARM_2 in config:
|
||||
cg.add(hub.set_monitor_alarms(True))
|
||||
@@ -9,29 +9,184 @@ namespace esphome::ds3231 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ds3231");
|
||||
|
||||
static constexpr uint8_t DS3231_REG_SECONDS = 0x00;
|
||||
static constexpr uint8_t DS3231_REG_ALARM_1 = 0x07;
|
||||
static constexpr uint8_t DS3231_REG_ALARM_2 = 0x0B;
|
||||
static constexpr uint8_t DS3231_REG_CONTROL = 0x0E;
|
||||
static constexpr uint8_t DS3231_REG_STATUS = 0x0F;
|
||||
static constexpr uint8_t DS3231_REG_AGING_OFFSET = 0x10;
|
||||
static constexpr uint8_t DS3231_REG_TEMPERATURE = 0x11;
|
||||
|
||||
static constexpr uint8_t ALARM_MASK_BIT = 0x80; // AxMy: this register is ignored when matching
|
||||
static constexpr uint8_t ALARM_DAY_OF_WEEK = 0x40; // DY/DT: match day of week instead of date
|
||||
|
||||
/// How long an alarm binary sensor stays on when the flag is cleared automatically.
|
||||
static constexpr uint32_t ALARM_PULSE_MS = 1000;
|
||||
|
||||
static const LogString *alarm_state_to_string(uint8_t control, uint8_t alarm) {
|
||||
return (control & (alarm == 1 ? 0x01 : 0x02)) != 0 ? LOG_STR("enabled") : LOG_STR("disabled");
|
||||
}
|
||||
|
||||
static const LogString *square_wave_to_string(SquareWaveFrequency frequency) {
|
||||
switch (frequency) {
|
||||
case SQUARE_WAVE_1HZ:
|
||||
return LOG_STR("1 Hz");
|
||||
case SQUARE_WAVE_1024HZ:
|
||||
return LOG_STR("1.024 kHz");
|
||||
case SQUARE_WAVE_4096HZ:
|
||||
return LOG_STR("4.096 kHz");
|
||||
case SQUARE_WAVE_8192HZ:
|
||||
return LOG_STR("8.192 kHz");
|
||||
default:
|
||||
return LOG_STR("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
void IRAM_ATTR DS3231Component::interrupt_handler(DS3231Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void DS3231Component::setup() {
|
||||
if (!this->read_control_status_()) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
this->control_reg_ &= ~CONTROL_EOSC; // keep the oscillator running on battery
|
||||
// INT/SQW is the (unused) interrupt line rather than a square-wave output.
|
||||
this->control_reg_ |= CONTROL_INTCN;
|
||||
if (!this->write_control_()) {
|
||||
// Keep the oscillator running on battery. INT/SQW is the alarm interrupt line unless a square
|
||||
// wave is configured. The alarm interrupt enables (A1IE/A2IE) are kept as they are: the alarms
|
||||
// live on the chip and survive a reboot of the ESP on battery.
|
||||
uint8_t mask = CONTROL_EOSC | CONTROL_INTCN;
|
||||
uint8_t value = CONTROL_INTCN;
|
||||
if (this->square_wave_configured_) {
|
||||
mask |= CONTROL_RS_MASK;
|
||||
value = this->square_wave_frequency_ << CONTROL_RS_SHIFT;
|
||||
}
|
||||
if (this->battery_backed_square_wave_ >= 0) {
|
||||
mask |= CONTROL_BBSQW;
|
||||
assign_bits(value, CONTROL_BBSQW, this->battery_backed_square_wave_ != 0);
|
||||
}
|
||||
if (!this->update_control_(mask, value)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->output_32khz_ >= 0) {
|
||||
assign_bits(this->status_reg_, STATUS_EN32KHZ, this->output_32khz_ != 0);
|
||||
if (!this->write_status_(0)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->aging_offset_configured_ && !this->write_aging_offset_(this->aging_offset_)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((this->status_reg_ & STATUS_OSF) != 0) {
|
||||
ESP_LOGW(TAG, "Oscillator was stopped (battery empty or missing?), the stored time is invalid.");
|
||||
}
|
||||
this->publish_oscillator_stopped_();
|
||||
|
||||
if (!this->monitor_alarms_) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start from a known state; an alarm that fired while the ESP was off is picked up by the first
|
||||
// check, which runs from the loop right after setup.
|
||||
this->publish_alarm_state_(1, false);
|
||||
this->publish_alarm_state_(2, false);
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
// INT/SQW is open drain and active low: a falling edge means an enabled alarm matched.
|
||||
// The handler only wakes the loop; the flags are read from there.
|
||||
this->interrupt_pin_->attach_interrupt(&DS3231Component::interrupt_handler, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
// Without an interrupt pin this is how alarms are seen. With one it is a fallback: it picks up an
|
||||
// edge that was missed, and latched flags (auto_clear_alarms: false) that were cleared elsewhere.
|
||||
this->set_interval(this->alarm_check_interval_, [this]() { this->check_alarms_(); });
|
||||
}
|
||||
|
||||
void DS3231Component::loop() {
|
||||
// Only runs once after setup and when woken by the interrupt pin.
|
||||
this->disable_loop();
|
||||
this->check_alarms_();
|
||||
}
|
||||
|
||||
void DS3231Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DS3231:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed())
|
||||
return;
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
if ((this->control_reg_ & CONTROL_INTCN) == 0) {
|
||||
ESP_LOGCONFIG(TAG, " INT/SQW: square wave %s",
|
||||
LOG_STR_ARG(square_wave_to_string(
|
||||
static_cast<SquareWaveFrequency>((this->control_reg_ & CONTROL_RS_MASK) >> CONTROL_RS_SHIFT))));
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " INT/SQW: alarm interrupt");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Battery-backed square wave: %s\n"
|
||||
" 32 kHz output: %s\n"
|
||||
" Alarm 1: %s\n"
|
||||
" Alarm 2: %s\n"
|
||||
" Auto clear alarms: %s",
|
||||
YESNO(this->control_reg_ & CONTROL_BBSQW), ONOFF(this->status_reg_ & STATUS_EN32KHZ),
|
||||
LOG_STR_ARG(alarm_state_to_string(this->control_reg_, 1)),
|
||||
LOG_STR_ARG(alarm_state_to_string(this->control_reg_, 2)), YESNO(this->auto_clear_alarms_));
|
||||
if (this->aging_offset_configured_) {
|
||||
ESP_LOGCONFIG(TAG, " Aging offset: %d", this->aging_offset_);
|
||||
}
|
||||
if (this->monitor_alarms_) {
|
||||
ESP_LOGCONFIG(TAG, " Alarm check interval: %" PRIu32 " ms", this->alarm_check_interval_);
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Alarm 1", this->alarm_1_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Alarm 2", this->alarm_2_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Oscillator Stopped", this->oscillator_stopped_binary_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Clock
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
bool DS3231Component::read_calendar(uint8_t *raw) {
|
||||
if (this->is_failed() || !this->status_comm_result(this->read_bytes(DS3231_REG_SECONDS, raw, CALENDAR_LEN)))
|
||||
return false;
|
||||
// Without a valid status the oscillator-stop flag is unknown, so the time can't be trusted.
|
||||
if (!this->read_control_status_())
|
||||
return false;
|
||||
this->publish_oscillator_stopped_();
|
||||
if ((this->status_reg_ & STATUS_OSF) != 0) {
|
||||
ESP_LOGW(TAG, "Oscillator stop flag set, the RTC time is invalid. Write a valid time first.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::write_calendar(const uint8_t *raw) {
|
||||
if (this->is_failed() || !this->status_comm_result(this->write_bytes(DS3231_REG_SECONDS, raw, CALENDAR_LEN)))
|
||||
return false;
|
||||
|
||||
// The time is valid again: clear the oscillator stop flag. If that fails, the RTC would keep
|
||||
// reporting its time as invalid, so the write is not reported as successful.
|
||||
if (!this->read_control_status_())
|
||||
return false;
|
||||
if ((this->status_reg_ & STATUS_OSF) != 0) {
|
||||
if (!this->write_status_(STATUS_OSF))
|
||||
return false;
|
||||
this->status_reg_ &= ~STATUS_OSF;
|
||||
}
|
||||
this->publish_oscillator_stopped_();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Temperature
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
bool DS3231Component::read_temperature(float &out) {
|
||||
uint8_t raw[2];
|
||||
if (this->is_failed() || !this->read_bytes(DS3231_REG_TEMPERATURE, raw, sizeof(raw))) {
|
||||
@@ -53,26 +208,280 @@ void DS3231Component::force_temperature_conversion() {
|
||||
ESP_LOGW(TAG, "A temperature conversion is already in progress.");
|
||||
return;
|
||||
}
|
||||
this->control_reg_ |= CONTROL_CONV;
|
||||
this->write_control_();
|
||||
// CONV is a one-shot command bit, so it is not kept in the cached value.
|
||||
this->status_comm_result(this->write_byte(DS3231_REG_CONTROL, this->control_reg_ | CONTROL_CONV));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Alarms
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
void DS3231Component::set_alarm_1(AlarmMode mode, uint8_t second, uint8_t minute, uint8_t hour, uint8_t day_of_month,
|
||||
uint8_t day_of_week, bool enabled) {
|
||||
if (second > 59 || minute > 59 || hour > 23 || day_of_month < 1 || day_of_month > 31 || day_of_week < 1 ||
|
||||
day_of_week > 7 || mode > ALARM_MODE_WEEKLY) {
|
||||
ESP_LOGE(TAG, "Invalid alarm 1 setting.");
|
||||
return;
|
||||
}
|
||||
// Mask bits A1M1..A1M4: a set bit means the register is ignored when matching.
|
||||
uint8_t regs[4] = {
|
||||
uint8_to_bcd(second),
|
||||
uint8_to_bcd(minute),
|
||||
uint8_to_bcd(hour),
|
||||
mode == ALARM_MODE_WEEKLY ? static_cast<uint8_t>(ALARM_DAY_OF_WEEK | day_of_week) : uint8_to_bcd(day_of_month),
|
||||
};
|
||||
switch (mode) {
|
||||
case ALARM_MODE_EVERY_SECOND:
|
||||
regs[0] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_EVERY_MINUTE:
|
||||
regs[1] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_HOURLY:
|
||||
regs[2] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_DAILY:
|
||||
regs[3] |= ALARM_MASK_BIT;
|
||||
break;
|
||||
default: // MONTHLY / WEEKLY: everything matches
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting alarm 1: mode %u, %02u:%02u:%02u, day of month %u, day of week %u, %s", mode, hour, minute,
|
||||
second, day_of_month, day_of_week, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
this->write_alarm_(1, regs, sizeof(regs), enabled);
|
||||
}
|
||||
|
||||
void DS3231Component::set_alarm_2(AlarmMode mode, uint8_t minute, uint8_t hour, uint8_t day_of_month,
|
||||
uint8_t day_of_week, bool enabled) {
|
||||
if (minute > 59 || hour > 23 || day_of_month < 1 || day_of_month > 31 || day_of_week < 1 || day_of_week > 7 ||
|
||||
mode == ALARM_MODE_EVERY_SECOND || mode > ALARM_MODE_WEEKLY) {
|
||||
ESP_LOGE(TAG, "Invalid alarm 2 setting.");
|
||||
return;
|
||||
}
|
||||
// Mask bits A2M2..A2M4.
|
||||
uint8_t regs[3] = {
|
||||
uint8_to_bcd(minute),
|
||||
uint8_to_bcd(hour),
|
||||
mode == ALARM_MODE_WEEKLY ? static_cast<uint8_t>(ALARM_DAY_OF_WEEK | day_of_week) : uint8_to_bcd(day_of_month),
|
||||
};
|
||||
switch (mode) {
|
||||
case ALARM_MODE_EVERY_MINUTE:
|
||||
regs[0] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_HOURLY:
|
||||
regs[1] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_DAILY:
|
||||
regs[2] |= ALARM_MASK_BIT;
|
||||
break;
|
||||
default: // MONTHLY / WEEKLY: everything matches
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting alarm 2: mode %u, %02u:%02u, day of month %u, day of week %u, %s", mode, hour, minute,
|
||||
day_of_month, day_of_week, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
this->write_alarm_(2, regs, sizeof(regs), enabled);
|
||||
}
|
||||
|
||||
void DS3231Component::write_alarm_(uint8_t alarm, const uint8_t *regs, size_t len, bool enabled) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (!this->status_comm_result(this->write_bytes(alarm == 1 ? DS3231_REG_ALARM_1 : DS3231_REG_ALARM_2, regs, len)))
|
||||
return;
|
||||
// Changing the registers may have matched on the way: start from a clean flag. Enabling the
|
||||
// alarm with a stale flag would assert INT/SQW right away.
|
||||
if (!this->clear_alarm_flag_(alarm)) {
|
||||
ESP_LOGW(TAG, "Alarm %u written, but its flag could not be cleared; not changing its enable.", alarm);
|
||||
return;
|
||||
}
|
||||
this->set_alarm_enabled_(alarm, enabled);
|
||||
}
|
||||
|
||||
void DS3231Component::set_alarm_enabled_(uint8_t alarm, bool enabled) {
|
||||
if (alarm != 1 && alarm != 2) {
|
||||
ESP_LOGE(TAG, "Invalid alarm number %u.", alarm);
|
||||
return;
|
||||
}
|
||||
if (this->is_failed())
|
||||
return;
|
||||
const uint8_t bit = alarm == 1 ? CONTROL_A1IE : CONTROL_A2IE;
|
||||
if (!this->update_control_(bit, enabled ? bit : 0))
|
||||
return;
|
||||
if (enabled && (this->control_reg_ & CONTROL_INTCN) == 0) {
|
||||
ESP_LOGW(TAG, "Alarm %u enabled, but INT/SQW is a square-wave output: only the flag will be set.", alarm);
|
||||
}
|
||||
ESP_LOGD(TAG, "Alarm %u %s.", alarm, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
}
|
||||
|
||||
void DS3231Component::enable_alarm(uint8_t alarm) {
|
||||
// The chip sets the flag on every match even while the alarm is disabled; enabling with it set
|
||||
// would fire the trigger (and INT/SQW) for that old match right away.
|
||||
if (this->clear_alarm_flag_(alarm))
|
||||
this->set_alarm_enabled_(alarm, true);
|
||||
}
|
||||
|
||||
void DS3231Component::clear_alarm(uint8_t alarm) { this->clear_alarm_flag_(alarm); }
|
||||
|
||||
bool DS3231Component::clear_alarm_flag_(uint8_t alarm) {
|
||||
if (alarm != 1 && alarm != 2) {
|
||||
ESP_LOGE(TAG, "Invalid alarm number %u.", alarm);
|
||||
return false;
|
||||
}
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return false;
|
||||
const uint8_t flag = alarm == 1 ? STATUS_A1F : STATUS_A2F;
|
||||
if ((this->status_reg_ & flag) != 0) {
|
||||
// On failure the flag is still set on the chip: keep reporting the alarm as it is.
|
||||
if (!this->write_status_(flag))
|
||||
return false;
|
||||
this->status_reg_ &= ~flag;
|
||||
}
|
||||
this->alarm_active_[alarm - 1] = false;
|
||||
this->publish_alarm_state_(alarm, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::is_alarm_triggered(uint8_t alarm) {
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return false;
|
||||
return (this->status_reg_ & (alarm == 1 ? STATUS_A1F : STATUS_A2F)) != 0;
|
||||
}
|
||||
|
||||
void DS3231Component::check_alarms_() {
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return;
|
||||
this->publish_oscillator_stopped_();
|
||||
|
||||
uint8_t to_clear = 0;
|
||||
for (uint8_t alarm = 1; alarm <= 2; alarm++) {
|
||||
const uint8_t flag = alarm == 1 ? STATUS_A1F : STATUS_A2F;
|
||||
const uint8_t enable = alarm == 1 ? CONTROL_A1IE : CONTROL_A2IE;
|
||||
const bool triggered = (this->status_reg_ & flag) != 0;
|
||||
const bool enabled = (this->control_reg_ & enable) != 0;
|
||||
bool &active = this->alarm_active_[alarm - 1];
|
||||
|
||||
if (!triggered) {
|
||||
if (active && !this->auto_clear_alarms_)
|
||||
this->publish_alarm_state_(alarm, false);
|
||||
active = false;
|
||||
continue;
|
||||
}
|
||||
// The flag is set on every match even when the alarm is disabled; only enabled alarms count.
|
||||
if (enabled && !active) {
|
||||
ESP_LOGI(TAG, "Alarm %u triggered.", alarm);
|
||||
active = true;
|
||||
this->publish_alarm_state_(alarm, true);
|
||||
this->alarm_callback_.call(alarm);
|
||||
}
|
||||
if (this->auto_clear_alarms_)
|
||||
to_clear |= flag;
|
||||
}
|
||||
|
||||
if (to_clear != 0 && this->write_status_(to_clear)) {
|
||||
this->status_reg_ &= ~to_clear;
|
||||
for (uint8_t alarm = 1; alarm <= 2; alarm++) {
|
||||
if ((to_clear & (alarm == 1 ? STATUS_A1F : STATUS_A2F)) == 0 || !this->alarm_active_[alarm - 1])
|
||||
continue;
|
||||
this->alarm_active_[alarm - 1] = false;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// Keep the binary sensor on long enough for the state to be seen, then turn it off again.
|
||||
this->set_timeout(ALARM_PULSE_TIMEOUT_ID + alarm - 1, ALARM_PULSE_MS,
|
||||
[this, alarm]() { this->publish_alarm_state_(alarm, false); });
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::publish_alarm_state_(uint8_t alarm, bool state) {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
auto *sensor = alarm == 1 ? this->alarm_1_binary_sensor_ : this->alarm_2_binary_sensor_;
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(state);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DS3231Component::publish_oscillator_stopped_() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->oscillator_stopped_binary_sensor_ != nullptr)
|
||||
this->oscillator_stopped_binary_sensor_->publish_state((this->status_reg_ & STATUS_OSF) != 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Outputs and calibration
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
void DS3231Component::enable_square_wave(SquareWaveFrequency frequency) {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
ESP_LOGE(TAG, "Can't enable the square wave: INT/SQW is used as the alarm interrupt pin.");
|
||||
return;
|
||||
}
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (this->update_control_(CONTROL_INTCN | CONTROL_RS_MASK, frequency << CONTROL_RS_SHIFT)) {
|
||||
ESP_LOGD(TAG, "Square wave enabled at %s.", LOG_STR_ARG(square_wave_to_string(frequency)));
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::disable_square_wave() {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (this->update_control_(CONTROL_INTCN, CONTROL_INTCN)) {
|
||||
ESP_LOGD(TAG, "Square wave disabled, INT/SQW is the alarm interrupt.");
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::set_output_32khz(bool enabled) {
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return;
|
||||
assign_bits(this->status_reg_, STATUS_EN32KHZ, enabled);
|
||||
if (this->write_status_(0)) {
|
||||
ESP_LOGD(TAG, "32 kHz output %s.", ONOFF(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::set_aging_offset(int8_t offset) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (!this->write_aging_offset_(offset))
|
||||
return;
|
||||
ESP_LOGD(TAG, "Aging offset set to %d.", offset);
|
||||
// The new offset takes effect with the next temperature conversion.
|
||||
this->force_temperature_conversion();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Register access
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
bool DS3231Component::read_control_status_() {
|
||||
uint8_t raw[2];
|
||||
if (!this->read_bytes(DS3231_REG_CONTROL, raw, sizeof(raw))) {
|
||||
ESP_LOGW(TAG, "Can't read control/status registers.");
|
||||
if (!this->status_comm_result(this->read_bytes(DS3231_REG_CONTROL, raw, sizeof(raw))))
|
||||
return false;
|
||||
}
|
||||
this->control_reg_ = raw[0];
|
||||
this->control_reg_ = raw[0] & ~CONTROL_CONV;
|
||||
this->status_reg_ = raw[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::write_control_() {
|
||||
if (!this->write_byte(DS3231_REG_CONTROL, this->control_reg_)) {
|
||||
ESP_LOGW(TAG, "Can't write control register.");
|
||||
bool DS3231Component::update_control_(uint8_t mask, uint8_t value) {
|
||||
// CONV is a one-shot command bit: always write it back as 0 so a control change never starts
|
||||
// a temperature conversion.
|
||||
if (!this->status_comm_result(this->update_register(DS3231_REG_CONTROL, mask | CONTROL_CONV, value)))
|
||||
return false;
|
||||
}
|
||||
this->control_reg_ = (this->control_reg_ & ~mask) | (value & mask);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::write_status_(uint8_t clear) {
|
||||
// The flags can only be written to 0; writing 1 leaves them unchanged, so a flag that was set
|
||||
// between the read and this write is never lost.
|
||||
const uint8_t value = ((this->status_reg_ & STATUS_EN32KHZ) | STATUS_FLAGS) & ~clear;
|
||||
return this->status_comm_result(this->write_byte(DS3231_REG_STATUS, value));
|
||||
}
|
||||
|
||||
bool DS3231Component::write_aging_offset_(int8_t offset) {
|
||||
if (!this->status_comm_result(this->write_byte(DS3231_REG_AGING_OFFSET, static_cast<uint8_t>(offset))))
|
||||
return false;
|
||||
this->aging_offset_ = offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::ds3231 {
|
||||
|
||||
/// When an alarm fires. Alarm 1 supports every mode, alarm 2 has no seconds register and so no EVERY_SECOND.
|
||||
enum AlarmMode : uint8_t {
|
||||
ALARM_MODE_EVERY_SECOND = 0, ///< Alarm 1 only.
|
||||
ALARM_MODE_EVERY_MINUTE, ///< Alarm 1: when the seconds match. Alarm 2: at second 00 of every minute.
|
||||
ALARM_MODE_HOURLY, ///< When minutes (and seconds) match.
|
||||
ALARM_MODE_DAILY, ///< When hours, minutes (and seconds) match.
|
||||
ALARM_MODE_MONTHLY, ///< When date, hours, minutes (and seconds) match.
|
||||
ALARM_MODE_WEEKLY, ///< When day of week, hours, minutes (and seconds) match.
|
||||
};
|
||||
|
||||
/// Frequency of the INT/SQW pin when it is a square-wave output (RS2:RS1 in the control register).
|
||||
enum SquareWaveFrequency : uint8_t {
|
||||
SQUARE_WAVE_1HZ = 0,
|
||||
SQUARE_WAVE_1024HZ = 1,
|
||||
SQUARE_WAVE_4096HZ = 2,
|
||||
SQUARE_WAVE_8192HZ = 3,
|
||||
};
|
||||
|
||||
/// Hub for a single DS3231 real-time clock. Owns all register access; the platforms talk to the
|
||||
/// chip through this class.
|
||||
class DS3231Component final : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
/// Seconds through year, the 7-byte BCD calendar the time platform decodes with time::DS_BCD_CALENDAR.
|
||||
static constexpr size_t CALENDAR_LEN = 7;
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
// ---- Configuration (set from codegen) ----
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_square_wave_frequency(SquareWaveFrequency frequency) {
|
||||
this->square_wave_frequency_ = frequency;
|
||||
this->square_wave_configured_ = true;
|
||||
}
|
||||
void set_battery_backed_square_wave(bool enabled) { this->battery_backed_square_wave_ = enabled ? 1 : 0; }
|
||||
void set_output_32khz_config(bool enabled) { this->output_32khz_ = enabled ? 1 : 0; }
|
||||
void set_aging_offset_config(int8_t offset) {
|
||||
this->aging_offset_ = offset;
|
||||
this->aging_offset_configured_ = true;
|
||||
}
|
||||
void set_auto_clear_alarms(bool auto_clear) { this->auto_clear_alarms_ = auto_clear; }
|
||||
void set_alarm_check_interval(uint32_t interval) { this->alarm_check_interval_ = interval; }
|
||||
void set_monitor_alarms(bool monitor) { this->monitor_alarms_ = monitor; }
|
||||
|
||||
/// Register a void(uint8_t alarm) callback that runs when an enabled alarm (1 or 2) matches.
|
||||
template<typename F> void add_on_alarm_callback(F &&callback) {
|
||||
this->alarm_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(alarm_1)
|
||||
SUB_BINARY_SENSOR(alarm_2)
|
||||
SUB_BINARY_SENSOR(oscillator_stopped)
|
||||
#endif
|
||||
|
||||
// ---- Clock registers (used by the time platform) ----
|
||||
/// Read the calendar registers into @p raw. Fails if the oscillator was stopped since the time was last set.
|
||||
bool read_calendar(uint8_t *raw);
|
||||
/// Write the calendar registers from @p raw and clear the oscillator-stop flag.
|
||||
bool write_calendar(const uint8_t *raw);
|
||||
|
||||
// ---- Temperature ----
|
||||
bool read_temperature(float &out);
|
||||
void force_temperature_conversion();
|
||||
|
||||
// ---- Alarms ----
|
||||
/// Program alarm 1. @p day_of_month is used in MONTHLY mode, @p day_of_week (1 = Sunday) in WEEKLY mode.
|
||||
void set_alarm_1(AlarmMode mode, uint8_t second, uint8_t minute, uint8_t hour, uint8_t day_of_month,
|
||||
uint8_t day_of_week, bool enabled);
|
||||
/// Program alarm 2 (minute resolution, fires at second 00).
|
||||
void set_alarm_2(AlarmMode mode, uint8_t minute, uint8_t hour, uint8_t day_of_month, uint8_t day_of_week,
|
||||
bool enabled);
|
||||
/// Enable @p alarm. A match from while it was disabled is cleared first, so only the next one fires.
|
||||
void enable_alarm(uint8_t alarm);
|
||||
void disable_alarm(uint8_t alarm) { this->set_alarm_enabled_(alarm, false); }
|
||||
/// Clear the alarm flag, which also releases the INT/SQW line.
|
||||
void clear_alarm(uint8_t alarm);
|
||||
/// True while the alarm flag of @p alarm (1 or 2) is set on the chip.
|
||||
bool is_alarm_triggered(uint8_t alarm);
|
||||
|
||||
// ---- Outputs and calibration ----
|
||||
/// Switch INT/SQW to a square-wave output at @p frequency. Alarms then no longer drive the pin.
|
||||
void enable_square_wave(SquareWaveFrequency frequency);
|
||||
/// Switch INT/SQW back to the (active-low) alarm interrupt output.
|
||||
void disable_square_wave();
|
||||
void set_output_32khz(bool enabled);
|
||||
/// Aging offset in LSB (about 0.1 ppm each at 25 degC); positive values slow the clock down.
|
||||
void set_aging_offset(int8_t offset);
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t CONTROL_A1IE = 0x01;
|
||||
static constexpr uint8_t CONTROL_A2IE = 0x02;
|
||||
static constexpr uint8_t CONTROL_INTCN = 0x04;
|
||||
static constexpr uint8_t CONTROL_RS_MASK = 0x18;
|
||||
static constexpr uint8_t CONTROL_RS_SHIFT = 3;
|
||||
static constexpr uint8_t CONTROL_CONV = 0x20;
|
||||
static constexpr uint8_t CONTROL_BBSQW = 0x40;
|
||||
static constexpr uint8_t CONTROL_EOSC = 0x80;
|
||||
|
||||
static constexpr uint8_t STATUS_A1F = 0x01;
|
||||
static constexpr uint8_t STATUS_A2F = 0x02;
|
||||
static constexpr uint8_t STATUS_BSY = 0x04;
|
||||
static constexpr uint8_t STATUS_EN32KHZ = 0x08;
|
||||
static constexpr uint8_t STATUS_OSF = 0x80;
|
||||
static constexpr uint8_t STATUS_FLAGS = STATUS_A1F | STATUS_A2F | STATUS_OSF;
|
||||
|
||||
/// Scheduler ids: one pulse timer per alarm (id + alarm - 1).
|
||||
static constexpr uint32_t ALARM_PULSE_TIMEOUT_ID = 0;
|
||||
|
||||
static void interrupt_handler(DS3231Component *arg);
|
||||
|
||||
/// Read the control and status registers into the cached copies. Sets the warning flag on failure.
|
||||
bool read_control_status_();
|
||||
bool write_control_();
|
||||
/// Replace the bits in @p mask of the control register with @p value, on the chip and in the cache.
|
||||
bool update_control_(uint8_t mask, uint8_t value);
|
||||
/// Write the status register, clearing the flags in @p clear (writing 1 to a flag leaves it unchanged).
|
||||
bool write_status_(uint8_t clear);
|
||||
bool write_aging_offset_(int8_t offset);
|
||||
/// Clear the flag of @p alarm and report it off. Returns false (state unchanged) if that fails.
|
||||
bool clear_alarm_flag_(uint8_t alarm);
|
||||
void set_alarm_enabled_(uint8_t alarm, bool enabled);
|
||||
/// Write the alarm registers, clear the stale flag and set the interrupt enable.
|
||||
void write_alarm_(uint8_t alarm, const uint8_t *regs, size_t len, bool enabled);
|
||||
/// Read the flags, fire triggers and update the binary sensors.
|
||||
void check_alarms_();
|
||||
void publish_alarm_state_(uint8_t alarm, bool state);
|
||||
void publish_oscillator_stopped_();
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
LazyCallbackManager<void(uint8_t)> alarm_callback_;
|
||||
|
||||
uint32_t alarm_check_interval_{1000};
|
||||
uint8_t control_reg_{0};
|
||||
uint8_t status_reg_{0};
|
||||
int8_t aging_offset_{0};
|
||||
SquareWaveFrequency square_wave_frequency_{SQUARE_WAVE_1HZ};
|
||||
/// -1: leave the chip as it is, 0: off, 1: on.
|
||||
int8_t battery_backed_square_wave_{-1};
|
||||
int8_t output_32khz_{-1};
|
||||
bool square_wave_configured_{false};
|
||||
bool aging_offset_configured_{false};
|
||||
bool auto_clear_alarms_{true};
|
||||
bool monitor_alarms_{false};
|
||||
/// Set while an alarm flag has been reported, so a latched flag fires its trigger only once.
|
||||
bool alarm_active_[2]{false, false};
|
||||
};
|
||||
|
||||
} // namespace esphome::ds3231
|
||||
@@ -0,0 +1,40 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DS3231_ID, DS3231Component, ds3231_ns
|
||||
|
||||
DEPENDENCIES = ["ds3231"]
|
||||
|
||||
DS3231Time = ds3231_ns.class_(
|
||||
"DS3231Time", time.RealTimeClock, cg.Parented.template(DS3231Component)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DS3231Time),
|
||||
cv.GenerateID(CONF_DS3231_ID): cv.use_id(DS3231Component),
|
||||
}
|
||||
)
|
||||
|
||||
DS3231_TIME_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(DS3231Time)}
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.write_time", DS3231_TIME_ACTION_SCHEMA, automation.ApplyCall("write_time()")
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.read_time", DS3231_TIME_ACTION_SCHEMA, automation.ApplyCall("read_time()")
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_DS3231_ID])
|
||||
await time.register_time(var, config)
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "ds3231_time.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ds3231 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ds3231.time");
|
||||
|
||||
void DS3231Time::update() { this->read_time(); }
|
||||
|
||||
void DS3231Time::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DS3231 Time:");
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
void DS3231Time::read_time() {
|
||||
uint8_t raw[DS3231Component::CALENDAR_LEN];
|
||||
if (!this->status_comm_result(this->parent_->read_calendar(raw), LOG_STR("No valid time from the RTC")))
|
||||
return;
|
||||
const ESPTime rtc_time = time::decode_bcd_calendar<time::DS_BCD_CALENDAR>(raw);
|
||||
ESP_LOGD(TAG, "Read UTC time: %04u-%02u-%02u %02u:%02u:%02u (day of week %u)", rtc_time.year, rtc_time.month,
|
||||
rtc_time.day_of_month, rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.day_of_week);
|
||||
// The day of week is a free-running counter on the DS3231 and may have been set with a different
|
||||
// numbering by other software, so it is not used to validate the time.
|
||||
if (!this->status_comm_result(rtc_time.is_valid(/*check_day_of_week=*/false, /*check_day_of_year=*/false),
|
||||
LOG_STR("Invalid RTC time")))
|
||||
return;
|
||||
this->synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
void DS3231Time::write_time() {
|
||||
auto now = this->utcnow();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
|
||||
return;
|
||||
}
|
||||
if (now.year > 2199) {
|
||||
ESP_LOGE(TAG, "Year %u is out of the DS3231 range (2000-2199).", now.year);
|
||||
return;
|
||||
}
|
||||
uint8_t raw[DS3231Component::CALENDAR_LEN];
|
||||
time::encode_bcd_calendar<time::DS_BCD_CALENDAR>(now, raw);
|
||||
if (!this->status_comm_result(this->parent_->write_calendar(raw)))
|
||||
return;
|
||||
ESP_LOGD(TAG, "Wrote UTC time: %04u-%02u-%02u %02u:%02u:%02u", now.year, now.month, now.day_of_month, now.hour,
|
||||
now.minute, now.second);
|
||||
}
|
||||
|
||||
} // namespace esphome::ds3231
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
#include "../ds3231.h"
|
||||
|
||||
namespace esphome::ds3231 {
|
||||
|
||||
class DS3231Time final : public time::RealTimeClock, public Parented<DS3231Component> {
|
||||
public:
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Set the system clock from the RTC.
|
||||
void read_time();
|
||||
/// Store the system clock in the RTC.
|
||||
void write_time();
|
||||
};
|
||||
|
||||
} // namespace esphome::ds3231
|
||||
@@ -2213,6 +2213,22 @@ def idf_version() -> cv.Version:
|
||||
return CORE.data[KEY_ESP32][KEY_IDF_VERSION]
|
||||
|
||||
|
||||
def require_idf_version(
|
||||
minimum: cv.Version, feature: str = "This feature"
|
||||
) -> Callable[[Any], Any]:
|
||||
"""Validator for an option that needs the underlying ESP-IDF at ``minimum`` or newer."""
|
||||
|
||||
def validator(value: Any) -> Any:
|
||||
cv.only_on_esp32(value)
|
||||
if (version := idf_version()) < minimum:
|
||||
raise cv.Invalid(
|
||||
f"{feature} requires ESP-IDF {minimum} or newer, got {version}"
|
||||
)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def require_fatfs() -> None:
|
||||
"""Mark that FATFS support is required by a component.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -83,6 +84,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
# The other build's rename finishes within milliseconds; a few short checks cover it
|
||||
CACHE_RACE_RETRIES = 5
|
||||
CACHE_RACE_RETRY_S = 0.05
|
||||
|
||||
|
||||
def _check_firmware_hash(data: bytes, expected: str, source: str | Path) -> None:
|
||||
calculated = hashlib.sha256(data).hexdigest()
|
||||
if calculated != expected:
|
||||
@@ -91,6 +97,14 @@ def _check_firmware_hash(data: bytes, expected: str, source: str | Path) -> None
|
||||
)
|
||||
|
||||
|
||||
def _cached_image_digest(target: Path) -> str | None:
|
||||
"""The SHA256 of the cached image, or None while it is missing or another build still writes it."""
|
||||
try:
|
||||
return hashlib.sha256(target.read_bytes()).hexdigest()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def _firmware_path(config: ConfigType) -> Path:
|
||||
"""The configured image file, or the cache slot of the pinned digest for a downloaded one."""
|
||||
if CONF_URL in config:
|
||||
@@ -109,10 +123,9 @@ def _validate_firmware(config: ConfigType) -> None:
|
||||
_check_firmware_hash(target.read_bytes(), expected, target)
|
||||
return
|
||||
# A pinned digest needs no remote freshness check.
|
||||
if target.is_file():
|
||||
actual = hashlib.sha256(target.read_bytes()).hexdigest()
|
||||
if actual == expected:
|
||||
return
|
||||
if (actual := _cached_image_digest(target)) == expected:
|
||||
return
|
||||
if actual is not None:
|
||||
# Named after its digest, so a mismatch means the file was damaged or edited
|
||||
_LOGGER.warning(
|
||||
"Cached firmware %s hashes to %s, downloading it again", target, actual
|
||||
@@ -123,10 +136,20 @@ def _validate_firmware(config: ConfigType) -> None:
|
||||
candidate = Path(temporary) / "firmware.bin"
|
||||
data = external_files.download_content(url, candidate, allow_stale=False)
|
||||
_check_firmware_hash(data, expected, url)
|
||||
try:
|
||||
candidate.replace(target)
|
||||
except OSError as err:
|
||||
raise cv.Invalid(f"Could not cache the firmware from {url}: {err}") from err
|
||||
for attempt in range(CACHE_RACE_RETRIES + 1):
|
||||
try:
|
||||
candidate.replace(target)
|
||||
return
|
||||
except OSError as err:
|
||||
# Windows refuses to replace a file another build holds at that moment; retry, or
|
||||
# take that build's copy: named after its digest, a verified one is the same image
|
||||
if _cached_image_digest(target) == expected:
|
||||
return
|
||||
if attempt == CACHE_RACE_RETRIES:
|
||||
raise cv.Invalid(
|
||||
f"Could not cache the firmware from {url}: {err}"
|
||||
) from err
|
||||
time.sleep(CACHE_RACE_RETRY_S)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_firmware
|
||||
|
||||
@@ -362,7 +362,7 @@ def _validate(config: ConfigType) -> ConfigType:
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S31,
|
||||
get_esp32_variant,
|
||||
idf_version,
|
||||
require_idf_version,
|
||||
)
|
||||
|
||||
eth_type = config[CONF_TYPE]
|
||||
@@ -372,10 +372,9 @@ def _validate(config: ConfigType) -> ConfigType:
|
||||
f"The '{eth_type}' (RGMII) PHY is only supported on gigabit-capable "
|
||||
f"variants (ESP32-S31), not {variant}"
|
||||
)
|
||||
if idf_version() < cv.Version(6, 0, 0):
|
||||
raise cv.Invalid(
|
||||
f"The '{eth_type}' (RGMII) PHY requires ESP-IDF 6.0 or newer."
|
||||
)
|
||||
require_idf_version(cv.Version(6, 0, 0), f"The '{eth_type}' (RGMII) PHY")(
|
||||
config
|
||||
)
|
||||
elif config[CONF_TYPE] != "OPENETH":
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "islandaire"
|
||||
@@ -0,0 +1,27 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
from esphome.components.climate_ir import CONF_SUPPORTS_HEAT_COOL
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SENSOR
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
CODEOWNERS = ["@ipopov"]
|
||||
|
||||
islandaire_ns = cg.esphome_ns.namespace("islandaire")
|
||||
IslandaireClimate = islandaire_ns.class_("IslandaireClimate", climate_ir.ClimateIR)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(IslandaireClimate).extend(
|
||||
{
|
||||
# The unit has no auto (heat/cool) mode
|
||||
cv.Optional(CONF_SUPPORTS_HEAT_COOL, default=False): cv.boolean_false,
|
||||
# The unit works in Fahrenheit, so a Celsius temperature sensor cannot feed it
|
||||
cv.Optional(CONF_SENSOR): cv.invalid(
|
||||
"'sensor' is not supported by the Islandaire climate component"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await climate_ir.new_climate_ir(config)
|
||||
@@ -0,0 +1,199 @@
|
||||
#include "islandaire.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::islandaire {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "islandaire.climate");
|
||||
|
||||
// 14-byte frame, LSB first:
|
||||
// Bytes 0-4: 0x22 preamble
|
||||
// Byte 5: power (0x20 off, 0x24 on); bit 3 (0x08) set while the auto-off timer is active
|
||||
// Byte 6: mode (0x01 heat, 0x03 cool, 0x04 fan only)
|
||||
// Byte 7: target temperature in °F (61-88)
|
||||
// Byte 8: fan speed (0x00 auto, 0x02 low, 0x04 high, 0x01 read as low too); bit 6 (0x40) set while the
|
||||
// timer is active
|
||||
// Byte 9: 0x00
|
||||
// Byte 10: auto-off timer in hours (1-12, 0 = off)
|
||||
// Bytes 11-12: 0x00
|
||||
// Byte 13: sum of bytes 0-12
|
||||
// The auto-off timer is not implemented: the unit switches itself off with no feedback, which
|
||||
// would leave the climate state stale.
|
||||
static constexpr uint8_t STATE_LENGTH = 14;
|
||||
static constexpr uint8_t PREAMBLE_LENGTH = 5;
|
||||
static constexpr uint8_t PREAMBLE = 0x22;
|
||||
|
||||
static constexpr uint8_t POWER_OFF = 0x20;
|
||||
static constexpr uint8_t POWER_MASK = 0x04;
|
||||
static constexpr uint8_t POWER_ON = POWER_OFF | POWER_MASK;
|
||||
|
||||
static constexpr uint8_t MODE_HEAT = 0x01;
|
||||
static constexpr uint8_t MODE_COOL = 0x03;
|
||||
static constexpr uint8_t MODE_FAN = 0x04;
|
||||
|
||||
static constexpr uint8_t FAN_AUTO = 0x00;
|
||||
static constexpr uint8_t FAN_LOW_ALT = 0x01;
|
||||
static constexpr uint8_t FAN_LOW = 0x02;
|
||||
static constexpr uint8_t FAN_HIGH = 0x04;
|
||||
static constexpr uint8_t FAN_TIMER_FLAG = 0x40;
|
||||
|
||||
static constexpr uint8_t TEMP_DEFAULT = 72;
|
||||
|
||||
static constexpr uint16_t HEADER_MARK = 3608;
|
||||
static constexpr uint16_t HEADER_SPACE = 1520;
|
||||
static constexpr uint16_t BIT_MARK = 560;
|
||||
static constexpr uint16_t ONE_SPACE = 1208;
|
||||
static constexpr uint16_t ZERO_SPACE = 408;
|
||||
static constexpr uint32_t CARRIER_FREQUENCY = 38000;
|
||||
static constexpr remote_base::BitTimings BIT_TIMINGS{
|
||||
.bit_mark = BIT_MARK, .one_space = ONE_SPACE, .zero_space = ZERO_SPACE};
|
||||
|
||||
void IslandaireClimate::setup() {
|
||||
ClimateIR::setup();
|
||||
// The base class default assumes Celsius
|
||||
if (std::isnan(this->target_temperature) || this->target_temperature < TEMP_MIN)
|
||||
this->target_temperature = TEMP_DEFAULT;
|
||||
this->target_temperature = std::min<float>(this->target_temperature, TEMP_MAX);
|
||||
}
|
||||
|
||||
void IslandaireClimate::dump_config() {
|
||||
// ClimateIR::dump_config() is not called: it prints the temperature range in Celsius
|
||||
LOG_CLIMATE("", "Islandaire Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Temperature range: %u to %u °F", TEMP_MIN, TEMP_MAX);
|
||||
}
|
||||
|
||||
climate::ClimateTraits IslandaireClimate::traits() {
|
||||
auto traits = ClimateIR::traits();
|
||||
traits.set_temperature_unit(TemperatureUnit::FAHRENHEIT);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void IslandaireClimate::transmit_state() {
|
||||
uint8_t remote_state[STATE_LENGTH] = {0};
|
||||
|
||||
std::fill_n(remote_state, PREAMBLE_LENGTH, PREAMBLE);
|
||||
|
||||
remote_state[5] = this->mode == climate::CLIMATE_MODE_OFF ? POWER_OFF : POWER_ON;
|
||||
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
remote_state[6] = MODE_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
remote_state[6] = MODE_FAN;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
default:
|
||||
remote_state[6] = MODE_COOL;
|
||||
break;
|
||||
}
|
||||
|
||||
// Whole degrees within the unit's range, stored as well so the published state matches what was sent
|
||||
const auto temperature =
|
||||
static_cast<uint8_t>(std::clamp<float>(std::round(this->target_temperature), TEMP_MIN, TEMP_MAX));
|
||||
this->target_temperature = temperature;
|
||||
remote_state[7] = temperature;
|
||||
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
remote_state[8] = FAN_LOW;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
remote_state[8] = FAN_HIGH;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
remote_state[8] = FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
|
||||
remote_state[STATE_LENGTH - 1] = sum_bytes(remote_state, STATE_LENGTH - 1);
|
||||
|
||||
ESP_LOGV_HEX(TAG, remote_state, STATE_LENGTH, STATE_LENGTH, "Sending: %s");
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
|
||||
data->set_carrier_frequency(CARRIER_FREQUENCY);
|
||||
|
||||
data->reserve(2 + STATE_LENGTH * 16 + 1);
|
||||
data->item(HEADER_MARK, HEADER_SPACE);
|
||||
data->item_bytes<BIT_TIMINGS>(remote_state, STATE_LENGTH);
|
||||
data->mark(BIT_MARK);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
bool IslandaireClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
if (!data.expect_item(HEADER_MARK, HEADER_SPACE)) {
|
||||
ESP_LOGVV(TAG, "Header fail");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t remote_state[STATE_LENGTH];
|
||||
if (!data.expect_bytes<BIT_TIMINGS>(remote_state, STATE_LENGTH)) {
|
||||
ESP_LOGVV(TAG, "Data fail");
|
||||
return false;
|
||||
}
|
||||
if (!data.expect_mark(BIT_MARK)) {
|
||||
ESP_LOGVV(TAG, "Footer fail");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sum_bytes(remote_state, STATE_LENGTH - 1) != remote_state[STATE_LENGTH - 1]) {
|
||||
ESP_LOGVV(TAG, "Checksum fail");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < PREAMBLE_LENGTH; i++) {
|
||||
if (remote_state[i] != PREAMBLE)
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGV_HEX(TAG, remote_state, STATE_LENGTH, STATE_LENGTH, "Received: %s");
|
||||
|
||||
if ((remote_state[5] & POWER_MASK) == 0) {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
} else {
|
||||
switch (remote_state[6]) {
|
||||
case MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
case MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGV(TAG, "Unknown mode 0x%02X", remote_state[6]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t temp_f = remote_state[7];
|
||||
if (temp_f >= TEMP_MIN && temp_f <= TEMP_MAX)
|
||||
this->target_temperature = temp_f;
|
||||
|
||||
switch (remote_state[8] & ~FAN_TIMER_FLAG) {
|
||||
case FAN_HIGH:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
case FAN_LOW:
|
||||
case FAN_LOW_ALT:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case FAN_AUTO:
|
||||
default:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::islandaire
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome::islandaire {
|
||||
|
||||
class IslandaireClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
// The unit works in whole degrees Fahrenheit (see traits())
|
||||
static constexpr uint8_t TEMP_MIN = 61;
|
||||
static constexpr uint8_t TEMP_MAX = 88;
|
||||
|
||||
IslandaireClimate()
|
||||
: ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, false, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_HIGH}, {}) {}
|
||||
|
||||
protected:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
climate::ClimateTraits traits() override;
|
||||
void transmit_state() override;
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::islandaire
|
||||
@@ -285,12 +285,8 @@ void LD6002BComponent::setup() {
|
||||
#ifdef USE_SWITCH
|
||||
if (this->target_display_switch_ != nullptr) {
|
||||
target_display_controlled = true;
|
||||
// Nothing reports this switch back, so its restored state is the only state
|
||||
// there is. Restoring through the switch keeps its inversion in the path:
|
||||
// the restored value is logical, and driving the switch is what turns it
|
||||
// into the raw command, the published state and the stream flag.
|
||||
const bool state = this->target_display_switch_->get_initial_state_with_restore_mode().value_or(true);
|
||||
this->target_display_switch_->control(state);
|
||||
// Nothing reports this switch back, so its restored state is the only state there is.
|
||||
this->target_display_switch_->restore(true);
|
||||
}
|
||||
#endif
|
||||
if (!target_display_controlled) {
|
||||
@@ -307,10 +303,7 @@ void LD6002BComponent::setup() {
|
||||
#ifdef USE_SWITCH
|
||||
if (this->point_cloud_switch_ != nullptr) {
|
||||
point_cloud_controlled = true;
|
||||
// The switch owns the stream, so it is also what applies the restored state:
|
||||
// driving it rather than the module keeps the entity's inversion in the path.
|
||||
const bool state = this->point_cloud_switch_->get_initial_state_with_restore_mode().value_or(false);
|
||||
this->point_cloud_switch_->control(state);
|
||||
this->point_cloud_switch_->restore(false);
|
||||
}
|
||||
#endif
|
||||
if (!point_cloud_controlled) {
|
||||
@@ -349,11 +342,9 @@ void LD6002BComponent::setup() {
|
||||
#ifdef USE_SWITCH
|
||||
bool want_low_power = this->low_power_switch_ != nullptr;
|
||||
if (want_low_power) {
|
||||
// The module reports this one back, so the query below confirms what it took.
|
||||
// Driving the switch applies its inversion; it also marks the restored value
|
||||
// as reported, so the work mode fallback runs on that until the query lands.
|
||||
const bool state = this->low_power_switch_->get_initial_state_with_restore_mode().value_or(false);
|
||||
this->low_power_switch_->control(state);
|
||||
// The query below confirms what the module took; until it lands, the work mode
|
||||
// fallback runs on the restored value.
|
||||
this->low_power_switch_->restore(false);
|
||||
}
|
||||
#else
|
||||
bool want_low_power = false;
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.const import (
|
||||
CONF_COLD_WHITE_COLOR_TEMPERATURE,
|
||||
CONF_COLOR_CORRECT,
|
||||
CONF_DEFAULT_TRANSITION_LENGTH,
|
||||
CONF_EFFECT,
|
||||
CONF_EFFECTS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_FLASH_TRANSITION_LENGTH,
|
||||
@@ -31,12 +32,21 @@ from esphome.const import (
|
||||
CONF_RESTORE_MODE,
|
||||
CONF_RESTORE_STATE,
|
||||
CONF_RGB_ORDER,
|
||||
CONF_STATE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WARM_WHITE_COLOR_TEMPERATURE,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
ID,
|
||||
CoroPriority,
|
||||
EsphomeError,
|
||||
HexInt,
|
||||
coroutine_with_priority,
|
||||
)
|
||||
from esphome.core.entity_helpers import (
|
||||
RESTORE_STATE_NONE,
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
@@ -56,7 +66,6 @@ from .effects import (
|
||||
)
|
||||
from .restore_state import (
|
||||
LEGACY_RESTORE_MODES,
|
||||
RESTORE_STATE_NONE,
|
||||
RESTORE_STATE_SCHEMA,
|
||||
_build_state_lambda,
|
||||
_initial_state_overridden_by_legacy_mode,
|
||||
@@ -191,9 +200,11 @@ def _get_or_create_gamma_table(gamma_correct: float, table_id: ID) -> cg.RawExpr
|
||||
def find_effect_index(effects: list, effect_name: str) -> int | None:
|
||||
"""Find the 1-based index of an effect by name (case-insensitive).
|
||||
|
||||
Returns the 1-based index if found, or None if not found.
|
||||
Returns 0 for "None" (no effect), the 1-based index if found, or None if not found.
|
||||
"""
|
||||
effect_name_lower = effect_name.lower()
|
||||
if effect_name_lower == "none":
|
||||
return 0
|
||||
for i, effect_conf in enumerate(effects):
|
||||
key = next(iter(effect_conf))
|
||||
if effect_conf[key][CONF_NAME].lower() == effect_name_lower:
|
||||
@@ -209,6 +220,14 @@ def available_effects_str(effects: list) -> str:
|
||||
return ", ".join(f"'{name}'" for name in available) if available else "none"
|
||||
|
||||
|
||||
def effect_not_found_message(effect_name: str, effects: list, light: str) -> str:
|
||||
"""Return the error for an effect name that `light` (e.g. "this light") lacks."""
|
||||
return (
|
||||
f"Effect '{effect_name}' not found for {light}. "
|
||||
f"Available effects: {available_effects_str(effects)}"
|
||||
)
|
||||
|
||||
|
||||
# Accepted values of the deprecated `rgb_order` key.
|
||||
RGB_ORDERS = ("RGB", "RBG", "GRB", "GBR", "BGR", "BRG")
|
||||
|
||||
@@ -316,9 +335,12 @@ def _final_validate(config: ConfigType) -> None:
|
||||
platform entries across the file, not a single light's own config.
|
||||
"""
|
||||
for light_config in config:
|
||||
initial_state = light_config.get(CONF_INITIAL_STATE) or {}
|
||||
cold_boot_state = initial_state.get(CONF_STATE, False)
|
||||
restore_mode = light_config.get(CONF_RESTORE_MODE)
|
||||
if restore_mode is not None:
|
||||
legacy = LEGACY_RESTORE_MODES[restore_mode]
|
||||
cold_boot_state = legacy.cold_boot_state
|
||||
if _initial_state_overridden_by_legacy_mode(
|
||||
legacy, light_config.get(CONF_INITIAL_STATE)
|
||||
):
|
||||
@@ -330,6 +352,19 @@ def _final_validate(config: ConfigType) -> None:
|
||||
restore_mode,
|
||||
"ON" if legacy.cold_boot_state else "OFF",
|
||||
)
|
||||
effect_name = initial_state.get(CONF_EFFECT, "none")
|
||||
if (
|
||||
effect_name.lower() != "none"
|
||||
and not cold_boot_state
|
||||
and not light_config.get(CONF_RESUME_EFFECT)
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"[%s] 'initial_state: effect: %s' does not start at boot because the "
|
||||
"light boots OFF; add 'resume_effect: true' to start it at the first "
|
||||
"turn-on",
|
||||
light_config.get(CONF_NAME) or light_config[CONF_ID],
|
||||
effect_name,
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
if not data.effect_refs and not data.effect_cycle_refs:
|
||||
@@ -355,9 +390,9 @@ def _final_validate(config: ConfigType) -> None:
|
||||
|
||||
if find_effect_index(effects, ref.effect_name) is None:
|
||||
raise cv.FinalExternalInvalid(
|
||||
f"Effect '{ref.effect_name}' not found for light "
|
||||
f"'{ref.light_id}'. "
|
||||
f"Available effects: {available_effects_str(effects)}",
|
||||
effect_not_found_message(
|
||||
ref.effect_name, effects, f"light '{ref.light_id}'"
|
||||
),
|
||||
path=[cv.ROOT_CONFIG_PATH] + ref.component_path,
|
||||
)
|
||||
|
||||
@@ -385,6 +420,13 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
DEFAULT_FLASH_TRANSITION_LENGTH = "0s"
|
||||
CONF_TRANSITION_STATE_PUBLISH_INTERVAL = "transition_state_publish_interval"
|
||||
|
||||
# `effect` stays out of LIGHT_STATE_SCHEMA: light.control defines it templatable and transformer-exclusive
|
||||
INITIAL_STATE_SCHEMA = LIGHT_STATE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_EFFECT): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
LIGHT_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
@@ -414,7 +456,7 @@ LIGHT_SCHEMA = (
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightStateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_INITIAL_STATE): LIGHT_STATE_SCHEMA,
|
||||
cv.Optional(CONF_INITIAL_STATE): INITIAL_STATE_SCHEMA,
|
||||
cv.Optional(CONF_RESUME_EFFECT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
@@ -422,6 +464,35 @@ LIGHT_SCHEMA = (
|
||||
|
||||
LIGHT_SCHEMA.add_extra(entity_duplicate_validator("light"))
|
||||
|
||||
|
||||
def _validate_initial_state_effect(config: ConfigType) -> ConfigType:
|
||||
"""Check that `initial_state: effect` names an effect of this light."""
|
||||
initial_state = config.get(CONF_INITIAL_STATE) or {}
|
||||
if (effect_name := initial_state.get(CONF_EFFECT)) is not None:
|
||||
effects = config.get(CONF_EFFECTS, [])
|
||||
if find_effect_index(effects, effect_name) is None:
|
||||
raise cv.Invalid(
|
||||
effect_not_found_message(effect_name, effects, "this light"),
|
||||
path=[CONF_INITIAL_STATE, CONF_EFFECT],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
LIGHT_SCHEMA.add_extra(_validate_initial_state_effect)
|
||||
|
||||
|
||||
def _initial_effect_index(config: ConfigType) -> int:
|
||||
"""Resolve `initial_state: effect` to its effect index, 0 when not set."""
|
||||
effect_name = (config.get(CONF_INITIAL_STATE) or {}).get(CONF_EFFECT, "none")
|
||||
effects = config.get(CONF_EFFECTS, [])
|
||||
if (index := find_effect_index(effects, effect_name)) is not None:
|
||||
return index
|
||||
# Safety net for a light whose schema skipped _validate_initial_state_effect
|
||||
raise EsphomeError(
|
||||
effect_not_found_message(effect_name, effects, f"light '{config[CONF_ID]}'")
|
||||
)
|
||||
|
||||
|
||||
BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS),
|
||||
@@ -557,7 +628,9 @@ async def setup_light_core_(light_var, config, output_var):
|
||||
# ways to build the same state callback and save_enabled flag that LightState's
|
||||
# runtime actually understands.
|
||||
initial_state_config = config.get(CONF_INITIAL_STATE)
|
||||
initial_statements = await _initial_state_statements(initial_state_config)
|
||||
initial_statements = await _initial_state_statements(
|
||||
initial_state_config, _initial_effect_index(config)
|
||||
)
|
||||
if config[CONF_RESUME_EFFECT]:
|
||||
cg.add_define("USE_LIGHT_RESUME_EFFECT")
|
||||
cg.add(light_var.set_resume_effect(True))
|
||||
|
||||
@@ -250,10 +250,8 @@ def _resolve_effect_index(config: ConfigType, original_name: str) -> int:
|
||||
Effect index 0 means "None" (no effect). Effects are 1-indexed matching
|
||||
the C++ convention in LightState.
|
||||
"""
|
||||
from . import available_effects_str, find_effect_index
|
||||
from . import effect_not_found_message, find_effect_index
|
||||
|
||||
if original_name.lower() == "none":
|
||||
return 0
|
||||
light_id = config[CONF_ID]
|
||||
light_path = CORE.config.get_path_for_id(light_id)[:-1]
|
||||
light_config = CORE.config.get_config_for_path(light_path)
|
||||
@@ -264,8 +262,7 @@ def _resolve_effect_index(config: ConfigType, original_name: str) -> int:
|
||||
# Should never reach here — effect names are validated during config
|
||||
# validation in FINAL_VALIDATE_SCHEMA. This is a safety net.
|
||||
raise EsphomeError(
|
||||
f"Effect '{original_name}' not found for light '{light_id}'. "
|
||||
f"Available effects: {available_effects_str(effects)}"
|
||||
effect_not_found_message(original_name, effects, f"light '{light_id}'")
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -83,9 +83,13 @@ void LightState::setup() {
|
||||
call.set_color_temperature_if_supported(recovered.color_temp);
|
||||
call.set_cold_white_if_supported(recovered.cold_white);
|
||||
call.set_warm_white_if_supported(recovered.warm_white);
|
||||
if (recovered.effect != 0) {
|
||||
if (recovered.state && recovered.effect != 0) {
|
||||
call.set_effect(recovered.effect);
|
||||
} else {
|
||||
#ifdef USE_LIGHT_RESUME_EFFECT
|
||||
// An off light cannot start its effect now; keep it for the first turn-on instead
|
||||
this->previous_effect_index_ = recovered.effect;
|
||||
#endif // USE_LIGHT_RESUME_EFFECT
|
||||
call.set_transition_length_if_supported(0);
|
||||
}
|
||||
call.perform();
|
||||
|
||||
@@ -11,6 +11,14 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_COLOR_MODE, CONF_STATE
|
||||
from esphome.core import Lambda
|
||||
from esphome.core.entity_helpers import (
|
||||
RESTORE_STATE_INITIAL,
|
||||
RESTORE_STATE_INVERT,
|
||||
RESTORE_STATE_KEEP,
|
||||
RESTORE_STATE_NONE,
|
||||
restore_state_keyword,
|
||||
restore_state_schema,
|
||||
)
|
||||
from esphome.cpp_generator import call_lambda
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
@@ -18,12 +26,6 @@ from esphome.types import ConfigType
|
||||
from .automation import LIGHT_STATE_FIELDS, validate_light_state
|
||||
from .types import ColorMode, LightStateRTCState
|
||||
|
||||
RESTORE_STATE_KEEP = "KEEP"
|
||||
RESTORE_STATE_INVERT = "INVERT"
|
||||
RESTORE_STATE_INITIAL = "INITIAL"
|
||||
RESTORE_STATE_ALL = "ALL"
|
||||
RESTORE_STATE_NONE = "NONE"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LegacyRestoreMode:
|
||||
@@ -56,7 +58,11 @@ _STATE_STRUCT_FIELDS: tuple[tuple[str, str], ...] = tuple(
|
||||
)
|
||||
# Canonical struct-member order, used only to make generated code deterministic --
|
||||
# these are independent field assignments, so the actual order never affects behavior.
|
||||
_MEMBER_ORDER: tuple[str, ...] = tuple(member for _, member in _ALL_STATE_FIELDS)
|
||||
_MEMBER_ORDER: tuple[str, ...] = (
|
||||
*(member for _, member in _ALL_STATE_FIELDS),
|
||||
# Not a LightStateField: its value is a name looked up in this light's effects: list
|
||||
"effect",
|
||||
)
|
||||
|
||||
# A pending `s.<member> = <value>;` statement, tagged with the member it writes.
|
||||
StateStatement = tuple[str, str]
|
||||
@@ -170,12 +176,13 @@ async def _process_value(value: Any, member: str) -> str:
|
||||
|
||||
|
||||
async def _initial_state_statements(
|
||||
initial_state_config: ConfigType | None,
|
||||
initial_state_config: ConfigType | None, effect_index: int = 0
|
||||
) -> list[StateStatement]:
|
||||
"""
|
||||
Create assignments for every field the user set in `initial_state:`, in canonical
|
||||
struct-member order. A field given as `!lambda` is resolved and called immediately,
|
||||
the same way `light.control`'s own field lambdas are.
|
||||
the same way `light.control`'s own field lambdas are. `effect_index` is the resolved
|
||||
`initial_state: effect`; 0 (no effect) is the struct default and emits nothing.
|
||||
"""
|
||||
if not initial_state_config:
|
||||
return []
|
||||
@@ -188,6 +195,8 @@ async def _initial_state_statements(
|
||||
)
|
||||
if (inferred := _inferred_color_mode(initial_state_config)) is not None:
|
||||
statements.append(("color_mode", f"s.color_mode = {inferred};"))
|
||||
if effect_index:
|
||||
statements.append(("effect", f"s.effect = {effect_index};"))
|
||||
return statements
|
||||
|
||||
|
||||
@@ -290,10 +299,12 @@ def _keep_or(validator: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
||||
except cv.Invalid:
|
||||
inner = ()
|
||||
return (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL, *inner)
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper in (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL):
|
||||
return upper
|
||||
if (
|
||||
keyword := restore_state_keyword(
|
||||
value, (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL)
|
||||
)
|
||||
) is not None:
|
||||
return keyword
|
||||
return validator(value)
|
||||
|
||||
return validate
|
||||
@@ -308,10 +319,12 @@ def _validate_restore_state_state(value: Any) -> str | bool:
|
||||
RESTORE_STATE_INITIAL,
|
||||
*validate_light_state(SCHEMA_EXTRACT),
|
||||
)
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper in (RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL):
|
||||
return upper
|
||||
if (
|
||||
keyword := restore_state_keyword(
|
||||
value, (RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL)
|
||||
)
|
||||
) is not None:
|
||||
return keyword
|
||||
return validate_light_state(value)
|
||||
|
||||
|
||||
@@ -327,19 +340,7 @@ _RESTORE_STATE_FIELDS_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@schema_extractor("schema")
|
||||
def RESTORE_STATE_SCHEMA(value: Any) -> ConfigType | str:
|
||||
"""
|
||||
The restore_state: config key can be a mapping of per-field overrides, `all`, or `none`.
|
||||
"""
|
||||
if value == SCHEMA_EXTRACT:
|
||||
# The `all`/`none` string shorthands have no representation in the extracted
|
||||
# docs schema; only the per-field mapping form is walked here.
|
||||
return _RESTORE_STATE_FIELDS_SCHEMA
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper == RESTORE_STATE_ALL:
|
||||
value = {}
|
||||
elif upper == RESTORE_STATE_NONE:
|
||||
return RESTORE_STATE_NONE
|
||||
return _RESTORE_STATE_FIELDS_SCHEMA(value)
|
||||
# A mapping of per-field overrides, `all`, or `none`, which stays the keyword for to_code
|
||||
RESTORE_STATE_SCHEMA = restore_state_schema(
|
||||
_RESTORE_STATE_FIELDS_SCHEMA, RESTORE_STATE_NONE
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "ln882x_spi_led_strip"
|
||||
@@ -0,0 +1,210 @@
|
||||
#include "led_strip.h"
|
||||
|
||||
#ifdef USE_LN882X
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
extern "C" {
|
||||
// Skip the SDK register headers: their unused *_unpack inline functions copy volatile unions, which C++ rejects.
|
||||
// Same as LibreTiny's sdk_private.h; the HAL headers below use nothing from them.
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
|
||||
#define __REG_DMA_H__
|
||||
#define __REG_GPIO_H__
|
||||
#define __REG_SPI_H__
|
||||
#define __REG_SYSC_AWO_H__
|
||||
#define __REG_SYSC_CMP_H__
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
|
||||
#include "hal/hal_clock.h"
|
||||
#include "hal/hal_dma.h"
|
||||
#include "hal/hal_gpio.h"
|
||||
#include "hal/hal_spi.h"
|
||||
}
|
||||
|
||||
namespace esphome::ln882x_spi_led_strip {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ln882x_spi_led_strip");
|
||||
|
||||
// light.py derives the SPI encoding from this APB0 clock, which the SDK's SystemInit() sets.
|
||||
static constexpr uint32_t APB0_HZ = 40000000;
|
||||
// Each LED data bit is sent as one SPI byte, MSB first: the leading 1-bits are the high time.
|
||||
static constexpr uint8_t SPI_BITS_PER_LED_BIT = 8;
|
||||
// SPI0 TX is hard-wired to DMA channel 4 (see hal_dma.h).
|
||||
static constexpr uint32_t LED_STRIP_DMA_CH = DMA_CH_4;
|
||||
|
||||
static uint8_t spi_pattern(uint8_t high_bits) { return uint8_t(0xFF00 >> high_bits); }
|
||||
|
||||
static void spi_dma_init(uint8_t pin, uint8_t prescaler) {
|
||||
// LibreTiny numbers pins as (port << 4) | index. Every pin the LN882x boards expose
|
||||
// (PA0-PA12, PB3-PB9) can be muxed to SPI0 MOSI.
|
||||
const uint32_t gpio_base = (pin >> 4) ? GPIOB_BASE : GPIOA_BASE;
|
||||
const auto gpio_pin = static_cast<gpio_pin_t>(1U << (pin & 0xF));
|
||||
hal_gpio_pin_afio_select(gpio_base, gpio_pin, SPI0_MOSI);
|
||||
hal_gpio_pin_afio_en(gpio_base, gpio_pin, HAL_ENABLE);
|
||||
|
||||
// Only MOSI is routed to a pin; NSS is left unrouted with its output disabled.
|
||||
spi_init_type_def spi_init{};
|
||||
spi_init.spi_direction = SPI_DIRECTION_2LINES_FULLDUPLEX;
|
||||
spi_init.spi_mode = SPI_MODE_MASTER;
|
||||
spi_init.spi_data_size = SPI_DATASIZE_8B;
|
||||
spi_init.spi_cpol = SPI_CPOL_LOW;
|
||||
spi_init.spi_cpha = SPI_CPHA_1EDGE;
|
||||
spi_init.spi_nss_mode = SPI_NSS_HARD;
|
||||
spi_init.spi_baud_rate_prescaler = static_cast<spi_baud_rate_precaler_t>(prescaler);
|
||||
spi_init.spi_first_bit = SPI_FIRST_BIT_MSB;
|
||||
hal_spi_init(SPI0_BASE, &spi_init);
|
||||
hal_spi_en(SPI0_BASE, HAL_ENABLE);
|
||||
hal_spi_ssoe_en(SPI0_BASE, HAL_DISABLE);
|
||||
|
||||
dma_init_t_def dma_init{};
|
||||
dma_init.dma_mem_to_mem_en = DMA_MEM_TO_MEM_DIS;
|
||||
dma_init.dma_pri_lev = DMA_PRI_LEV_HIGH;
|
||||
dma_init.dma_mem_size = DMA_MEM_SIZE_8_BIT;
|
||||
dma_init.dma_p_size = DMA_P_SIZE_8_BIT;
|
||||
dma_init.dma_mem_inc_en = DMA_MEM_INC_EN;
|
||||
dma_init.dma_p_inc_en = DMA_P_INC_DIS;
|
||||
dma_init.dma_circ_mode_en = DMA_CIRC_MODE_DIS;
|
||||
dma_init.dma_dir = DMA_READ_FORM_MEM;
|
||||
dma_init.dma_p_addr = SPI0_DATA_REG;
|
||||
hal_dma_init(LED_STRIP_DMA_CH, &dma_init);
|
||||
hal_dma_en(LED_STRIP_DMA_CH, HAL_DISABLE);
|
||||
}
|
||||
|
||||
static void spi_dma_transmit(const uint8_t *data, uint16_t len) {
|
||||
hal_dma_en(LED_STRIP_DMA_CH, HAL_DISABLE);
|
||||
hal_spi_dma_en(SPI0_BASE, SPI_DMA_TX_EN, HAL_DISABLE);
|
||||
hal_dma_set_mem_addr(LED_STRIP_DMA_CH, reinterpret_cast<uint32_t>(data));
|
||||
hal_dma_set_data_num(LED_STRIP_DMA_CH, len);
|
||||
hal_spi_dma_en(SPI0_BASE, SPI_DMA_TX_EN, HAL_ENABLE);
|
||||
hal_dma_en(LED_STRIP_DMA_CH, HAL_ENABLE);
|
||||
}
|
||||
|
||||
// The count reaches zero once the last byte is in the SPI data register. The tail of every
|
||||
// transfer is the reset gap, so starting the next one while that byte shifts out is harmless.
|
||||
static bool spi_dma_busy() { return hal_dma_get_data_num(LED_STRIP_DMA_CH) != 0; }
|
||||
|
||||
LN882xSPILEDStripLightOutput::LN882xSPILEDStripLightOutput(uint8_t pin, uint16_t num_leds,
|
||||
light::ChannelColors channel_colors, uint8_t prescaler,
|
||||
uint8_t bit0_high_bits, uint8_t bit1_high_bits,
|
||||
uint16_t reset_bytes)
|
||||
: color_data_bytes_(num_leds * channel_colors.bytes_per_led()),
|
||||
// The tail of the DMA buffer stays zero: it is the reset gap.
|
||||
dma_data_bytes_(color_data_bytes_ * SPI_BITS_PER_LED_BIT + reset_bytes),
|
||||
channel_colors_(channel_colors),
|
||||
num_leds_(num_leds),
|
||||
pin_(pin),
|
||||
prescaler_(prescaler),
|
||||
bit0_high_bits_(bit0_high_bits),
|
||||
bit1_high_bits_(bit1_high_bits) {}
|
||||
|
||||
void LN882xSPILEDStripLightOutput::setup() {
|
||||
const uint32_t apb0_hz = hal_clock_get_apb0_clk();
|
||||
if (apb0_hz != APB0_HZ) {
|
||||
ESP_LOGE(TAG, "APB0 clock is %" PRIu32 " Hz", apb0_hz);
|
||||
this->mark_failed(LOG_STR("APB0 clock is not 40 MHz"));
|
||||
return;
|
||||
}
|
||||
|
||||
// One block for the color, effect and DMA data.
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buffer = allocator.allocate(this->color_data_bytes_ + this->num_leds_ + this->dma_data_bytes_);
|
||||
if (buffer == nullptr) {
|
||||
this->mark_failed(LOG_STR("Allocation error"));
|
||||
return;
|
||||
}
|
||||
memset(buffer, 0, this->color_data_bytes_ + this->num_leds_ + this->dma_data_bytes_);
|
||||
this->color_data_ = buffer;
|
||||
this->effect_data_ = buffer + this->color_data_bytes_;
|
||||
this->dma_data_ = this->effect_data_ + this->num_leds_;
|
||||
|
||||
spi_dma_init(this->pin_, this->prescaler_);
|
||||
}
|
||||
|
||||
void LN882xSPILEDStripLightOutput::dump_config() {
|
||||
const uint32_t spi_bit_ns = (1000000000 / APB0_HZ) << this->prescaler_;
|
||||
char channel_colors[5];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LN882x SPI LED Strip:\n"
|
||||
" Pin: %u\n"
|
||||
" Channel colors: %s\n"
|
||||
" Number of LEDs: %" PRIu16 "\n"
|
||||
" Max refresh rate: %" PRIu32 " us\n"
|
||||
" SPI frequency: %" PRIu32 " Hz\n"
|
||||
" Bit 0 / bit 1 high: %" PRIu32 " / %" PRIu32 " ns of %" PRIu32 " ns",
|
||||
this->pin_, this->channel_colors_.to_string(channel_colors), this->num_leds_, this->max_refresh_rate_,
|
||||
APB0_HZ >> this->prescaler_, this->bit0_high_bits_ * spi_bit_ns, this->bit1_high_bits_ * spi_bit_ns,
|
||||
SPI_BITS_PER_LED_BIT * spi_bit_ns);
|
||||
}
|
||||
|
||||
float LN882xSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
light::LightTraits LN882xSPILEDStripLightOutput::get_traits() {
|
||||
auto traits = light::LightTraits();
|
||||
if (this->channel_colors_.has_white()) {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
|
||||
} else {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
void LN882xSPILEDStripLightOutput::clear_effect_data() {
|
||||
if (this->effect_data_ != nullptr)
|
||||
memset(this->effect_data_, 0, this->num_leds_);
|
||||
}
|
||||
|
||||
void LN882xSPILEDStripLightOutput::write_state(light::LightState *state) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
// protect from refreshing too often
|
||||
const uint32_t now = micros();
|
||||
if (this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < this->max_refresh_rate_) {
|
||||
// try again next loop iteration, so that this change won't get lost
|
||||
this->schedule_show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (spi_dma_busy()) {
|
||||
// Busy sending the last frame, try again later.
|
||||
this->schedule_show();
|
||||
return;
|
||||
}
|
||||
|
||||
this->last_refresh_ = now;
|
||||
this->mark_shown_();
|
||||
|
||||
const uint8_t bit0 = spi_pattern(this->bit0_high_bits_);
|
||||
const uint8_t bit1 = spi_pattern(this->bit1_high_bits_);
|
||||
const uint8_t *color_data = this->color_data_;
|
||||
uint8_t *dma_data = this->dma_data_;
|
||||
for (size_t i = 0; i < this->color_data_bytes_; i++) {
|
||||
const uint8_t color_byte = *(color_data++);
|
||||
for (uint8_t mask = 0x80; mask != 0; mask >>= 1) {
|
||||
*(dma_data++) = (color_byte & mask) ? bit1 : bit0;
|
||||
}
|
||||
}
|
||||
|
||||
spi_dma_transmit(this->dma_data_, this->dma_data_bytes_);
|
||||
}
|
||||
|
||||
light::ESPColorView LN882xSPILEDStripLightOutput::get_view_internal(int32_t index) const {
|
||||
const light::ChannelColors &colors = this->channel_colors_;
|
||||
// After a failed setup() effects and partitions still write pixels, so they get a scratch pixel.
|
||||
static uint8_t scratch[5];
|
||||
const bool ready = this->color_data_ != nullptr;
|
||||
uint8_t *led = ready ? this->color_data_ + (index * colors.bytes_per_led()) : scratch;
|
||||
return {led + colors.r,
|
||||
led + colors.g,
|
||||
led + colors.b,
|
||||
colors.has_white() ? led + colors.w : nullptr,
|
||||
ready ? &this->effect_data_[index] : scratch + 4,
|
||||
&this->correction_};
|
||||
}
|
||||
|
||||
} // namespace esphome::ln882x_spi_led_strip
|
||||
|
||||
#endif // USE_LN882X
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LN882X
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/light/addressable_light.h"
|
||||
#include "esphome/components/light/channel_colors.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::ln882x_spi_led_strip {
|
||||
|
||||
class LN882xSPILEDStripLightOutput final : public light::AddressableLight {
|
||||
public:
|
||||
/// The SPI encoding comes from light.py: SPI clock APB0 / 2^prescaler, the number of high SPI bits for a
|
||||
/// 0 and a 1 bit, and the zero bytes of the reset gap.
|
||||
LN882xSPILEDStripLightOutput(uint8_t pin, uint16_t num_leds, light::ChannelColors channel_colors, uint8_t prescaler,
|
||||
uint8_t bit0_high_bits, uint8_t bit1_high_bits, uint16_t reset_bytes);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
light::LightTraits get_traits() override;
|
||||
int32_t size() const override { return this->num_leds_; }
|
||||
void clear_effect_data() override;
|
||||
void write_state(light::LightState *state) override;
|
||||
|
||||
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
|
||||
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
|
||||
|
||||
protected:
|
||||
light::ESPColorView get_view_internal(int32_t index) const override;
|
||||
|
||||
const size_t color_data_bytes_;
|
||||
const size_t dma_data_bytes_;
|
||||
uint8_t *color_data_{nullptr};
|
||||
uint8_t *effect_data_{nullptr};
|
||||
uint8_t *dma_data_{nullptr};
|
||||
uint32_t last_refresh_{0};
|
||||
uint32_t max_refresh_rate_{0};
|
||||
|
||||
const light::ChannelColors channel_colors_;
|
||||
const uint16_t num_leds_;
|
||||
const uint8_t pin_;
|
||||
const uint8_t prescaler_;
|
||||
const uint8_t bit0_high_bits_;
|
||||
const uint8_t bit1_high_bits_;
|
||||
};
|
||||
|
||||
} // namespace esphome::ln882x_spi_led_strip
|
||||
|
||||
#endif // USE_LN882X
|
||||
@@ -0,0 +1,156 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import light
|
||||
from esphome.components.const import CONF_CHANNEL_COLORS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHIPSET,
|
||||
CONF_MAX_REFRESH_RATE,
|
||||
CONF_NUM_LEDS,
|
||||
CONF_OUTPUT_ID,
|
||||
CONF_PIN,
|
||||
CONF_PLATFORM,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
CODEOWNERS = ["@sebo-b"]
|
||||
DEPENDENCIES = ["ln882x"]
|
||||
|
||||
ln882x_spi_led_strip_ns = cg.esphome_ns.namespace("ln882x_spi_led_strip")
|
||||
LN882xSPILEDStripLightOutput = ln882x_spi_led_strip_ns.class_(
|
||||
"LN882xSPILEDStripLightOutput", light.AddressableLight
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LEDStripTimings:
|
||||
bit0_high: int # ns
|
||||
bit0_low: int
|
||||
bit1_high: int
|
||||
bit1_low: int
|
||||
|
||||
|
||||
# As esp32_rmt_led_strip.
|
||||
CHIPSETS = {
|
||||
"WS2811": LEDStripTimings(300, 1090, 1090, 320),
|
||||
"WS2812": LEDStripTimings(400, 1000, 1000, 400),
|
||||
"SK6812": LEDStripTimings(300, 900, 600, 600),
|
||||
"APA106": LEDStripTimings(350, 1360, 1360, 350),
|
||||
"SM16703": LEDStripTimings(300, 900, 900, 300),
|
||||
}
|
||||
|
||||
# SystemInit() in the SDK sets APB0 to 40 MHz; led_strip.cpp checks it at boot.
|
||||
APB0_HZ = 40_000_000
|
||||
# SPI0 runs at APB0 / 2^prescaler, prescaler 1 (/2) to 8 (/256).
|
||||
SPI_PRESCALER_MIN = 1
|
||||
SPI_PRESCALER_MAX = 8
|
||||
# Each LED data bit is sent as one SPI byte, MSB first: the leading 1-bits are the high time.
|
||||
SPI_BITS_PER_LED_BIT = 8
|
||||
# At most this many high bits, so every LED bit ends with at least two low SPI bits.
|
||||
SPI_HIGH_BITS_MAX = SPI_BITS_PER_LED_BIT - 2
|
||||
# Low time after the data that latches it. Minimum 50 us for WS2811, 80 us for SK6812, 280 us for WS2812B-V5.
|
||||
RESET_DURATION_NS = 300_000
|
||||
# One DMA transfer moves at most this many bytes: the whole frame plus the reset gap.
|
||||
DMA_MAX_TRANSFER = 0xFFFF
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SPIEncoding:
|
||||
prescaler: int
|
||||
bit0_high_bits: int
|
||||
bit1_high_bits: int
|
||||
reset_bytes: int
|
||||
|
||||
|
||||
def _spi_bit_ns(prescaler: int) -> int:
|
||||
return (1_000_000_000 << prescaler) // APB0_HZ
|
||||
|
||||
|
||||
def spi_encoding(timings: LEDStripTimings) -> SPIEncoding:
|
||||
"""SPI clock and bit patterns that reproduce the chipset timings."""
|
||||
# The prescaler whose 8-bit frame is closest to the bit period. Each step doubles the frame,
|
||||
# so step on while the period is at least 1.5 frames (a tie takes the longer frame).
|
||||
bit_period_ns = max(
|
||||
timings.bit0_high + timings.bit0_low, timings.bit1_high + timings.bit1_low
|
||||
)
|
||||
prescaler = SPI_PRESCALER_MIN
|
||||
while (
|
||||
prescaler < SPI_PRESCALER_MAX
|
||||
and 3 * SPI_BITS_PER_LED_BIT * _spi_bit_ns(prescaler) <= 2 * bit_period_ns
|
||||
):
|
||||
prescaler += 1
|
||||
bit_ns = _spi_bit_ns(prescaler)
|
||||
|
||||
def high_bits(duration_ns: int, max_bits: int) -> int:
|
||||
# Rounded half up, from 1 to max_bits.
|
||||
return max(1, min(max_bits, (2 * duration_ns + bit_ns) // (2 * bit_ns)))
|
||||
|
||||
# Bit 0 stops one below the maximum, so bit 1 can always be longer.
|
||||
bit0_high_bits = high_bits(timings.bit0_high, SPI_HIGH_BITS_MAX - 1)
|
||||
bit1_high_bits = max(
|
||||
high_bits(timings.bit1_high, SPI_HIGH_BITS_MAX), bit0_high_bits + 1
|
||||
)
|
||||
reset_bytes = -(-RESET_DURATION_NS // (SPI_BITS_PER_LED_BIT * bit_ns))
|
||||
return SPIEncoding(prescaler, bit0_high_bits, bit1_high_bits, reset_bytes)
|
||||
|
||||
|
||||
def _validate_num_leds(config: ConfigType) -> ConfigType:
|
||||
reset_bytes = spi_encoding(CHIPSETS[config[CONF_CHIPSET]]).reset_bytes
|
||||
bytes_per_led = len(config[CONF_CHANNEL_COLORS])
|
||||
max_num_leds = (
|
||||
(DMA_MAX_TRANSFER - reset_bytes) // SPI_BITS_PER_LED_BIT // bytes_per_led
|
||||
)
|
||||
if config[CONF_NUM_LEDS] > max_num_leds:
|
||||
raise cv.Invalid(
|
||||
f"The maximum number of LEDs for this configuration is {max_num_leds}.",
|
||||
path=[CONF_NUM_LEDS],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LN882xSPILEDStripLightOutput),
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
|
||||
cv.Required(CONF_CHANNEL_COLORS): light.validate_channel_colors,
|
||||
cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
||||
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
|
||||
}
|
||||
),
|
||||
_validate_num_leds,
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
# The strip owns SPI0 and its DMA channel, so only one can exist.
|
||||
lights = fv.full_config.get().get(light.DOMAIN, [])
|
||||
if sum(entry.get(CONF_PLATFORM) == DOMAIN for entry in lights) > 1:
|
||||
raise cv.Invalid(f"Only one {DOMAIN} is supported (it uses SPI0)")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
encoding = spi_encoding(CHIPSETS[config[CONF_CHIPSET]])
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_OUTPUT_ID],
|
||||
config[CONF_PIN],
|
||||
config[CONF_NUM_LEDS],
|
||||
light.channel_colors_struct(config[CONF_CHANNEL_COLORS]),
|
||||
encoding.prescaler,
|
||||
encoding.bit0_high_bits,
|
||||
encoding.bit1_high_bits,
|
||||
encoding.reset_bytes,
|
||||
)
|
||||
await light.register_light(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add_optional(var.set_max_refresh_rate, config.get(CONF_MAX_REFRESH_RATE))
|
||||
@@ -35,12 +35,12 @@ class LockTraits {
|
||||
public:
|
||||
LockTraits() = default;
|
||||
|
||||
bool get_supports_open() const { return this->supports_open_; }
|
||||
void set_supports_open(bool supports_open) { this->supports_open_ = supports_open; }
|
||||
bool get_requires_code() const { return this->requires_code_; }
|
||||
void set_requires_code(bool requires_code) { this->requires_code_ = requires_code; }
|
||||
bool get_assumed_state() const { return this->assumed_state_; }
|
||||
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
|
||||
bool get_supports_open() const { return this->flags_.supports_open; }
|
||||
void set_supports_open(bool supports_open) { this->flags_.supports_open = supports_open; }
|
||||
bool get_requires_code() const { return this->flags_.requires_code; }
|
||||
void set_requires_code(bool requires_code) { this->flags_.requires_code = requires_code; }
|
||||
bool get_assumed_state() const { return this->flags_.assumed_state; }
|
||||
void set_assumed_state(bool assumed_state) { this->flags_.assumed_state = assumed_state; }
|
||||
|
||||
bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); }
|
||||
void set_supported_states(std::initializer_list<LockState> states) {
|
||||
@@ -54,9 +54,13 @@ class LockTraits {
|
||||
void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); }
|
||||
|
||||
protected:
|
||||
bool supports_open_{false};
|
||||
bool requires_code_{false};
|
||||
bool assumed_state_{false};
|
||||
// Bit-packed flags to save memory (1 byte instead of 3)
|
||||
struct LockTraitFlags {
|
||||
uint8_t supports_open : 1;
|
||||
uint8_t requires_code : 1;
|
||||
uint8_t assumed_state : 1;
|
||||
uint8_t reserved : 5;
|
||||
} flags_{};
|
||||
uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)};
|
||||
};
|
||||
|
||||
@@ -176,8 +180,8 @@ class Lock : public EntityBase {
|
||||
*/
|
||||
virtual void control(const LockCall &call) = 0;
|
||||
|
||||
Deduplicator<LockState> publish_dedup_; // packs with state and traits ahead of the pointer members
|
||||
LazyCallbackManager<void(LockState)> state_callback_{};
|
||||
Deduplicator<LockState> publish_dedup_;
|
||||
ESPPreferenceObject rtc_;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@KoenBreeman"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "m5stack_4relay"
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_M5STACK_4RELAY_ID = "m5stack_4relay_id"
|
||||
|
||||
m5stack_4relay_ns = cg.esphome_ns.namespace("m5stack_4relay")
|
||||
M5Stack4Relay = m5stack_4relay_ns.class_("M5Stack4Relay", cg.Component, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(M5Stack4Relay),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x26))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await i2c.new_i2c_component(config)
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "m5stack_4relay.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::m5stack_4relay {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "m5stack_4relay");
|
||||
|
||||
static constexpr uint8_t REG_MODE = 0x10;
|
||||
static constexpr uint8_t REG_RELAY = 0x11;
|
||||
static constexpr uint8_t MODE_SYNC = 1;
|
||||
|
||||
void M5Stack4Relay::setup() {
|
||||
if (!this->write_byte(REG_MODE, MODE_SYNC) || !this->read_byte(REG_RELAY, &this->relays_)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
for (size_t channel = 0; channel < this->switches_.size(); channel++) {
|
||||
switch_::Switch *sw = this->switches_[channel];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
// The chip keeps the relays across an ESP reboot, so a switch with restore mode DISABLED keeps what it
|
||||
// holds. Every mode goes through control(), so inverted: applies and the first publish is the saved one.
|
||||
sw->restore(((this->relays_ >> channel) & 1) != sw->is_inverted());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void M5Stack4Relay::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "M5Stack 4 Relay:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
bool M5Stack4Relay::set_relay(uint8_t channel, bool on) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
uint8_t value = this->relays_;
|
||||
assign_bits(value, 1 << channel, on);
|
||||
if (!this->status_comm_result(this->write_byte(REG_RELAY, value)))
|
||||
return false;
|
||||
this->relays_ = value;
|
||||
#ifdef USE_SWITCH
|
||||
if (this->switches_[channel] != nullptr)
|
||||
this->switches_[channel]->publish_state(on);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::m5stack_4relay
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::m5stack_4relay {
|
||||
|
||||
class M5Stack4Relay : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Switch relay `channel` (0-3) on or off; returns false when the write failed.
|
||||
bool set_relay(uint8_t channel, bool on);
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
void register_switch(uint8_t channel, switch_::Switch *sw) { this->switches_[channel] = sw; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
uint8_t relays_{0};
|
||||
#ifdef USE_SWITCH
|
||||
std::array<switch_::Switch *, 4> switches_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::m5stack_4relay
|
||||
@@ -0,0 +1,50 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_PLATFORM
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_M5STACK_4RELAY_ID, DOMAIN, M5Stack4Relay
|
||||
|
||||
DEPENDENCIES = ["m5stack_4relay"]
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
switch.ApplySwitch, default_restore_mode="DISABLED"
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_M5STACK_4RELAY_ID): cv.use_id(M5Stack4Relay),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=4),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_unique_channel(config: ConfigType) -> ConfigType:
|
||||
# The hub has one slot per channel, so a second switch on it would take over the first
|
||||
hub, channel = config[CONF_M5STACK_4RELAY_ID], config[CONF_CHANNEL]
|
||||
users = [
|
||||
other
|
||||
for other in fv.full_config.get().get(switch.DOMAIN, [])
|
||||
if other.get(CONF_PLATFORM) == DOMAIN
|
||||
and other[CONF_M5STACK_4RELAY_ID] == hub
|
||||
and other[CONF_CHANNEL] == channel
|
||||
]
|
||||
if len(users) > 1:
|
||||
raise cv.Invalid(
|
||||
f"Channel {channel} of {hub} is used by more than one switch",
|
||||
path=[CONF_CHANNEL],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_unique_channel
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_M5STACK_4RELAY_ID])
|
||||
channel = config[CONF_CHANNEL] - 1
|
||||
await switch.sub_switches(config, parent=hub).create(
|
||||
config,
|
||||
lambda sw: hub.register_switch(channel, sw),
|
||||
apply=f"set_relay({channel}, state)",
|
||||
)
|
||||
@@ -12,13 +12,7 @@ ESPHOME_LOG_TAG(TAG, "modbus_controller.switch");
|
||||
// Maximum bytes to log in verbose hex output
|
||||
static constexpr size_t MODBUS_SWITCH_MAX_LOG_BYTES = 64;
|
||||
|
||||
void ModbusSwitch::setup() {
|
||||
optional<bool> initial_state = Switch::get_initial_state_with_restore_mode();
|
||||
if (initial_state.has_value()) {
|
||||
// if it has a value, restore_mode is not "DISABLED", therefore act on the switch:
|
||||
this->control(initial_state.value());
|
||||
}
|
||||
}
|
||||
void ModbusSwitch::setup() { this->restore(); }
|
||||
void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); }
|
||||
|
||||
bool ModbusSwitch::assumed_state() { return this->assumed_state_; }
|
||||
|
||||
@@ -50,6 +50,10 @@ void MQTTCoverComponent::setup() {
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("STOP")) == 0) {
|
||||
this->cover_->make_call().set_command_stop_tilt().perform();
|
||||
return;
|
||||
}
|
||||
auto value = parse_number<float>(payload);
|
||||
if (!value.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::output {
|
||||
ESPHOME_LOG_TAG(TAG, "output.switch");
|
||||
|
||||
void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); }
|
||||
void OutputSwitch::setup() { this->control(this->get_initial_state_with_restore_mode().value_or(false)); }
|
||||
void OutputSwitch::setup() { this->restore(false); }
|
||||
void OutputSwitch::write_state(bool state) {
|
||||
this->output_->set_state(state);
|
||||
this->publish_state(state);
|
||||
|
||||
@@ -92,7 +92,17 @@ class JPEGFormat(Format):
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
|
||||
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
|
||||
# 1.8.4 plus decodeBegin()/decodeRows(), so an image can be decoded a few rows at a time and
|
||||
# can wait for input that has not arrived yet, and fixes for: input that arrives in short
|
||||
# reads, garbled RGB8888 output of grayscale images, and two ARM NEON faults seen on arm64 hosts
|
||||
# (swapped red and blue in the 4:2:0 RGB8888 path, writes past the row on clipped MCUs). It also
|
||||
# uses the ESP32-S3 SIMD colour conversion in ESP-IDF builds, not only Arduino ones, and is built
|
||||
# with -O2.
|
||||
cg.add_library(
|
||||
"JPEGDEC",
|
||||
"1.8.4",
|
||||
"https://github.com/esphome-libs/JPEGDEC#1.8.4-esphome.3",
|
||||
)
|
||||
if CORE.is_host:
|
||||
# JPEGDEC's host detection checks __MACH__/__LINUX__, but gcc only
|
||||
# predefines the lowercase __linux__; without this a Linux host
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/color.h"
|
||||
#include "image_format.h"
|
||||
|
||||
@@ -9,6 +11,14 @@ enum DecodeError : int {
|
||||
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
|
||||
DECODE_ERROR_OUT_OF_MEMORY = -3,
|
||||
DECODE_ERROR_INTERNAL_DECODER_ERROR = -4,
|
||||
DECODE_ERROR_READ = -5,
|
||||
};
|
||||
|
||||
/// Results of ImageDecoder::decode_step() other than a DecodeError
|
||||
enum DecodeStep : int {
|
||||
DECODE_STEP_DONE = 0,
|
||||
DECODE_STEP_MORE = 1,
|
||||
DECODE_STEP_NEED_DATA = 2, // The source has nothing more yet; call again once more has arrived
|
||||
};
|
||||
|
||||
constexpr const char *decode_error_to_string(int error) {
|
||||
@@ -21,6 +31,8 @@ constexpr const char *decode_error_to_string(int error) {
|
||||
return "Out of memory";
|
||||
case DECODE_ERROR_INTERNAL_DECODER_ERROR:
|
||||
return "Internal decoder error";
|
||||
case DECODE_ERROR_READ:
|
||||
return "Read error";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
@@ -28,6 +40,19 @@ constexpr const char *decode_error_to_string(int error) {
|
||||
|
||||
class RuntimeImage;
|
||||
|
||||
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
|
||||
class DecodeSource {
|
||||
public:
|
||||
virtual ~DecodeSource() = default;
|
||||
/**
|
||||
* Read up to len bytes at the current position. Returns the number read, which is less than len, even 0, when
|
||||
* no more has arrived yet or at the end of the input, or negative on error.
|
||||
*/
|
||||
virtual int32_t read(uint8_t *buf, uint32_t len) = 0;
|
||||
/// Move to a position counted from the start. Returns false on error.
|
||||
virtual bool seek(uint32_t position) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Class to abstract decoding different image formats.
|
||||
*/
|
||||
@@ -88,6 +113,28 @@ class ImageDecoder {
|
||||
*/
|
||||
virtual int decode(uint8_t *buffer, size_t size) = 0;
|
||||
|
||||
/// Whether this decoder can read a DecodeSource itself, a step at a time, instead of being fed data.
|
||||
virtual bool supports_source() const { return false; }
|
||||
|
||||
/**
|
||||
* @brief Start decoding from a source the decoder reads itself. Call after prepare(), in place of decode().
|
||||
*
|
||||
* The image headers (everything before the image data) must be readable from the source when this is called;
|
||||
* only decode_step() can wait for more to arrive. The source must hold expected_size bytes in the end, and stay
|
||||
* valid until the session ends.
|
||||
* @return int 0 on success, or a {@see DecodeError} value.
|
||||
*/
|
||||
virtual int begin_source(DecodeSource *source) { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
|
||||
|
||||
/**
|
||||
* @brief Decode a little more of an image started with begin_source(), keeping each call short.
|
||||
*
|
||||
* @return int DECODE_STEP_MORE while there is more to do, DECODE_STEP_NEED_DATA when the source has not
|
||||
* delivered enough to go on, DECODE_STEP_DONE when the image is complete, or a {@see DecodeError}
|
||||
* value.
|
||||
*/
|
||||
virtual int decode_step() { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
|
||||
|
||||
/**
|
||||
* @brief Request the image to be resized once the actual dimensions are known.
|
||||
* Called by the callback functions, to be able to access the parent Image class.
|
||||
@@ -112,6 +159,9 @@ class ImageDecoder {
|
||||
*/
|
||||
void draw(int x, int y, int w, int h, const Color &color);
|
||||
|
||||
/// Bytes of the image the decoder has used so far
|
||||
size_t get_decoded_bytes() const { return this->decoded_bytes_; }
|
||||
|
||||
/**
|
||||
* @brief Check if the decoder has finished processing.
|
||||
*
|
||||
|
||||
@@ -135,13 +135,24 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int err = this->start_();
|
||||
if (err != 0)
|
||||
return err;
|
||||
if (!this->jpeg_.decode(0, 0, 0))
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = size;
|
||||
this->jpeg_.close();
|
||||
return size;
|
||||
}
|
||||
|
||||
int JpegDecoder::start_() {
|
||||
auto jpeg_type = this->jpeg_.getJPEGType();
|
||||
if (jpeg_type == JPEG_MODE_INVALID) {
|
||||
ESP_LOGE(TAG, "Unsupported JPEG image");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
} else if (jpeg_type == JPEG_MODE_PROGRESSIVE) {
|
||||
ESP_LOGE(TAG, "Progressive JPEG images not supported");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
|
||||
|
||||
@@ -160,26 +171,109 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (!this->jpeg_.decode(0, 0, 0)) {
|
||||
auto error = this->jpeg_.getLastError();
|
||||
ESP_LOGE(TAG, "Error while decoding: %d", error);
|
||||
this->jpeg_.close();
|
||||
switch (error) {
|
||||
case JPEG_ERROR_MEMORY:
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
case JPEG_UNSUPPORTED_FEATURE:
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
case JPEG_INVALID_FILE:
|
||||
case JPEG_INVALID_PARAMETER:
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
case JPEG_DECODE_ERROR:
|
||||
default:
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
}
|
||||
}
|
||||
this->decoded_bytes_ = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int JpegDecoder::fail_() {
|
||||
const bool read_failed = this->source_ != nullptr && this->source_failed_;
|
||||
const int error = this->jpeg_.getLastError();
|
||||
this->jpeg_.close();
|
||||
return size;
|
||||
this->source_ = nullptr;
|
||||
if (read_failed) {
|
||||
ESP_LOGE(TAG, "Reading the image failed");
|
||||
return DECODE_ERROR_READ;
|
||||
}
|
||||
ESP_LOGE(TAG, "Error while decoding: %d", error);
|
||||
switch (error) {
|
||||
case JPEG_ERROR_MEMORY:
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
case JPEG_UNSUPPORTED_FEATURE:
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
case JPEG_INVALID_FILE:
|
||||
case JPEG_INVALID_PARAMETER:
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
case JPEG_DECODE_ERROR:
|
||||
default:
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void JpegDecoder::reset() {
|
||||
// A decode from a source that was given up part way still has the image open
|
||||
if (this->source_ != nullptr) {
|
||||
this->jpeg_.close();
|
||||
this->source_ = nullptr;
|
||||
}
|
||||
ImageDecoder::reset();
|
||||
}
|
||||
|
||||
int32_t JpegDecoder::source_read(JPEGFILE *file, uint8_t *buf, int32_t len) {
|
||||
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
|
||||
const int32_t n = decoder->source_->read(buf, len);
|
||||
if (n < 0) {
|
||||
// JPEGDEC has no way to report a failed read, so remember it and give it no data
|
||||
decoder->source_failed_ = true;
|
||||
return 0;
|
||||
}
|
||||
file->iPos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
int32_t JpegDecoder::source_seek(JPEGFILE *file, int32_t position) {
|
||||
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
|
||||
if (position < 0 || !decoder->source_->seek(position)) {
|
||||
decoder->source_failed_ = true;
|
||||
return file->iPos;
|
||||
}
|
||||
file->iPos = position;
|
||||
return position;
|
||||
}
|
||||
|
||||
int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
if (this->expected_size_ == 0 || this->expected_size_ > INT32_MAX) {
|
||||
ESP_LOGE(TAG, "Image size %zu cannot be read from a source", this->expected_size_);
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
if (!source->seek(0)) {
|
||||
ESP_LOGE(TAG, "Could not read the image");
|
||||
return DECODE_ERROR_READ;
|
||||
}
|
||||
this->source_ = source;
|
||||
this->source_failed_ = false;
|
||||
if (!this->jpeg_.open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
|
||||
// The caller may recover, for example by reading the whole image first, so it reports the failure
|
||||
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||
this->source_ = nullptr;
|
||||
return this->source_failed_ ? DECODE_ERROR_READ : DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int err = this->start_();
|
||||
if (err != 0) {
|
||||
this->jpeg_.close();
|
||||
this->source_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
if (!this->jpeg_.decodeBegin(0, 0, 0))
|
||||
return this->fail_();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int HOT JpegDecoder::decode_step() {
|
||||
if (this->source_ == nullptr)
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
const int result = this->jpeg_.decodeRows(1);
|
||||
// JPEGDEC may decode on with the missing data, so check the read before its result
|
||||
if (this->source_failed_)
|
||||
return this->fail_();
|
||||
if (result == JPEG_DECODE_MORE)
|
||||
return DECODE_STEP_MORE;
|
||||
if (result == JPEG_DECODE_NEED_DATA)
|
||||
return DECODE_STEP_NEED_DATA;
|
||||
if (result == 0)
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = this->expected_size_;
|
||||
this->jpeg_.close();
|
||||
this->source_ = nullptr;
|
||||
return DECODE_STEP_DONE;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -22,19 +22,36 @@ class JpegDecoder : public ImageDecoder {
|
||||
~JpegDecoder() override {}
|
||||
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
void reset() override;
|
||||
|
||||
/// The source is read as the decode needs it, through JPEGDEC's own 2 KB buffer
|
||||
bool supports_source() const override { return true; }
|
||||
int begin_source(DecodeSource *source) override;
|
||||
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
|
||||
int HOT decode_step() override;
|
||||
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
|
||||
|
||||
protected:
|
||||
bool prepare_rgb565_scaling_(int width, int height);
|
||||
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
|
||||
int start_();
|
||||
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
|
||||
int fail_();
|
||||
|
||||
static int32_t source_read(JPEGFILE *file, uint8_t *buf, int32_t len);
|
||||
static int32_t source_seek(JPEGFILE *file, int32_t position);
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
JPEGDEC jpeg_{};
|
||||
FixedVector<uint16_t> x_boundaries_{};
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
int source_width_{0};
|
||||
int source_height_{0};
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool direct_rgb565_{false};
|
||||
bool source_failed_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -239,6 +239,25 @@ int RuntimeImage::feed_data(uint8_t *data, size_t len) {
|
||||
return consumed;
|
||||
}
|
||||
|
||||
int RuntimeImage::begin_source(DecodeSource *source) {
|
||||
if (!this->decoder_supports_source()) {
|
||||
ESP_LOGE(TAG, "Decoder cannot read a source");
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
return this->decoder_->begin_source(source);
|
||||
}
|
||||
|
||||
int RuntimeImage::decode_step() {
|
||||
if (!this->is_decoding()) {
|
||||
ESP_LOGE(TAG, "No decoder initialized");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int result = this->decoder_->decode_step();
|
||||
if (result == DECODE_STEP_DONE)
|
||||
this->decoded_bytes_ = this->decoder_->get_decoded_bytes();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RuntimeImage::end_decode() {
|
||||
if (!this->is_decoding()) {
|
||||
return false;
|
||||
|
||||
@@ -76,6 +76,22 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
int feed_data(uint8_t *data, size_t len);
|
||||
|
||||
/// Whether the active decoder can read a DecodeSource itself (see begin_source()).
|
||||
bool decoder_supports_source() const { return this->is_decoding() && this->decoder_->supports_source(); }
|
||||
|
||||
/**
|
||||
* @brief Decode from a source the decoder reads itself, in place of feed_data().
|
||||
*
|
||||
* Call after begin_decode() when decoder_supports_source() is true, once the image headers can be read from the
|
||||
* source, then call decode_step() until it
|
||||
* returns DECODE_STEP_DONE, and finish with end_decode().
|
||||
* @return 0 on success, or a DecodeError value.
|
||||
*/
|
||||
int begin_source(DecodeSource *source);
|
||||
|
||||
/// Decode a little more from the source; see ImageDecoder::decode_step().
|
||||
int decode_step();
|
||||
|
||||
/**
|
||||
* @brief Complete the decoding process.
|
||||
*
|
||||
|
||||
@@ -1,79 +1,226 @@
|
||||
#include "rx8111.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::rx8111 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "rx8111");
|
||||
|
||||
// Indexed by the TSEL value stored in timer_clock_
|
||||
PROGMEM_STRING_TABLE(TimerClockNames, "4096 Hz", "64 Hz", "1 Hz", "1/60 Hz");
|
||||
|
||||
static constexpr uint8_t RX8111_REG_SEC = 0x10;
|
||||
static constexpr uint8_t RX8111_REG_MIN_ALARM = 0x17;
|
||||
static constexpr uint8_t RX8111_REG_HOUR_ALARM = 0x18;
|
||||
static constexpr uint8_t RX8111_REG_DAY_ALARM = 0x19;
|
||||
static constexpr uint8_t RX8111_REG_EXTENSION = 0x1D;
|
||||
static constexpr uint8_t RX8111_REG_FLAG = 0x1E;
|
||||
static constexpr uint8_t RX8111_REG_TIMER0 = 0x1A;
|
||||
static constexpr uint8_t RX8111_REG_CONTROL = 0x1F;
|
||||
static constexpr uint8_t RX8111_REG_SEC_ALARM = 0x2C;
|
||||
static constexpr uint8_t RX8111_ALARM_DISABLED = 0x80;
|
||||
static constexpr uint8_t RX8111_EXTENSION_WADA = 0x08;
|
||||
static constexpr uint8_t RX8111_CONTROL_AIE = 0x08;
|
||||
static constexpr uint8_t RX8111_CONTROL_TIE = 0x10;
|
||||
static constexpr uint8_t RX8111_EXTENSION_TE = 0x10;
|
||||
static constexpr uint8_t RX8111_EXTENSION_TSEL = 0x03;
|
||||
static constexpr uint8_t RX8111_FLAG_TF = 0x10;
|
||||
static constexpr uint8_t RX8111_FLAG_AF = 0x08;
|
||||
static constexpr uint8_t RX8111_FLAG_POR = 0x80;
|
||||
static constexpr uint8_t RX8111_FLAG_VLF = 0x02;
|
||||
static constexpr uint8_t RX8111_FLAG_XST = 0x01;
|
||||
static constexpr uint8_t RX8111_CTRL_STOP = 0x01;
|
||||
// FLAG bits are write-zero-to-clear; keep UF/TF/AF/EVF even if they latch during the write.
|
||||
static constexpr uint8_t RX8111_KEEP_EVENT_FLAGS = 0x3C;
|
||||
static constexpr uint32_t RX8111_EVENT_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t RX8111_BOOT_ALARM_RETRY_ID = 1;
|
||||
static constexpr uint32_t RX8111_BOOT_TIMER_RETRY_ID = 2;
|
||||
static constexpr uint8_t RX8111_BOOT_MAX_ATTEMPTS = 3;
|
||||
|
||||
void RX8111Component::setup() { this->read_time(); }
|
||||
void RX8111Component::setup() {
|
||||
this->configure_boot_alarm_();
|
||||
this->configure_boot_timer_();
|
||||
this->read_time();
|
||||
if (this->alarm_enabled_ || this->watching_()) {
|
||||
this->start_event_polling_();
|
||||
}
|
||||
}
|
||||
|
||||
void RX8111Component::update() { this->read_time(); }
|
||||
void RX8111Component::configure_boot_alarm_() {
|
||||
bool success = true;
|
||||
if (this->alarm_enabled_) {
|
||||
if (this->alarm_second_ != ALARM_FIELD_UNSET || this->alarm_minute_ != ALARM_FIELD_UNSET ||
|
||||
this->alarm_hour_ != ALARM_FIELD_UNSET || this->alarm_weekday_ != ALARM_FIELD_UNSET ||
|
||||
this->alarm_day_ != ALARM_FIELD_UNSET) {
|
||||
success = this->program_alarm_(this->alarm_second_, this->alarm_minute_, this->alarm_hour_, this->alarm_weekday_,
|
||||
this->alarm_day_, true);
|
||||
if (success) {
|
||||
// Only the first successful boot program can introduce a boot wake-up edge.
|
||||
if (!this->boot_alarm_event_seen_)
|
||||
this->alarm_flag_seen_ = false;
|
||||
this->start_event_polling_();
|
||||
}
|
||||
} else {
|
||||
// Preserve an alarm programmed by an action before a reboot or deep sleep.
|
||||
uint8_t control;
|
||||
success = this->read_byte(RX8111_REG_CONTROL, &control);
|
||||
if (success) {
|
||||
this->alarm_armed_ = (control & RX8111_CONTROL_AIE) != 0;
|
||||
if (this->alarm_armed_)
|
||||
this->start_event_polling_();
|
||||
}
|
||||
}
|
||||
} else if (this->alarm_explicitly_configured_) {
|
||||
success = this->set_alarm_interrupt_(false);
|
||||
}
|
||||
this->boot_config_result_(true, success);
|
||||
}
|
||||
|
||||
void RX8111Component::configure_boot_timer_() {
|
||||
bool success = true;
|
||||
// An omitted timer setting leaves battery-backed state alone; explicit false stops it.
|
||||
if (this->timer_configured_) {
|
||||
const bool start_timer = this->timer_enabled_;
|
||||
if (start_timer) {
|
||||
success = this->configure_timer_();
|
||||
} else {
|
||||
uint8_t extension, control;
|
||||
success = this->disable_timer_(extension, control);
|
||||
}
|
||||
if (success && !start_timer && this->timer_count_ != 0) {
|
||||
this->timer_retained_event_pending_ = true;
|
||||
this->start_event_polling_();
|
||||
}
|
||||
}
|
||||
this->boot_config_result_(false, success);
|
||||
}
|
||||
|
||||
void RX8111Component::boot_config_result_(bool alarm, bool success) {
|
||||
auto &attempts = alarm ? this->boot_alarm_attempts_ : this->boot_timer_attempts_;
|
||||
const uint8_t warning = alarm ? WARN_ALARM : WARN_TIMER;
|
||||
++attempts;
|
||||
if (success) {
|
||||
this->clear_warning_(warning);
|
||||
return;
|
||||
}
|
||||
this->set_warning_(warning, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
if (attempts >= RX8111_BOOT_MAX_ATTEMPTS)
|
||||
return;
|
||||
this->set_timeout(alarm ? RX8111_BOOT_ALARM_RETRY_ID : RX8111_BOOT_TIMER_RETRY_ID, this->boot_retry_delay_ms_,
|
||||
[this, alarm]() {
|
||||
if (alarm) {
|
||||
this->configure_boot_alarm_();
|
||||
} else {
|
||||
this->configure_boot_timer_();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// A runtime action supersedes pending boot programming of the same feature
|
||||
void RX8111Component::cancel_boot_retry_(bool alarm) {
|
||||
this->cancel_timeout(alarm ? RX8111_BOOT_ALARM_RETRY_ID : RX8111_BOOT_TIMER_RETRY_ID);
|
||||
}
|
||||
|
||||
void RX8111Component::check_event_flags_() {
|
||||
if (this->watching_() && !this->poll_event_flags_())
|
||||
this->set_warning_(WARN_EVENT_POLL, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
// A callback or a runtime action may have stopped the last watched feature
|
||||
if (!this->watching_() && this->event_polling_started_) {
|
||||
this->cancel_interval(RX8111_EVENT_INTERVAL_ID);
|
||||
this->event_polling_started_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void RX8111Component::start_event_polling_() {
|
||||
if (this->event_polling_started_)
|
||||
return;
|
||||
this->set_interval(RX8111_EVENT_INTERVAL_ID, this->event_poll_interval_, [this]() { this->check_event_flags_(); });
|
||||
this->event_polling_started_ = true;
|
||||
}
|
||||
|
||||
void RX8111Component::update() {
|
||||
this->read_time();
|
||||
this->check_event_flags_();
|
||||
}
|
||||
|
||||
void RX8111Component::set_warning_(uint8_t bit, const LogString *message) {
|
||||
this->warnings_ |= bit;
|
||||
this->status_set_warning(message);
|
||||
}
|
||||
|
||||
void RX8111Component::clear_warning_(uint8_t bit) {
|
||||
if ((this->warnings_ & bit) == 0)
|
||||
return;
|
||||
this->warnings_ &= ~bit;
|
||||
if (this->warnings_ == 0)
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void RX8111Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "RX8111:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Alarm interrupt: %s\n"
|
||||
" Wake-up timer: %s",
|
||||
ONOFF(this->alarm_enabled_), ONOFF(this->timer_enabled_));
|
||||
if (this->timer_enabled_) {
|
||||
ESP_LOGCONFIG(TAG, " Count: %" PRIu32 ", clock: %s", this->timer_count_,
|
||||
LOG_STR_ARG(TimerClockNames::get_log_str(this->timer_clock_, TimerClockNames::LAST_INDEX)));
|
||||
}
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
void RX8111Component::read_time() {
|
||||
ESPTime rtc_time;
|
||||
if (!this->read_calendar_(rtc_time))
|
||||
return;
|
||||
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
bool RX8111Component::read_calendar_(ESPTime &rtc_time) {
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags)) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
if (flags & (RX8111_FLAG_VLF | RX8111_FLAG_XST | RX8111_FLAG_POR)) {
|
||||
ESP_LOGW(TAG, "RTC time invalidated by power loss or oscillator stop; write trusted time");
|
||||
this->status_set_warning(LOG_STR("RTC time invalidated; write trusted time"));
|
||||
return;
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR("RTC time invalidated; write trusted time"));
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t buf[7];
|
||||
// I2C access freezes the RX8111 counter, yielding a coherent calendar snapshot.
|
||||
if (!this->read_bytes(RX8111_REG_SEC, buf, sizeof(buf))) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
// decode_bcd_calendar() masks the same bits; the check here catches a corrupted register
|
||||
static constexpr uint8_t CALENDAR_MASKS[7] = {0x7F, 0x7F, 0x3F, 0x00, 0x3F, 0x1F, 0xFF};
|
||||
bool valid = true;
|
||||
for (size_t i = 0; i < sizeof(buf); i++) {
|
||||
if (i == 3)
|
||||
continue; // Weekday uses one-hot encoding, not BCD.
|
||||
buf[i] &= CALENDAR_MASKS[i];
|
||||
if ((buf[i] & 0x0F) > 9 || (buf[i] >> 4) > 9) {
|
||||
ESP_LOGW(TAG, "Invalid BCD in RTC time");
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
}
|
||||
valid = valid && (buf[i] & 0x0F) <= 9 && (buf[i] >> 4) <= 9;
|
||||
}
|
||||
const uint8_t onehot = buf[3];
|
||||
if (onehot == 0 || onehot > 0x40 || (onehot & (onehot - 1)) != 0) {
|
||||
ESP_LOGW(TAG, "Invalid RTC weekday");
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
valid = valid && onehot != 0 && onehot <= 0x40 && (onehot & (onehot - 1)) == 0;
|
||||
if (valid) {
|
||||
rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(buf);
|
||||
valid = rtc_time.is_valid(true, false) && rtc_time.second < 60 &&
|
||||
ESPTime::from_epoch_utc(rtc_time.timestamp).day_of_week == rtc_time.day_of_week;
|
||||
}
|
||||
ESPTime rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(buf);
|
||||
if (!rtc_time.is_valid(true, false) || rtc_time.second >= 60) {
|
||||
if (!valid) {
|
||||
ESP_LOGW(TAG, "Invalid RTC time, not syncing to system clock");
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR("Invalid RTC time"));
|
||||
return false;
|
||||
}
|
||||
if (!this->status_comm_result(ESPTime::from_epoch_utc(rtc_time.timestamp).day_of_week == rtc_time.day_of_week,
|
||||
LOG_STR("Invalid RTC time"))) {
|
||||
ESP_LOGW(TAG, "RTC weekday does not match calendar date");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
this->clear_warning_(WARN_CALENDAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RX8111Component::write_time() {
|
||||
@@ -83,7 +230,7 @@ void RX8111Component::write_time() {
|
||||
return;
|
||||
}
|
||||
if (!this->set_stop_bit_(true)) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,25 +240,233 @@ void RX8111Component::write_time() {
|
||||
// Always try to restart the counter, even after a failed calendar write.
|
||||
const bool restarted = this->set_stop_bit_(false);
|
||||
if (!written || !restarted) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->status_comm_result(this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS)))
|
||||
if (!this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS)) {
|
||||
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->clear_warning_(WARN_WRITE_TIME);
|
||||
this->clear_warning_(WARN_CALENDAR);
|
||||
ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
|
||||
now.minute, now.second);
|
||||
}
|
||||
|
||||
bool RX8111Component::set_stop_bit_(bool stop) {
|
||||
uint8_t control;
|
||||
if (!this->read_byte(RX8111_REG_CONTROL, &control))
|
||||
return this->update_register(RX8111_REG_CONTROL, RX8111_CTRL_STOP, stop ? RX8111_CTRL_STOP : 0);
|
||||
}
|
||||
|
||||
bool RX8111Component::set_alarm_interrupt_(bool enabled) {
|
||||
return this->update_register(RX8111_REG_CONTROL, RX8111_CONTROL_AIE, enabled ? RX8111_CONTROL_AIE : 0);
|
||||
}
|
||||
|
||||
bool RX8111Component::program_alarm_(uint8_t second, uint8_t minute, uint8_t hour, uint8_t weekday, uint8_t day,
|
||||
bool boot) {
|
||||
uint8_t extension;
|
||||
if (!this->read_byte(RX8111_REG_EXTENSION, &extension))
|
||||
return false;
|
||||
if (stop) {
|
||||
control |= RX8111_CTRL_STOP;
|
||||
} else {
|
||||
control &= ~RX8111_CTRL_STOP;
|
||||
if (weekday != ALARM_FIELD_UNSET) {
|
||||
extension &= ~RX8111_EXTENSION_WADA;
|
||||
} else if (day != ALARM_FIELD_UNSET) {
|
||||
extension |= RX8111_EXTENSION_WADA;
|
||||
}
|
||||
return this->write_byte(RX8111_REG_CONTROL, control);
|
||||
|
||||
const uint8_t fields[] = {
|
||||
second == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(second),
|
||||
minute == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(minute),
|
||||
hour == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(hour),
|
||||
weekday != ALARM_FIELD_UNSET ? static_cast<uint8_t>(1U << (weekday - 1))
|
||||
: day != ALARM_FIELD_UNSET ? uint8_to_bcd(day)
|
||||
: RX8111_ALARM_DISABLED,
|
||||
};
|
||||
// Disable the interrupt during partial programming; keep AF (past events) latched.
|
||||
if (!this->set_alarm_interrupt_(false))
|
||||
return false;
|
||||
this->alarm_armed_ = false;
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags))
|
||||
return false;
|
||||
if (!boot)
|
||||
this->alarm_flag_seen_ = (flags & RX8111_FLAG_AF) != 0;
|
||||
if (!this->write_byte(RX8111_REG_SEC_ALARM, fields[0]) || !this->write_bytes(RX8111_REG_MIN_ALARM, &fields[1], 3) ||
|
||||
!this->write_byte(RX8111_REG_EXTENSION, extension))
|
||||
return false;
|
||||
if (!this->set_alarm_interrupt_(this->alarm_enabled_))
|
||||
return false;
|
||||
this->alarm_armed_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::set_alarm(uint16_t second, uint16_t minute, uint16_t hour, uint16_t weekday, uint16_t day) {
|
||||
if (!this->alarm_enabled_) {
|
||||
ESP_LOGE(TAG, "Alarm interrupt disabled; set alarm_enabled: true");
|
||||
return false;
|
||||
}
|
||||
constexpr uint16_t unset = ALARM_FIELD_UNSET;
|
||||
if ((second != unset && second > 59) || (minute != unset && minute > 59) || (hour != unset && hour > 23) ||
|
||||
(weekday != unset && (weekday < 1 || weekday > 7)) || (day != unset && (day < 1 || day > 31)) ||
|
||||
(weekday != unset && day != unset) ||
|
||||
(second == unset && minute == unset && hour == unset && weekday == unset && day == unset)) {
|
||||
ESP_LOGE(TAG, "Invalid alarm fields");
|
||||
return false;
|
||||
}
|
||||
this->cancel_boot_retry_(true);
|
||||
if (!this->program_alarm_(second, minute, hour, weekday, day)) {
|
||||
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->clear_warning_(WARN_ALARM);
|
||||
this->boot_alarm_event_seen_ = true;
|
||||
this->start_event_polling_();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::schedule_alarm_in(uint32_t seconds) {
|
||||
if (!this->alarm_enabled_ || seconds == 0 || seconds >= 28U * 86400U) {
|
||||
ESP_LOGE(TAG, "Relative alarm requires alarm_enabled and a duration of 1 second to less than 28 days");
|
||||
return false;
|
||||
}
|
||||
ESPTime now;
|
||||
if (!this->read_calendar_(now)) {
|
||||
ESP_LOGE(TAG, "Cannot read valid RTC time for relative alarm");
|
||||
return false;
|
||||
}
|
||||
auto target = ESPTime::from_epoch_utc(now.timestamp + seconds);
|
||||
if (!target.is_valid() || target.year > 2099) {
|
||||
ESP_LOGE(TAG, "Relative alarm target is outside RTC year range");
|
||||
return false;
|
||||
}
|
||||
return this->set_alarm(target.second, target.minute, target.hour, ALARM_FIELD_UNSET, target.day_of_month);
|
||||
}
|
||||
|
||||
bool RX8111Component::unset_alarm() {
|
||||
this->cancel_boot_retry_(true);
|
||||
if (!this->set_alarm_interrupt_(false)) {
|
||||
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->alarm_armed_ = false;
|
||||
// Minute, hour and day alarm registers are contiguous
|
||||
static constexpr uint8_t DISABLED_FIELDS[3] = {RX8111_ALARM_DISABLED, RX8111_ALARM_DISABLED, RX8111_ALARM_DISABLED};
|
||||
if (!this->write_byte(RX8111_REG_SEC_ALARM, RX8111_ALARM_DISABLED) ||
|
||||
!this->write_bytes(RX8111_REG_MIN_ALARM, DISABLED_FIELDS, sizeof(DISABLED_FIELDS))) {
|
||||
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->clear_warning_(WARN_ALARM);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::clear_alarm_flag() {
|
||||
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_AF))) {
|
||||
this->set_warning_(WARN_ALARM_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->alarm_flag_seen_ = false;
|
||||
this->clear_warning_(WARN_ALARM_FLAG_CLEAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clears TE and TIE; returns the register values read so callers can build on them
|
||||
bool RX8111Component::disable_timer_(uint8_t &extension, uint8_t &control) {
|
||||
return this->read_byte(RX8111_REG_EXTENSION, &extension) &&
|
||||
this->write_byte(RX8111_REG_EXTENSION, extension & ~RX8111_EXTENSION_TE) &&
|
||||
this->read_byte(RX8111_REG_CONTROL, &control) &&
|
||||
this->write_byte(RX8111_REG_CONTROL, control & ~RX8111_CONTROL_TIE);
|
||||
}
|
||||
|
||||
bool RX8111Component::configure_timer_(bool clear_stale_flag) {
|
||||
// Stop counting before updating the 24-bit reload value; preserve the boot flag unless explicitly starting.
|
||||
uint8_t extension;
|
||||
uint8_t control;
|
||||
if (!this->disable_timer_(extension, control))
|
||||
return false;
|
||||
|
||||
const uint8_t count[] = {static_cast<uint8_t>(this->timer_count_), static_cast<uint8_t>(this->timer_count_ >> 8),
|
||||
static_cast<uint8_t>(this->timer_count_ >> 16)};
|
||||
if (!this->write_bytes(RX8111_REG_TIMER0, count, sizeof(count)) ||
|
||||
!this->write_byte(RX8111_REG_EXTENSION,
|
||||
(extension & ~(RX8111_EXTENSION_TE | RX8111_EXTENSION_TSEL)) | this->timer_clock_) ||
|
||||
(clear_stale_flag && !this->clear_timer_flag()) ||
|
||||
!this->write_byte(RX8111_REG_CONTROL, control | RX8111_CONTROL_TIE))
|
||||
return false;
|
||||
return this->write_byte(RX8111_REG_EXTENSION,
|
||||
(extension & ~RX8111_EXTENSION_TSEL) | this->timer_clock_ | RX8111_EXTENSION_TE);
|
||||
}
|
||||
|
||||
bool RX8111Component::start_timer() {
|
||||
if (this->timer_count_ == 0 || this->timer_count_ > 0xFFFFFF) {
|
||||
ESP_LOGW(TAG, "Cannot start timer without a valid timer_count");
|
||||
return false;
|
||||
}
|
||||
this->cancel_boot_retry_(false);
|
||||
if (!this->configure_timer_(true)) {
|
||||
// A partial configuration can leave either enable bit set. Disable both if the bus permits it;
|
||||
// otherwise keep watching a timer that may still be running.
|
||||
uint8_t extension, control;
|
||||
if (this->disable_timer_(extension, control))
|
||||
this->timer_enabled_ = false;
|
||||
this->set_warning_(WARN_TIMER, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->clear_warning_(WARN_TIMER);
|
||||
this->timer_enabled_ = true;
|
||||
this->timer_retained_event_pending_ = false;
|
||||
this->start_event_polling_();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::clear_timer_flag() {
|
||||
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_TF))) {
|
||||
this->set_warning_(WARN_TIMER_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->timer_flag_seen_ = false;
|
||||
this->timer_retained_event_pending_ = false;
|
||||
this->clear_warning_(WARN_TIMER_FLAG_CLEAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::stop_timer() {
|
||||
this->cancel_boot_retry_(false);
|
||||
uint8_t extension, control;
|
||||
if (!this->disable_timer_(extension, control)) {
|
||||
this->set_warning_(WARN_TIMER, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->timer_enabled_ = false;
|
||||
this->clear_warning_(WARN_TIMER);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::poll_event_flags_() {
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags))
|
||||
return false;
|
||||
this->clear_warning_(WARN_EVENT_POLL);
|
||||
const bool alarm_active = (flags & RX8111_FLAG_AF) != 0;
|
||||
const bool timer_active = (flags & RX8111_FLAG_TF) != 0;
|
||||
// alarm_armed_ is only ever set while the alarm is enabled
|
||||
const bool timer_watched = this->timer_enabled_ || this->timer_retained_event_pending_;
|
||||
if (alarm_active && this->alarm_armed_)
|
||||
this->boot_alarm_event_seen_ = true;
|
||||
const bool alarm_rising = alarm_active && !this->alarm_flag_seen_ && this->alarm_armed_;
|
||||
const bool timer_rising = timer_active && !this->timer_flag_seen_ && timer_watched;
|
||||
const bool deliver_stopped_event = timer_rising && this->timer_retained_event_pending_;
|
||||
// Record eligible edges before callbacks, which may clear flags synchronously.
|
||||
// Leave flags unseen while boot configuration is still waiting to enable delivery.
|
||||
if (this->alarm_armed_)
|
||||
this->alarm_flag_seen_ = alarm_active;
|
||||
if (timer_watched)
|
||||
this->timer_flag_seen_ = timer_active;
|
||||
this->timer_retained_event_pending_ = false;
|
||||
if (alarm_rising)
|
||||
this->alarm_callbacks_.call();
|
||||
// An alarm callback may stop the timer before its callback is delivered.
|
||||
if (timer_rising && this->timer_flag_seen_ && (this->timer_enabled_ || deliver_stopped_event))
|
||||
this->timer_callbacks_.call();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::rx8111
|
||||
@@ -1,10 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome::rx8111 {
|
||||
|
||||
namespace testing {
|
||||
class RX8111Test;
|
||||
} // namespace testing
|
||||
|
||||
class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -12,9 +17,91 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
void dump_config() override;
|
||||
void read_time();
|
||||
void write_time();
|
||||
void set_alarm_enabled(bool enabled) { this->alarm_enabled_ = enabled; }
|
||||
void set_alarm_explicitly_configured(bool configured) { this->alarm_explicitly_configured_ = configured; }
|
||||
void set_alarm_second(uint8_t value) { this->alarm_second_ = value; }
|
||||
void set_alarm_minute(uint8_t value) { this->alarm_minute_ = value; }
|
||||
void set_alarm_hour(uint8_t value) { this->alarm_hour_ = value; }
|
||||
void set_alarm_weekday(uint8_t value) { this->alarm_weekday_ = value; }
|
||||
void set_alarm_day(uint8_t value) { this->alarm_day_ = value; }
|
||||
void set_timer_count(uint32_t count) { this->timer_count_ = count; }
|
||||
void set_timer_clock(uint8_t clock) { this->timer_clock_ = clock; }
|
||||
void set_timer_enabled(bool enabled) { this->timer_enabled_ = enabled; }
|
||||
void set_timer_configured(bool configured) { this->timer_configured_ = configured; }
|
||||
void set_event_poll_interval(uint32_t interval) { this->event_poll_interval_ = interval; }
|
||||
template<typename F> void add_on_alarm_callback(F &&callback) {
|
||||
this->alarm_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
bool set_alarm(uint16_t second, uint16_t minute, uint16_t hour, uint16_t weekday, uint16_t day);
|
||||
bool schedule_alarm_in(uint32_t seconds);
|
||||
bool unset_alarm();
|
||||
bool clear_alarm_flag();
|
||||
bool clear_timer_flag();
|
||||
bool start_timer();
|
||||
bool stop_timer();
|
||||
template<typename F> void add_on_timer_callback(F &&callback) {
|
||||
this->timer_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
friend class testing::RX8111Test;
|
||||
|
||||
static constexpr uint32_t BOOT_RETRY_DELAY_MS = 1000;
|
||||
// set_alarm() and the boot alarm ignore a field set to this value
|
||||
static constexpr uint8_t ALARM_FIELD_UNSET = 0xFF;
|
||||
|
||||
// One bit per fault source, so a recovery in one path cannot hide another path's fault
|
||||
enum Warning : uint8_t {
|
||||
WARN_CALENDAR = 1 << 0,
|
||||
WARN_EVENT_POLL = 1 << 1,
|
||||
WARN_ALARM = 1 << 2,
|
||||
WARN_TIMER = 1 << 3,
|
||||
WARN_WRITE_TIME = 1 << 4,
|
||||
WARN_ALARM_FLAG_CLEAR = 1 << 5,
|
||||
WARN_TIMER_FLAG_CLEAR = 1 << 6,
|
||||
};
|
||||
|
||||
bool set_stop_bit_(bool stop);
|
||||
bool read_calendar_(ESPTime &rtc_time);
|
||||
void set_warning_(uint8_t bit, const LogString *message);
|
||||
void clear_warning_(uint8_t bit);
|
||||
bool watching_() const { return this->alarm_armed_ || this->timer_enabled_ || this->timer_retained_event_pending_; }
|
||||
void check_event_flags_();
|
||||
bool program_alarm_(uint8_t second, uint8_t minute, uint8_t hour, uint8_t weekday, uint8_t day, bool boot = false);
|
||||
bool set_alarm_interrupt_(bool enabled);
|
||||
bool disable_timer_(uint8_t &extension, uint8_t &control);
|
||||
bool configure_timer_(bool clear_stale_flag = false);
|
||||
void start_event_polling_();
|
||||
bool poll_event_flags_();
|
||||
void configure_boot_alarm_();
|
||||
void configure_boot_timer_();
|
||||
void boot_config_result_(bool alarm, bool success);
|
||||
void cancel_boot_retry_(bool alarm);
|
||||
|
||||
LazyCallbackManager<void()> alarm_callbacks_;
|
||||
LazyCallbackManager<void()> timer_callbacks_;
|
||||
uint32_t timer_count_{0};
|
||||
uint32_t event_poll_interval_{1000};
|
||||
uint32_t boot_retry_delay_ms_{BOOT_RETRY_DELAY_MS};
|
||||
uint8_t warnings_{0};
|
||||
uint8_t timer_clock_{2}; // 1 Hz
|
||||
uint8_t boot_alarm_attempts_{0};
|
||||
uint8_t boot_timer_attempts_{0};
|
||||
uint8_t alarm_second_{ALARM_FIELD_UNSET};
|
||||
uint8_t alarm_minute_{ALARM_FIELD_UNSET};
|
||||
uint8_t alarm_hour_{ALARM_FIELD_UNSET};
|
||||
uint8_t alarm_weekday_{ALARM_FIELD_UNSET};
|
||||
uint8_t alarm_day_{ALARM_FIELD_UNSET};
|
||||
bool timer_enabled_{false};
|
||||
bool timer_configured_{false};
|
||||
bool timer_flag_seen_{false};
|
||||
bool timer_retained_event_pending_{false};
|
||||
bool event_polling_started_{false};
|
||||
bool boot_alarm_event_seen_{false};
|
||||
bool alarm_enabled_{false};
|
||||
bool alarm_explicitly_configured_{false};
|
||||
bool alarm_flag_seen_{false};
|
||||
bool alarm_armed_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::rx8111
|
||||
@@ -4,6 +4,14 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DAY,
|
||||
CONF_DURATION,
|
||||
CONF_HOUR,
|
||||
CONF_ID,
|
||||
CONF_MINUTE,
|
||||
CONF_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@techdregs"]
|
||||
@@ -11,23 +19,186 @@ DEPENDENCIES = ["i2c"]
|
||||
|
||||
rx8111_ns = cg.esphome_ns.namespace("rx8111")
|
||||
RX8111Component = rx8111_ns.class_("RX8111Component", time.RealTimeClock, i2c.I2CDevice)
|
||||
TIMER_CLOCKS = {"4096hz": 0, "64hz": 1, "1hz": 2, "1_60hz": 3}
|
||||
# set_alarm() ignores a field set to this value
|
||||
ALARM_FIELD_UNSET = 0xFF
|
||||
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(RX8111Component)}
|
||||
).extend(i2c.i2c_device_schema(0x32))
|
||||
CONF_ALARM_SECOND = "alarm_second"
|
||||
CONF_ALARM_MINUTE = "alarm_minute"
|
||||
CONF_ALARM_HOUR = "alarm_hour"
|
||||
CONF_ALARM_WEEKDAY = "alarm_weekday"
|
||||
CONF_ALARM_DAY = "alarm_day"
|
||||
CONF_ALARM_ENABLED = "alarm_enabled"
|
||||
CONF_ON_ALARM = "on_alarm"
|
||||
CONF_TIMER_COUNT = "timer_count"
|
||||
CONF_TIMER_CLOCK = "timer_clock"
|
||||
CONF_TIMER_ENABLED = "timer_enabled"
|
||||
CONF_ON_TIMER = "on_timer"
|
||||
CONF_WEEKDAY = "weekday"
|
||||
CONF_EVENT_POLL_INTERVAL = "event_poll_interval"
|
||||
|
||||
_ALARM_FIELDS = (
|
||||
CONF_ALARM_SECOND,
|
||||
CONF_ALARM_MINUTE,
|
||||
CONF_ALARM_HOUR,
|
||||
CONF_ALARM_WEEKDAY,
|
||||
CONF_ALARM_DAY,
|
||||
)
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_ALARM, "add_on_alarm_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_TIMER, "add_on_timer_callback"),
|
||||
)
|
||||
|
||||
|
||||
def _validate_alarm_config(config: ConfigType) -> ConfigType:
|
||||
"""Rules that depend on the derived alarm_enabled and timer_enabled defaults."""
|
||||
if CONF_ON_ALARM in config and not config.get(
|
||||
CONF_ALARM_ENABLED, any(f in config for f in _ALARM_FIELDS)
|
||||
):
|
||||
raise cv.Invalid(
|
||||
"on_alarm requires alarm_enabled: true or alarm matching fields"
|
||||
)
|
||||
if CONF_TIMER_COUNT not in config and (
|
||||
config.get(CONF_TIMER_ENABLED) or CONF_ON_TIMER in config
|
||||
):
|
||||
raise cv.Invalid("timer_enabled or on_timer requires timer_count")
|
||||
return config
|
||||
|
||||
|
||||
_validate_alarm_options = cv.All(
|
||||
cv.has_at_most_one_key(CONF_ALARM_DAY, CONF_ALARM_WEEKDAY), _validate_alarm_config
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(RX8111Component),
|
||||
cv.Optional(CONF_ALARM_SECOND): cv.int_range(min=0, max=59),
|
||||
cv.Optional(CONF_ALARM_MINUTE): cv.int_range(min=0, max=59),
|
||||
cv.Optional(CONF_ALARM_HOUR): cv.int_range(min=0, max=23),
|
||||
cv.Optional(CONF_ALARM_WEEKDAY): cv.int_range(min=1, max=7),
|
||||
cv.Optional(CONF_ALARM_DAY): cv.int_range(min=1, max=31),
|
||||
cv.Optional(CONF_ALARM_ENABLED): cv.boolean,
|
||||
cv.Optional(CONF_ON_ALARM): automation.validate_automation({}),
|
||||
cv.Optional(CONF_TIMER_COUNT): cv.int_range(min=1, max=0xFFFFFF),
|
||||
cv.Optional(CONF_TIMER_CLOCK, default="1hz"): cv.enum(
|
||||
TIMER_CLOCKS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_TIMER_ENABLED): cv.boolean,
|
||||
cv.Optional(CONF_ON_TIMER): automation.validate_automation({}),
|
||||
# Below 500 ms a scheduler interval costs more than it saves
|
||||
cv.Optional(CONF_EVENT_POLL_INTERVAL, default="1s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(milliseconds=500)),
|
||||
),
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x32)),
|
||||
_validate_alarm_options,
|
||||
)
|
||||
|
||||
RX8111_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(RX8111Component)}
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"rx8111.write_time", RX8111_ACTION_SCHEMA, automation.ApplyCall("write_time()")
|
||||
# Actions that call one argument-less method on the component
|
||||
for _method in (
|
||||
"write_time",
|
||||
"read_time",
|
||||
"clear_alarm_flag",
|
||||
"clear_timer_flag",
|
||||
"unset_alarm",
|
||||
"stop_timer",
|
||||
"start_timer",
|
||||
):
|
||||
automation.register_apply_action(
|
||||
f"rx8111.{_method}", RX8111_ACTION_SCHEMA, automation.ApplyCall(f"{_method}()")
|
||||
)
|
||||
|
||||
_SET_ALARM_FIELDS = (CONF_SECOND, CONF_MINUTE, CONF_HOUR, CONF_WEEKDAY, CONF_DAY)
|
||||
|
||||
|
||||
def _fill_unset_alarm_fields(config: ConfigType) -> ConfigType:
|
||||
"""Marks the fields not given as unset, so set_alarm() always gets all five."""
|
||||
for field in _SET_ALARM_FIELDS:
|
||||
config.setdefault(field, ALARM_FIELD_UNSET)
|
||||
return config
|
||||
|
||||
|
||||
_validate_set_alarm = cv.All(
|
||||
cv.has_at_least_one_key(*_SET_ALARM_FIELDS),
|
||||
cv.has_at_most_one_key(CONF_WEEKDAY, CONF_DAY),
|
||||
_fill_unset_alarm_fields,
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"rx8111.read_time", RX8111_ACTION_SCHEMA, automation.ApplyCall("read_time()")
|
||||
"rx8111.set_alarm",
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(RX8111Component),
|
||||
cv.Optional(CONF_SECOND): cv.templatable(cv.int_range(min=0, max=59)),
|
||||
cv.Optional(CONF_MINUTE): cv.templatable(cv.int_range(min=0, max=59)),
|
||||
cv.Optional(CONF_HOUR): cv.templatable(cv.int_range(min=0, max=23)),
|
||||
cv.Optional(CONF_WEEKDAY): cv.templatable(cv.int_range(min=1, max=7)),
|
||||
cv.Optional(CONF_DAY): cv.templatable(cv.int_range(min=1, max=31)),
|
||||
}
|
||||
),
|
||||
_validate_set_alarm,
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"set_alarm({}, {}, {}, {}, {})",
|
||||
(
|
||||
(CONF_SECOND, cg.uint16),
|
||||
(CONF_MINUTE, cg.uint16),
|
||||
(CONF_HOUR, cg.uint16),
|
||||
(CONF_WEEKDAY, cg.uint16),
|
||||
(CONF_DAY, cg.uint16),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# The alarm registers hold a day of month, so the target must stay inside the current month cycle
|
||||
_ALARM_DURATION = cv.All(
|
||||
cv.positive_time_period_seconds,
|
||||
cv.Range(
|
||||
min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(days=28), max_included=False
|
||||
),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"rx8111.schedule_alarm_in",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(RX8111Component),
|
||||
cv.Required(CONF_DURATION): cv.templatable(_ALARM_DURATION),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_DURATION, "schedule_alarm_in", cg.uint32),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
await time.register_time(var, config)
|
||||
cg.add(var.set_event_poll_interval(config[CONF_EVENT_POLL_INTERVAL]))
|
||||
cg.add_optional(var.set_alarm_second, config.get(CONF_ALARM_SECOND))
|
||||
cg.add_optional(var.set_alarm_minute, config.get(CONF_ALARM_MINUTE))
|
||||
cg.add_optional(var.set_alarm_hour, config.get(CONF_ALARM_HOUR))
|
||||
cg.add_optional(var.set_alarm_weekday, config.get(CONF_ALARM_WEEKDAY))
|
||||
cg.add_optional(var.set_alarm_day, config.get(CONF_ALARM_DAY))
|
||||
cg.add(
|
||||
var.set_alarm_enabled(
|
||||
config.get(CONF_ALARM_ENABLED, any(f in config for f in _ALARM_FIELDS))
|
||||
)
|
||||
)
|
||||
cg.add(var.set_alarm_explicitly_configured(CONF_ALARM_ENABLED in config))
|
||||
if (count := config.get(CONF_TIMER_COUNT)) is not None:
|
||||
cg.add(var.set_timer_count(count))
|
||||
cg.add(var.set_timer_clock(config[CONF_TIMER_CLOCK]))
|
||||
# Validation guarantees on_timer comes with a timer_count
|
||||
cg.add(var.set_timer_enabled(config.get(CONF_TIMER_ENABLED, count is not None)))
|
||||
cg.add(var.set_timer_configured(CONF_TIMER_ENABLED in config or count is not None))
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
@@ -0,0 +1,48 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ADDRESS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@KoenBreeman"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "seeed_multi_channel_relay"
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_SEEED_MULTI_CHANNEL_RELAY_ID = "seeed_multi_channel_relay_id"
|
||||
|
||||
seeed_multi_channel_relay_ns = cg.esphome_ns.namespace("seeed_multi_channel_relay")
|
||||
SeeedMultiChannelRelay = seeed_multi_channel_relay_ns.class_(
|
||||
"SeeedMultiChannelRelay", cg.Component, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SeeedMultiChannelRelay),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x11))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await i2c.new_i2c_component(config)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"seeed_multi_channel_relay.change_address",
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SeeedMultiChannelRelay),
|
||||
cv.Required(CONF_ADDRESS): cv.templatable(
|
||||
cv.hex_int_range(min=0x08, max=0x77)
|
||||
),
|
||||
},
|
||||
key=CONF_ADDRESS,
|
||||
),
|
||||
automation.ApplyField(CONF_ADDRESS, "change_address", cg.uint8),
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "seeed_multi_channel_relay.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::seeed_multi_channel_relay {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "seeed_multi_channel_relay");
|
||||
|
||||
static constexpr uint8_t CMD_CHANNEL_CTRL = 0x10;
|
||||
static constexpr uint8_t CMD_SAVE_I2C_ADDR = 0x11;
|
||||
static constexpr uint8_t CMD_READ_FIRMWARE_VER = 0x13;
|
||||
|
||||
void SeeedMultiChannelRelay::setup() {
|
||||
if (!this->read_byte(CMD_READ_FIRMWARE_VER, &this->firmware_version_)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
// channels_ owns all 8 relays and the board has no read-back: after an ESP-only reboot the board keeps its
|
||||
// relays, so every channel is set from the switches' restore modes in one write, and a channel already on
|
||||
// never drops out while the others are written; a power cycle starts with all off. A switch with restore_mode
|
||||
// DISABLED is driven off too, so its state matches what the next write does
|
||||
uint8_t value = 0;
|
||||
for (uint8_t channel = 0; channel != this->switches_.size(); channel++) {
|
||||
switch_::Switch *sw = this->switches_[channel];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
const bool on = sw->get_initial_state_with_restore_mode().value_or(sw->is_inverted()) != sw->is_inverted();
|
||||
assign_bits(value, 1 << channel, on);
|
||||
}
|
||||
if (!this->status_comm_result(this->write_byte(CMD_CHANNEL_CTRL, value)))
|
||||
return;
|
||||
this->channels_ = value;
|
||||
for (uint8_t channel = 0; channel != this->switches_.size(); channel++) {
|
||||
if (this->switches_[channel] != nullptr)
|
||||
this->switches_[channel]->publish_state((value >> channel) & 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SeeedMultiChannelRelay::change_address(uint8_t address) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
// A lambda bypasses the schema range; a reserved address would strand the board
|
||||
if (address < 0x08 || address > 0x77) {
|
||||
ESP_LOGW(TAG, "I2C address 0x%02X is reserved; not stored", address);
|
||||
return;
|
||||
}
|
||||
if (!this->status_comm_result(this->write_byte(CMD_SAVE_I2C_ADDR, address)))
|
||||
return;
|
||||
this->set_i2c_address(address);
|
||||
ESP_LOGI(TAG, "Board now answers at 0x%02X; update address: in the configuration", address);
|
||||
}
|
||||
|
||||
void SeeedMultiChannelRelay::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Seeed Multi Channel Relay:\n"
|
||||
" Firmware version: %u",
|
||||
this->firmware_version_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
bool SeeedMultiChannelRelay::set_relay(uint8_t channel, bool on) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
uint8_t value = this->channels_;
|
||||
assign_bits(value, 1 << channel, on);
|
||||
if (!this->status_comm_result(this->write_byte(CMD_CHANNEL_CTRL, value)))
|
||||
return false;
|
||||
this->channels_ = value;
|
||||
#ifdef USE_SWITCH
|
||||
if (this->switches_[channel] != nullptr)
|
||||
this->switches_[channel]->publish_state(on);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::seeed_multi_channel_relay
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::seeed_multi_channel_relay {
|
||||
|
||||
class SeeedMultiChannelRelay : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Store a new I2C address in the board's EEPROM and talk to it there from now on.
|
||||
void change_address(uint8_t address);
|
||||
|
||||
/// Switch relay `channel` (0-7) on or off; returns false when the write failed.
|
||||
bool set_relay(uint8_t channel, bool on);
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
void register_switch(uint8_t channel, switch_::Switch *sw) { this->switches_[channel] = sw; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
uint8_t channels_{0};
|
||||
uint8_t firmware_version_{0};
|
||||
#ifdef USE_SWITCH
|
||||
std::array<switch_::Switch *, 8> switches_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::seeed_multi_channel_relay
|
||||
@@ -0,0 +1,52 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_PLATFORM
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SEEED_MULTI_CHANNEL_RELAY_ID, DOMAIN, SeeedMultiChannelRelay
|
||||
|
||||
DEPENDENCIES = ["seeed_multi_channel_relay"]
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
switch.ApplySwitch, default_restore_mode="RESTORE_DEFAULT_OFF"
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_SEEED_MULTI_CHANNEL_RELAY_ID): cv.use_id(
|
||||
SeeedMultiChannelRelay
|
||||
),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=8),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_unique_channel(config: ConfigType) -> ConfigType:
|
||||
# The hub has one slot per channel, so a second switch on it would take over the first
|
||||
hub, channel = config[CONF_SEEED_MULTI_CHANNEL_RELAY_ID], config[CONF_CHANNEL]
|
||||
users = [
|
||||
other
|
||||
for other in fv.full_config.get().get(switch.DOMAIN, [])
|
||||
if other.get(CONF_PLATFORM) == DOMAIN
|
||||
and other[CONF_SEEED_MULTI_CHANNEL_RELAY_ID] == hub
|
||||
and other[CONF_CHANNEL] == channel
|
||||
]
|
||||
if len(users) > 1:
|
||||
raise cv.Invalid(
|
||||
f"Channel {channel} of {hub} is used by more than one switch",
|
||||
path=[CONF_CHANNEL],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_unique_channel
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_SEEED_MULTI_CHANNEL_RELAY_ID])
|
||||
channel = config[CONF_CHANNEL] - 1
|
||||
await switch.sub_switches(config, parent=hub).create(
|
||||
config,
|
||||
lambda sw: hub.register_switch(channel, sw),
|
||||
apply=f"set_relay({channel}, state)",
|
||||
)
|
||||
@@ -13,7 +13,7 @@ ESPHOME_LOG_TAG(TAG, "sendspin.switch");
|
||||
|
||||
void SendspinEnabledSwitch::setup() {
|
||||
// The hub waits for this request, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
this->restore(true);
|
||||
}
|
||||
|
||||
void SendspinEnabledSwitch::dump_config() { LOG_SWITCH("", "Sendspin Enabled Switch", this); }
|
||||
@@ -28,7 +28,7 @@ void SendspinEnabledSwitch::write_state(bool state) {
|
||||
|
||||
void SendspinUnpairedAccessSwitch::setup() {
|
||||
// The hub holds the client's first start for this, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
this->restore(true);
|
||||
}
|
||||
|
||||
void SendspinUnpairedAccessSwitch::dump_config() { LOG_SWITCH("", "Sendspin Unpaired Access Switch", this); }
|
||||
|
||||
@@ -220,10 +220,9 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
|
||||
}
|
||||
if (flow_control) {
|
||||
ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
}
|
||||
// The API carries only RTS/CTS, as CRTSCTS does
|
||||
const uart::UARTFlowControl uart_flow_control =
|
||||
flow_control ? uart::UART_FLOW_CONTROL_RTS_CTS : uart::UART_FLOW_CONTROL_NONE;
|
||||
|
||||
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
|
||||
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
|
||||
@@ -235,7 +234,8 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
|
||||
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
|
||||
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity] &&
|
||||
uart_comp->get_flow_control() == uart_flow_control) {
|
||||
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
@@ -246,6 +246,7 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
uart_comp->set_data_bits(data_size);
|
||||
|
||||
uart_comp->set_parity(PARITY_MAP[parity]);
|
||||
uart_comp->set_flow_control(uart_flow_control);
|
||||
|
||||
// load_settings() is available on ESP8266 and ESP32 platforms
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
|
||||
@@ -17,10 +17,11 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
idf_version,
|
||||
only_on_variant,
|
||||
require_idf_version,
|
||||
)
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
from esphome.components.zephyr import zephyr_add_overlay, zephyr_add_prj_conf
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -39,6 +40,7 @@ from esphome.const import (
|
||||
KEY_VARIANT,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2,
|
||||
PlatformFramework,
|
||||
)
|
||||
@@ -59,9 +61,17 @@ SPIMode = spi_ns.enum("SPIMode")
|
||||
PLATFORM_SPI_CLOCKS = {
|
||||
PLATFORM_ESP8266: 40e6,
|
||||
PLATFORM_ESP32: 80e6,
|
||||
# nRF52 exposes spi2 (SPIM2), whose documented max data rate is 8 MHz;
|
||||
# the 16/32 MHz "fast" rates are only available on the separate SPIM3
|
||||
# peripheral, which isn't used here.
|
||||
PLATFORM_NRF52: 8e6,
|
||||
PLATFORM_RP2: 62.5e6,
|
||||
}
|
||||
|
||||
# SPIM2 only implements these discrete clock steps - unlike the other platforms
|
||||
# it does not derive an arbitrary rate from a divisor of its max clock.
|
||||
PLATFORM_NRF52_SPI_RATES = (125e3, 250e3, 500e3, 1e6, 2e6, 4e6, 8e6)
|
||||
|
||||
MAX_DATA_RATE_ERROR = 0.05 # Max allowable actual data rate difference from requested
|
||||
|
||||
|
||||
@@ -98,6 +108,9 @@ def _frequency_validator(value: Any) -> float:
|
||||
)
|
||||
if value < 1000:
|
||||
raise cv.Invalid("The configured SPI data rate must be at least 1000Hz")
|
||||
if platform == PLATFORM_NRF52:
|
||||
# The step check needs the device's bus, which is only known in final validation
|
||||
return value
|
||||
divisor = round(frequency / value)
|
||||
actual = frequency / divisor
|
||||
error = abs(actual - value) / value
|
||||
@@ -143,19 +156,12 @@ TYPE_CLASS = {
|
||||
}
|
||||
|
||||
|
||||
def _require_psram_dma_idf(value: Any) -> Any:
|
||||
if idf_version() < cv.Version(5, 5, 3):
|
||||
raise cv.Invalid("psram_dma requires ESP-IDF 5.5.3 or newer")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_psram_dma(value: Any) -> bool:
|
||||
value = cv.boolean(value)
|
||||
if not value:
|
||||
return value
|
||||
return cv.All(
|
||||
cv.only_on_esp32,
|
||||
_require_psram_dma_idf,
|
||||
require_idf_version(cv.Version(5, 5, 3), "psram_dma"),
|
||||
only_on_variant(
|
||||
supported=[
|
||||
VARIANT_ESP32C5,
|
||||
@@ -215,6 +221,8 @@ def get_hw_interface_list() -> list[list[str]]:
|
||||
return [["spi", "spi2"], ["spi3"]]
|
||||
if target_platform == PLATFORM_RP2:
|
||||
return [["spi"], ["spi1"]]
|
||||
if target_platform == PLATFORM_NRF52:
|
||||
return [["spi", "spi2"]]
|
||||
return []
|
||||
|
||||
|
||||
@@ -281,6 +289,14 @@ def validate_hw_pins(spi: ConfigType, index: int = -1) -> bool:
|
||||
if target_platform == PLATFORM_ESP32:
|
||||
return clk_pin_no >= 0
|
||||
|
||||
if target_platform == PLATFORM_NRF52:
|
||||
# Expander pin numbers would be mapped into NRF_PSEL and route the bus to the wrong pads
|
||||
return all(
|
||||
pins.PIN_SCHEMA_REGISTRY.get_key(spi[key]) == PLATFORM_NRF52
|
||||
for key in (CONF_CLK_PIN, CONF_MOSI_PIN, CONF_MISO_PIN)
|
||||
if key in spi
|
||||
)
|
||||
|
||||
if target_platform == PLATFORM_RP2:
|
||||
if index == -1:
|
||||
matches = list(
|
||||
@@ -359,6 +375,8 @@ def get_spi_interface(index: int) -> str:
|
||||
# Arduino code follows
|
||||
if platform == PLATFORM_RP2:
|
||||
return ["&SPI", "&SPI1"][index]
|
||||
if platform == PLATFORM_NRF52:
|
||||
return "DEVICE_DT_GET(DT_NODELABEL(spi2))"
|
||||
if index == 0:
|
||||
return "&SPI"
|
||||
return "new SPIClass(HSPI)"
|
||||
@@ -383,7 +401,7 @@ SPI_SINGLE_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2]),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_NRF52, PLATFORM_RP2]),
|
||||
)
|
||||
|
||||
|
||||
@@ -433,6 +451,45 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _add_nrf52_spi2_overlay(spi: ConfigType) -> None:
|
||||
"""Route SPIM2 to the configured pins; a board file may leave spi2 on the legacy non-DMA driver."""
|
||||
zephyr_add_prj_conf("SPI", True)
|
||||
signals = [("SCK", spi[CONF_CLK_PIN])]
|
||||
if (mosi := spi.get(CONF_MOSI_PIN)) is not None:
|
||||
signals.append(("MOSI", mosi))
|
||||
if (miso := spi.get(CONF_MISO_PIN)) is not None:
|
||||
signals.append(("MISO", miso))
|
||||
psels = ", ".join(
|
||||
f"<NRF_PSEL(SPIM_{signal}, {pin[CONF_NUMBER] // 32}, {pin[CONF_NUMBER] % 32})>"
|
||||
for signal, pin in signals
|
||||
)
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&pinctrl {{
|
||||
spi2_default: spi2_default {{
|
||||
group1 {{
|
||||
psels = {psels};
|
||||
}};
|
||||
}};
|
||||
spi2_sleep: spi2_sleep {{
|
||||
group1 {{
|
||||
psels = {psels};
|
||||
low-power-enable;
|
||||
}};
|
||||
}};
|
||||
}};
|
||||
&spi2 {{
|
||||
compatible = "nordic,nrf-spim";
|
||||
status = "okay";
|
||||
pinctrl-0 = <&spi2_default>;
|
||||
pinctrl-1 = <&spi2_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
overrun-character = <0>;
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.BUS)
|
||||
async def to_code(configs: list[ConfigType]) -> None:
|
||||
cg.add_define("USE_SPI")
|
||||
@@ -450,11 +507,15 @@ async def to_code(configs: list[ConfigType]) -> None:
|
||||
if (index := spi.get(CONF_INTERFACE_INDEX)) is not None:
|
||||
interface = get_spi_interface(index)
|
||||
cg.add(var.set_interface(cg.RawExpression(interface)))
|
||||
cg.add(
|
||||
var.set_interface_name(
|
||||
re.sub(r"\W", "", interface.replace("new SPIClass", ""))
|
||||
if CORE.using_zephyr:
|
||||
_add_nrf52_spi2_overlay(spi)
|
||||
cg.add(var.set_interface_name("spi2"))
|
||||
else:
|
||||
cg.add(
|
||||
var.set_interface_name(
|
||||
re.sub(r"\W", "", interface.replace("new SPIClass", ""))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def spi_device_schema(
|
||||
@@ -551,20 +612,45 @@ def _walk_config(value: Any, path: tuple[Any, ...] = ()):
|
||||
yield from _walk_config(child, (*path, index))
|
||||
|
||||
|
||||
def _validate_nrf52_hw_data_rate(value: float) -> None:
|
||||
lowest = PLATFORM_NRF52_SPI_RATES[0]
|
||||
if value < lowest:
|
||||
raise cv.Invalid(
|
||||
f"The configured SPI data rate ({_render_hz(value)}) is below {_render_hz(lowest)}, "
|
||||
"the lowest rate of the nRF52 hardware SPI interface"
|
||||
)
|
||||
# The SPIM driver runs at the highest step not above the request
|
||||
actual = max(rate for rate in PLATFORM_NRF52_SPI_RATES if rate <= value)
|
||||
if abs(actual - value) / value > MAX_DATA_RATE_ERROR:
|
||||
raise cv.Invalid(
|
||||
f"The configured SPI data rate ({_render_hz(value)}) is not available for this chip - closest is {_render_hz(actual)}"
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: Any) -> Any:
|
||||
buses = config if isinstance(config, list) else [config]
|
||||
software_bus_ids = {
|
||||
bus[CONF_ID] for bus in buses if CONF_INTERFACE_INDEX not in bus
|
||||
}
|
||||
if not software_bus_ids:
|
||||
nrf52_hw_bus_ids = (
|
||||
{bus[CONF_ID] for bus in buses if CONF_INTERFACE_INDEX in bus}
|
||||
if get_target_platform() == PLATFORM_NRF52
|
||||
else set()
|
||||
)
|
||||
if not software_bus_ids and not nrf52_hw_bus_ids:
|
||||
return config
|
||||
for candidate, path in _walk_config(fv.full_config.get()):
|
||||
if (
|
||||
candidate.get(CONF_PSRAM_DMA)
|
||||
and candidate.get(CONF_SPI_ID) in software_bus_ids
|
||||
):
|
||||
if (spi_id := candidate.get(CONF_SPI_ID)) is None:
|
||||
continue
|
||||
if candidate.get(CONF_PSRAM_DMA) and spi_id in software_bus_ids:
|
||||
with cv.prepend_path([cv.ROOT_CONFIG_PATH, *path, CONF_PSRAM_DMA]):
|
||||
raise cv.Invalid("psram_dma requires a hardware SPI interface")
|
||||
if (
|
||||
spi_id in nrf52_hw_bus_ids
|
||||
and (data_rate := candidate.get(CONF_DATA_RATE)) is not None
|
||||
):
|
||||
with cv.prepend_path([cv.ROOT_CONFIG_PATH, *path, CONF_DATA_RATE]):
|
||||
_validate_nrf52_hw_data_rate(data_rate)
|
||||
return config
|
||||
|
||||
|
||||
@@ -584,5 +670,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
"spi_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
@@ -2,6 +2,10 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "spi_zephyr.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::spi {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "spi");
|
||||
@@ -20,6 +24,9 @@ SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIB
|
||||
ESP_LOGE(TAG, "Device already registered");
|
||||
return this->devices_[device];
|
||||
}
|
||||
if (this->spi_bus_ == nullptr) {
|
||||
return SPIDelegate::NULL_DELEGATE; // the bus failed in setup(); the device stays not ready
|
||||
}
|
||||
SPIDelegate *delegate =
|
||||
this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin, release_device, write_only); // NOLINT
|
||||
this->devices_[device] = delegate;
|
||||
@@ -27,6 +34,9 @@ SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIB
|
||||
}
|
||||
|
||||
void SPIComponent::unregister_device(SPIClient *device) {
|
||||
if (this->spi_bus_ == nullptr) {
|
||||
return; // nothing was registered on a failed bus
|
||||
}
|
||||
if (!this->devices_.contains(device)) {
|
||||
esph_log_e(TAG, "Device not registered");
|
||||
return;
|
||||
@@ -70,6 +80,9 @@ void SPIComponent::dump_config() {
|
||||
for (size_t i = 0; i != this->data_pins_.size(); i++) {
|
||||
ESP_LOGCONFIG(TAG, " Data pin %zu: GPIO%d", i, this->data_pins_[i]);
|
||||
}
|
||||
if (this->spi_bus_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (this->spi_bus_->is_hw()) {
|
||||
ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_);
|
||||
} else {
|
||||
@@ -118,8 +131,19 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) {
|
||||
return out_data;
|
||||
}
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_ARDUINO)
|
||||
// Stub for unsupported platforms (host, Zephyr, etc.) - hardware SPI is unavailable
|
||||
#if defined(USE_ZEPHYR)
|
||||
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
if (!device_is_ready(interface)) {
|
||||
ESP_LOGE(TAG, "SPI device is not ready");
|
||||
return nullptr;
|
||||
}
|
||||
return new ZephyrSPIBus(interface); // NOLINT
|
||||
}
|
||||
|
||||
#elif !defined(USE_ESP32) && !defined(USE_ARDUINO)
|
||||
// Stub for unsupported platforms (host, etc.) - hardware SPI is unavailable
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
return nullptr;
|
||||
|
||||
@@ -24,9 +24,14 @@ using SPIInterface = SPIClassRP2040 *;
|
||||
using SPIInterface = SPIClass *;
|
||||
#endif
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
|
||||
struct device;
|
||||
using SPIInterface = const device *;
|
||||
|
||||
#elif defined(USE_HOST) || defined(CLANG_TIDY)
|
||||
|
||||
using SPIInterface = void *; // Stub for platforms without SPI (e.g., host, Zephyr)
|
||||
using SPIInterface = void *; // Stub for platforms without SPI (e.g., host)
|
||||
|
||||
#endif // USE_ESP32 / USE_ARDUINO
|
||||
|
||||
@@ -178,6 +183,7 @@ class SPIDelegateDummy;
|
||||
// a thin wrapper over SPIClass.
|
||||
class SPIDelegate {
|
||||
friend class SPIClient;
|
||||
friend class SPIComponent; // hands out NULL_DELEGATE for a failed bus
|
||||
|
||||
public:
|
||||
SPIDelegate() = default;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "spi_zephyr.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::spi {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "spi.zephyr");
|
||||
|
||||
// Words swapped per transceive for MSB-first 16-bit writes
|
||||
static constexpr size_t SWAP_BUFFER_WORDS = 64;
|
||||
|
||||
static spi_operation_t build_operation(SPIBitOrder bit_order, SPIMode mode) {
|
||||
spi_operation_t op = SPI_OP_MODE_MASTER | SPI_WORD_SET(8);
|
||||
op |= (bit_order == BIT_ORDER_LSB_FIRST) ? SPI_TRANSFER_LSB : SPI_TRANSFER_MSB;
|
||||
if (Utility::get_polarity(mode) == CLOCK_POLARITY_HIGH)
|
||||
op |= SPI_MODE_CPOL;
|
||||
if (Utility::get_phase(mode) == CLOCK_PHASE_TRAILING)
|
||||
op |= SPI_MODE_CPHA;
|
||||
return op;
|
||||
}
|
||||
|
||||
// release_device and write_only are not needed here: the Zephyr driver locks the bus per transfer,
|
||||
// and a null rx buffer already skips the receive DMA
|
||||
SPIDelegate *ZephyrSPIBus::get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin,
|
||||
bool release_device, bool write_only) {
|
||||
return new ZephyrSPIDelegate(this, data_rate, bit_order, mode, cs_pin); // NOLINT
|
||||
}
|
||||
|
||||
ZephyrSPIDelegate::ZephyrSPIDelegate(ZephyrSPIBus *bus, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode,
|
||||
GPIOPin *cs_pin)
|
||||
: SPIDelegate(data_rate, bit_order, mode, cs_pin), bus_(bus) {
|
||||
this->cfg_.frequency = data_rate;
|
||||
this->cfg_.operation = build_operation(bit_order, mode);
|
||||
}
|
||||
|
||||
// The driver skips a config at the address it applied last, so a delegate created later in this allocation
|
||||
// would run with the old settings: hand it the bus's own config before this one goes away.
|
||||
ZephyrSPIDelegate::~ZephyrSPIDelegate() {
|
||||
if (this->bus_->active_ != this)
|
||||
return;
|
||||
this->bus_->active_ = nullptr;
|
||||
spi_transceive(this->bus_->dev_, &this->bus_->idle_cfg_, nullptr, nullptr);
|
||||
}
|
||||
|
||||
// The driver applies cfg_ (and the SCK idle level for CPOL) at the start of each transfer, so switch
|
||||
// it over with an empty transfer before CS goes low; otherwise SCK changes level while CS is active.
|
||||
void ZephyrSPIDelegate::begin_transaction() {
|
||||
if (this->bus_->active_ != this) {
|
||||
const int err = spi_transceive(this->bus_->dev_, &this->cfg_, nullptr, nullptr);
|
||||
if (err == 0) {
|
||||
this->bus_->active_ = this;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "spi_transceive failed: %d", err);
|
||||
}
|
||||
}
|
||||
SPIDelegate::begin_transaction();
|
||||
}
|
||||
|
||||
// Buffers EasyDMA cannot reach (flash tables, for one) are copied by the nrfx SPIM driver itself,
|
||||
// CONFIG_SPI_NRFX_RAM_BUFFER_SIZE bytes at a time
|
||||
void ZephyrSPIDelegate::transceive_(const uint8_t *tx, uint8_t *rx, size_t length) {
|
||||
if (length == 0)
|
||||
return;
|
||||
const spi_buf tx_buf{const_cast<uint8_t *>(tx), length}; // NOLINT(cppcoreguidelines-pro-type-const-cast)
|
||||
const spi_buf rx_buf{rx, length};
|
||||
const spi_buf_set tx_set{&tx_buf, 1};
|
||||
const spi_buf_set rx_set{&rx_buf, 1};
|
||||
const int err = spi_transceive(this->bus_->dev_, &this->cfg_, tx != nullptr ? &tx_set : nullptr,
|
||||
rx != nullptr ? &rx_set : nullptr);
|
||||
if (err != 0) {
|
||||
ESP_LOGE(TAG, "spi_transceive failed: %d", err);
|
||||
if (rx != nullptr)
|
||||
memset(rx, 0, length);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ZephyrSPIDelegate::transfer(uint8_t data) {
|
||||
uint8_t rx = 0;
|
||||
this->transceive_(&data, &rx, 1);
|
||||
return rx;
|
||||
}
|
||||
|
||||
void ZephyrSPIDelegate::transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) {
|
||||
this->transceive_(txbuf, rxbuf, length);
|
||||
}
|
||||
|
||||
void ZephyrSPIDelegate::write_array(const uint8_t *ptr, size_t length) { this->transceive_(ptr, nullptr, length); }
|
||||
|
||||
void ZephyrSPIDelegate::write_array16(const uint16_t *data, size_t length) {
|
||||
if (this->bit_order_ == BIT_ORDER_LSB_FIRST) {
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(data), length * 2);
|
||||
return;
|
||||
}
|
||||
uint16_t buffer[SWAP_BUFFER_WORDS];
|
||||
while (length != 0) {
|
||||
const size_t partial = std::min(length, SWAP_BUFFER_WORDS);
|
||||
for (size_t i = 0; i != partial; i++)
|
||||
buffer[i] = byteswap(data[i]);
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(buffer), partial * 2);
|
||||
data += partial;
|
||||
length -= partial;
|
||||
}
|
||||
}
|
||||
|
||||
void ZephyrSPIDelegate::read_array(uint8_t *ptr, size_t length) { this->transceive_(nullptr, ptr, length); }
|
||||
|
||||
} // namespace esphome::spi
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "spi.h"
|
||||
#include <zephyr/drivers/spi.h>
|
||||
|
||||
namespace esphome::spi {
|
||||
|
||||
class ZephyrSPIDelegate;
|
||||
|
||||
class ZephyrSPIBus : public SPIBus {
|
||||
friend class ZephyrSPIDelegate;
|
||||
|
||||
public:
|
||||
explicit ZephyrSPIBus(const device *dev) : dev_(dev) {
|
||||
this->idle_cfg_.frequency = IDLE_FREQUENCY;
|
||||
this->idle_cfg_.operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8);
|
||||
}
|
||||
|
||||
bool is_hw() override { return true; }
|
||||
|
||||
SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin,
|
||||
bool release_device, bool write_only) override;
|
||||
|
||||
protected:
|
||||
// The lowest rate the SPIM driver accepts; only ever applied with no device selected
|
||||
static constexpr uint32_t IDLE_FREQUENCY = 125000;
|
||||
|
||||
const device *dev_;
|
||||
// Delegate whose mode and frequency the driver last applied
|
||||
const ZephyrSPIDelegate *active_{nullptr};
|
||||
// Applied when the active delegate is deleted, so the driver never keeps a pointer to a freed config
|
||||
spi_config idle_cfg_{};
|
||||
};
|
||||
|
||||
class ZephyrSPIDelegate : public SPIDelegate {
|
||||
public:
|
||||
ZephyrSPIDelegate(ZephyrSPIBus *bus, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin);
|
||||
~ZephyrSPIDelegate() override;
|
||||
|
||||
void begin_transaction() override;
|
||||
|
||||
uint8_t transfer(uint8_t data) override;
|
||||
void transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) override;
|
||||
void write_array(const uint8_t *ptr, size_t length) override;
|
||||
void write_array16(const uint16_t *data, size_t length) override;
|
||||
void read_array(uint8_t *ptr, size_t length) override;
|
||||
|
||||
protected:
|
||||
/// One spi_transceive; rx (if given) is zeroed when the transfer fails
|
||||
void transceive_(const uint8_t *tx, uint8_t *rx, size_t length);
|
||||
|
||||
ZephyrSPIBus *bus_;
|
||||
// CS is driven by ESPHome via SPIDelegate::cs_pin_; cfg_.cs stays unset so
|
||||
// the Zephyr driver never touches a devicetree cs-gpios pin itself.
|
||||
spi_config cfg_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::spi
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
@@ -1,4 +1,6 @@
|
||||
from esphome import automation
|
||||
from collections.abc import Callable
|
||||
|
||||
from esphome import automation, preferences
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server, zigbee
|
||||
@@ -8,13 +10,16 @@ from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_INVERTED,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
CONF_ON_TURN_OFF,
|
||||
CONF_ON_TURN_ON,
|
||||
CONF_RESTORE_MODE,
|
||||
CONF_RESTORE_STATE,
|
||||
CONF_STATE,
|
||||
CONF_STORAGE,
|
||||
CONF_WEB_SERVER,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_OUTLET,
|
||||
@@ -22,10 +27,14 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
RESTORE_STATE_INITIAL,
|
||||
RESTORE_STATE_INVERT,
|
||||
RESTORE_STATE_KEEP,
|
||||
ApplyEntity,
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
restore_state_schema,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -47,6 +56,8 @@ SwitchPtr = Switch.operator("ptr")
|
||||
ApplySwitch = switch_ns.class_("ApplySwitch", Switch)
|
||||
|
||||
SwitchRestoreMode = switch_ns.enum("SwitchRestoreMode")
|
||||
RESTORE_MODE_STORAGE_FLASH_MASK = switch_ns.RESTORE_MODE_STORAGE_FLASH_MASK
|
||||
RESTORE_MODE_STORAGE_RTC_MASK = switch_ns.RESTORE_MODE_STORAGE_RTC_MASK
|
||||
|
||||
RESTORE_MODES = {
|
||||
"RESTORE_DEFAULT_OFF": SwitchRestoreMode.SWITCH_RESTORE_DEFAULT_OFF,
|
||||
@@ -60,6 +71,115 @@ RESTORE_MODES = {
|
||||
|
||||
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True)
|
||||
validate_restore_mode = cv.enum(RESTORE_MODES, upper=True, space="_")
|
||||
|
||||
|
||||
def _validate_restore_state_storage(config: ConfigType) -> ConfigType:
|
||||
if CONF_STORAGE in config and config[CONF_STATE] == RESTORE_STATE_INITIAL:
|
||||
raise cv.Invalid(
|
||||
"'storage' only applies when a saved state is restored (KEEP or INVERT)",
|
||||
path=[CONF_STORAGE],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _validate_restore_state_state(value: object) -> str:
|
||||
# light accepts ON/OFF here (restore, then force a state); a switch has no such mode, and
|
||||
# YAML turns ON into True, so say what to use instead of asking for quotes
|
||||
if isinstance(value, bool) or (
|
||||
isinstance(value, str) and value.upper() in ("ON", "OFF")
|
||||
):
|
||||
raise cv.Invalid(
|
||||
"a switch can't restore and then force ON or OFF; to always start in a fixed state, "
|
||||
"use 'initial_state: ON' (or OFF) with 'restore_state: none'"
|
||||
)
|
||||
return cv.one_of(
|
||||
RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL, upper=True
|
||||
)(value)
|
||||
|
||||
|
||||
_RESTORE_STATE_FIELDS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_STATE, default=RESTORE_STATE_KEEP
|
||||
): _validate_restore_state_state,
|
||||
# No default: when unset, the existing restore_mode storage applies (on ESP8266,
|
||||
# restore_from_flash decides), as with wifi
|
||||
cv.Optional(CONF_STORAGE): preferences.validate_storage,
|
||||
}
|
||||
)
|
||||
_RESTORE_STATE_FIELDS_SCHEMA.add_extra(_validate_restore_state_storage)
|
||||
|
||||
|
||||
# A mapping of `state` and `storage`, or the shorthands `all` (KEEP) and `none` (INITIAL)
|
||||
RESTORE_STATE_SCHEMA = restore_state_schema(
|
||||
_RESTORE_STATE_FIELDS_SCHEMA, {CONF_STATE: RESTORE_STATE_INITIAL}
|
||||
)
|
||||
|
||||
|
||||
def _validate_initial_state(config: ConfigType) -> ConfigType:
|
||||
if CONF_INITIAL_STATE in config and CONF_RESTORE_MODE in config:
|
||||
raise cv.Invalid(
|
||||
"'initial_state' can't be combined with 'restore_mode', which already sets "
|
||||
"the boot state; use 'restore_state' instead",
|
||||
path=[CONF_INITIAL_STATE],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
# (restore_state state, initial_state) -> restore mode
|
||||
_RESTORE_MODE_FOR_STATE = {
|
||||
(RESTORE_STATE_KEEP, False): "RESTORE_DEFAULT_OFF",
|
||||
(RESTORE_STATE_KEEP, True): "RESTORE_DEFAULT_ON",
|
||||
(RESTORE_STATE_INVERT, False): "RESTORE_INVERTED_DEFAULT_OFF",
|
||||
(RESTORE_STATE_INVERT, True): "RESTORE_INVERTED_DEFAULT_ON",
|
||||
(RESTORE_STATE_INITIAL, False): "ALWAYS_OFF",
|
||||
(RESTORE_STATE_INITIAL, True): "ALWAYS_ON",
|
||||
}
|
||||
_STATE_FOR_RESTORE_MODE = {
|
||||
mode: state for (state, _), mode in _RESTORE_MODE_FOR_STATE.items()
|
||||
}
|
||||
|
||||
|
||||
def _apply_default_restore_mode(
|
||||
default_restore_mode: str,
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
# cv.Exclusive has no default, so apply it here when no restore option is configured.
|
||||
# With only initial_state set, a restoring default keeps restoring and initial_state
|
||||
# supplies the state used when nothing is saved.
|
||||
default = validate_restore_mode(default_restore_mode)
|
||||
state = _STATE_FOR_RESTORE_MODE.get(str(default), RESTORE_STATE_INITIAL)
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
if CONF_RESTORE_MODE in config or CONF_RESTORE_STATE in config:
|
||||
return config
|
||||
if CONF_INITIAL_STATE in config:
|
||||
config[CONF_RESTORE_STATE] = {CONF_STATE: state}
|
||||
else:
|
||||
config[CONF_RESTORE_MODE] = default
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def _resolve_restore_mode(config: ConfigType) -> MockObj | str:
|
||||
"""Translate `initial_state` / `restore_state` into one of the RESTORE_MODES, with the storage bit."""
|
||||
if (restore_mode := config.get(CONF_RESTORE_MODE)) is not None:
|
||||
return restore_mode
|
||||
restore_state = config.get(CONF_RESTORE_STATE, {})
|
||||
state = restore_state.get(CONF_STATE, RESTORE_STATE_INITIAL)
|
||||
mode = RESTORE_MODES[
|
||||
_RESTORE_MODE_FOR_STATE[state, config.get(CONF_INITIAL_STATE, False)]
|
||||
]
|
||||
if (storage := restore_state.get(CONF_STORAGE)) is None:
|
||||
return mode
|
||||
cg.add_define("USE_SWITCH_STORAGE")
|
||||
mask = (
|
||||
RESTORE_MODE_STORAGE_FLASH_MASK
|
||||
if preferences.is_in_flash(storage)
|
||||
else RESTORE_MODE_STORAGE_RTC_MASK
|
||||
)
|
||||
return cg.RawExpression(f"static_cast<{SwitchRestoreMode}>({mode} | {mask})")
|
||||
|
||||
|
||||
_SWITCH_SCHEMA = (
|
||||
@@ -70,9 +190,9 @@ _SWITCH_SCHEMA = (
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent),
|
||||
cv.Optional(CONF_INVERTED): cv.boolean,
|
||||
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
|
||||
RESTORE_MODES, upper=True, space="_"
|
||||
),
|
||||
cv.Exclusive(CONF_RESTORE_MODE, "restore"): validate_restore_mode,
|
||||
cv.Exclusive(CONF_RESTORE_STATE, "restore"): RESTORE_STATE_SCHEMA,
|
||||
cv.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({}),
|
||||
@@ -86,6 +206,7 @@ _SWITCH_SCHEMA = (
|
||||
|
||||
_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch"))
|
||||
_SWITCH_SCHEMA.add_extra(zigbee.validate_switch)
|
||||
_SWITCH_SCHEMA.add_extra(_validate_initial_state)
|
||||
|
||||
|
||||
def switch_schema(
|
||||
@@ -103,13 +224,6 @@ def switch_schema(
|
||||
(CONF_DEVICE_CLASS, device_class, validate_device_class),
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
(
|
||||
CONF_RESTORE_MODE,
|
||||
default_restore_mode,
|
||||
cv.enum(RESTORE_MODES, upper=True, space="_")
|
||||
if default_restore_mode is not cv.UNDEFINED
|
||||
else cv.UNDEFINED,
|
||||
),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
@@ -119,7 +233,15 @@ def switch_schema(
|
||||
"Inverted is not supported for this platform!"
|
||||
)
|
||||
|
||||
return _SWITCH_SCHEMA.extend(schema)
|
||||
result = _SWITCH_SCHEMA.extend(schema)
|
||||
result.add_extra(
|
||||
_apply_default_restore_mode(
|
||||
"ALWAYS_OFF"
|
||||
if default_restore_mode is cv.UNDEFINED
|
||||
else default_restore_mode
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
@@ -159,7 +281,7 @@ async def setup_switch_core_(var, config):
|
||||
|
||||
setup_device_class(config)
|
||||
|
||||
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
cg.add(var.set_restore_mode(_resolve_restore_mode(config)))
|
||||
await zigbee.setup_switch(var, config)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,14 @@ optional<bool> Switch::get_initial_state() {
|
||||
if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK))
|
||||
return {};
|
||||
|
||||
this->rtc_ = this->make_entity_preference<bool>();
|
||||
#ifdef USE_SWITCH_STORAGE
|
||||
if (restore_mode & RESTORE_MODE_STORAGE_MASK) {
|
||||
this->rtc_ = this->make_entity_preference<bool>(0, restore_mode & RESTORE_MODE_STORAGE_FLASH_MASK);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->rtc_ = this->make_entity_preference<bool>();
|
||||
}
|
||||
bool initial_state;
|
||||
if (!this->rtc_.load(&initial_state))
|
||||
return {};
|
||||
@@ -53,11 +60,11 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
|
||||
}
|
||||
return initial_state;
|
||||
}
|
||||
void Switch::restore_state() {
|
||||
void Switch::restore() {
|
||||
if (auto initial = this->get_initial_state_with_restore_mode())
|
||||
this->control(*initial);
|
||||
}
|
||||
void Switch::restore_state(bool when_disabled) {
|
||||
void Switch::restore(bool when_disabled) {
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(when_disabled));
|
||||
}
|
||||
void Switch::publish_state(bool state) {
|
||||
|
||||
@@ -14,11 +14,16 @@ namespace esphome::switch_ {
|
||||
public: \
|
||||
void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; }
|
||||
|
||||
// bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled
|
||||
// bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled.
|
||||
// bit4: restore from flash. bit5: restore from RTC memory (neither: platform default).
|
||||
// Code generation ors the storage bit into the restore mode passed to set_restore_mode().
|
||||
constexpr int RESTORE_MODE_ON_MASK = 0x01;
|
||||
constexpr int RESTORE_MODE_PERSISTENT_MASK = 0x02;
|
||||
constexpr int RESTORE_MODE_INVERTED_MASK = 0x04;
|
||||
constexpr int RESTORE_MODE_DISABLED_MASK = 0x08;
|
||||
constexpr int RESTORE_MODE_STORAGE_FLASH_MASK = 0x10;
|
||||
constexpr int RESTORE_MODE_STORAGE_RTC_MASK = 0x20;
|
||||
constexpr int RESTORE_MODE_STORAGE_MASK = RESTORE_MODE_STORAGE_FLASH_MASK | RESTORE_MODE_STORAGE_RTC_MASK;
|
||||
|
||||
enum SwitchRestoreMode : uint8_t {
|
||||
SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK,
|
||||
@@ -111,9 +116,9 @@ class Switch : public EntityBase {
|
||||
optional<bool> get_initial_state_with_restore_mode();
|
||||
|
||||
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
|
||||
/// write_state() apply; a disabled restore mode drives `when_disabled`, or nothing without one.
|
||||
void restore_state();
|
||||
void restore_state(bool when_disabled);
|
||||
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
|
||||
void restore();
|
||||
void restore(bool when_disabled);
|
||||
|
||||
/** Return whether this switch uses an assumed state - i.e. if both the ON/OFF actions should be displayed in Home
|
||||
* Assistant because the real state is unknown.
|
||||
@@ -144,9 +149,9 @@ class Switch : public EntityBase {
|
||||
LazyCallbackManager<void(bool)> state_callback_{};
|
||||
|
||||
// Small types grouped together
|
||||
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)
|
||||
Deduplicator<bool> publish_dedup_; // 3 bytes (bool has_value_ + bool value_unknown_ + bool last_value_)
|
||||
bool inverted_{false}; // 1 byte
|
||||
// Total: 3 bytes, 1 byte padding
|
||||
// Total: 4 bytes, no padding
|
||||
};
|
||||
|
||||
#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -9,6 +11,7 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
@@ -17,12 +20,15 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_HOUR,
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOWATT,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MILLIAMP,
|
||||
UNIT_MILLIVOLT,
|
||||
UNIT_MINUTE,
|
||||
UNIT_SECOND,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
@@ -33,6 +39,8 @@ from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
|
||||
@@ -59,6 +67,9 @@ _APPARENT_POWER_KVA = _preset(
|
||||
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
|
||||
_DURATION_MIN = _preset(UNIT_MINUTE, DEVICE_CLASS_DURATION, STATE_CLASS_MEASUREMENT)
|
||||
# Numeric values without a physical unit (indexes, bit fields, versions)
|
||||
_NO_UNIT: dict[str, str] = {}
|
||||
|
||||
# Legacy defaults, kept for tags without a known preset
|
||||
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
|
||||
@@ -75,6 +86,7 @@ _COMPATIBLE_UNITS = {
|
||||
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
|
||||
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
|
||||
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
|
||||
DEVICE_CLASS_DURATION: {UNIT_SECOND, UNIT_MINUTE, UNIT_HOUR},
|
||||
}
|
||||
|
||||
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
|
||||
@@ -90,6 +102,10 @@ TIC_TAG_CONFIGS = {
|
||||
"CC": _POWER,
|
||||
"PREF": _APPARENT_POWER_KVA,
|
||||
"PCOUP": _APPARENT_POWER_KVA,
|
||||
"VTIC": _NO_UNIT,
|
||||
"NTARF": _NO_UNIT,
|
||||
"NJOURF": _NO_UNIT,
|
||||
"RELAIS": _NO_UNIT,
|
||||
# Historical mode
|
||||
"BASE": _ENERGY,
|
||||
"HCH": _ENERGY,
|
||||
@@ -101,15 +117,42 @@ TIC_TAG_CONFIGS = {
|
||||
"ADPS": _CURRENT,
|
||||
"ADIR": _CURRENT,
|
||||
"PAPP": _APPARENT_POWER_VA,
|
||||
"PMAX": _POWER,
|
||||
"PMAX": _APPARENT_POWER_VA,
|
||||
"PEJP": _DURATION_MIN,
|
||||
}
|
||||
|
||||
# Prefixes of text tags, which a sensor cannot publish; a text_sensor should be used
|
||||
# instead. They get no preset, and no prefix here is a prefix of a preset one either.
|
||||
TIC_TEXT_TAGS = (
|
||||
# Standard mode
|
||||
"ADSC",
|
||||
"DATE",
|
||||
"NGTF",
|
||||
"LTARF",
|
||||
"STGE",
|
||||
"DPM",
|
||||
"FPM",
|
||||
"MSG",
|
||||
"PRM",
|
||||
"PJOURF",
|
||||
"PPOINTE",
|
||||
# Historical mode
|
||||
"ADCO",
|
||||
"OPTARIF",
|
||||
"PTEC",
|
||||
"DEMAIN",
|
||||
"HHPHC",
|
||||
"MOTDETAT",
|
||||
"PPOT",
|
||||
)
|
||||
|
||||
|
||||
def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
"""Apply preset configurations based on the tag name.
|
||||
|
||||
Only keys not set by the user are filled in. For known tags, a preset unit or
|
||||
device class is dropped if it is incompatible with the one set by the user.
|
||||
Known text tags get no preset and log a warning, as a sensor cannot publish them.
|
||||
Unknown tags get the legacy defaults, merged key by key as before.
|
||||
"""
|
||||
if CONF_TAG_NAME not in config:
|
||||
@@ -117,6 +160,12 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
|
||||
# Runs before cv.string, so the raw value may not be a string yet
|
||||
tag_name = str(config[CONF_TAG_NAME])
|
||||
if tag_name.startswith(TIC_TEXT_TAGS):
|
||||
_LOGGER.warning(
|
||||
"TeleInfo tag '%s' holds a text value, use a text_sensor instead", tag_name
|
||||
)
|
||||
return config
|
||||
|
||||
preset = next(
|
||||
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
|
||||
None,
|
||||
@@ -128,8 +177,8 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
config.setdefault(key, value)
|
||||
return config
|
||||
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
|
||||
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset.get(CONF_UNIT_OF_MEASUREMENT))
|
||||
device_class = config.get(CONF_DEVICE_CLASS, preset.get(CONF_DEVICE_CLASS))
|
||||
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
|
||||
|
||||
for key, value in preset.items():
|
||||
|
||||
@@ -37,13 +37,7 @@ void TemplateSwitch::setup() {
|
||||
if (!this->f_.has_value())
|
||||
this->disable_loop();
|
||||
|
||||
optional<bool> initial_state = this->get_initial_state_with_restore_mode();
|
||||
|
||||
if (initial_state.has_value()) {
|
||||
ESP_LOGD(TAG, " Restored state %s", ONOFF(initial_state.value()));
|
||||
// if it has a value, restore_mode is not "DISABLED", therefore act on the switch:
|
||||
this->control(initial_state.value());
|
||||
}
|
||||
this->restore();
|
||||
}
|
||||
void TemplateSwitch::dump_config() {
|
||||
LOG_SWITCH("", "Template Switch", this);
|
||||
|
||||
@@ -23,6 +23,14 @@ enum UARTParityOptions {
|
||||
UART_CONFIG_PARITY_SPACE,
|
||||
};
|
||||
|
||||
// A runtime setting like CRTSCTS and IXON/IXOFF in termios. A backend without the lines
|
||||
// keeps the value and ignores it, as a Linux driver does.
|
||||
enum UARTFlowControl : uint8_t {
|
||||
UART_FLOW_CONTROL_NONE,
|
||||
UART_FLOW_CONTROL_RTS_CTS,
|
||||
UART_FLOW_CONTROL_XON_XOFF,
|
||||
};
|
||||
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
enum UARTDirection {
|
||||
UART_DIRECTION_RX,
|
||||
@@ -185,6 +193,15 @@ class UARTComponent {
|
||||
// @return Baud rate in bits per second.
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
|
||||
// Set the line handshaking (RTS/CTS or XON/XOFF) used in UART communication. Unrelated to
|
||||
// flow_control_pin, the RS485 driver enable; backends without handshaking ignore it.
|
||||
// @param flow_control Flow control option.
|
||||
void set_flow_control(UARTFlowControl flow_control) { this->flow_control_ = flow_control; }
|
||||
|
||||
// Get the flow control used in UART communication.
|
||||
// @return Flow control option.
|
||||
UARTFlowControl get_flow_control() const { return this->flow_control_; }
|
||||
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
/**
|
||||
* Load the UART settings.
|
||||
@@ -231,6 +248,7 @@ class UARTComponent {
|
||||
uint32_t baud_rate_{0};
|
||||
uint8_t stop_bits_{0};
|
||||
uint8_t data_bits_{0};
|
||||
UARTFlowControl flow_control_{UART_FLOW_CONTROL_NONE};
|
||||
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; // Must match the parity check in __init__.py
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
CallbackManager<void(UARTDirection, uint8_t)> debug_callback_{};
|
||||
|
||||
@@ -2,95 +2,57 @@
|
||||
|
||||
namespace esphome::unicode {
|
||||
|
||||
uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
|
||||
// Safely cast to uint8_t* for correct bitwise operations on bytes
|
||||
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
uint32_t code_point = 0;
|
||||
uint8_t c1 = *current++;
|
||||
// Reads count continuation bytes into code_point, stopping at the first byte that is not one so a
|
||||
// sequence cut short by the terminator is never read past
|
||||
static bool read_continuation(const uint8_t *¤t, uint8_t count, uint32_t &code_point) {
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
const uint8_t c = *current;
|
||||
if ((c & 0xC0) != 0x80) {
|
||||
return false;
|
||||
}
|
||||
current++;
|
||||
code_point = (code_point << 6) | (c & 0x3F);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// check for end of string
|
||||
uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
|
||||
const auto *start = reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
const uint8_t *current = start;
|
||||
const uint8_t c1 = *current++;
|
||||
uint8_t count;
|
||||
uint32_t code_point;
|
||||
uint32_t lowest; // the smallest code point this sequence length may encode; shorter forms are overlong
|
||||
if (c1 == 0) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
|
||||
if (c1 < 0x80) {
|
||||
// Valid ASCII byte.
|
||||
count = 0;
|
||||
code_point = c1;
|
||||
// Optimization: No need to check for continuation bytes.
|
||||
lowest = 0;
|
||||
} else if ((c1 & 0xE0) == 0xC0) {
|
||||
count = 1;
|
||||
code_point = c1 & 0x1F;
|
||||
lowest = 0x80;
|
||||
} else if ((c1 & 0xF0) == 0xE0) {
|
||||
count = 2;
|
||||
code_point = c1 & 0x0F;
|
||||
lowest = 0x800;
|
||||
} else if ((c1 & 0xF8) == 0xF0) {
|
||||
count = 3;
|
||||
code_point = c1 & 0x07;
|
||||
lowest = 0x10000;
|
||||
} else {
|
||||
*length = 0; // a continuation byte or an invalid lead byte
|
||||
return 0;
|
||||
}
|
||||
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xE0) == 0xC0) {
|
||||
uint8_t c2 = *current++;
|
||||
|
||||
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
|
||||
if ((c2 & 0xC0) != 0x80) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x1F) << 6;
|
||||
code_point |= (c2 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (2-byte must be > 0x7F)
|
||||
if (code_point <= 0x7F) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF0) == 0xE0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x0F) << 12;
|
||||
code_point |= (c2 & 0x3F) << 6;
|
||||
code_point |= (c3 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
|
||||
// Also check for surrogates (0xD800-0xDFFF)
|
||||
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF8) == 0xF0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
uint8_t c4 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x07) << 18;
|
||||
code_point |= (c2 & 0x3F) << 12;
|
||||
code_point |= (c3 & 0x3F) << 6;
|
||||
code_point |= (c4 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
|
||||
// Also check for valid Unicode range (must be <= 0x10FFFF)
|
||||
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
|
||||
else {
|
||||
if (!read_continuation(current, count, code_point) || code_point < lowest || code_point > 0x10FFFF ||
|
||||
(code_point >= 0xD800 && code_point <= 0xDFFF)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
*length = current - start;
|
||||
return code_point;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
idf_version,
|
||||
only_on_variant,
|
||||
require_idf_version,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DEVICES, CONF_FRAMEWORK, CONF_ID
|
||||
@@ -201,10 +202,9 @@ def fifo_split() -> tuple[int, int, int] | None:
|
||||
def _reconcile_fifo() -> None:
|
||||
if (split := fifo_split()) is None:
|
||||
return
|
||||
if idf_version() < FIFO_CUSTOM_MIN_IDF:
|
||||
raise cv.Invalid(
|
||||
f"Sizing the USB FIFO on the {get_esp32_variant()} needs ESP-IDF {FIFO_CUSTOM_MIN_IDF} or later"
|
||||
)
|
||||
require_idf_version(
|
||||
FIFO_CUSTOM_MIN_IDF, f"Sizing the USB FIFO on the {get_esp32_variant()}"
|
||||
)(None)
|
||||
rx, nptx, ptx = split
|
||||
cg.add_define("USE_USB_HOST_FIFO_CONFIG")
|
||||
cg.add_define("USB_HOST_FIFO_RX_LINES", rx)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
|
||||
@@ -154,6 +156,19 @@ FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
)
|
||||
|
||||
|
||||
def _channel_limit(type_: Type) -> Callable[[list], list]:
|
||||
"""Check the channel count against the type's limit, which depends on the target."""
|
||||
|
||||
def validator(value: list) -> list:
|
||||
if len(value) > (limit := type_.max_channels):
|
||||
raise cv.Invalid(
|
||||
f"Device type {type_.name} supports a maximum of {limit} channels"
|
||||
)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def channel_schema(type_: "Type") -> cv.Schema:
|
||||
schema = cv.Schema(
|
||||
{
|
||||
@@ -192,10 +207,7 @@ def channel_schema(type_: "Type") -> cv.Schema:
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.Length(
|
||||
max=type_.max_channels,
|
||||
msg=f"Device type {type_.name} supports a maximum of {type_.max_channels} channels",
|
||||
),
|
||||
_channel_limit(type_),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
namespace esphome::usb_uart {
|
||||
|
||||
using namespace bytebuffer;
|
||||
@@ -15,6 +18,13 @@ static constexpr uint8_t CH341_REQ_MODEM_CTRL = 0xA4;
|
||||
static constexpr uint8_t CH341_BIT_DTR = 1 << 5;
|
||||
static constexpr uint8_t CH341_BIT_RTS = 1 << 6;
|
||||
|
||||
static constexpr uint8_t CH341_REQ_WRITE_REG = 0x9A;
|
||||
static constexpr uint8_t CH341_REG_FLOW_CTL = 0x27;
|
||||
static constexpr uint8_t CH341_FLOW_CTL_NONE = 0x00;
|
||||
static constexpr uint8_t CH341_FLOW_CTL_RTSCTS = 0x01;
|
||||
// The single-port chips, which have the flow control register
|
||||
static constexpr uint16_t CH341_PIDS[] = {0x5523, 0x7522, 0x7523};
|
||||
|
||||
struct CH34xEntry {
|
||||
const char *name;
|
||||
uint16_t pid;
|
||||
@@ -102,6 +112,12 @@ void USBUartTypeCH34X::dump_config() {
|
||||
|
||||
bool USBUartTypeCH34X::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok,
|
||||
const uint8_t *response) {
|
||||
// TODO: flow control on the multi-port chips (PIDs 0x55Dx)
|
||||
const bool has_flow_ctl_reg =
|
||||
std::find(std::begin(CH341_PIDS), std::end(CH341_PIDS), this->pid_) != std::end(CH341_PIDS);
|
||||
if (!has_flow_ctl_reg && step >= 1)
|
||||
step++;
|
||||
|
||||
uint8_t cmd = 0xA1 + channel->index_;
|
||||
if (channel->index_ >= 2)
|
||||
cmd += 0xE;
|
||||
@@ -149,7 +165,16 @@ bool USBUartTypeCH34X::config_step(USBUartChannelBase *channel, uint8_t step, bo
|
||||
this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor);
|
||||
return true;
|
||||
}
|
||||
case 1:
|
||||
case 1: {
|
||||
// Re-applied on every settings change. The chip has no XON/XOFF.
|
||||
const bool rts_cts = channel->flow_control_ == uart::UART_FLOW_CONTROL_RTS_CTS;
|
||||
const uint8_t flow_ctl = rts_cts ? CH341_FLOW_CTL_RTSCTS : CH341_FLOW_CTL_NONE;
|
||||
ESP_LOGD(TAG, "FLOW_CTL: RTS/CTS=%s", ONOFF(rts_cts));
|
||||
this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, CH341_REQ_WRITE_REG,
|
||||
(CH341_REG_FLOW_CTL << 8) | CH341_REG_FLOW_CTL, (flow_ctl << 8) | flow_ctl);
|
||||
return true;
|
||||
}
|
||||
case 2:
|
||||
this->modem_control_transfer(channel);
|
||||
return true;
|
||||
default:
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <iterator>
|
||||
|
||||
namespace esphome::usb_uart {
|
||||
|
||||
using namespace bytebuffer;
|
||||
@@ -43,12 +46,56 @@ static constexpr uint8_t SET_BAUDRATE = 0x1E; // Set the baud rate.
|
||||
static constexpr uint8_t SET_CHARS = 0x19; // Set special characters.
|
||||
static constexpr uint8_t VENDOR_SPECIFIC = 0xFF; // Vendor specific command.
|
||||
|
||||
// VENDOR_SPECIFIC wValue reads
|
||||
static constexpr uint16_t GET_FW_VER_2N = 0x0010;
|
||||
static constexpr uint16_t GET_PARTNUM = 0x370B;
|
||||
|
||||
static constexpr uint8_t PARTNUM_CP2102N_QFN28 = 0x20;
|
||||
static constexpr uint8_t PARTNUM_CP2102N_QFN24 = 0x21;
|
||||
static constexpr uint8_t PARTNUM_CP2102N_QFN20 = 0x22;
|
||||
|
||||
// CP2102N firmware up to 1.0.4 reads xon_limit as flow_replace (erratum E104)
|
||||
static constexpr uint32_t CP2102N_LAST_FW_WITHOUT_FLOW_CONTROL = 0x010004;
|
||||
|
||||
// SET_MHS value bits. The high byte selects which lines the low byte changes.
|
||||
static constexpr uint16_t CONTROL_DTR = 0x0001;
|
||||
static constexpr uint16_t CONTROL_RTS = 0x0002;
|
||||
static constexpr uint16_t CONTROL_WRITE_DTR = 0x0100;
|
||||
static constexpr uint16_t CONTROL_WRITE_RTS = 0x0200;
|
||||
|
||||
// CP210xFlowBlock::control_handshake bits
|
||||
static constexpr uint32_t SERIAL_DTR_MASK = 0x03;
|
||||
static constexpr uint32_t SERIAL_DTR_INACTIVE = 0x00;
|
||||
static constexpr uint32_t SERIAL_DTR_ACTIVE = 0x01;
|
||||
static constexpr uint32_t SERIAL_CTS_HANDSHAKE = 1 << 3;
|
||||
static constexpr uint32_t SERIAL_DSR_HANDSHAKE = 1 << 4;
|
||||
static constexpr uint32_t SERIAL_DCD_HANDSHAKE = 1 << 5;
|
||||
static constexpr uint32_t SERIAL_DSR_SENSITIVITY = 1 << 6;
|
||||
|
||||
// CP210xFlowBlock::flow_replace bits
|
||||
static constexpr uint32_t SERIAL_AUTO_TRANSMIT = 1 << 0;
|
||||
static constexpr uint32_t SERIAL_AUTO_RECEIVE = 1 << 1;
|
||||
static constexpr uint32_t SERIAL_RTS_MASK = 0xC0;
|
||||
static constexpr uint32_t SERIAL_RTS_INACTIVE = 0x00;
|
||||
static constexpr uint32_t SERIAL_RTS_ACTIVE = 0x40;
|
||||
static constexpr uint32_t SERIAL_RTS_FLOW_CTL = 0x80;
|
||||
|
||||
// Receive queue levels at which the chip sends XON and XOFF
|
||||
static constexpr uint32_t XON_XOFF_LIMIT = 128;
|
||||
// SET_CHARS block: EOF, error, break, event, XON and XOFF characters. XON and XOFF are the
|
||||
// termios defaults DC1 and DC3.
|
||||
static constexpr uint8_t SPECIAL_CHARS[] = {0, 0, 0, 0, 0x11, 0x13};
|
||||
|
||||
// Put DTR and RTS into the block. Under RTS/CTS an asserted RTS hands the line to the chip.
|
||||
static void set_block_modem_lines(CP210xFlowBlock &block, bool dtr, bool rts) {
|
||||
block.control_handshake =
|
||||
(block.control_handshake & ~SERIAL_DTR_MASK) | (dtr ? SERIAL_DTR_ACTIVE : SERIAL_DTR_INACTIVE);
|
||||
uint32_t rts_bits = SERIAL_RTS_INACTIVE;
|
||||
if (rts)
|
||||
rts_bits = (block.control_handshake & SERIAL_CTS_HANDSHAKE) ? SERIAL_RTS_FLOW_CTL : SERIAL_RTS_ACTIVE;
|
||||
block.flow_replace = (block.flow_replace & ~SERIAL_RTS_MASK) | rts_bits;
|
||||
}
|
||||
|
||||
std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev_hdl) {
|
||||
const usb_config_desc_t *config_desc;
|
||||
const usb_device_desc_t *device_desc;
|
||||
@@ -103,11 +150,57 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
|
||||
return cdc_devs;
|
||||
}
|
||||
|
||||
bool USBUartTypeCP210X::config_device_step(uint8_t step, bool ok, const uint8_t *response) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
// A fresh device: the previous one's cached blocks must not steer modem_control_transfer()
|
||||
this->no_flow_control_ = false;
|
||||
this->flow_blocks_.fill({});
|
||||
this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_IN, VENDOR_SPECIFIC, GET_PARTNUM, 0,
|
||||
std::vector<uint8_t>(1), this->cfg_response_);
|
||||
return true;
|
||||
case 1:
|
||||
if (!ok)
|
||||
return false;
|
||||
ESP_LOGD(TAG, "Part number 0x%02X", response[0]);
|
||||
if (response[0] != PARTNUM_CP2102N_QFN28 && response[0] != PARTNUM_CP2102N_QFN24 &&
|
||||
response[0] != PARTNUM_CP2102N_QFN20)
|
||||
return false;
|
||||
this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_IN, VENDOR_SPECIFIC, GET_FW_VER_2N, 0,
|
||||
std::vector<uint8_t>(3), this->cfg_response_);
|
||||
return true;
|
||||
case 2: {
|
||||
if (!ok)
|
||||
return false;
|
||||
const uint32_t version = (response[0] << 16) | (response[1] << 8) | response[2];
|
||||
ESP_LOGD(TAG, "CP2102N firmware %u.%u.%u", response[0], response[1], response[2]);
|
||||
this->no_flow_control_ = version <= CP2102N_LAST_FW_WITHOUT_FLOW_CONTROL;
|
||||
if (this->no_flow_control_) {
|
||||
ESP_LOGW(TAG, "CP2102N firmware 1.0.4 and older cannot do flow control (erratum E104), using none");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool USBUartTypeCP210X::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok,
|
||||
const uint8_t *response) {
|
||||
// On reload, skip the one-time IFC_ENABLE step (the interface is already enabled).
|
||||
if (reload)
|
||||
step++;
|
||||
const bool xon_xoff = channel->flow_control_ == uart::UART_FLOW_CONTROL_XON_XOFF;
|
||||
if (step >= 3) {
|
||||
if (this->no_flow_control_) {
|
||||
// Firmware with erratum E104 takes flow_replace from xon_limit, so a SET_FLOW can drop RTS.
|
||||
// Skip the flow control steps; the lines then stay on SET_MHS as the cached block has no CTS.
|
||||
step += 3;
|
||||
} else if (!xon_xoff) {
|
||||
// SET_CHARS is only needed for XON/XOFF
|
||||
step++;
|
||||
}
|
||||
}
|
||||
switch (step) {
|
||||
case 0:
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_);
|
||||
@@ -125,7 +218,36 @@ bool USBUartTypeCP210X::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, baud.get_data());
|
||||
return true;
|
||||
}
|
||||
case 3: // Assert DTR+RTS (init only)
|
||||
case 3:
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_CHARS, 0, channel->index_,
|
||||
std::vector<uint8_t>(std::begin(SPECIAL_CHARS), std::end(SPECIAL_CHARS)));
|
||||
return true;
|
||||
case 4: {
|
||||
// Read into the channel's cached block, which the next step edits and writes back
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_IN, GET_FLOW, 0, channel->index_,
|
||||
std::vector<uint8_t>(sizeof(CP210xFlowBlock)),
|
||||
reinterpret_cast<uint8_t *>(&this->flow_blocks_[channel->index_]));
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
// Set flow control, re-applied on every settings change. The block also carries DTR and RTS,
|
||||
// so this puts them on the wire too.
|
||||
auto &block = this->flow_blocks_[channel->index_];
|
||||
block.control_handshake &=
|
||||
~(SERIAL_DSR_HANDSHAKE | SERIAL_DCD_HANDSHAKE | SERIAL_DSR_SENSITIVITY | SERIAL_CTS_HANDSHAKE);
|
||||
if (channel->flow_control_ == uart::UART_FLOW_CONTROL_RTS_CTS)
|
||||
block.control_handshake |= SERIAL_CTS_HANDSHAKE;
|
||||
assign_bits(block.flow_replace, SERIAL_AUTO_TRANSMIT | SERIAL_AUTO_RECEIVE, xon_xoff);
|
||||
if (xon_xoff) {
|
||||
block.xon_limit = XON_XOFF_LIMIT;
|
||||
block.xoff_limit = XON_XOFF_LIMIT;
|
||||
}
|
||||
set_block_modem_lines(block, channel->dtr_, channel->rts_);
|
||||
this->set_flow_transfer_(channel);
|
||||
return true;
|
||||
}
|
||||
case 6:
|
||||
// Raise the lines on open (init only)
|
||||
if (reload)
|
||||
return false;
|
||||
this->modem_control_transfer(channel);
|
||||
@@ -136,11 +258,27 @@ bool USBUartTypeCP210X::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
}
|
||||
|
||||
void USBUartTypeCP210X::modem_control_transfer(USBUartChannelBase *channel) {
|
||||
// Under RTS/CTS the chip drives RTS, so the lines go through SET_FLOW
|
||||
auto &block = this->flow_blocks_[channel->index_];
|
||||
if (block.control_handshake & SERIAL_CTS_HANDSHAKE) {
|
||||
set_block_modem_lines(block, channel->dtr_, channel->rts_);
|
||||
this->set_flow_transfer_(channel);
|
||||
return;
|
||||
}
|
||||
const uint16_t value =
|
||||
CONTROL_WRITE_DTR | CONTROL_WRITE_RTS | (channel->dtr_ ? CONTROL_DTR : 0) | (channel->rts_ ? CONTROL_RTS : 0);
|
||||
ESP_LOGD(TAG, "SET_MHS: DTR=%s RTS=%s", ONOFF(channel->dtr_), ONOFF(channel->rts_));
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_MHS, value, channel->index_);
|
||||
}
|
||||
|
||||
void USBUartTypeCP210X::set_flow_transfer_(USBUartChannelBase *channel) {
|
||||
const auto &block = this->flow_blocks_[channel->index_];
|
||||
ESP_LOGD(TAG, "SET_FLOW: control handshake 0x%02" PRIX32 ", flow replace 0x%02" PRIX32, block.control_handshake,
|
||||
block.flow_replace);
|
||||
const auto *bytes = reinterpret_cast<const uint8_t *>(&block);
|
||||
this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_FLOW, 0, channel->index_,
|
||||
std::vector<uint8_t>(bytes, bytes + sizeof(block)));
|
||||
}
|
||||
} // namespace esphome::usb_uart
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 ||
|
||||
|
||||
@@ -14,6 +14,13 @@ namespace esphome::usb_uart {
|
||||
using namespace bytebuffer;
|
||||
|
||||
static constexpr uint8_t SIO_SET_MODEM_CTRL = 0x01;
|
||||
static constexpr uint8_t SIO_SET_FLOW_CTRL = 0x02;
|
||||
// The high byte of wIndex selects the flow control mode, the low byte the channel
|
||||
static constexpr uint16_t SIO_DISABLE_FLOW_CTRL = 0x0000;
|
||||
static constexpr uint16_t SIO_RTS_CTS_HS = 0x0100;
|
||||
static constexpr uint16_t SIO_XON_XOFF_HS = 0x0400;
|
||||
// wValue carries the XOFF and XON characters, which the termios defaults fix as DC3 and DC1
|
||||
static constexpr uint16_t SIO_XON_XOFF_CHARS = 0x1311;
|
||||
// The high byte selects which lines the low byte changes
|
||||
static constexpr uint16_t SIO_SET_DTR_MASK = 0x0100;
|
||||
static constexpr uint16_t SIO_SET_RTS_MASK = 0x0200;
|
||||
@@ -377,7 +384,21 @@ bool USBUartTypeFT23XX::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
channel->cdc_dev_.bulk_interface_number + 1);
|
||||
return true;
|
||||
}
|
||||
case 3: // set modem control DTR+RTS (init only)
|
||||
case 3: { // set flow control, re-applied on every settings change
|
||||
uint16_t mode = SIO_DISABLE_FLOW_CTRL;
|
||||
uint16_t value = 0;
|
||||
if (channel->flow_control_ == uart::UART_FLOW_CONTROL_RTS_CTS) {
|
||||
mode = SIO_RTS_CTS_HS;
|
||||
} else if (channel->flow_control_ == uart::UART_FLOW_CONTROL_XON_XOFF) {
|
||||
mode = SIO_XON_XOFF_HS;
|
||||
value = SIO_XON_XOFF_CHARS;
|
||||
}
|
||||
ESP_LOGD(TAG, "SIO_SET_FLOW_CTRL: mode=0x%04X", mode);
|
||||
this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, SIO_SET_FLOW_CTRL, value,
|
||||
mode | (channel->cdc_dev_.bulk_interface_number + 1));
|
||||
return true;
|
||||
}
|
||||
case 4: // set modem control DTR+RTS (init only)
|
||||
if (reload)
|
||||
return false;
|
||||
this->modem_control_transfer(channel);
|
||||
|
||||
@@ -22,6 +22,25 @@ static constexpr uint8_t VENDOR_WRITE_REQUEST = 0x01;
|
||||
static constexpr uint8_t VENDOR_READ_REQUEST_TYPE = 0xc0;
|
||||
static constexpr uint8_t VENDOR_READ_REQUEST = 0x01;
|
||||
|
||||
// HXN takes its own vendor requests and reads a register at its plain address. The other
|
||||
// chips read it at the address with bit 7 set.
|
||||
static constexpr uint8_t VENDOR_WRITE_NREQUEST = 0x80;
|
||||
static constexpr uint8_t VENDOR_READ_NREQUEST = 0x81;
|
||||
static constexpr uint16_t VENDOR_READ_REG_FLAG = 0x80;
|
||||
|
||||
static constexpr uint8_t FLOWCTRL_REG = 0x00;
|
||||
static constexpr uint8_t FLOWCTRL_MASK = 0xf0;
|
||||
static constexpr uint8_t FLOWCTRL_NONE = 0x00;
|
||||
static constexpr uint8_t FLOWCTRL_RTS_CTS_LEGACY = 0x40;
|
||||
static constexpr uint8_t FLOWCTRL_RTS_CTS = 0x60;
|
||||
static constexpr uint8_t FLOWCTRL_XON_XOFF = 0xc0;
|
||||
|
||||
static constexpr uint8_t HXN_FLOWCTRL_REG = 0x0a;
|
||||
static constexpr uint8_t HXN_FLOWCTRL_MASK = 0x1c;
|
||||
static constexpr uint8_t HXN_FLOWCTRL_NONE = 0x1c;
|
||||
static constexpr uint8_t HXN_FLOWCTRL_RTS_CTS = 0x18;
|
||||
static constexpr uint8_t HXN_FLOWCTRL_XON_XOFF = 0x0c;
|
||||
|
||||
// Supported standard baud rates for direct encoding (TYPE_H, TYPE_HX, TYPE_HXD, TYPE_HXN)
|
||||
static const uint32_t SUPPORTED_BAUD_RATES[] = {
|
||||
75, 150, 300, 600, 1200, 1800, 2400, 3600, 4800, 7200, 9600, 14400, 19200,
|
||||
@@ -47,6 +66,29 @@ static const char *pl2303_type_name(Pl2303ChipType type) {
|
||||
}
|
||||
}
|
||||
|
||||
// The flow control register bits for the mode on each chip type
|
||||
static uint8_t pl2303_flow_control_bits(Pl2303ChipType type, uart::UARTFlowControl flow_control) {
|
||||
if (type == PL2303_TYPE_HXN) {
|
||||
switch (flow_control) {
|
||||
case uart::UART_FLOW_CONTROL_RTS_CTS:
|
||||
return HXN_FLOWCTRL_RTS_CTS;
|
||||
case uart::UART_FLOW_CONTROL_XON_XOFF:
|
||||
return HXN_FLOWCTRL_XON_XOFF;
|
||||
default:
|
||||
return HXN_FLOWCTRL_NONE;
|
||||
}
|
||||
}
|
||||
switch (flow_control) {
|
||||
case uart::UART_FLOW_CONTROL_RTS_CTS:
|
||||
return type == PL2303_TYPE_H ? FLOWCTRL_RTS_CTS_LEGACY : FLOWCTRL_RTS_CTS;
|
||||
case uart::UART_FLOW_CONTROL_XON_XOFF:
|
||||
// The legacy chip has no automatic XON/XOFF
|
||||
return type == PL2303_TYPE_H ? FLOWCTRL_NONE : FLOWCTRL_XON_XOFF;
|
||||
default:
|
||||
return FLOWCTRL_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
// Find nearest supported baud rate for direct encoding
|
||||
static uint32_t nearest_supported_baud(uint32_t baud) {
|
||||
size_t n = sizeof(SUPPORTED_BAUD_RATES) / sizeof(SUPPORTED_BAUD_RATES[0]);
|
||||
@@ -279,6 +321,24 @@ bool USBUartTypePL2303::config_step(USBUartChannelBase *channel, uint8_t step, b
|
||||
return true;
|
||||
}
|
||||
case 1:
|
||||
// Read the flow control register, re-applied on every settings change. A short reply fails
|
||||
// the step, so no write follows.
|
||||
this->config_transfer_(VENDOR_READ_REQUEST_TYPE, is_hxn ? VENDOR_READ_NREQUEST : VENDOR_READ_REQUEST,
|
||||
is_hxn ? HXN_FLOWCTRL_REG : (FLOWCTRL_REG | VENDOR_READ_REG_FLAG), 0,
|
||||
std::vector<uint8_t>(1), this->cfg_response_);
|
||||
return true;
|
||||
case 2: {
|
||||
// Write it back with only the flow control bits changed
|
||||
const uint8_t reg = is_hxn ? HXN_FLOWCTRL_REG : FLOWCTRL_REG;
|
||||
const uint8_t mask = is_hxn ? HXN_FLOWCTRL_MASK : FLOWCTRL_MASK;
|
||||
const uint8_t new_byte =
|
||||
(response[0] & ~mask) | pl2303_flow_control_bits(this->chip_type_, channel->flow_control_);
|
||||
ESP_LOGD(TAG, "PL2303: flow control register 0x%02X: 0x%02X -> 0x%02X", reg, response[0], new_byte);
|
||||
this->config_transfer_(VENDOR_WRITE_REQUEST_TYPE, is_hxn ? VENDOR_WRITE_NREQUEST : VENDOR_WRITE_REQUEST, reg,
|
||||
new_byte);
|
||||
return true;
|
||||
}
|
||||
case 3:
|
||||
// Assert DTR + RTS (init only)
|
||||
if (reload)
|
||||
return false;
|
||||
|
||||
@@ -596,14 +596,18 @@ void USBUartComponent::start_config_(ConfigMode mode) {
|
||||
this->cfg_step_ = 0;
|
||||
this->cfg_ok_ = true;
|
||||
this->cfg_in_flight_ = false;
|
||||
this->cfg_done_.store(false);
|
||||
this->cfg_done_.store(0);
|
||||
this->cfg_active_ = true;
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
|
||||
const std::vector<uint8_t> &data) {
|
||||
this->cfg_done_.store(false);
|
||||
const std::vector<uint8_t> &data, uint8_t *dest) {
|
||||
this->cfg_done_.store(0);
|
||||
// Members, not captures: a lambda holding more than `this` outgrows std::function's inline
|
||||
// storage and would allocate on every step
|
||||
this->cfg_read_dest_ = dest;
|
||||
this->cfg_read_len_ = data.size();
|
||||
// The completion callback runs in the USB-task context: it only records the result and
|
||||
// wakes the loop. The next transfer is issued from run_config_machine_() on the loop thread.
|
||||
bool submitted = this->control_transfer(
|
||||
@@ -612,11 +616,22 @@ void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t
|
||||
this->cfg_ok_ = status.success;
|
||||
if (!status.success) {
|
||||
ESP_LOGW(TAG, "Config control transfer failed: %s", esp_err_to_name(status.error_code));
|
||||
} else if (status.data_len > 0) {
|
||||
memcpy(this->cfg_response_, status.data, std::min<size_t>(status.data_len, sizeof(this->cfg_response_)));
|
||||
} else {
|
||||
// The buffer and its length both start with the setup packet
|
||||
const uint8_t *payload = status.data + usb_host::SETUP_PACKET_SIZE;
|
||||
const size_t received =
|
||||
status.data_len > usb_host::SETUP_PACKET_SIZE ? status.data_len - usb_host::SETUP_PACKET_SIZE : 0;
|
||||
if (this->cfg_read_dest_ == nullptr) {
|
||||
memcpy(this->cfg_response_, payload, std::min(received, sizeof(this->cfg_response_)));
|
||||
} else if (received == this->cfg_read_len_) {
|
||||
memcpy(this->cfg_read_dest_, payload, received);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Config read returned %zu of %zu bytes", received, this->cfg_read_len_);
|
||||
this->cfg_ok_ = false;
|
||||
}
|
||||
}
|
||||
// Release: publishes cfg_ok_/cfg_response_ before the loop observes cfg_done_.
|
||||
this->cfg_done_.store(true, std::memory_order_release);
|
||||
// Release: publishes cfg_ok_ and the reply before the loop observes cfg_done_.
|
||||
this->cfg_done_.store(1, std::memory_order_release);
|
||||
this->enable_loop_soon_any_context();
|
||||
App.wake_loop_threadsafe();
|
||||
},
|
||||
@@ -626,7 +641,7 @@ void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t
|
||||
// a failed completion here so the state machine advances/aborts instead of hanging.
|
||||
ESP_LOGW(TAG, "Config control transfer submit failed");
|
||||
this->cfg_ok_ = false;
|
||||
this->cfg_done_.store(true, std::memory_order_release);
|
||||
this->cfg_done_.store(1, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,7 +654,7 @@ bool USBUartComponent::run_config_machine_() {
|
||||
if (!this->cfg_done_.load(std::memory_order_acquire))
|
||||
return false; // still waiting; the callback will re-wake the loop (no busy spin)
|
||||
this->cfg_in_flight_ = false;
|
||||
this->cfg_done_.store(false);
|
||||
this->cfg_done_.store(0);
|
||||
this->cfg_step_++;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
#include "esphome/core/event_pool.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
@@ -289,8 +290,10 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
// callback (USB-task context) records the result/IN data, marks the step done and
|
||||
// wakes the loop so run_config_machine_() advances on the loop thread. Call exactly
|
||||
// once from config_step_()/config_device_step_() when issuing a step.
|
||||
// An IN reply goes to cfg_response_, or to `dest` when set. A `dest` reply must be
|
||||
// exactly data.size() bytes, else the step fails.
|
||||
void config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
|
||||
const std::vector<uint8_t> &data = {});
|
||||
const std::vector<uint8_t> &data = {}, uint8_t *dest = nullptr);
|
||||
// (Re)start the config state machine. INIT runs over all channels; the other modes act
|
||||
// on cfg_single_ only.
|
||||
void start_config_(ConfigMode mode);
|
||||
@@ -318,8 +321,11 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
|
||||
// Config state machine
|
||||
USBUartChannelBase *cfg_single_{nullptr}; // non-null: single-channel run (RELOAD or MODEM)
|
||||
std::atomic<bool> cfg_done_{false}; // synchronizes cfg_ok_/cfg_response_ across threads
|
||||
uint8_t cfg_response_[8]{}; // last IN transfer payload (for detection reads)
|
||||
uint8_t *cfg_read_dest_{nullptr}; // where the step in flight stores its reply, if not cfg_response_
|
||||
size_t cfg_read_len_{0}; // and the exact length it then expects
|
||||
// synchronizes cfg_ok_/cfg_response_ across threads; a byte, since atomic<bool> does not inline on Xtensa
|
||||
std::atomic<uint8_t> cfg_done_{0};
|
||||
uint8_t cfg_response_[8]{}; // last IN transfer payload
|
||||
uint8_t cfg_channel_idx_{0};
|
||||
uint8_t cfg_step_{0};
|
||||
ConfigMode cfg_mode_{ConfigMode::CONFIG_MODE_INIT};
|
||||
@@ -345,14 +351,32 @@ class USBUartTypeCdcAcm : public USBUartComponent {
|
||||
bool claim_comm_interface_{true};
|
||||
};
|
||||
|
||||
// The block GET_FLOW and SET_FLOW transfer, little-endian on the wire as in memory
|
||||
struct CP210xFlowBlock {
|
||||
uint32_t control_handshake;
|
||||
uint32_t flow_replace;
|
||||
uint32_t xon_limit;
|
||||
uint32_t xoff_limit;
|
||||
};
|
||||
|
||||
class USBUartTypeCP210X : public USBUartTypeCdcAcm {
|
||||
public:
|
||||
USBUartTypeCP210X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
|
||||
|
||||
protected:
|
||||
// Matches max_channels of the CP210X type in __init__.py
|
||||
static constexpr uint8_t MAX_CHANNELS = 3;
|
||||
|
||||
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
|
||||
bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override;
|
||||
void modem_control_transfer(USBUartChannelBase *channel) override;
|
||||
bool config_device_step(uint8_t step, bool ok, const uint8_t *response) override;
|
||||
void set_flow_transfer_(USBUartChannelBase *channel);
|
||||
|
||||
// The block last read for each channel, so a DTR/RTS change under RTS/CTS is a single SET_FLOW
|
||||
std::array<CP210xFlowBlock, MAX_CHANNELS> flow_blocks_{};
|
||||
// A CP2102N whose firmware has erratum E104
|
||||
bool no_flow_control_{false};
|
||||
};
|
||||
class USBUartTypeCH34X : public USBUartTypeCdcAcm {
|
||||
public:
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::valve {
|
||||
|
||||
class ValveTraits {
|
||||
public:
|
||||
ValveTraits() = default;
|
||||
|
||||
bool get_is_assumed_state() const { return this->is_assumed_state_; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->is_assumed_state_ = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->supports_position_; }
|
||||
void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; }
|
||||
bool get_supports_toggle() const { return this->supports_toggle_; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->supports_stop_; }
|
||||
void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; }
|
||||
bool get_is_assumed_state() const { return this->flags_.is_assumed_state; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->flags_.is_assumed_state = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->flags_.supports_position; }
|
||||
void set_supports_position(bool supports_position) { this->flags_.supports_position = supports_position; }
|
||||
bool get_supports_toggle() const { return this->flags_.supports_toggle; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->flags_.supports_toggle = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->flags_.supports_stop; }
|
||||
void set_supports_stop(bool supports_stop) { this->flags_.supports_stop = supports_stop; }
|
||||
|
||||
protected:
|
||||
bool is_assumed_state_{false};
|
||||
bool supports_position_{false};
|
||||
bool supports_toggle_{false};
|
||||
bool supports_stop_{false};
|
||||
// Bit-packed flags to save memory (1 byte instead of 4)
|
||||
struct ValveTraitFlags {
|
||||
uint8_t is_assumed_state : 1;
|
||||
uint8_t supports_position : 1;
|
||||
uint8_t supports_toggle : 1;
|
||||
uint8_t supports_stop : 1;
|
||||
uint8_t reserved : 4;
|
||||
} flags_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::valve
|
||||
@@ -29,9 +29,15 @@ MULTI_CONF = True
|
||||
|
||||
CONF_MAX_UPDATE_DELAY = "max_update_delay"
|
||||
CONF_UPDATE_DELAY = "update_delay"
|
||||
CONF_PIXEL_FORMAT = "pixel_format"
|
||||
|
||||
vnc_ns = cg.esphome_ns.namespace("vnc")
|
||||
VncClient = vnc_ns.class_("VncClient", cg.Component)
|
||||
VncPixelFormat = vnc_ns.enum("VncPixelFormat", is_class=True)
|
||||
|
||||
PIXEL_FORMATS = VncPixelFormat.options(
|
||||
"auto", "rgb565", "rgb332", prefix="VNC_PIXEL_FORMAT_"
|
||||
)
|
||||
|
||||
# Drivers that cannot take another component's pixels yet.
|
||||
UNSUPPORTED_DISPLAYS = dict.fromkeys(
|
||||
@@ -63,6 +69,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_MAX_UPDATE_DELAY, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_PIXEL_FORMAT, default="auto"): cv.enum(
|
||||
PIXEL_FORMATS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
|
||||
}
|
||||
@@ -128,6 +137,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
|
||||
cg.add(var.set_update_delay(config[CONF_UPDATE_DELAY]))
|
||||
cg.add(var.set_max_update_delay(config[CONF_MAX_UPDATE_DELAY]))
|
||||
cg.add(var.set_pixel_format(config[CONF_PIXEL_FORMAT]))
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@@ -135,4 +145,8 @@ VNC_ID_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(VncClient)},
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"vnc.refresh", VNC_ID_SCHEMA, automation.ApplyCall("refresh()")
|
||||
)
|
||||
|
||||
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
|
||||
@@ -4,10 +4,12 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
@@ -32,7 +34,19 @@ static constexpr uint8_t SECURITY_NONE = 1;
|
||||
static constexpr uint8_t SECURITY_VNC_AUTH = 2;
|
||||
|
||||
static constexpr int32_t ENCODING_RAW = 0;
|
||||
static constexpr int32_t ENCODING_HEXTILE = 5;
|
||||
static constexpr int32_t ENCODING_RICH_CURSOR = -239;
|
||||
// In preference order: TightVNC uses the first real encoding it supports for every rectangle.
|
||||
static constexpr int32_t ENCODINGS[] = {ENCODING_HEXTILE, ENCODING_RAW, ENCODING_RICH_CURSOR};
|
||||
|
||||
// Indexed by VncPixelFormat
|
||||
PROGMEM_STRING_TABLE(PixelFormatStrings, "auto", "RGB565", "RGB332");
|
||||
|
||||
static constexpr uint8_t HEXTILE_RAW = 0x01;
|
||||
static constexpr uint8_t HEXTILE_BACKGROUND = 0x02;
|
||||
static constexpr uint8_t HEXTILE_FOREGROUND = 0x04;
|
||||
static constexpr uint8_t HEXTILE_ANY_SUBRECTS = 0x08;
|
||||
static constexpr uint8_t HEXTILE_SUBRECTS_COLOURED = 0x10;
|
||||
|
||||
static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
|
||||
static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
|
||||
@@ -66,6 +80,32 @@ static int parse_three_digits(const uint8_t *p) {
|
||||
void VncClient::setup() {
|
||||
this->setup_ms_ = App.get_loop_component_start_time();
|
||||
this->link_.begin(TAG);
|
||||
// Sized for the display now; a rotation changed later swaps the sides but keeps the cell count.
|
||||
const uint32_t wanted = static_cast<uint32_t>((this->display_->get_width() + 15) / 16) *
|
||||
static_cast<uint32_t>((this->display_->get_height() + 15) / 16);
|
||||
const uint32_t cells = std::min<uint32_t>(wanted, UINT16_MAX);
|
||||
if (cells < wanted) {
|
||||
ESP_LOGW(TAG, "Display has %" PRIu32 " tiles, more than the %u the cache holds", wanted, UINT16_MAX);
|
||||
}
|
||||
RAMAllocator<uint32_t> allocator;
|
||||
this->tile_hashes_ = allocator.allocate(cells);
|
||||
if (this->tile_hashes_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Not enough memory to skip unchanged tiles");
|
||||
return;
|
||||
}
|
||||
this->hash_capacity_ = cells;
|
||||
this->clear_tiles_();
|
||||
}
|
||||
|
||||
void VncClient::refresh() {
|
||||
this->need_full_ = true;
|
||||
this->clear_tiles_();
|
||||
}
|
||||
|
||||
void VncClient::clear_tiles_() {
|
||||
if (this->tile_hashes_ != nullptr) {
|
||||
std::memset(this->tile_hashes_, 0, sizeof(uint32_t) * this->hash_capacity_);
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::dump_config() {
|
||||
@@ -74,9 +114,13 @@ void VncClient::dump_config() {
|
||||
" Server: %s:%u\n"
|
||||
" Reconnect Interval: %" PRIu32 "ms\n"
|
||||
" Update Delay: %" PRIu32 "ms\n"
|
||||
" Max Update Delay: %" PRIu32 "ms",
|
||||
" Max Update Delay: %" PRIu32 "ms\n"
|
||||
" Pixel Format: %s\n"
|
||||
" Tile Cache: %u tiles",
|
||||
this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
|
||||
this->max_update_delay_ms_);
|
||||
this->max_update_delay_ms_,
|
||||
LOG_STR_ARG(PixelFormatStrings::get_log_str(static_cast<uint8_t>(this->pixel_format_), 0)),
|
||||
this->hash_capacity_);
|
||||
}
|
||||
|
||||
void VncClient::on_shutdown() { this->link_.close(); }
|
||||
@@ -95,12 +139,28 @@ void VncClient::loop() {
|
||||
}
|
||||
// A display that parks its loop when idle is busy while that loop runs. Displays that never
|
||||
// park (no loop, or one that always runs) update synchronously, so they are never busy.
|
||||
const bool was_busy = this->display_busy_;
|
||||
if (this->display_->is_idle()) {
|
||||
this->display_parks_ = true;
|
||||
this->display_busy_ = false;
|
||||
} else {
|
||||
this->display_busy_ = this->display_parks_;
|
||||
}
|
||||
if (was_busy && !this->display_busy_) {
|
||||
// A refresh the driver gave up on (it sets its warning) leaves the panel behind the buffer: retry it
|
||||
// once and forget the cached tiles, which no longer say what the panel shows
|
||||
if (!this->display_->status_has_warning()) {
|
||||
this->refresh_retried_ = false;
|
||||
} else if (!this->refresh_retried_) {
|
||||
this->refresh_retried_ = true;
|
||||
this->clear_tiles_();
|
||||
if (!this->refresh_pending_) {
|
||||
this->first_change_ms_ = now;
|
||||
}
|
||||
this->last_change_ms_ = now;
|
||||
this->refresh_pending_ = true;
|
||||
}
|
||||
}
|
||||
// A stuck panel, or another owner of the display's loop, would otherwise freeze the screen silently
|
||||
if (!this->display_busy_) {
|
||||
this->busy_since_ms_ = now;
|
||||
@@ -147,6 +207,11 @@ void VncClient::loop() {
|
||||
this->service_rx_(now);
|
||||
}
|
||||
if (this->state_ == VncState::VNC_STATE_RUNNING) {
|
||||
// A new session lays the screen out again for the new rotation and redraws all of it.
|
||||
if (static_cast<uint8_t>(this->display_->get_rotation() / 90) != this->hash_rotation_) {
|
||||
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display rotation changed"));
|
||||
return;
|
||||
}
|
||||
// Mid-frame there is nothing to send, so a vanished server would never fail a write.
|
||||
if (this->in_update_ && !blocked && !this->rx_pending_ && now - this->last_rx_ms_ >= HEARTBEAT_MS) {
|
||||
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Server stopped sending"));
|
||||
@@ -183,12 +248,20 @@ void VncClient::connect_step_(uint32_t now) {
|
||||
}
|
||||
|
||||
void VncClient::reset_session_() {
|
||||
// Pixels drawn but never shown would match the cache when the next session resends them.
|
||||
if (this->refresh_pending_ || this->frame_drawn_) {
|
||||
this->clear_tiles_();
|
||||
}
|
||||
// Hextile colours carry from tile to tile; a new server must not inherit the last one's
|
||||
std::memset(this->hex_bg_, 0, sizeof(this->hex_bg_));
|
||||
std::memset(this->hex_fg_, 0, sizeof(this->hex_fg_));
|
||||
this->rx_pos_ = 0;
|
||||
this->rx_len_ = 0;
|
||||
this->rx_pending_ = false;
|
||||
this->in_update_ = false;
|
||||
this->request_outstanding_ = false;
|
||||
this->refresh_pending_ = false;
|
||||
this->need_full_ = false;
|
||||
this->frame_drawn_ = false;
|
||||
this->step_ = VncStep::VNC_STEP_VERSION;
|
||||
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
@@ -478,6 +551,7 @@ VncParse VncClient::parse_() {
|
||||
this->in_update_ = true;
|
||||
this->update_start_ms_ = millis();
|
||||
this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
|
||||
this->drawn_tiles_ = 0;
|
||||
this->rect_done_();
|
||||
break;
|
||||
case MSG_SET_COLOUR_MAP:
|
||||
@@ -517,7 +591,7 @@ VncParse VncClient::parse_() {
|
||||
const auto encoding = static_cast<int32_t>(read_be32(p + 8));
|
||||
this->rx_pos_ += 12;
|
||||
this->rects_left_--;
|
||||
if (encoding == ENCODING_RAW) {
|
||||
if (encoding == ENCODING_RAW || encoding == ENCODING_HEXTILE) {
|
||||
if (static_cast<uint32_t>(this->rect_x_) + this->rect_w_ > this->fb_w_ ||
|
||||
static_cast<uint32_t>(this->rect_y_) + this->rect_h_ > this->fb_h_) {
|
||||
ESP_LOGW(TAG, "Rectangle %ux%u at %u,%u is outside the %ux%u screen", this->rect_w_, this->rect_h_,
|
||||
@@ -526,10 +600,15 @@ VncParse VncClient::parse_() {
|
||||
}
|
||||
if (this->rect_w_ == 0 || this->rect_h_ == 0) {
|
||||
this->rect_done_();
|
||||
} else {
|
||||
} else if (encoding == ENCODING_RAW) {
|
||||
this->forget_tiles_(this->rect_x_, this->rect_y_, this->rect_w_, this->rect_h_);
|
||||
this->pos_x_ = 0;
|
||||
this->pos_y_ = 0;
|
||||
this->step_ = VncStep::VNC_STEP_RAW;
|
||||
} else {
|
||||
this->pos_x_ = this->rect_x_;
|
||||
this->pos_y_ = this->rect_y_;
|
||||
this->step_ = VncStep::VNC_STEP_TILE;
|
||||
}
|
||||
} else if (encoding == ENCODING_RICH_CURSOR) {
|
||||
if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
|
||||
@@ -570,6 +649,84 @@ VncParse VncClient::parse_() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_TILE: {
|
||||
if (this->display_busy_) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
if (avail < 1) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
if (this->budget_exhausted_()) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
const uint8_t flags = p[0];
|
||||
this->tile_w_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_x_) + this->rect_w_ - this->pos_x_);
|
||||
this->tile_h_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_y_) + this->rect_h_ - this->pos_y_);
|
||||
this->tile_hidden_ = this->pos_x_ >= this->vis_w_ || this->pos_y_ >= this->vis_h_;
|
||||
// With the raw bit set the other bits mean nothing and the pixels follow at once.
|
||||
if (flags & HEXTILE_RAW) {
|
||||
this->rx_pos_ += 1;
|
||||
this->tile_fill_ = 0;
|
||||
this->step_ = VncStep::VNC_STEP_TILE_RAW;
|
||||
break;
|
||||
}
|
||||
const size_t need = 1 + ((flags & HEXTILE_BACKGROUND) ? this->bpp_ : 0) +
|
||||
((flags & HEXTILE_FOREGROUND) ? this->bpp_ : 0) + ((flags & HEXTILE_ANY_SUBRECTS) ? 1 : 0);
|
||||
if (avail < need) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint8_t *q = p + 1;
|
||||
if (flags & HEXTILE_BACKGROUND) {
|
||||
std::memcpy(this->hex_bg_, q, this->bpp_);
|
||||
q += this->bpp_;
|
||||
}
|
||||
if (flags & HEXTILE_FOREGROUND) {
|
||||
std::memcpy(this->hex_fg_, q, this->bpp_);
|
||||
q += this->bpp_;
|
||||
}
|
||||
this->subrects_left_ = (flags & HEXTILE_ANY_SUBRECTS) ? *q : 0;
|
||||
this->subrects_coloured_ = (flags & HEXTILE_SUBRECTS_COLOURED) != 0;
|
||||
this->rx_pos_ += need;
|
||||
this->fill_tile_(0, 0, this->tile_w_, this->tile_h_, this->hex_bg_);
|
||||
this->step_ = VncStep::VNC_STEP_SUBRECTS;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_TILE_RAW: {
|
||||
const size_t total = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
|
||||
const size_t count = std::min(avail, total - this->tile_fill_);
|
||||
if (count == 0) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
std::memcpy(this->tile_ + this->tile_fill_, p, count);
|
||||
this->rx_pos_ += count;
|
||||
this->tile_fill_ += count;
|
||||
if (this->tile_fill_ == total) {
|
||||
this->draw_tile_();
|
||||
this->next_tile_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SUBRECTS: {
|
||||
if (this->subrects_left_ == 0) {
|
||||
this->draw_tile_();
|
||||
this->next_tile_();
|
||||
break;
|
||||
}
|
||||
const size_t need = (this->subrects_coloured_ ? this->bpp_ : 0) + 2;
|
||||
if (avail < need) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint8_t *pixel = this->hex_fg_;
|
||||
const uint8_t *q = p;
|
||||
if (this->subrects_coloured_) {
|
||||
pixel = q;
|
||||
q += this->bpp_;
|
||||
}
|
||||
this->fill_tile_(q[0] >> 4, q[0] & 0x0F, (q[1] >> 4) + 1, (q[1] & 0x0F) + 1, pixel);
|
||||
this->rx_pos_ += need;
|
||||
this->subrects_left_--;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SKIP: {
|
||||
const auto count = static_cast<uint32_t>(std::min<size_t>(avail, this->skip_left_));
|
||||
this->rx_pos_ += count;
|
||||
@@ -592,6 +749,20 @@ void VncClient::rect_done_() {
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::next_tile_() {
|
||||
this->step_ = VncStep::VNC_STEP_TILE;
|
||||
if (static_cast<uint32_t>(this->pos_x_) + 16 < static_cast<uint32_t>(this->rect_x_) + this->rect_w_) {
|
||||
this->pos_x_ += 16;
|
||||
return;
|
||||
}
|
||||
this->pos_x_ = this->rect_x_;
|
||||
if (static_cast<uint32_t>(this->pos_y_) + 16 < static_cast<uint32_t>(this->rect_y_) + this->rect_h_) {
|
||||
this->pos_y_ += 16;
|
||||
return;
|
||||
}
|
||||
this->rect_done_();
|
||||
}
|
||||
|
||||
void VncClient::frame_done_() {
|
||||
this->in_update_ = false;
|
||||
this->request_outstanding_ = false;
|
||||
@@ -604,9 +775,9 @@ void VncClient::frame_done_() {
|
||||
}
|
||||
this->last_change_ms_ = now;
|
||||
this->refresh_pending_ = true;
|
||||
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms",
|
||||
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms, %u hextile tiles drawn",
|
||||
this->rx_total_ - (this->rx_len_ - this->rx_pos_) - this->update_start_bytes_,
|
||||
millis() - this->update_start_ms_);
|
||||
millis() - this->update_start_ms_, this->drawn_tiles_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,8 +790,96 @@ void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t
|
||||
this->frame_drawn_ = true;
|
||||
}
|
||||
|
||||
void VncClient::draw_tile_() {
|
||||
if (this->tile_hidden_) {
|
||||
return;
|
||||
}
|
||||
const int w = std::min<int>(this->tile_w_, this->vis_w_ - this->pos_x_);
|
||||
const int h = std::min<int>(this->tile_h_, this->vis_h_ - this->pos_y_);
|
||||
uint32_t *cell = nullptr;
|
||||
uint32_t hash = 0;
|
||||
if (this->hash_cols_ != 0) {
|
||||
if ((this->pos_x_ | this->pos_y_) & 15) {
|
||||
// An off-grid tile spans several cells, and a later write to any of them would make its hash stale.
|
||||
this->forget_tiles_(this->pos_x_, this->pos_y_, w, h);
|
||||
} else {
|
||||
cell = this->tile_hashes_ + (this->pos_y_ / 16) * this->hash_cols_ + this->pos_x_ / 16;
|
||||
hash = this->tile_hash_();
|
||||
if (*cell == hash) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->display_->draw_pixels_at(this->pos_x_, this->pos_y_, w, h, this->tile_, this->bitness_, this->big_endian_, 0, 0,
|
||||
this->tile_w_ - w);
|
||||
this->frame_drawn_ = true;
|
||||
this->drawn_tiles_++;
|
||||
if (cell != nullptr) {
|
||||
*cell = hash;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t VncClient::tile_hash_() const {
|
||||
// The tile's size, pixel size and pixels; the cell gives its place. Zero means "unknown".
|
||||
uint32_t hash = fnv1a_hash_extend(FNV1_OFFSET_BASIS,
|
||||
static_cast<uint32_t>(this->tile_w_) << 16 | this->tile_h_ << 8 | this->bpp_);
|
||||
const size_t len = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
|
||||
size_t i = 0;
|
||||
for (; i + 4 <= len; i += 4) {
|
||||
uint32_t word;
|
||||
std::memcpy(&word, this->tile_ + i, sizeof(word)); // tile_ is aligned, so this is one load
|
||||
// Byte by byte: XOR-ing whole words lets high-bit changes in two words cancel.
|
||||
hash = fnv1a_hash_extend(hash, word);
|
||||
}
|
||||
for (; i < len; i++) {
|
||||
hash = fnv1a_hash_extend(hash, this->tile_[i]);
|
||||
}
|
||||
return hash == 0 ? 1 : hash;
|
||||
}
|
||||
|
||||
void VncClient::forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||
if (this->hash_cols_ == 0 || w == 0 || h == 0) {
|
||||
return;
|
||||
}
|
||||
const uint16_t col_end = std::min<uint16_t>((x + w - 1) / 16, this->hash_cols_ - 1);
|
||||
const uint16_t row_end = std::min<uint16_t>((y + h - 1) / 16, this->hash_rows_ - 1);
|
||||
for (uint16_t row = y / 16; row <= row_end; row++) {
|
||||
for (uint16_t col = x / 16; col <= col_end; col++) {
|
||||
this->tile_hashes_[row * this->hash_cols_ + col] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel) {
|
||||
if (this->tile_hidden_ || x >= this->tile_w_ || y >= this->tile_h_) {
|
||||
return;
|
||||
}
|
||||
w = std::min<uint8_t>(w, this->tile_w_ - x);
|
||||
h = std::min<uint8_t>(h, this->tile_h_ - y);
|
||||
const size_t stride = static_cast<size_t>(this->tile_w_) * this->bpp_;
|
||||
const size_t row_len = static_cast<size_t>(w) * this->bpp_;
|
||||
uint8_t *first = this->tile_ + y * stride + x * this->bpp_;
|
||||
if (this->bpp_ == 1) {
|
||||
std::memset(first, pixel[0], w);
|
||||
} else {
|
||||
for (size_t i = 0; i < row_len; i += 2) {
|
||||
first[i] = pixel[0];
|
||||
first[i + 1] = pixel[1];
|
||||
}
|
||||
}
|
||||
for (uint8_t row = 1; row < h; row++) {
|
||||
std::memcpy(first + row * stride, first, row_len);
|
||||
}
|
||||
}
|
||||
|
||||
bool VncClient::start_session_(uint32_t now) {
|
||||
const bool rgb565 = this->display_->get_display_type() != display::DISPLAY_TYPE_BINARY;
|
||||
VncPixelFormat format = this->pixel_format_;
|
||||
if (format == VncPixelFormat::VNC_PIXEL_FORMAT_AUTO) {
|
||||
format = this->display_->get_display_type() == display::DISPLAY_TYPE_BINARY
|
||||
? VncPixelFormat::VNC_PIXEL_FORMAT_RGB332
|
||||
: VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
|
||||
}
|
||||
const bool rgb565 = format == VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
|
||||
this->bpp_ = rgb565 ? 2 : 1;
|
||||
this->bitness_ = rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_332;
|
||||
|
||||
@@ -632,6 +891,24 @@ bool VncClient::start_session_(uint32_t now) {
|
||||
ESP_LOGW(TAG, "Server screen is %ux%u but the display is %dx%d; showing the top-left %ux%u", this->fb_w_,
|
||||
this->fb_h_, disp_w, disp_h, this->vis_w_, this->vis_h_);
|
||||
}
|
||||
const uint16_t cols = (this->vis_w_ + 15) / 16;
|
||||
const uint16_t rows = (this->vis_h_ + 15) / 16;
|
||||
const auto rotation = static_cast<uint8_t>(this->display_->get_rotation() / 90);
|
||||
const bool rotated = rotation != this->hash_rotation_;
|
||||
this->hash_rotation_ = rotation;
|
||||
if (this->tile_hashes_ == nullptr) {
|
||||
this->hash_cols_ = 0;
|
||||
this->hash_rows_ = 0;
|
||||
} else if (static_cast<uint32_t>(cols) * rows > this->hash_capacity_) {
|
||||
ESP_LOGW(TAG, "Screen has %u tiles, more than the %u cached; every tile is drawn", cols * rows,
|
||||
this->hash_capacity_);
|
||||
this->hash_cols_ = 0;
|
||||
this->hash_rows_ = 0;
|
||||
} else if (cols != this->hash_cols_ || rows != this->hash_rows_ || rotated) {
|
||||
this->hash_cols_ = cols;
|
||||
this->hash_rows_ = rows;
|
||||
this->clear_tiles_();
|
||||
}
|
||||
|
||||
// SetPixelFormat: true colour, in the byte order the display takes
|
||||
uint8_t pixel_format[20] = {};
|
||||
@@ -648,10 +925,12 @@ bool VncClient::start_session_(uint32_t now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SetEncodings: TightVNC uses the first real encoding it supports for every rectangle.
|
||||
uint8_t encodings[12] = {2, 0, 0, 2};
|
||||
write_be32(encodings + 4, ENCODING_RAW);
|
||||
write_be32(encodings + 8, static_cast<uint32_t>(ENCODING_RICH_CURSOR));
|
||||
// SetEncodings
|
||||
uint8_t encodings[4 + 4 * std::size(ENCODINGS)] = {2};
|
||||
write_be16(encodings + 2, std::size(ENCODINGS));
|
||||
for (size_t i = 0; i < std::size(ENCODINGS); i++) {
|
||||
write_be32(encodings + 4 + 4 * i, static_cast<uint32_t>(ENCODINGS[i]));
|
||||
}
|
||||
if (!this->send_(encodings, sizeof(encodings)) || !this->send_update_request_(false, now)) {
|
||||
return false;
|
||||
}
|
||||
@@ -669,6 +948,12 @@ void VncClient::pace_(uint32_t now) {
|
||||
if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
|
||||
return;
|
||||
}
|
||||
if (this->need_full_) {
|
||||
if (this->send_update_request_(false, now)) {
|
||||
this->need_full_ = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Repeating a parked request costs nothing on the server and detects a dead link.
|
||||
if (!this->request_outstanding_ ||
|
||||
(now - this->last_rx_ms_ >= HEARTBEAT_MS && now - this->last_request_ms_ >= HEARTBEAT_MS)) {
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
enum class VncPixelFormat : uint8_t {
|
||||
VNC_PIXEL_FORMAT_AUTO,
|
||||
VNC_PIXEL_FORMAT_RGB565,
|
||||
VNC_PIXEL_FORMAT_RGB332,
|
||||
};
|
||||
|
||||
enum class VncState : uint8_t {
|
||||
VNC_STATE_IDLE,
|
||||
VNC_STATE_HANDSHAKE,
|
||||
@@ -31,6 +37,9 @@ enum class VncStep : uint8_t {
|
||||
VNC_STEP_RECT,
|
||||
VNC_STEP_RECT_DONE,
|
||||
VNC_STEP_RAW,
|
||||
VNC_STEP_TILE,
|
||||
VNC_STEP_TILE_RAW,
|
||||
VNC_STEP_SUBRECTS,
|
||||
VNC_STEP_SKIP,
|
||||
};
|
||||
|
||||
@@ -55,6 +64,7 @@ class VncClient : public Component {
|
||||
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
|
||||
void set_update_delay(uint32_t ms) { this->update_delay_ms_ = ms; }
|
||||
void set_max_update_delay(uint32_t ms) { this->max_update_delay_ms_ = ms; }
|
||||
void set_pixel_format(VncPixelFormat format) { this->pixel_format_ = format; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
@@ -63,6 +73,8 @@ class VncClient : public Component {
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
bool is_connected() const { return this->state_ == VncState::VNC_STATE_RUNNING; }
|
||||
/// Ask the server for the whole screen again.
|
||||
void refresh();
|
||||
|
||||
template<typename F> void add_on_connect_callback(F &&callback) {
|
||||
this->connect_callback_.add(std::forward<F>(callback));
|
||||
@@ -73,6 +85,7 @@ class VncClient : public Component {
|
||||
|
||||
protected:
|
||||
static constexpr size_t RX_BUFFER_SIZE = 512;
|
||||
static constexpr size_t TILE_BUFFER_SIZE = 16 * 16 * 2;
|
||||
|
||||
void connect_step_(uint32_t now);
|
||||
void start_handshake_(uint32_t now);
|
||||
@@ -83,7 +96,13 @@ class VncClient : public Component {
|
||||
bool send_client_init_();
|
||||
void frame_done_();
|
||||
void rect_done_();
|
||||
void next_tile_();
|
||||
void draw_tile_();
|
||||
uint32_t tile_hash_() const;
|
||||
void clear_tiles_();
|
||||
void forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels);
|
||||
void fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel);
|
||||
void skip_(uint32_t count, VncStep next);
|
||||
bool budget_exhausted_() const;
|
||||
bool send_(const uint8_t *data, size_t len);
|
||||
@@ -100,6 +119,9 @@ class VncClient : public Component {
|
||||
LazyCallbackManager<void()> connect_callback_;
|
||||
LazyCallbackManager<void()> disconnect_callback_;
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
// Fingerprint of the tile last drawn at each 16x16 cell, so tiles the server resends unchanged are skipped.
|
||||
// A 32-bit FNV-1a collision keeps a stale tile until it changes again or vnc.refresh runs; accepted.
|
||||
uint32_t *tile_hashes_{nullptr};
|
||||
|
||||
uint32_t update_delay_ms_{200};
|
||||
uint32_t max_update_delay_ms_{1000};
|
||||
@@ -131,15 +153,28 @@ class VncClient : public Component {
|
||||
uint16_t rect_y_{0};
|
||||
uint16_t rect_w_{0};
|
||||
uint16_t rect_h_{0};
|
||||
// Raw: column and row inside the rect.
|
||||
// Raw: column and row inside the rect. Hextile: absolute origin of the current tile.
|
||||
uint16_t pos_x_{0};
|
||||
uint16_t pos_y_{0};
|
||||
uint16_t tile_fill_{0};
|
||||
uint16_t drawn_tiles_{0};
|
||||
uint16_t hash_capacity_{0};
|
||||
uint16_t hash_cols_{0};
|
||||
uint16_t hash_rows_{0};
|
||||
|
||||
uint8_t tile_w_{0};
|
||||
uint8_t tile_h_{0};
|
||||
uint8_t subrects_left_{0};
|
||||
uint8_t bpp_{2};
|
||||
uint8_t minor_{8};
|
||||
uint8_t sec_count_{0};
|
||||
uint8_t text_len_{0}; // clamped to MAX_TEXT_LEN
|
||||
// The display rotation the cached tiles were drawn at, in quarter turns.
|
||||
uint8_t hash_rotation_{0};
|
||||
uint8_t hex_bg_[2]{};
|
||||
uint8_t hex_fg_[2]{};
|
||||
display::ColorBitness bitness_{display::COLOR_BITNESS_565};
|
||||
VncPixelFormat pixel_format_{VncPixelFormat::VNC_PIXEL_FORMAT_AUTO};
|
||||
VncState state_{VncState::VNC_STATE_IDLE};
|
||||
VncStep step_{VncStep::VNC_STEP_VERSION};
|
||||
VncStep after_skip_{VncStep::VNC_STEP_MESSAGE};
|
||||
@@ -149,14 +184,19 @@ class VncClient : public Component {
|
||||
bool in_update_{false};
|
||||
bool request_outstanding_{false};
|
||||
bool refresh_pending_{false};
|
||||
bool need_full_{false};
|
||||
bool frame_drawn_{false};
|
||||
bool subrects_coloured_{false};
|
||||
bool tile_hidden_{false};
|
||||
bool display_parks_{false};
|
||||
bool display_busy_{false};
|
||||
bool busy_warned_{false};
|
||||
bool refresh_retried_{false}; // one retry per refresh the driver abandoned
|
||||
bool network_was_up_{false};
|
||||
bool big_endian_; // the pixel order the display takes
|
||||
|
||||
uint8_t rx_[RX_BUFFER_SIZE];
|
||||
alignas(4) uint8_t tile_[TILE_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
} // namespace esphome::vnc
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "weikai.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::weikai {
|
||||
|
||||
@@ -37,35 +38,34 @@ const char *p2s(uart::UARTParityOptions parity) {
|
||||
}
|
||||
|
||||
/// @brief Display a buffer in hexadecimal format (32 hex values / line) for debug
|
||||
void print_buffer(const uint8_t *data, size_t length) {
|
||||
void print_buffer(const char *tag, const uint8_t *data, size_t length, int level) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
|
||||
}
|
||||
}
|
||||
if (length % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (length % 32) + 2] = 0;
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
PROGMEM_STRING_TABLE(RegNamesPage0, "GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER", "SIFR", "TFCNT",
|
||||
"RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485");
|
||||
PROGMEM_STRING_TABLE(RegNamesPage1, "GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL", "TFTL", "FWTH",
|
||||
"FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY");
|
||||
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
const LogString *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
return LOG_STR("GPDAT");
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
return LOG_STR("GPDIR");
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
return page1 ? RegNamesPage1::get_log_str(reg & 0x0F, 0) : RegNamesPage0::get_log_str(reg & 0x0F, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,24 +495,6 @@ class Increment {
|
||||
uint8_t i_;
|
||||
};
|
||||
|
||||
/// @brief Hex converter to print/display a buffer in hexadecimal format (32 hex values / line).
|
||||
/// @param buffer contains the values to display
|
||||
void print_buffer(std::vector<uint8_t> buffer) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < buffer.size(); i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]);
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGI(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (buffer.size() % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (buffer.size() % 32) + 1] = 0;
|
||||
ESP_LOGI(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief test the write_array method
|
||||
void WeikaiChannel::uart_send_test_(char *message) {
|
||||
auto start_exec = micros();
|
||||
@@ -560,7 +542,7 @@ bool WeikaiChannel::uart_receive_test_(char *message) {
|
||||
for (size_t i = 0; i < RING_BUFFER_SIZE; i++) {
|
||||
if (buffer[i] != i % XFER_MAX_SIZE) {
|
||||
ESP_LOGE(TAG, "Read buffer contains error b=%x i=%x", buffer[i], i % XFER_MAX_SIZE);
|
||||
print_buffer(buffer);
|
||||
print_buffer(TAG, buffer.data(), buffer.size(), ESPHOME_LOG_LEVEL_INFO);
|
||||
status = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ constexpr size_t FIFO_SIZE = 256;
|
||||
/// @brief size of the ring buffer set to size of the FIFO
|
||||
constexpr size_t RING_BUFFER_SIZE = FIFO_SIZE;
|
||||
|
||||
/// @brief Name of a register for the log messages
|
||||
const LogString *reg_to_str(int reg, bool page1);
|
||||
|
||||
/// Logs a buffer in hexadecimal, 32 values per line, under @p tag at @p level.
|
||||
void print_buffer(const char *tag, const uint8_t *data, size_t length, int level = ESPHOME_LOG_LEVEL_VERY_VERBOSE);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief This is an helper class that provides a simple ring buffers that works as a FIFO
|
||||
/// @details This ring buffer is used to buffer the bytes received in the FIFO of the Weika device. The best way to read
|
||||
|
||||
@@ -8,38 +8,7 @@
|
||||
namespace esphome::weikai_i2c {
|
||||
ESPHOME_LOG_TAG(TAG, "weikai_i2c");
|
||||
|
||||
/// @brief Display a buffer in hexadecimal format (32 hex values / line).
|
||||
void print_buffer(const uint8_t *data, size_t length) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (length % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (length % 32) + 2] = 0;
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
using namespace weikai;
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
}
|
||||
}
|
||||
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
|
||||
|
||||
/// @brief Computes the I²C bus's address used to access the component
|
||||
@@ -72,11 +41,11 @@ uint8_t WeikaiRegisterI2C::read_reg() const {
|
||||
if (error == i2c::NO_ERROR) {
|
||||
this->comp_->status_clear_warning();
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
ESP_LOGE(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -91,7 +60,7 @@ void WeikaiRegisterI2C::read_fifo(uint8_t *data, size_t length) const {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_,
|
||||
(int) error, length);
|
||||
print_buffer(data, length);
|
||||
print_buffer(TAG, data, length);
|
||||
#endif
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
@@ -108,11 +77,11 @@ void WeikaiRegisterI2C::write_reg(uint8_t value) {
|
||||
if (error == i2c::NO_ERROR) {
|
||||
this->comp_->status_clear_warning();
|
||||
ESP_LOGVV(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%02X", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
ESP_LOGE(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%d", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +95,7 @@ void WeikaiRegisterI2C::write_fifo(uint8_t *data, size_t length) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WK2168Reg::write_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_,
|
||||
(int) error, length);
|
||||
print_buffer(data, length);
|
||||
print_buffer(TAG, data, length);
|
||||
#endif
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
|
||||
@@ -35,39 +35,6 @@ const char *p2s(uart::UARTParityOptions parity) {
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Display a buffer in hexadecimal format (32 hex values / line).
|
||||
void print_buffer(const uint8_t *data, size_t length) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (length % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (length % 32) + 2] = 0;
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
|
||||
enum CmdType { WRITE_CMD = 0, READ_CMD = 1 }; ///< Read or Write transfer
|
||||
|
||||
@@ -101,7 +68,7 @@ uint8_t WeikaiRegisterSPI::read_reg() const {
|
||||
spi_comp->disable();
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, cmd), cmd,
|
||||
reg_to_str(this->register_, this->comp_->page1()), this->channel_, val);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, this->comp_->page1())), this->channel_, val);
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -116,7 +83,7 @@ void WeikaiRegisterSPI::read_fifo(uint8_t *data, size_t length) const {
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(data, length);
|
||||
print_buffer(TAG, data, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -128,7 +95,7 @@ void WeikaiRegisterSPI::write_reg(uint8_t value) {
|
||||
spi_comp->disable();
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, buf[0]),
|
||||
buf[0], reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
|
||||
buf[0], LOG_STR_ARG(reg_to_str(this->register_, this->comp_->page1())), this->channel_, buf[1]);
|
||||
}
|
||||
|
||||
void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
@@ -143,7 +110,7 @@ void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(data, length);
|
||||
print_buffer(TAG, data, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
only_on_variant,
|
||||
request_wifi,
|
||||
require_idf_version,
|
||||
require_mbedtls_tls,
|
||||
require_mbedtls_tls_extras,
|
||||
)
|
||||
@@ -65,7 +66,6 @@ from esphome.const import (
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_USERNAME,
|
||||
PLACEHOLDER_WIFI_SSID,
|
||||
Framework,
|
||||
Platform,
|
||||
PlatformFramework,
|
||||
)
|
||||
@@ -233,10 +233,7 @@ def _validate_dns_servers(value: list) -> list:
|
||||
if any(ip.is_unspecified for ip in value):
|
||||
raise cv.Invalid("0.0.0.0 is not a DNS server the access point can offer")
|
||||
if len(value) > 1:
|
||||
cv.require_framework_version(
|
||||
esp_idf=cv.Version(6, 0, 0),
|
||||
extra_message="The access point offers a second DNS server from ESP-IDF 6.0",
|
||||
)(value)
|
||||
require_idf_version(cv.Version(6, 0, 0), "A second DNS server")(value)
|
||||
return value
|
||||
|
||||
|
||||
@@ -645,7 +642,6 @@ def _lease_cache_schema(value: Any) -> ConfigType:
|
||||
value = LEASE_CACHE_SCHEMA(value)
|
||||
if value[CONF_ENABLED]:
|
||||
cv.only_on_esp32(value)
|
||||
cv.only_with_framework(Framework.ESP_IDF)(value)
|
||||
if CONF_STORAGE not in value:
|
||||
if not preferences.rtc_supported():
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -297,6 +297,7 @@
|
||||
#define USE_STATUS_LED
|
||||
#define USE_STATUS_SENSOR
|
||||
#define USE_SWITCH
|
||||
#define USE_SWITCH_STORAGE
|
||||
#define USE_TEXT
|
||||
#define USE_TEXT_SENSOR
|
||||
#define USE_TEXT_SENSOR_FILTER
|
||||
|
||||
@@ -42,6 +42,7 @@ from esphome.helpers import (
|
||||
sanitize,
|
||||
snake_case,
|
||||
)
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType, EntityMetadata, Expression, SafeExpType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -764,3 +765,50 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
# restore_state: keywords, shared by the entity domains that save their state
|
||||
RESTORE_STATE_KEEP = "KEEP"
|
||||
RESTORE_STATE_INVERT = "INVERT"
|
||||
RESTORE_STATE_INITIAL = "INITIAL"
|
||||
RESTORE_STATE_ALL = "ALL"
|
||||
RESTORE_STATE_NONE = "NONE"
|
||||
|
||||
|
||||
def restore_state_keyword(value: Any, keywords: tuple[str, ...]) -> str | None:
|
||||
"""The upper-cased keyword when `value` spells one of `keywords`, else None."""
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper in keywords:
|
||||
return upper
|
||||
return None
|
||||
|
||||
|
||||
def restore_state_schema(fields_schema: cv.Schema, none: Any) -> Callable[[Any], Any]:
|
||||
"""The validator for a domain's `restore_state:` key.
|
||||
|
||||
A mapping is validated with `fields_schema`. The shorthand `all` is the empty mapping,
|
||||
so every field keeps its saved value, and `none` is returned as it is; a mapping goes through
|
||||
`fields_schema` like one the user wrote, so each config gets its own copy.
|
||||
"""
|
||||
|
||||
@schema_extractor("schema")
|
||||
def validate(value: Any) -> Any:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
# The shorthands have no representation in the extracted schema
|
||||
return fields_schema
|
||||
keyword = restore_state_keyword(value, (RESTORE_STATE_ALL, RESTORE_STATE_NONE))
|
||||
if keyword == RESTORE_STATE_ALL:
|
||||
value = {}
|
||||
elif keyword == RESTORE_STATE_NONE:
|
||||
if not isinstance(none, dict):
|
||||
return none
|
||||
value = none
|
||||
elif isinstance(value, str):
|
||||
raise cv.Invalid(
|
||||
f"Unknown restore_state '{value}'; expected 'all', 'none' or a mapping, "
|
||||
"for example '{state: INVERT}'"
|
||||
)
|
||||
return fields_schema(value)
|
||||
|
||||
return validate
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Collects fixed length binary frames of N bytes that start with the header bytes HEADER...; the binary twin of
|
||||
/// LineReader. Bytes before a header are skipped; checksums are left to the caller.
|
||||
template<size_t N, uint8_t... HEADER> class FrameReader {
|
||||
using pos_t = std::conditional_t<(N <= 0xFF), uint8_t, uint16_t>;
|
||||
static constexpr size_t HEADER_LEN = sizeof...(HEADER);
|
||||
static_assert(HEADER_LEN >= 1 && HEADER_LEN <= 8, "FrameReader takes 1 to 8 header bytes"); // bounds replay_() depth
|
||||
static_assert(HEADER_LEN < N && N <= 0xFFFF, "FrameReader size out of range");
|
||||
|
||||
public:
|
||||
/// Feed one byte; returns the completed frame, header included, valid until the next call, or nullptr.
|
||||
const uint8_t *feed(uint8_t c) {
|
||||
pos_t pos = this->pos_;
|
||||
if (pos < HEADER_LEN && c != header_byte(pos)) {
|
||||
// A later byte of the broken header, or c, may start the real one
|
||||
this->buf_[pos] = c;
|
||||
this->replay_(pos);
|
||||
return nullptr;
|
||||
}
|
||||
this->buf_[pos++] = c;
|
||||
if (pos < N) {
|
||||
this->pos_ = pos;
|
||||
return nullptr;
|
||||
}
|
||||
this->pos_ = 0;
|
||||
return this->buf_;
|
||||
}
|
||||
|
||||
/// Number of bytes of the partial frame; 0 after a frame is returned and while looking for a header.
|
||||
size_t length() const { return this->pos_; }
|
||||
|
||||
/// Forget the partial frame and look for a header from the next byte.
|
||||
void reset() { this->pos_ = 0; }
|
||||
|
||||
/// For a frame the caller rejected (bad checksum): drop its first byte and realign on a header in the rest.
|
||||
/// Call only right after feed() returned that frame; use reset() otherwise.
|
||||
void resync() { this->replay_(N - 1); }
|
||||
|
||||
protected:
|
||||
// A fold, not an array, so no header table lands in RAM on ESP8266
|
||||
static constexpr uint8_t header_byte(size_t i) {
|
||||
size_t j = 0;
|
||||
uint8_t r = 0;
|
||||
((r = (j++ == i) ? HEADER : r), ...);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Start over and feed buf_[1..last] again; never completes a frame as last < N
|
||||
void replay_(size_t last) {
|
||||
this->pos_ = 0;
|
||||
for (size_t i = 1; i <= last; i++) {
|
||||
this->feed(this->buf_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t buf_[N];
|
||||
pos_t pos_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
+2
-2
@@ -132,8 +132,8 @@ UDP_FLAG_COMMITTED = 0x80
|
||||
UDP_MSG_ACK = 0x40
|
||||
UDP_HEADER_SIZE = 9
|
||||
UDP_ACK_HEADER_SIZE = 12
|
||||
# Stream bytes per datagram, within one Ethernet frame
|
||||
UDP_MAX_PAYLOAD = 1400
|
||||
# A 1280-byte Thread IPv6 packet minus the IPv6, UDP, and OTA headers
|
||||
UDP_MAX_PAYLOAD = 1280 - 40 - 8 - UDP_HEADER_SIZE
|
||||
# Messages the device queues unconsumed (UDP_WINDOW in ota_esphome.h); as many
|
||||
# as TCP's window holds, so UDP costs the device no more memory than TCP
|
||||
UDP_WINDOW = 4
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ lib_deps =
|
||||
improv/Improv@1.2.7 ; improv_serial / improv_ble
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
|
||||
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
|
||||
; This dependency is used only in unit tests.
|
||||
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
|
||||
; See scripts/cpp_unit_test.py and tests/components/README.md
|
||||
@@ -253,7 +253,7 @@ lib_deps =
|
||||
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
|
||||
improv/Improv@1.2.7 ; improv_serial
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
|
||||
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
-DUSE_LIBRETINY
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ pyserial==3.5
|
||||
platformio==6.2.0
|
||||
esptool==5.6.0
|
||||
click==8.5.0
|
||||
aioesphomeapi==46.21.0
|
||||
aioesphomeapi==46.22.0
|
||||
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
|
||||
zeroconf==0.151.5
|
||||
puremagic==2.2.0
|
||||
@@ -31,9 +31,9 @@ smp==4.2.0; platform_system != "Darwin" or platform_machine != "x86_64"
|
||||
smp==4.1.0; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
requests==2.34.2
|
||||
py7zr==1.1.4
|
||||
platformdirs==4.13.4 # native esp-idf toolchain global cache dir
|
||||
platformdirs==4.13.5 # native esp-idf toolchain global cache dir
|
||||
ninja==1.13.2 # native host and esp8266 arduino toolchain build driver
|
||||
filelock==4.2.2 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
|
||||
filelock==4.2.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.3.3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pylint==4.1.3
|
||||
ruff==0.17.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
pyupgrade==3.22.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
prek==0.5.5 # .github/workflows/ci.yml reads this pin
|
||||
prek==0.5.6 # .github/workflows/ci.yml reads this pin
|
||||
yamlrocks==0.7.1 # used by script/sync_dependency_versions.py
|
||||
|
||||
# Unit tests
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Tests for the ds3231 hub and alarm action validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components import ds3231
|
||||
from esphome.components.ds3231 import (
|
||||
ALARM_1_MODES,
|
||||
ALARM_2_MODES,
|
||||
CONF_DAY_OF_MONTH,
|
||||
CONF_DAY_OF_WEEK,
|
||||
SQUARE_WAVE_FREQUENCIES,
|
||||
SquareWaveFrequency,
|
||||
validate_square_wave_frequency,
|
||||
)
|
||||
from esphome.const import CONF_MODE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "frequency"),
|
||||
[
|
||||
("1Hz", SquareWaveFrequency.SQUARE_WAVE_1HZ),
|
||||
("1024Hz", SquareWaveFrequency.SQUARE_WAVE_1024HZ),
|
||||
("1.024kHz", SquareWaveFrequency.SQUARE_WAVE_1024HZ),
|
||||
("4096 Hz", SquareWaveFrequency.SQUARE_WAVE_4096HZ),
|
||||
(8192, SquareWaveFrequency.SQUARE_WAVE_8192HZ),
|
||||
],
|
||||
)
|
||||
def test_square_wave_frequency_accepts_every_spelling(
|
||||
value: str | int, frequency: object
|
||||
) -> None:
|
||||
assert validate_square_wave_frequency(value) == frequency
|
||||
|
||||
|
||||
def test_square_wave_frequency_rejects_other_rates() -> None:
|
||||
with pytest.raises(cv.Invalid, match="valid options are"):
|
||||
validate_square_wave_frequency("2048Hz")
|
||||
assert list(SQUARE_WAVE_FREQUENCIES) == [1, 1024, 4096, 8192]
|
||||
|
||||
|
||||
def test_alarm_modes_map_to_their_enumerators() -> None:
|
||||
assert str(ALARM_1_MODES["every_second"]) == "ds3231::ALARM_MODE_EVERY_SECOND"
|
||||
assert str(ALARM_1_MODES["weekly"]) == "ds3231::ALARM_MODE_WEEKLY"
|
||||
assert "every_second" not in ALARM_2_MODES
|
||||
|
||||
|
||||
def _alarm_1_schema() -> cv.Schema:
|
||||
return ds3231._alarm_schema(ALARM_1_MODES, with_second=True)
|
||||
|
||||
|
||||
def test_monthly_alarm_needs_a_day_of_month() -> None:
|
||||
with pytest.raises(cv.Invalid, match="'day_of_month' is required in monthly mode"):
|
||||
_alarm_1_schema()({CONF_MODE: "monthly"})
|
||||
|
||||
|
||||
def test_weekly_alarm_needs_a_day_of_week() -> None:
|
||||
with pytest.raises(cv.Invalid, match="'day_of_week' is required in weekly mode"):
|
||||
_alarm_1_schema()({CONF_MODE: "weekly"})
|
||||
|
||||
|
||||
def test_daily_alarm_fills_the_unused_days() -> None:
|
||||
config = _alarm_1_schema()({CONF_MODE: "daily"})
|
||||
assert config[CONF_DAY_OF_MONTH] == 1
|
||||
assert config[CONF_DAY_OF_WEEK] == 1
|
||||
|
||||
|
||||
def test_alarm_2_rejects_every_second() -> None:
|
||||
schema = ds3231._alarm_schema(ALARM_2_MODES, with_second=False)
|
||||
with pytest.raises(cv.Invalid, match="Unknown value 'every_second'"):
|
||||
schema({CONF_MODE: "every_second"})
|
||||
@@ -32,8 +32,10 @@ from esphome.components.esp32 import (
|
||||
_user_sdkconfig_wants_tls,
|
||||
allow_insecure_tls,
|
||||
idf_version,
|
||||
require_idf_version,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_BOARD,
|
||||
KEY_ESP32,
|
||||
KEY_EXCLUDE_COMPONENTS,
|
||||
KEY_IDF_VERSION,
|
||||
@@ -2442,3 +2444,44 @@ def test_allow_insecure_tls_only_switches_the_flags_on(
|
||||
"CONFIG_ESP_TLS_INSECURE": True,
|
||||
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform_framework", "idf", "ok"),
|
||||
[
|
||||
(PlatformFramework.ESP32_IDF, cv.Version(5, 5, 3), True),
|
||||
(PlatformFramework.ESP32_IDF, cv.Version(5, 5, 2), False),
|
||||
(PlatformFramework.ESP32_ARDUINO, cv.Version(5, 5, 3), True),
|
||||
(PlatformFramework.ESP32_ARDUINO, cv.Version(5, 5, 2), False),
|
||||
],
|
||||
)
|
||||
def test_require_idf_version_compares_the_idf_underneath(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
platform_framework: PlatformFramework,
|
||||
idf: cv.Version,
|
||||
ok: bool,
|
||||
) -> None:
|
||||
set_core_config(
|
||||
platform_framework,
|
||||
platform_data={
|
||||
KEY_BOARD: "esp32dev",
|
||||
KEY_VARIANT: VARIANT_ESP32,
|
||||
KEY_IDF_VERSION: idf,
|
||||
},
|
||||
)
|
||||
validator = require_idf_version(cv.Version(5, 5, 3), "The thing")
|
||||
if ok:
|
||||
assert validator("x") == "x"
|
||||
else:
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="The thing requires ESP-IDF 5.5.3 or newer, got 5.5.2"
|
||||
):
|
||||
validator("x")
|
||||
|
||||
|
||||
def test_require_idf_version_rejects_other_platforms(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
with pytest.raises(cv.Invalid, match="only available on"):
|
||||
require_idf_version(cv.Version(5, 5, 3))("x")
|
||||
@@ -10,6 +10,7 @@ from esphome.components.esp32 import (
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
)
|
||||
from esphome.components.ethernet import (
|
||||
CONF_CLOCK_SPEED,
|
||||
@@ -102,6 +103,39 @@ def test_ch390_rejects_clock_speed_above_the_datasheet_maximum(
|
||||
CONFIG_SCHEMA({**_CH390_CONFIG, CONF_CLOCK_SPEED: "80MHz"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("idf_version", "match"),
|
||||
[
|
||||
(
|
||||
cv.Version(5, 5, 3),
|
||||
r"\(RGMII\) PHY requires ESP-IDF 6\.0\.0 or newer, got 5\.5\.3",
|
||||
),
|
||||
(cv.Version(6, 0, 0), None),
|
||||
],
|
||||
)
|
||||
def test_rgmii_phy_needs_idf_6(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
idf_version: cv.Version,
|
||||
match: str | None,
|
||||
) -> None:
|
||||
"""The internal EMAC only drives RGMII from ESP-IDF 6.0 on."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={
|
||||
KEY_BOARD: "esp32-s31-devkitc-1",
|
||||
KEY_VARIANT: VARIANT_ESP32S31,
|
||||
KEY_IDF_VERSION: idf_version,
|
||||
},
|
||||
)
|
||||
CORE.name = "rgmii-test"
|
||||
config = {"type": "GENERIC", "mdc_pin": 23, "mdio_pin": 18}
|
||||
if match is None:
|
||||
CONFIG_SCHEMA(config)
|
||||
else:
|
||||
with pytest.raises(Invalid, match=match):
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
_LAN8720_CONFIG = {"type": "LAN8720", "mdc_pin": 23, "mdio_pin": 18}
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ from esphome.components.light import (
|
||||
EffectRef,
|
||||
_final_validate,
|
||||
_get_data,
|
||||
_initial_effect_index,
|
||||
_validate_initial_state_effect,
|
||||
available_effects_str,
|
||||
find_effect_index,
|
||||
)
|
||||
@@ -21,8 +23,14 @@ from esphome.components.light.automation import (
|
||||
_record_effect_ref,
|
||||
)
|
||||
from esphome.config import Config, path_context
|
||||
from esphome.const import CONF_EFFECT, CONF_EFFECTS, CONF_ID, CONF_NAME
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.const import (
|
||||
CONF_EFFECT,
|
||||
CONF_EFFECTS,
|
||||
CONF_ID,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_NAME,
|
||||
)
|
||||
from esphome.core import ID, EsphomeError, Lambda
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -56,6 +64,12 @@ def test_find_effect_index_empty() -> None:
|
||||
assert find_effect_index([], "anything") is None
|
||||
|
||||
|
||||
def test_find_effect_index_none_is_zero() -> None:
|
||||
effects = _make_effects("Fast Pulse")
|
||||
assert find_effect_index(effects, "None") == 0
|
||||
assert find_effect_index([], "none") == 0
|
||||
|
||||
|
||||
# --- available_effects_str ---
|
||||
|
||||
|
||||
@@ -403,3 +417,55 @@ def test_record_effect_cycle_ref() -> None:
|
||||
assert len(data.effect_cycle_refs) == 1
|
||||
assert data.effect_cycle_refs[0].light_id is light_id
|
||||
assert data.effect_cycle_refs[0].component_path == ["esphome"]
|
||||
|
||||
|
||||
# --- _validate_initial_state_effect ---
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["slow pulse", "SLOW PULSE", "none"])
|
||||
def test_initial_state_effect_valid(name: str) -> None:
|
||||
config: ConfigType = {
|
||||
CONF_INITIAL_STATE: {CONF_EFFECT: name},
|
||||
CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse"),
|
||||
}
|
||||
assert _validate_initial_state_effect(config) is config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["none", "NONE", "None"])
|
||||
def test_initial_state_effect_none_without_effects(name: str) -> None:
|
||||
config: ConfigType = {CONF_INITIAL_STATE: {CONF_EFFECT: name}}
|
||||
assert _validate_initial_state_effect(config) is config
|
||||
|
||||
|
||||
def test_initial_state_effect_unknown_raises() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_INITIAL_STATE: {CONF_EFFECT: "Missing"},
|
||||
CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse"),
|
||||
}
|
||||
with pytest.raises(cv.Invalid) as exc_info:
|
||||
_validate_initial_state_effect(config)
|
||||
assert "Missing" in str(exc_info.value)
|
||||
assert "'Fast Pulse', 'Slow Pulse'" in str(exc_info.value)
|
||||
assert exc_info.value.path == [CONF_INITIAL_STATE, CONF_EFFECT]
|
||||
|
||||
|
||||
def test_initial_effect_index_unknown_raises() -> None:
|
||||
"""Codegen never emits an unresolved effect, even if validation was skipped."""
|
||||
config: ConfigType = {
|
||||
CONF_ID: ID("led1", is_declaration=True),
|
||||
CONF_INITIAL_STATE: {CONF_EFFECT: "Missing"},
|
||||
CONF_EFFECTS: _make_effects("Fast Pulse"),
|
||||
}
|
||||
with pytest.raises(EsphomeError, match="light 'led1'.*'Fast Pulse'"):
|
||||
_initial_effect_index(config)
|
||||
|
||||
|
||||
def test_initial_effect_index_resolves() -> None:
|
||||
effects = _make_effects("Fast Pulse", "Slow Pulse")
|
||||
assert _initial_effect_index({CONF_EFFECTS: effects}) == 0
|
||||
assert (
|
||||
_initial_effect_index(
|
||||
{CONF_INITIAL_STATE: {CONF_EFFECT: "slow pulse"}, CONF_EFFECTS: effects}
|
||||
)
|
||||
== 2
|
||||
)
|
||||
Loaded 100 of 268 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user