mirror of
https://github.com/esphome/esphome.git
synced 2026-10-25 23:42:15 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
852676e47e | ||
|
|
21fb25a45f | ||
|
|
2a6feb0448 | ||
|
|
16b25e81aa |
No files matched your search
@@ -17,16 +17,20 @@ void IPAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const n
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ips[0].str_to(buf);
|
||||
this->publish_state(buf);
|
||||
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
|
||||
uint8_t sensor = 0;
|
||||
for (const auto &ip : ips) {
|
||||
if (ip.is_set()) {
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
if (!ip.is_set())
|
||||
continue;
|
||||
if (sensor == ETHERNET_INFO_IP_SENSOR_COUNT)
|
||||
break;
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void DNSAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
|
||||
|
||||
@@ -18,14 +18,19 @@ class IPAddressEthernetInfo final : public Component,
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
|
||||
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
|
||||
#endif
|
||||
|
||||
// EthernetIPStateListener interface
|
||||
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
|
||||
const network::IPAddress &dns2) override;
|
||||
|
||||
protected:
|
||||
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
|
||||
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
|
||||
// Sized to the highest configured address_N
|
||||
std::array<text_sensor::TextSensor *, ETHERNET_INFO_IP_SENSOR_COUNT> ip_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
class DNSAddressEthernetInfo final : public Component,
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.types import ConfigType
|
||||
DEPENDENCIES = ["ethernet"]
|
||||
|
||||
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
|
||||
_request_ip_sensor_slot = cg.slot_counter("ETHERNET_INFO_IP_SENSOR_COUNT")
|
||||
|
||||
IPAddressEthernetInfo = ethernet_info_ns.class_(
|
||||
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.Component
|
||||
@@ -57,10 +58,13 @@ async def to_code(config: ConfigType) -> None:
|
||||
if conf := config.get(CONF_IP_ADDRESS):
|
||||
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
||||
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
|
||||
for x in range(5):
|
||||
if sensor_conf := conf.get(f"address_{x}"):
|
||||
sens = await text_sensor.new_text_sensor(sensor_conf)
|
||||
cg.add(ip_info.add_ip_sensors(x, sens))
|
||||
indexes = [x for x in range(5) if f"address_{x}" in conf]
|
||||
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
|
||||
for _ in range(max(indexes, default=-1) + 1):
|
||||
_request_ip_sensor_slot(str(ip_info))
|
||||
for x in indexes:
|
||||
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
|
||||
cg.add(ip_info.add_ip_sensors(x, sens))
|
||||
if conf := config.get(CONF_DNS_ADDRESS):
|
||||
dns_info = await text_sensor.new_text_sensor(conf)
|
||||
await cg.register_component(dns_info, conf)
|
||||
|
||||
@@ -9,16 +9,33 @@ 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");
|
||||
PROGMEM_STRING_TABLE(FoutNames, "32768 Hz", "1024 Hz", "1 Hz", "off");
|
||||
PROGMEM_STRING_TABLE(EvinPullNames, "none", "pullup_500k", "pullup_1m", "pullup_10m", "pulldown_500k");
|
||||
PROGMEM_STRING_TABLE(BootFailureNames, "RX8111 battery configuration failed", "RX8111 FOUT configuration failed",
|
||||
"RX8111 EVIN configuration failed", ESP_LOG_MSG_COMM_FAIL, ESP_LOG_MSG_COMM_FAIL);
|
||||
|
||||
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_REG_TIMER_CONTROL = 0x2D;
|
||||
static constexpr uint8_t RX8111_REG_POWER_SWITCH = 0x32;
|
||||
static constexpr uint8_t RX8111_REG_TIMESTAMP_CONTROL = 0x34;
|
||||
static constexpr uint8_t RX8111_TIMESTAMP_EISEL = 0x04;
|
||||
static constexpr uint8_t RX8111_REG_EVIN = 0x2B;
|
||||
static constexpr uint8_t RX8111_EXTENSION_ETS = 0x04;
|
||||
static constexpr uint8_t RX8111_CONTROL_EIE = 0x04;
|
||||
static constexpr uint8_t RX8111_FLAG_EVF = 0x04;
|
||||
static constexpr uint8_t RX8111_POWER_CHGEN = 0x80;
|
||||
static constexpr uint8_t RX8111_POWER_INIEN = 0x40;
|
||||
static constexpr uint8_t RX8111_POWER_SWSEL = 0x0C;
|
||||
static constexpr uint8_t RX8111_POWER_VDD_ONLY = 0x08;
|
||||
static constexpr uint8_t RX8111_FOUT_FSEL = 0xC0;
|
||||
static constexpr uint8_t RX8111_TIMER_TMPIN = 0x02;
|
||||
static constexpr uint8_t RX8111_TIMER_MODE = 0x0F; // TBKON, TBKE, TMPIN, TSTP
|
||||
static constexpr uint8_t RX8111_ALARM_DISABLED = 0x80;
|
||||
static constexpr uint8_t RX8111_EXTENSION_WADA = 0x08;
|
||||
static constexpr uint8_t RX8111_CONTROL_AIE = 0x08;
|
||||
@@ -36,17 +53,121 @@ 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 uint32_t RX8111_BOOT_BATTERY_RETRY_ID = 3;
|
||||
static constexpr uint32_t RX8111_BOOT_FOUT_RETRY_ID = 4;
|
||||
static constexpr uint32_t RX8111_BOOT_EVENT_RETRY_ID = 5;
|
||||
static constexpr uint8_t RX8111_BOOT_MAX_ATTEMPTS = 3;
|
||||
|
||||
void RX8111Component::setup() {
|
||||
if (this->battery_configured_)
|
||||
this->configure_boot_battery_();
|
||||
if (this->fout_configured_)
|
||||
this->configure_boot_fout_();
|
||||
if (this->event_configured_)
|
||||
this->configure_boot_event_();
|
||||
this->configure_boot_alarm_();
|
||||
this->configure_boot_timer_();
|
||||
this->read_time();
|
||||
if (this->alarm_enabled_ || this->watching_()) {
|
||||
if (this->watching_()) {
|
||||
this->start_event_polling_();
|
||||
}
|
||||
}
|
||||
|
||||
void RX8111Component::configure_boot_event_() {
|
||||
const bool success = this->configure_event_();
|
||||
if (success) {
|
||||
this->event_boot_pending_ = false;
|
||||
if (this->watching_())
|
||||
this->start_event_polling_();
|
||||
} else {
|
||||
this->event_boot_pending_ = true;
|
||||
}
|
||||
this->boot_config_result_(BOOT_EVENT, success);
|
||||
}
|
||||
|
||||
bool RX8111Component::configure_event_() {
|
||||
// An explicit disable affects only /INT; leave retained capture settings untouched.
|
||||
if (!this->event_enabled_)
|
||||
return this->update_register(RX8111_REG_CONTROL, RX8111_CONTROL_EIE, 0);
|
||||
// ETS gates EVIN detection, not just timestamp publication. Preserve all other
|
||||
// extension bits, including FOUT, alarm and timer settings, and retained records.
|
||||
return this->update_register(RX8111_REG_EVIN, 0xFC, this->evin_settings_ & 0xFC) &&
|
||||
this->update_register(RX8111_REG_TIMESTAMP_CONTROL, RX8111_TIMESTAMP_EISEL, 0) &&
|
||||
this->update_register(RX8111_REG_EXTENSION, RX8111_EXTENSION_ETS, RX8111_EXTENSION_ETS) &&
|
||||
this->update_register(RX8111_REG_CONTROL, RX8111_CONTROL_EIE, RX8111_CONTROL_EIE);
|
||||
}
|
||||
|
||||
bool RX8111Component::clear_event_flag() {
|
||||
// FLAG is write-zero-to-clear; ones preserve other events, even if they latch during the write.
|
||||
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_EVF))) {
|
||||
this->set_warning_(WARN_EVENT_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->event_flag_seen_ = false;
|
||||
this->clear_warning_(WARN_EVENT_FLAG_CLEAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RX8111Component::configure_boot_battery_() {
|
||||
// Own SWSEL as well as INIEN/CHGEN: 10 selects VDD when automatic switching is disabled
|
||||
// and is legal when enabled (unlike 11). Preserve SMPT and reserved bits.
|
||||
const uint8_t value = RX8111_POWER_VDD_ONLY | (this->battery_backup_ ? RX8111_POWER_INIEN : 0) |
|
||||
(this->battery_backup_ && this->battery_charging_ ? RX8111_POWER_CHGEN : 0);
|
||||
this->boot_config_result_(BOOT_BATTERY,
|
||||
this->update_register(RX8111_REG_POWER_SWITCH,
|
||||
RX8111_POWER_INIEN | RX8111_POWER_CHGEN | RX8111_POWER_SWSEL, value));
|
||||
}
|
||||
|
||||
void RX8111Component::configure_boot_fout_() {
|
||||
// TMPIN routes timer interrupts to FOUT instead of /INT; do not silently change that routing.
|
||||
uint8_t timer_control;
|
||||
bool success = this->read_byte(RX8111_REG_TIMER_CONTROL, &timer_control);
|
||||
if (success && this->fout_frequency_ != 3 && (timer_control & RX8111_TIMER_TMPIN)) {
|
||||
ESP_LOGW(TAG, "FOUT is routed to the wake-up timer (TMPIN); cannot configure clock output");
|
||||
success = false;
|
||||
} else if (success) {
|
||||
success =
|
||||
this->update_register(RX8111_REG_EXTENSION, RX8111_FOUT_FSEL, static_cast<uint8_t>(this->fout_frequency_ << 6));
|
||||
}
|
||||
this->boot_config_result_(BOOT_FOUT, success);
|
||||
}
|
||||
|
||||
// A timer start clears TMPIN, which may be what failed FOUT; give it a fresh set of attempts.
|
||||
void RX8111Component::retry_fout_after_timer_start_() {
|
||||
if (!this->fout_configured_ || (this->warnings_ & WARN_FOUT) == 0)
|
||||
return;
|
||||
this->cancel_timeout(RX8111_BOOT_FOUT_RETRY_ID);
|
||||
this->boot_attempts_[BOOT_FOUT] = 0;
|
||||
this->configure_boot_fout_();
|
||||
}
|
||||
|
||||
void RX8111Component::boot_config_result_(BootFeature feature, bool success) {
|
||||
struct BootConfig {
|
||||
uint32_t retry_id;
|
||||
void (RX8111Component::*configure)();
|
||||
uint16_t warning;
|
||||
};
|
||||
static constexpr BootConfig CONFIGS[BOOT_FEATURE_COUNT] PROGMEM = {
|
||||
{RX8111_BOOT_BATTERY_RETRY_ID, &RX8111Component::configure_boot_battery_, WARN_BATTERY},
|
||||
{RX8111_BOOT_FOUT_RETRY_ID, &RX8111Component::configure_boot_fout_, WARN_FOUT},
|
||||
{RX8111_BOOT_EVENT_RETRY_ID, &RX8111Component::configure_boot_event_, WARN_EVENT},
|
||||
{RX8111_BOOT_ALARM_RETRY_ID, &RX8111Component::configure_boot_alarm_, WARN_ALARM},
|
||||
{RX8111_BOOT_TIMER_RETRY_ID, &RX8111Component::configure_boot_timer_, WARN_TIMER},
|
||||
};
|
||||
BootConfig config;
|
||||
progmem_memcpy(&config, &CONFIGS[feature], sizeof(config));
|
||||
const auto attempts = ++this->boot_attempts_[feature];
|
||||
if (success) {
|
||||
this->clear_warning_(config.warning);
|
||||
return;
|
||||
}
|
||||
this->set_warning_(config.warning, BootFailureNames::get_log_str(feature, BootFailureNames::LAST_INDEX));
|
||||
if (attempts < RX8111_BOOT_MAX_ATTEMPTS) {
|
||||
this->set_timeout(config.retry_id, this->boot_retry_delay_ms_,
|
||||
[this, configure = config.configure]() { (this->*configure)(); });
|
||||
}
|
||||
}
|
||||
|
||||
void RX8111Component::configure_boot_alarm_() {
|
||||
bool success = true;
|
||||
if (this->alarm_enabled_) {
|
||||
@@ -74,7 +195,7 @@ void RX8111Component::configure_boot_alarm_() {
|
||||
} else if (this->alarm_explicitly_configured_) {
|
||||
success = this->set_alarm_interrupt_(false);
|
||||
}
|
||||
this->boot_config_result_(true, success);
|
||||
this->boot_config_result_(BOOT_ALARM, success);
|
||||
}
|
||||
|
||||
void RX8111Component::configure_boot_timer_() {
|
||||
@@ -92,29 +213,10 @@ void RX8111Component::configure_boot_timer_() {
|
||||
this->timer_retained_event_pending_ = true;
|
||||
this->start_event_polling_();
|
||||
}
|
||||
if (success && start_timer)
|
||||
this->retry_fout_after_timer_start_();
|
||||
}
|
||||
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_();
|
||||
}
|
||||
});
|
||||
this->boot_config_result_(BOOT_TIMER, success);
|
||||
}
|
||||
|
||||
// A runtime action supersedes pending boot programming of the same feature
|
||||
@@ -144,12 +246,12 @@ void RX8111Component::update() {
|
||||
this->check_event_flags_();
|
||||
}
|
||||
|
||||
void RX8111Component::set_warning_(uint8_t bit, const LogString *message) {
|
||||
void RX8111Component::set_warning_(uint16_t bit, const LogString *message) {
|
||||
this->warnings_ |= bit;
|
||||
this->status_set_warning(message);
|
||||
}
|
||||
|
||||
void RX8111Component::clear_warning_(uint8_t bit) {
|
||||
void RX8111Component::clear_warning_(uint16_t bit) {
|
||||
if ((this->warnings_ & bit) == 0)
|
||||
return;
|
||||
this->warnings_ &= ~bit;
|
||||
@@ -160,6 +262,23 @@ void RX8111Component::clear_warning_(uint8_t bit) {
|
||||
void RX8111Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "RX8111:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->battery_configured_) {
|
||||
ESP_LOGCONFIG(TAG, " Battery backup: %s, charging: %s", ONOFF(this->battery_backup_),
|
||||
ONOFF(this->battery_charging_));
|
||||
}
|
||||
if (this->fout_configured_) {
|
||||
ESP_LOGCONFIG(TAG, " FOUT: %s", LOG_STR_ARG(FoutNames::get_log_str(this->fout_frequency_, FoutNames::LAST_INDEX)));
|
||||
}
|
||||
if (this->event_configured_) {
|
||||
ESP_LOGCONFIG(TAG, " EVIN interrupt: %s", ONOFF(this->event_enabled_));
|
||||
if (this->event_enabled_) {
|
||||
ESP_LOGCONFIG(
|
||||
TAG, " EVIN: %s, filter: %u, pull: %s",
|
||||
(this->evin_settings_ & 0x80) ? LOG_STR_LITERAL("high") : LOG_STR_LITERAL("low"),
|
||||
(this->evin_settings_ >> 5) & 3,
|
||||
LOG_STR_ARG(EvinPullNames::get_log_str((this->evin_settings_ >> 2) & 7, EvinPullNames::LAST_INDEX)));
|
||||
}
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Alarm interrupt: %s\n"
|
||||
" Wake-up timer: %s",
|
||||
@@ -181,12 +300,20 @@ void RX8111Component::read_time() {
|
||||
}
|
||||
|
||||
bool RX8111Component::read_calendar_(ESPTime &rtc_time) {
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags)) {
|
||||
if (this->time_write_invalid_) {
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR("RTC write incomplete; write trusted time again"));
|
||||
return false;
|
||||
}
|
||||
uint8_t regs[2]; // Adjacent FLAG and CONTROL registers.
|
||||
if (!this->read_bytes(RX8111_REG_FLAG, regs, sizeof(regs))) {
|
||||
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)) {
|
||||
if (regs[1] & RX8111_CTRL_STOP) {
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR("RTC counter stopped; not synchronizing"));
|
||||
return false;
|
||||
}
|
||||
if (regs[0] & (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->set_warning_(WARN_CALENDAR, LOG_STR("RTC time invalidated; write trusted time"));
|
||||
return false;
|
||||
@@ -199,12 +326,12 @@ bool RX8111Component::read_calendar_(ESPTime &rtc_time) {
|
||||
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};
|
||||
static constexpr uint8_t CALENDAR_MASKS[7] PROGMEM = {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];
|
||||
buf[i] &= progmem_read_byte(&CALENDAR_MASKS[i]);
|
||||
valid = valid && (buf[i] & 0x0F) <= 9 && (buf[i] >> 4) <= 9;
|
||||
}
|
||||
const uint8_t onehot = buf[3];
|
||||
@@ -236,23 +363,31 @@ void RX8111Component::write_time() {
|
||||
|
||||
uint8_t buf[7];
|
||||
time::encode_bcd_calendar<time::EPSON_BCD_CALENDAR>(now, buf);
|
||||
const bool written = this->write_bytes(RX8111_REG_SEC, buf, sizeof(buf));
|
||||
// Always try to restart the counter, even after a failed calendar write.
|
||||
const bool restarted = this->set_stop_bit_(false);
|
||||
if (!written || !restarted) {
|
||||
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->time_write_invalid_ = true;
|
||||
const bool calendar_written = this->write_bytes(RX8111_REG_SEC, buf, sizeof(buf));
|
||||
if (!this->finish_time_write_(calendar_written))
|
||||
return;
|
||||
}
|
||||
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::finish_time_write_(bool calendar_written) {
|
||||
// STOP is the battery-backed invalidity marker, including across host resets.
|
||||
// Never release a partial or delayed snapshot; only a fresh trusted write can restart it.
|
||||
if (!calendar_written) {
|
||||
this->set_warning_(WARN_WRITE_TIME, LOG_STR("RTC write incomplete; write trusted time again"));
|
||||
return false;
|
||||
}
|
||||
if (!this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS) || !this->set_stop_bit_(false)) {
|
||||
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->time_write_invalid_ = false;
|
||||
this->clear_warning_(WARN_WRITE_TIME);
|
||||
this->clear_warning_(WARN_CALENDAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::set_stop_bit_(bool stop) {
|
||||
return this->update_register(RX8111_REG_CONTROL, RX8111_CTRL_STOP, stop ? RX8111_CTRL_STOP : 0);
|
||||
}
|
||||
@@ -383,6 +518,11 @@ bool RX8111Component::configure_timer_(bool clear_stale_flag) {
|
||||
if (!this->disable_timer_(extension, control))
|
||||
return false;
|
||||
|
||||
// Explicit starts own the timer mode: count in both power modes, do not pause,
|
||||
// and route interrupt pulses to /INT rather than FOUT. Leave reserved bits alone.
|
||||
if (!this->update_register(RX8111_REG_TIMER_CONTROL, RX8111_TIMER_MODE, 0))
|
||||
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)) ||
|
||||
@@ -414,6 +554,7 @@ bool RX8111Component::start_timer() {
|
||||
this->timer_enabled_ = true;
|
||||
this->timer_retained_event_pending_ = false;
|
||||
this->start_event_polling_();
|
||||
this->retry_fout_after_timer_start_();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -445,6 +586,12 @@ bool RX8111Component::poll_event_flags_() {
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags))
|
||||
return false;
|
||||
this->clear_warning_(WARN_EVENT_POLL);
|
||||
// EVF is not cleared here: users may use it to hold /INT asserted across sleep.
|
||||
const bool event_active = (flags & RX8111_FLAG_EVF) != 0;
|
||||
const bool event_rising =
|
||||
event_active && !this->event_flag_seen_ && this->event_enabled_ && !this->event_boot_pending_;
|
||||
if (this->event_enabled_ && !this->event_boot_pending_)
|
||||
this->event_flag_seen_ = event_active;
|
||||
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
|
||||
@@ -466,6 +613,8 @@ bool RX8111Component::poll_event_flags_() {
|
||||
// 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();
|
||||
if (event_rising && this->event_flag_seen_)
|
||||
this->event_callbacks_.call();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,28 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
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; }
|
||||
void set_battery_backup(bool enabled, bool charging) {
|
||||
this->battery_configured_ = true;
|
||||
this->battery_backup_ = enabled;
|
||||
this->battery_charging_ = charging;
|
||||
}
|
||||
void set_fout_frequency(uint8_t frequency) {
|
||||
this->fout_configured_ = true;
|
||||
this->fout_frequency_ = frequency;
|
||||
}
|
||||
void set_event_config(uint8_t settings) {
|
||||
this->event_configured_ = true;
|
||||
this->event_enabled_ = true;
|
||||
this->evin_settings_ = settings;
|
||||
}
|
||||
void set_event_disabled() {
|
||||
this->event_configured_ = true;
|
||||
this->event_enabled_ = false;
|
||||
}
|
||||
template<typename F> void add_on_event_callback(F &&callback) {
|
||||
this->event_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
bool clear_event_flag();
|
||||
template<typename F> void add_on_alarm_callback(F &&callback) {
|
||||
this->alarm_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
@@ -51,7 +73,7 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
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 {
|
||||
enum Warning : uint16_t {
|
||||
WARN_CALENDAR = 1 << 0,
|
||||
WARN_EVENT_POLL = 1 << 1,
|
||||
WARN_ALARM = 1 << 2,
|
||||
@@ -59,13 +81,28 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
WARN_WRITE_TIME = 1 << 4,
|
||||
WARN_ALARM_FLAG_CLEAR = 1 << 5,
|
||||
WARN_TIMER_FLAG_CLEAR = 1 << 6,
|
||||
WARN_BATTERY = 1 << 7,
|
||||
WARN_FOUT = 1 << 8,
|
||||
WARN_EVENT = 1 << 9,
|
||||
WARN_EVENT_FLAG_CLEAR = 1 << 10,
|
||||
};
|
||||
|
||||
bool set_stop_bit_(bool stop);
|
||||
bool finish_time_write_(bool calendar_written);
|
||||
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 set_warning_(uint16_t bit, const LogString *message);
|
||||
void clear_warning_(uint16_t bit);
|
||||
void configure_boot_battery_();
|
||||
void configure_boot_fout_();
|
||||
void retry_fout_after_timer_start_();
|
||||
enum BootFeature : uint8_t { BOOT_BATTERY, BOOT_FOUT, BOOT_EVENT, BOOT_ALARM, BOOT_TIMER, BOOT_FEATURE_COUNT };
|
||||
void boot_config_result_(BootFeature feature, bool success);
|
||||
bool watching_() const {
|
||||
return this->alarm_armed_ || this->timer_enabled_ || this->timer_retained_event_pending_ ||
|
||||
(this->event_enabled_ && !this->event_boot_pending_ && !this->event_callbacks_.empty());
|
||||
}
|
||||
void configure_boot_event_();
|
||||
bool configure_event_();
|
||||
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);
|
||||
@@ -75,33 +112,43 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
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_;
|
||||
LazyCallbackManager<void()> event_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};
|
||||
uint16_t warnings_{0};
|
||||
uint8_t boot_attempts_[BOOT_FEATURE_COUNT]{};
|
||||
uint8_t evin_settings_{0};
|
||||
uint8_t fout_frequency_{3};
|
||||
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};
|
||||
bool time_write_invalid_ : 1 {false};
|
||||
bool battery_configured_ : 1 {false};
|
||||
bool battery_backup_ : 1 {false};
|
||||
bool battery_charging_ : 1 {false};
|
||||
bool fout_configured_ : 1 {false};
|
||||
bool event_configured_ : 1 {false};
|
||||
bool event_enabled_ : 1 {false};
|
||||
bool event_flag_seen_ : 1 {false};
|
||||
bool event_boot_pending_ : 1 {false};
|
||||
bool timer_enabled_ : 1 {false};
|
||||
bool timer_configured_ : 1 {false};
|
||||
bool timer_flag_seen_ : 1 {false};
|
||||
bool timer_retained_event_pending_ : 1 {false};
|
||||
bool event_polling_started_ : 1 {false};
|
||||
bool boot_alarm_event_seen_ : 1 {false};
|
||||
bool alarm_enabled_ : 1 {false};
|
||||
bool alarm_explicitly_configured_ : 1 {false};
|
||||
bool alarm_flag_seen_ : 1 {false};
|
||||
bool alarm_armed_ : 1 {false};
|
||||
};
|
||||
|
||||
} // namespace esphome::rx8111
|
||||
@@ -10,6 +10,7 @@ from esphome.const import (
|
||||
CONF_HOUR,
|
||||
CONF_ID,
|
||||
CONF_MINUTE,
|
||||
CONF_ON_EVENT,
|
||||
CONF_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
@@ -20,6 +21,14 @@ 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}
|
||||
FOUT_FREQUENCIES = {"32768hz": 0, "1024hz": 1, "1hz": 2, "off": 3}
|
||||
EVIN_PULLS = {
|
||||
"none": 0,
|
||||
"pullup_500k": 0x04,
|
||||
"pullup_1m": 0x08,
|
||||
"pullup_10m": 0x0C,
|
||||
"pulldown_500k": 0x10,
|
||||
}
|
||||
# set_alarm() ignores a field set to this value
|
||||
ALARM_FIELD_UNSET = 0xFF
|
||||
|
||||
@@ -36,6 +45,13 @@ CONF_TIMER_ENABLED = "timer_enabled"
|
||||
CONF_ON_TIMER = "on_timer"
|
||||
CONF_WEEKDAY = "weekday"
|
||||
CONF_EVENT_POLL_INTERVAL = "event_poll_interval"
|
||||
CONF_BATTERY_BACKUP = "battery_backup"
|
||||
CONF_BATTERY_CHARGING = "battery_charging"
|
||||
CONF_FOUT_FREQUENCY = "fout_frequency"
|
||||
CONF_EVENT_ENABLED = "event_enabled"
|
||||
CONF_EVENT_LEVEL = "event_level"
|
||||
CONF_EVIN_FILTER = "evin_filter"
|
||||
CONF_EVIN_PULL = "evin_pull"
|
||||
|
||||
_ALARM_FIELDS = (
|
||||
CONF_ALARM_SECOND,
|
||||
@@ -47,6 +63,7 @@ _ALARM_FIELDS = (
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_ALARM, "add_on_alarm_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_TIMER, "add_on_timer_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_EVENT, "add_on_event_callback"),
|
||||
)
|
||||
|
||||
|
||||
@@ -62,6 +79,16 @@ def _validate_alarm_config(config: ConfigType) -> ConfigType:
|
||||
config.get(CONF_TIMER_ENABLED) or CONF_ON_TIMER in config
|
||||
):
|
||||
raise cv.Invalid("timer_enabled or on_timer requires timer_count")
|
||||
if CONF_BATTERY_CHARGING in config and CONF_BATTERY_BACKUP not in config:
|
||||
raise cv.Invalid("battery_charging requires an explicit battery_backup setting")
|
||||
if config.get(CONF_BATTERY_CHARGING) and not config.get(CONF_BATTERY_BACKUP):
|
||||
raise cv.Invalid("battery_charging: true requires battery_backup: true")
|
||||
if CONF_ON_EVENT in config and not config.get(CONF_EVENT_ENABLED):
|
||||
raise cv.Invalid("on_event requires event_enabled: true")
|
||||
if any(
|
||||
k in config for k in (CONF_EVENT_LEVEL, CONF_EVIN_FILTER, CONF_EVIN_PULL)
|
||||
) and not config.get(CONF_EVENT_ENABLED):
|
||||
raise cv.Invalid("EVIN settings require event_enabled: true")
|
||||
return config
|
||||
|
||||
|
||||
@@ -86,6 +113,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
TIMER_CLOCKS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_TIMER_ENABLED): cv.boolean,
|
||||
cv.Optional(CONF_BATTERY_BACKUP): cv.boolean,
|
||||
cv.Optional(CONF_BATTERY_CHARGING): cv.boolean,
|
||||
cv.Optional(CONF_FOUT_FREQUENCY): cv.enum(FOUT_FREQUENCIES, lower=True),
|
||||
cv.Optional(CONF_EVENT_ENABLED): cv.boolean,
|
||||
cv.Optional(CONF_EVENT_LEVEL): cv.enum(
|
||||
{"low": 0, "high": 0x80}, lower=True
|
||||
),
|
||||
cv.Optional(CONF_EVIN_FILTER): cv.int_range(min=0, max=3),
|
||||
cv.Optional(CONF_EVIN_PULL): cv.enum(EVIN_PULLS, lower=True),
|
||||
cv.Optional(CONF_ON_EVENT): automation.validate_automation({}),
|
||||
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(
|
||||
@@ -107,6 +144,7 @@ for _method in (
|
||||
"read_time",
|
||||
"clear_alarm_flag",
|
||||
"clear_timer_flag",
|
||||
"clear_event_flag",
|
||||
"unset_alarm",
|
||||
"stop_timer",
|
||||
"start_timer",
|
||||
@@ -184,6 +222,16 @@ 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]))
|
||||
if (backup := config.get(CONF_BATTERY_BACKUP)) is not None:
|
||||
cg.add(var.set_battery_backup(backup, config.get(CONF_BATTERY_CHARGING, False)))
|
||||
cg.add_optional(var.set_fout_frequency, config.get(CONF_FOUT_FREQUENCY))
|
||||
if config.get(CONF_EVENT_ENABLED):
|
||||
level = config[CONF_EVENT_LEVEL].enum_value if CONF_EVENT_LEVEL in config else 0
|
||||
pull = config[CONF_EVIN_PULL].enum_value if CONF_EVIN_PULL in config else 0
|
||||
filter_bits = config.get(CONF_EVIN_FILTER, 0) << 5
|
||||
cg.add(var.set_event_config(level | filter_bits | pull))
|
||||
elif CONF_EVENT_ENABLED in config:
|
||||
cg.add(var.set_event_disabled())
|
||||
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))
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.types import ConfigType
|
||||
DEPENDENCIES = ["wifi"]
|
||||
|
||||
wifi_info_ns = cg.esphome_ns.namespace("wifi_info")
|
||||
_request_ip_sensor_slot = cg.slot_counter("WIFI_INFO_IP_SENSOR_COUNT")
|
||||
IPAddressWiFiInfo = wifi_info_ns.class_(
|
||||
"IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component
|
||||
)
|
||||
@@ -110,7 +111,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
if conf := config.get(CONF_IP_ADDRESS):
|
||||
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
||||
await cg.register_component(wifi_info, config[CONF_IP_ADDRESS])
|
||||
for x in range(5):
|
||||
if sensor_conf := conf.get(f"address_{x}"):
|
||||
sens = await text_sensor.new_text_sensor(sensor_conf)
|
||||
cg.add(wifi_info.add_ip_sensors(x, sens))
|
||||
indexes = [x for x in range(5) if f"address_{x}" in conf]
|
||||
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
|
||||
for _ in range(max(indexes, default=-1) + 1):
|
||||
_request_ip_sensor_slot(str(wifi_info))
|
||||
for x in indexes:
|
||||
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
|
||||
cg.add(wifi_info.add_ip_sensors(x, sens))
|
||||
@@ -26,16 +26,20 @@ void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const netwo
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ips[0].str_to(buf);
|
||||
this->publish_state(buf);
|
||||
#ifdef WIFI_INFO_IP_SENSOR_COUNT
|
||||
uint8_t sensor = 0;
|
||||
for (const auto &ip : ips) {
|
||||
if (ip.is_set()) {
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
if (!ip.is_set())
|
||||
continue;
|
||||
if (sensor == WIFI_INFO_IP_SENSOR_COUNT)
|
||||
break;
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*********************
|
||||
|
||||
@@ -16,14 +16,19 @@ class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
#ifdef WIFI_INFO_IP_SENSOR_COUNT
|
||||
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
|
||||
#endif
|
||||
|
||||
// WiFiIPStateListener interface
|
||||
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
|
||||
const network::IPAddress &dns2) override;
|
||||
|
||||
protected:
|
||||
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
|
||||
#ifdef WIFI_INFO_IP_SENSOR_COUNT
|
||||
// Sized to the highest configured address_N
|
||||
std::array<text_sensor::TextSensor *, WIFI_INFO_IP_SENSOR_COUNT> ip_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener {
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
#define LV_USE_LOG 1
|
||||
#define USE_MDNS
|
||||
#define USE_MDNS_STORE_SERVICES
|
||||
#define ETHERNET_INFO_IP_SENSOR_COUNT 5
|
||||
#define GPS_LISTENER_COUNT 1
|
||||
#define HDMI_CEC_MESSAGE_CALLBACK_COUNT 2
|
||||
#define I2C_PORT_FREQUENCY_COUNT 1
|
||||
@@ -194,6 +195,7 @@
|
||||
#define PYLONTECH_SENSOR_CELL_COUNT 2
|
||||
#define REMOTE_BASE_DUMPER_COUNT 1
|
||||
#define REMOTE_BASE_LISTENER_COUNT 1
|
||||
#define WIFI_INFO_IP_SENSOR_COUNT 5
|
||||
#define USE_REMOTE_PROTOCOL_ABBWELCOME
|
||||
#define USE_REMOTE_PROTOCOL_AEHA
|
||||
#define USE_REMOTE_PROTOCOL_AOK
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Validate RX8111 alarm and timer options before generating firmware."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.rx8111.time import (
|
||||
@@ -8,6 +10,8 @@ from esphome.components.rx8111.time import (
|
||||
CONF_ALARM_DAY,
|
||||
CONF_ALARM_ENABLED,
|
||||
CONF_ALARM_WEEKDAY,
|
||||
CONF_BATTERY_BACKUP,
|
||||
CONF_BATTERY_CHARGING,
|
||||
CONF_DAY,
|
||||
CONF_ON_ALARM,
|
||||
CONF_ON_TIMER,
|
||||
@@ -15,6 +19,7 @@ from esphome.components.rx8111.time import (
|
||||
CONF_TIMER_COUNT,
|
||||
CONF_TIMER_ENABLED,
|
||||
CONF_WEEKDAY,
|
||||
FOUT_FREQUENCIES,
|
||||
_validate_alarm_config,
|
||||
_validate_alarm_options,
|
||||
_validate_set_alarm,
|
||||
@@ -74,3 +79,104 @@ def test_on_alarm_requires_enabled_alarm() -> None:
|
||||
with pytest.raises(cv.Invalid, match="on_alarm"):
|
||||
_validate_alarm_config({CONF_ON_ALARM: [{}]})
|
||||
assert _validate_alarm_config({CONF_ON_ALARM: [{}], CONF_ALARM_ENABLED: True})
|
||||
|
||||
|
||||
def test_charging_requires_explicit_backup() -> None:
|
||||
for config in (
|
||||
{CONF_BATTERY_CHARGING: True},
|
||||
{CONF_BATTERY_CHARGING: False},
|
||||
{CONF_BATTERY_BACKUP: False, CONF_BATTERY_CHARGING: True},
|
||||
):
|
||||
with pytest.raises(cv.Invalid, match="battery_backup"):
|
||||
_validate_alarm_config(config)
|
||||
assert _validate_alarm_config(
|
||||
{CONF_BATTERY_BACKUP: True, CONF_BATTERY_CHARGING: True}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backup", [False, True])
|
||||
def test_charging_disable_requires_and_accepts_explicit_backup(backup: bool) -> None:
|
||||
config = {CONF_BATTERY_BACKUP: backup, CONF_BATTERY_CHARGING: False}
|
||||
assert _validate_alarm_config(config) == config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["32768hz", "1024hz", "1hz", "off"])
|
||||
def test_fout_frequencies(value: str) -> None:
|
||||
assert (
|
||||
cv.enum(FOUT_FREQUENCIES, lower=True)(value).enum_value
|
||||
== FOUT_FREQUENCIES[value]
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_fout_frequency() -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
cv.enum(FOUT_FREQUENCIES, lower=True)("100hz")
|
||||
|
||||
|
||||
def test_event_configuration_requires_enablement() -> None:
|
||||
from esphome.components.rx8111.time import (
|
||||
CONF_EVENT_ENABLED,
|
||||
CONF_EVENT_LEVEL,
|
||||
CONF_EVIN_FILTER,
|
||||
CONF_ON_EVENT,
|
||||
)
|
||||
|
||||
for config in (
|
||||
{CONF_ON_EVENT: [{}]},
|
||||
{CONF_EVENT_LEVEL: 0},
|
||||
{CONF_EVIN_FILTER: 2},
|
||||
):
|
||||
with pytest.raises(cv.Invalid):
|
||||
_validate_alarm_config(config)
|
||||
assert _validate_alarm_config({CONF_EVENT_ENABLED: True, CONF_ON_EVENT: [{}]})
|
||||
assert _validate_alarm_config({CONF_EVENT_ENABLED: False})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("option", ["event_timestamp", "timestamp_record_mode"])
|
||||
def test_timestamp_options_are_not_in_time_platform(option: str) -> None:
|
||||
from esphome.components.rx8111.time import CONFIG_SCHEMA
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({option: "latest"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("options", "settings"),
|
||||
[
|
||||
({"event_enabled": True}, 0),
|
||||
(
|
||||
{
|
||||
"event_enabled": True,
|
||||
"event_level": "high",
|
||||
"evin_filter": 2,
|
||||
"evin_pull": "pullup_500k",
|
||||
},
|
||||
0xC4,
|
||||
),
|
||||
({"event_enabled": False}, None),
|
||||
],
|
||||
)
|
||||
async def test_event_codegen(options: dict, settings: int | None) -> None:
|
||||
from esphome.components.rx8111 import time as rx8111
|
||||
|
||||
config = rx8111.CONFIG_SCHEMA(options)
|
||||
var = mock.MagicMock()
|
||||
with (
|
||||
mock.patch.object(
|
||||
rx8111.i2c, "new_i2c_component", mock.AsyncMock(return_value=var)
|
||||
),
|
||||
mock.patch.object(rx8111.time, "register_time", mock.AsyncMock()),
|
||||
mock.patch.object(rx8111.cg, "add"),
|
||||
mock.patch.object(rx8111.cg, "add_optional"),
|
||||
mock.patch.object(
|
||||
rx8111.automation, "build_callback_automations", mock.AsyncMock()
|
||||
),
|
||||
):
|
||||
await rx8111.to_code(config)
|
||||
if settings is None:
|
||||
var.set_event_config.assert_not_called()
|
||||
var.set_event_disabled.assert_called_once_with()
|
||||
else:
|
||||
var.set_event_config.assert_called_once_with(settings)
|
||||
var.set_event_disabled.assert_not_called()
|
||||
@@ -23,6 +23,7 @@ esphome:
|
||||
duration: !lambda return uint32_t{60};
|
||||
- rx8111.clear_alarm_flag: rtc
|
||||
- rx8111.clear_timer_flag: rtc
|
||||
- rx8111.clear_event_flag: rtc
|
||||
- rx8111.stop_timer: rtc
|
||||
- rx8111.start_timer: rtc
|
||||
- rx8111.unset_alarm: rtc
|
||||
@@ -38,6 +39,16 @@ time:
|
||||
timer_count: 2
|
||||
timer_clock: 1hz
|
||||
event_poll_interval: 500ms
|
||||
battery_backup: true
|
||||
battery_charging: true
|
||||
fout_frequency: 1024hz
|
||||
event_enabled: true
|
||||
event_level: low
|
||||
evin_filter: 2
|
||||
evin_pull: pullup_500k
|
||||
on_event:
|
||||
then:
|
||||
- rx8111.clear_event_flag: rtc
|
||||
on_alarm:
|
||||
then:
|
||||
- rx8111.clear_alarm_flag: rtc
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <ctime>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/rx8111/rx8111.h"
|
||||
@@ -19,14 +20,21 @@ static constexpr uint8_t CONTROL = 0x1F;
|
||||
static constexpr uint8_t EXTENSION = 0x1D;
|
||||
static constexpr uint8_t SEC_ALARM = 0x2C;
|
||||
static constexpr uint8_t MIN_ALARM = 0x17;
|
||||
static constexpr uint8_t POWER_SWITCH = 0x32;
|
||||
static constexpr uint8_t TIMER_CONTROL = 0x2D;
|
||||
static constexpr uint8_t EVIN = 0x2B;
|
||||
static constexpr uint8_t TIMESTAMP_CONTROL = 0x34;
|
||||
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
std::array<uint8_t, 256> regs{};
|
||||
std::vector<uint8_t> writes;
|
||||
std::vector<std::pair<uint8_t, size_t>> reads;
|
||||
int fail_read{-1};
|
||||
int fail_read_once{-1};
|
||||
int fail_write{-1};
|
||||
int fail_control_write_number{-1};
|
||||
unsigned control_writes{0};
|
||||
unsigned flag_reads{0};
|
||||
bool latch_event_before_flag_write{false};
|
||||
|
||||
@@ -38,17 +46,20 @@ class RegisterBus final : public i2c::I2CBus {
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
const uint8_t reg = out[0];
|
||||
if (in_size > 0) {
|
||||
this->reads.emplace_back(reg, in_size);
|
||||
if (reg == FLAG)
|
||||
this->flag_reads++;
|
||||
if (reg == this->fail_read)
|
||||
if (this->fail_read >= reg && this->fail_read < reg + in_size)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
if (reg == this->fail_read_once) {
|
||||
if (this->fail_read_once >= reg && this->fail_read_once < reg + in_size) {
|
||||
this->fail_read_once = -1;
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
}
|
||||
for (size_t i = 0; i < in_size; i++)
|
||||
in[i] = this->regs[reg + i];
|
||||
} else {
|
||||
if (reg == CONTROL && ++this->control_writes == this->fail_control_write_number)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
if (reg == this->fail_write)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
this->writes.push_back(reg);
|
||||
@@ -80,6 +91,9 @@ class RX8111Test : public ::testing::Test {
|
||||
App.scheduler.cancel_interval(&this->rtc_, uint32_t{0});
|
||||
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{1});
|
||||
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{2});
|
||||
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{3});
|
||||
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{4});
|
||||
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{5});
|
||||
App.scheduler.call(millis()); // Reap cancelled timers while the fixture is still alive.
|
||||
}
|
||||
|
||||
@@ -91,12 +105,501 @@ class RX8111Test : public ::testing::Test {
|
||||
// TEST_F bodies are subclasses, so the friendship stops here
|
||||
bool polling_started_() const { return this->rtc_.event_polling_started_; }
|
||||
bool timer_enabled_() const { return this->rtc_.timer_enabled_; }
|
||||
void configure_battery_() { this->rtc_.configure_boot_battery_(); }
|
||||
bool power_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_BATTERY) != 0; }
|
||||
bool fout_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_FOUT) != 0; }
|
||||
bool timer_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_TIMER) != 0; }
|
||||
bool event_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_EVENT) != 0; }
|
||||
bool event_clear_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_EVENT_FLAG_CLEAR) != 0; }
|
||||
void poll_() { this->rtc_.check_event_flags_(); }
|
||||
|
||||
RegisterBus bus_;
|
||||
RX8111Component rtc_;
|
||||
unsigned syncs_{0};
|
||||
};
|
||||
|
||||
TEST_F(RX8111Test, OmittedPowerAndFoutOptionsDoNotTouchBatteryBackedRegisters) {
|
||||
this->bus_.regs[POWER_SWITCH] = 0xC5;
|
||||
this->bus_.regs[EXTENSION] = 0xA7;
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0xC5);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xA7);
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, EventSettingsPreserveUnrelatedBitsAndLatchedFlag) {
|
||||
this->bus_.regs[TIMESTAMP_CONTROL] = 0x85; // EISEL plus TSRAM and a reserved bit.
|
||||
this->bus_.regs[EVIN] = 1;
|
||||
this->bus_.regs[EXTENSION] = 0x90;
|
||||
this->bus_.regs[CONTROL] = 0x90;
|
||||
this->bus_.regs[FLAG] = 0x04;
|
||||
this->rtc_.set_event_config(0xC4);
|
||||
int callbacks = 0;
|
||||
this->rtc_.add_on_event_callback([&]() { callbacks++; });
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[EVIN], 0xC5);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x94); // ETS enables EVIN detection; preserve other bits.
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL], 0x94);
|
||||
EXPECT_EQ(this->bus_.regs[TIMESTAMP_CONTROL], 0x81);
|
||||
this->poll_();
|
||||
this->poll_();
|
||||
EXPECT_EQ(callbacks, 1);
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x04);
|
||||
EXPECT_TRUE(this->rtc_.clear_event_flag());
|
||||
this->bus_.regs[FLAG] = 0x04;
|
||||
this->poll_();
|
||||
EXPECT_EQ(callbacks, 2);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, EventWithoutCallbackConfiguresHardwareWithoutPolling) {
|
||||
this->rtc_.set_event_config(0xC4);
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
|
||||
EXPECT_FALSE(this->polling_started_());
|
||||
const auto reads = this->bus_.flag_reads;
|
||||
this->rtc_.update();
|
||||
// One calendar FLAG read, no event FLAG read.
|
||||
EXPECT_EQ(this->bus_.flag_reads, reads + 1);
|
||||
EXPECT_TRUE(this->rtc_.clear_event_flag());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, EventWithCallbackStartsPolling) {
|
||||
this->rtc_.set_event_config(0);
|
||||
this->rtc_.add_on_event_callback([]() {});
|
||||
this->rtc_.setup();
|
||||
EXPECT_TRUE(this->polling_started_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, EventConfigurationRetriesWithoutDroppingRetainedFlag) {
|
||||
this->bus_.regs[EVIN] = 0x03; // Preserve EVIN's reserved and retained capture bits.
|
||||
this->bus_.regs[FLAG] = 0x04;
|
||||
this->bus_.fail_write = EVIN;
|
||||
this->rtc_.set_event_config(0xC4);
|
||||
int callbacks = 0;
|
||||
this->rtc_.add_on_event_callback([&]() { callbacks++; });
|
||||
this->rtc_.setup();
|
||||
EXPECT_TRUE(this->event_warning_());
|
||||
EXPECT_EQ(this->bus_.regs[EVIN], 0x03);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0);
|
||||
this->poll_();
|
||||
EXPECT_EQ(callbacks, 0);
|
||||
this->bus_.fail_write = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_FALSE(this->event_warning_());
|
||||
EXPECT_EQ(this->bus_.regs[EVIN], 0xC7);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0x04);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
|
||||
this->poll_();
|
||||
EXPECT_EQ(callbacks, 1);
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x04);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, EventExtensionFailureRetriesBeforeEnablingInterrupt) {
|
||||
this->bus_.regs[EXTENSION] = 0xA0;
|
||||
this->bus_.fail_write = EXTENSION;
|
||||
this->rtc_.set_event_config(0xD0);
|
||||
this->rtc_.setup();
|
||||
EXPECT_TRUE(this->event_warning_());
|
||||
EXPECT_EQ(this->bus_.regs[EVIN], 0xD0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xA0);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0);
|
||||
this->bus_.fail_write = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_FALSE(this->event_warning_());
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xA4);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, ExhaustedEventStartupRetriesDoNotKeepPollingFlags) {
|
||||
this->rtc_.set_event_config(0);
|
||||
unsigned callbacks = 0;
|
||||
this->rtc_.add_on_event_callback([&callbacks]() { callbacks++; });
|
||||
this->bus_.fail_write = EVIN;
|
||||
this->rtc_.setup();
|
||||
EXPECT_FALSE(this->polling_started_());
|
||||
this->advance_retry_();
|
||||
this->advance_retry_();
|
||||
EXPECT_TRUE(this->event_warning_());
|
||||
this->bus_.fail_write = -1;
|
||||
this->bus_.regs[FLAG] = 0x04;
|
||||
const auto reads = this->bus_.flag_reads;
|
||||
for (unsigned i = 0; i < 5; i++) {
|
||||
this->advance_retry_();
|
||||
this->poll_();
|
||||
}
|
||||
EXPECT_EQ(this->bus_.flag_reads, reads);
|
||||
EXPECT_EQ(callbacks, 0);
|
||||
EXPECT_FALSE(this->polling_started_());
|
||||
EXPECT_TRUE(this->event_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, FailedEventStartupDoesNotStopTimerPolling) {
|
||||
this->rtc_.set_event_config(0);
|
||||
this->rtc_.add_on_event_callback([]() { FAIL() << "EVIN configuration never succeeded"; });
|
||||
this->rtc_.set_timer_count(2);
|
||||
this->rtc_.set_timer_configured(true);
|
||||
this->rtc_.set_timer_enabled(true);
|
||||
unsigned callbacks = 0;
|
||||
this->rtc_.add_on_timer_callback([&callbacks]() { callbacks++; });
|
||||
this->bus_.fail_write = EVIN;
|
||||
this->rtc_.setup();
|
||||
this->advance_retry_();
|
||||
this->advance_retry_();
|
||||
EXPECT_TRUE(this->polling_started_());
|
||||
this->bus_.regs[FLAG] = 0x14;
|
||||
this->poll_();
|
||||
EXPECT_EQ(callbacks, 1);
|
||||
EXPECT_TRUE(this->event_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, ClearEventFlagPreservesOtherFlagsAndRetriesAfterFailure) {
|
||||
this->bus_.regs[FLAG] = 0x1C;
|
||||
this->bus_.fail_write = FLAG;
|
||||
EXPECT_FALSE(this->rtc_.clear_event_flag());
|
||||
EXPECT_TRUE(this->event_clear_warning_());
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x1C);
|
||||
this->bus_.fail_write = -1;
|
||||
EXPECT_TRUE(this->rtc_.clear_event_flag());
|
||||
EXPECT_FALSE(this->event_clear_warning_());
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x18);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, EventFlagClearDoesNotHideBootConfigurationFailure) {
|
||||
this->rtc_.set_event_config(0xC4);
|
||||
this->bus_.fail_write = EVIN;
|
||||
this->rtc_.setup();
|
||||
ASSERT_TRUE(this->event_warning_());
|
||||
this->bus_.fail_write = FLAG;
|
||||
EXPECT_FALSE(this->rtc_.clear_event_flag());
|
||||
EXPECT_TRUE(this->event_clear_warning_());
|
||||
this->bus_.fail_write = EVIN;
|
||||
EXPECT_TRUE(this->rtc_.clear_event_flag());
|
||||
EXPECT_FALSE(this->event_clear_warning_());
|
||||
EXPECT_TRUE(this->event_warning_());
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.fail_write = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_FALSE(this->event_warning_());
|
||||
EXPECT_FALSE(this->event_clear_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, ExplicitEventDisableDoesNotChangeCaptureOrFlag) {
|
||||
this->bus_.regs[EXTENSION] = 0x04;
|
||||
this->bus_.regs[CONTROL] = 0x0C;
|
||||
this->bus_.regs[FLAG] = 0x04;
|
||||
this->rtc_.set_event_disabled();
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL}));
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL], 0x08);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x04);
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x04);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, BatteryAndFoutPreserveUnrelatedBitsAndTimerRouting) {
|
||||
this->bus_.regs[POWER_SWITCH] = 0xB5;
|
||||
this->bus_.regs[EXTENSION] = 0xDA;
|
||||
this->rtc_.set_battery_backup(true, false);
|
||||
this->rtc_.set_fout_frequency(1);
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x79);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x5A);
|
||||
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{POWER_SWITCH, EXTENSION}));
|
||||
EXPECT_FALSE(this->power_warning_());
|
||||
EXPECT_FALSE(this->fout_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, ChargingRequiresExplicitEnableAndCanBeDisabled) {
|
||||
this->bus_.regs[POWER_SWITCH] = 0x15;
|
||||
this->rtc_.set_battery_backup(true, true);
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0xD9);
|
||||
this->rtc_.set_battery_backup(false, false);
|
||||
this->configure_battery_();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x19);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, FoutRefusesTimerPinRoutingWithoutModifyingIt) {
|
||||
this->bus_.regs[TIMER_CONTROL] = 0x02;
|
||||
this->bus_.regs[EXTENSION] = 0xC5;
|
||||
this->rtc_.set_fout_frequency(0);
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xC5);
|
||||
EXPECT_TRUE(this->fout_warning_());
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
this->bus_.regs[TIMER_CONTROL] = 0;
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x05);
|
||||
EXPECT_FALSE(this->fout_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, BootTimerClearingTimerPinRoutingLetsFoutFinishAfterItsRetriesRanOut) {
|
||||
this->bus_.regs[TIMER_CONTROL] = 0x02;
|
||||
this->bus_.regs[EXTENSION] = 0xC0;
|
||||
this->bus_.fail_write = TIMER_CONTROL;
|
||||
this->rtc_.set_fout_frequency(0);
|
||||
this->rtc_.set_timer_count(2);
|
||||
this->rtc_.set_timer_configured(true);
|
||||
this->rtc_.set_timer_enabled(true);
|
||||
this->rtc_.setup();
|
||||
this->advance_retry_();
|
||||
EXPECT_TRUE(this->fout_warning_());
|
||||
this->bus_.fail_write = -1;
|
||||
// The last FOUT attempt runs before the last timer attempt and still sees TMPIN
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0xC0, 0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
|
||||
EXPECT_FALSE(this->fout_warning_());
|
||||
EXPECT_FALSE(this->timer_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, RuntimeTimerStartClearingTimerPinRoutingLetsFoutFinish) {
|
||||
this->bus_.regs[TIMER_CONTROL] = 0x02;
|
||||
this->bus_.regs[EXTENSION] = 0xC0;
|
||||
this->rtc_.set_fout_frequency(0);
|
||||
this->rtc_.set_timer_count(2);
|
||||
this->rtc_.setup();
|
||||
this->advance_retry_();
|
||||
this->advance_retry_();
|
||||
EXPECT_TRUE(this->fout_warning_());
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0xC0, 0xC0);
|
||||
ASSERT_TRUE(this->rtc_.start_timer());
|
||||
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0xC0, 0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
|
||||
EXPECT_FALSE(this->fout_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, PowerWriteFailureRetriesAndLeavesWarningOnExhaustion) {
|
||||
this->bus_.regs[POWER_SWITCH] = 0x85;
|
||||
this->bus_.fail_write = POWER_SWITCH;
|
||||
this->rtc_.set_battery_backup(true, false);
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x85);
|
||||
EXPECT_TRUE(this->power_warning_());
|
||||
this->advance_retry_();
|
||||
this->advance_retry_();
|
||||
this->bus_.fail_write = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x85);
|
||||
EXPECT_TRUE(this->power_warning_());
|
||||
this->configure_battery_();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x49);
|
||||
EXPECT_FALSE(this->power_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, ExplicitBatteryConfigurationNormalizesEverySwitchSelection) {
|
||||
for (uint8_t selection = 0; selection < 4; selection++) {
|
||||
for (bool enabled : {false, true}) {
|
||||
this->bus_.regs[POWER_SWITCH] = 0xB3 | (selection << 2);
|
||||
this->rtc_.set_battery_backup(enabled, false);
|
||||
this->configure_battery_();
|
||||
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], enabled ? 0x7B : 0x3B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, TimestampSelectionFailureRetriesBeforeEnablingDetection) {
|
||||
this->bus_.regs[TIMESTAMP_CONTROL] = 0x05;
|
||||
this->bus_.fail_write = TIMESTAMP_CONTROL;
|
||||
this->rtc_.set_event_config(0);
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0);
|
||||
this->bus_.fail_write = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[TIMESTAMP_CONTROL], 0x01);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0x04);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, ExplicitTimerStartNormalizesRetainedModeAtBootAndRuntime) {
|
||||
this->rtc_.set_timer_count(2);
|
||||
this->rtc_.set_timer_configured(true);
|
||||
this->rtc_.set_timer_enabled(true);
|
||||
this->bus_.regs[TIMER_CONTROL] = 0xFF;
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0xF0);
|
||||
for (uint8_t mode : {0x01, 0x02, 0x04, 0x08, 0x0C, 0x0F}) {
|
||||
this->bus_.regs[TIMER_CONTROL] = 0xF0 | mode;
|
||||
ASSERT_TRUE(this->rtc_.start_timer());
|
||||
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0xF0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, TimerModeFailureDoesNotStartCounterAndBootRetries) {
|
||||
this->rtc_.set_timer_count(2);
|
||||
this->rtc_.set_timer_configured(true);
|
||||
this->rtc_.set_timer_enabled(true);
|
||||
this->bus_.regs[TIMER_CONTROL] = 1;
|
||||
this->bus_.fail_write = TIMER_CONTROL;
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0);
|
||||
this->bus_.fail_write = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
|
||||
this->bus_.fail_write = TIMER_CONTROL;
|
||||
EXPECT_FALSE(this->rtc_.start_timer());
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0);
|
||||
this->bus_.fail_write = -1;
|
||||
ASSERT_TRUE(this->rtc_.start_timer());
|
||||
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, FailedRestartKeepsStopAcrossHostResetUntilFreshTrustedWrite) {
|
||||
this->bus_.fail_control_write_number = 2;
|
||||
this->rtc_.write_time();
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
this->bus_.writes.clear();
|
||||
this->bus_.fail_write = CONTROL;
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.fail_write = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0); // Even with initially clear hardware validity flags.
|
||||
RX8111Component rebooted;
|
||||
rebooted.set_i2c_bus(&this->bus_);
|
||||
rebooted.set_i2c_address(ADDRESS);
|
||||
rebooted.add_on_time_sync_callback([this]() { this->syncs_++; });
|
||||
rebooted.setup();
|
||||
EXPECT_EQ(this->syncs_, 0); // The RAM guard is gone, but STOP survives.
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0); // Even with validity flags already cleared.
|
||||
rebooted.write_time();
|
||||
rebooted.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
this->rtc_.write_time();
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 2);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, FailedCalendarWriteKeepsStopAndDoesNotRetry) {
|
||||
this->bus_.regs[FLAG] = 0x83;
|
||||
this->bus_.fail_write = SEC;
|
||||
this->bus_.fail_control_write_number = 2;
|
||||
this->rtc_.write_time();
|
||||
this->bus_.fail_write = -1;
|
||||
this->bus_.writes.clear();
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x83);
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, FailedCalendarWriteRejectsPreviouslyValidCalendarUntilFreshWrite) {
|
||||
this->rtc_.write_time();
|
||||
ASSERT_EQ(this->bus_.regs[FLAG], 0);
|
||||
this->bus_.fail_write = SEC;
|
||||
this->rtc_.write_time();
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
this->bus_.fail_write = -1;
|
||||
this->rtc_.write_time();
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, IncompleteWritesRemainInvalidAfterComponentRecreation) {
|
||||
for (const uint8_t failed_register : {SEC, FLAG, CONTROL}) {
|
||||
this->bus_.fail_write = -1;
|
||||
this->rtc_.write_time();
|
||||
ASSERT_EQ(this->bus_.regs[FLAG], 0);
|
||||
this->bus_.fail_write = failed_register == CONTROL ? -1 : failed_register;
|
||||
this->bus_.fail_control_write_number = failed_register == CONTROL ? this->bus_.control_writes + 2 : -1;
|
||||
this->rtc_.write_time();
|
||||
ASSERT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
this->bus_.fail_write = -1;
|
||||
RX8111Component rebooted;
|
||||
rebooted.set_i2c_bus(&this->bus_);
|
||||
rebooted.set_i2c_address(ADDRESS);
|
||||
unsigned syncs = 0;
|
||||
rebooted.add_on_time_sync_callback([&syncs]() { syncs++; });
|
||||
rebooted.setup();
|
||||
rebooted.read_time();
|
||||
EXPECT_EQ(syncs, 0) << "failed register " << unsigned(failed_register);
|
||||
rebooted.write_time();
|
||||
rebooted.read_time();
|
||||
EXPECT_EQ(syncs, 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, PersistentControlFailureDoesNotRearmRecovery) {
|
||||
this->bus_.fail_control_write_number = 2;
|
||||
this->rtc_.write_time();
|
||||
this->bus_.fail_write = CONTROL;
|
||||
const auto control_writes = this->bus_.control_writes;
|
||||
for (unsigned i = 0; i < 10; i++)
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.control_writes, control_writes);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, NewTrustedWriteRestartsStoppedCounterWithoutDeferredRecovery) {
|
||||
this->bus_.fail_control_write_number = 2;
|
||||
this->rtc_.write_time();
|
||||
ASSERT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
this->rtc_.write_time();
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 0);
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
this->bus_.writes.clear();
|
||||
this->advance_retry_();
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, FailedValidityCleanupRequiresFreshWriteAndPreservesOtherFaults) {
|
||||
this->bus_.regs[FLAG] = 0x83;
|
||||
this->bus_.fail_write = FLAG;
|
||||
this->rtc_.write_time();
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
this->bus_.fail_write = POWER_SWITCH;
|
||||
this->rtc_.set_battery_backup(false, false);
|
||||
this->configure_battery_();
|
||||
this->bus_.writes.clear();
|
||||
this->advance_retry_();
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x83);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.fail_write = -1;
|
||||
this->configure_battery_();
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
this->rtc_.write_time();
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, FoutReadFailureDoesNotAlterExtensionAndRecovers) {
|
||||
this->bus_.regs[EXTENSION] = 0xC5;
|
||||
this->bus_.fail_read = EXTENSION;
|
||||
this->rtc_.set_fout_frequency(3);
|
||||
this->rtc_.setup();
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xC5);
|
||||
EXPECT_TRUE(this->fout_warning_());
|
||||
this->bus_.fail_read = -1;
|
||||
this->advance_retry_();
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xC5);
|
||||
EXPECT_FALSE(this->fout_warning_());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, WritesCalendarAndPreservesEventFlagsWithoutReadModifyWrite) {
|
||||
this->bus_.regs[FLAG] = 0xB8; // POR + existing UF/TF/AF.
|
||||
this->bus_.regs[CONTROL] = 0x5C;
|
||||
@@ -111,7 +614,7 @@ TEST_F(RX8111Test, WritesCalendarAndPreservesEventFlagsWithoutReadModifyWrite) {
|
||||
(this->bus_.regs[SEC] == bcd(after.second) && this->bus_.regs[DAY] == bcd(after.day_of_month)));
|
||||
EXPECT_TRUE(this->bus_.regs[WEEK] == (1 << (before.day_of_week - 1)) ||
|
||||
this->bus_.regs[WEEK] == (1 << (after.day_of_week - 1)));
|
||||
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL, SEC, CONTROL, FLAG}));
|
||||
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL, SEC, FLAG, CONTROL}));
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, RejectsInvalidFlagsBCDWeekdayAndCalendar) {
|
||||
@@ -154,7 +657,7 @@ TEST_F(RX8111Test, RejectsInvalidFlagsBCDWeekdayAndCalendar) {
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.regs[SEC] = 0x60; // ESPTime permits leap seconds, but RX8111 only counts 00..59.
|
||||
this->bus_.regs[SEC] = 0x60; // Hardware supports leap adjustment; synchronization requires normal seconds.
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
@@ -164,7 +667,27 @@ TEST_F(RX8111Test, RejectsInvalidFlagsBCDWeekdayAndCalendar) {
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, I2CFailuresDoNotSyncOrLeaveTheCounterStopped) {
|
||||
TEST_F(RX8111Test, CalendarReadCombinesFlagAndControlAndRejectsReadFailures) {
|
||||
this->rtc_.write_time();
|
||||
this->bus_.reads.clear();
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_EQ(this->bus_.reads, (std::vector<std::pair<uint8_t, size_t>>{{FLAG, 2}, {SEC, 7}}));
|
||||
for (const uint8_t failed_register : {FLAG, CONTROL}) {
|
||||
this->bus_.reads.clear();
|
||||
this->bus_.fail_read = failed_register;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
EXPECT_EQ(this->bus_.reads, (std::vector<std::pair<uint8_t, size_t>>{{FLAG, 2}}));
|
||||
}
|
||||
this->bus_.fail_read = -1;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 2);
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, I2CFailuresDoNotSyncAndIncompleteWritesLeaveCounterStopped) {
|
||||
this->bus_.fail_read = FLAG;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
@@ -178,13 +701,13 @@ TEST_F(RX8111Test, I2CFailuresDoNotSyncOrLeaveTheCounterStopped) {
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
this->bus_.fail_write = SEC;
|
||||
this->rtc_.write_time();
|
||||
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL, CONTROL}));
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 0);
|
||||
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL}));
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
this->bus_.fail_write = FLAG;
|
||||
this->bus_.regs[FLAG] = 0x83;
|
||||
this->rtc_.write_time();
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x83);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 0);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, WriteFailureClearsOnSuccessfulWriteButPreservesEventPollWarning) {
|
||||
@@ -690,7 +1213,8 @@ TEST_F(RX8111Test, StartTimerRestartsFullCountAndClearsOnlyStaleTimerFlag) {
|
||||
EXPECT_EQ(this->bus_.regs[0x1C], 0);
|
||||
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x10, 0x10);
|
||||
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{EXTENSION, CONTROL, 0x1A, EXTENSION, FLAG, CONTROL, EXTENSION}));
|
||||
EXPECT_EQ(this->bus_.writes,
|
||||
(std::vector<uint8_t>{EXTENSION, CONTROL, TIMER_CONTROL, 0x1A, EXTENSION, FLAG, CONTROL, EXTENSION}));
|
||||
this->rtc_.update();
|
||||
EXPECT_EQ(calls, 0);
|
||||
this->bus_.regs[FLAG] |= 0x10;
|
||||
@@ -1004,8 +1528,10 @@ TEST_F(RX8111Test, RetainedAlarmInspectionRetriesAndDeliversWakeFlag) {
|
||||
this->rtc_.add_on_alarm_callback([&calls]() { calls++; });
|
||||
this->bus_.regs[FLAG] = 0x08;
|
||||
this->advance_retry_();
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
// Alarm recovery cannot clear the calendar's independent failed CONTROL-read warning.
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->rtc_.update();
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
this->rtc_.update();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user