mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 11:42:51 +00:00
[rx8111] Add basic RX8111CE real-time clock support (#20785)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
6356458081
commit
108ed67d31
@@ -510,6 +510,7 @@ esphome/components/rtttl/* @glmnet @ximex
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8025t/* @remcom
|
||||
esphome/components/rx8111/* @techdregs
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/samsung/* @jorofi
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Epson RX8111CE real-time clock."""
|
||||
|
||||
CODEOWNERS = ["@techdregs"]
|
||||
DOMAIN = "rx8111"
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "rx8111.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::rx8111 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "rx8111");
|
||||
|
||||
static constexpr uint8_t RX8111_REG_SEC = 0x10;
|
||||
static constexpr uint8_t RX8111_REG_FLAG = 0x1E;
|
||||
static constexpr uint8_t RX8111_REG_CONTROL = 0x1F;
|
||||
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 uint8_t bcd_to_uint8(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); }
|
||||
static constexpr uint8_t uint8_to_bcd(uint8_t val) { return ((val / 10) << 4) | (val % 10); }
|
||||
|
||||
void RX8111Component::setup() { this->read_time(); }
|
||||
|
||||
void RX8111Component::update() { this->read_time(); }
|
||||
|
||||
void RX8111Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "RX8111:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
void RX8111Component::read_time() {
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags)) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
static constexpr uint8_t CALENDAR_MASKS[7] = {0x7F, 0x7F, 0x3F, 0x00, 0x3F, 0x1F, 0xFF};
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
const uint8_t weekday = static_cast<uint8_t>(__builtin_ctz(onehot) + 1);
|
||||
ESPTime rtc_time{
|
||||
.second = bcd_to_uint8(buf[0]),
|
||||
.minute = bcd_to_uint8(buf[1]),
|
||||
.hour = bcd_to_uint8(buf[2]),
|
||||
.day_of_week = weekday,
|
||||
.day_of_month = bcd_to_uint8(buf[4]),
|
||||
.month = bcd_to_uint8(buf[5]),
|
||||
.year = static_cast<uint16_t>(bcd_to_uint8(buf[6]) + 2000),
|
||||
.is_dst = false,
|
||||
.timestamp = 0,
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid(true, false) || rtc_time.second >= 60) {
|
||||
ESP_LOGW(TAG, "Invalid RTC time, not syncing to system clock");
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
}
|
||||
if (ESPTime::from_epoch_utc(rtc_time.timestamp).day_of_week != weekday) {
|
||||
ESP_LOGW(TAG, "RTC weekday does not match calendar date");
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
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);
|
||||
}
|
||||
|
||||
void RX8111Component::write_time() {
|
||||
auto now = time::RealTimeClock::utcnow();
|
||||
if (!now.is_valid() || now.year > 2099) {
|
||||
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC");
|
||||
return;
|
||||
}
|
||||
if (!this->set_stop_bit_(true)) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t buf[7];
|
||||
buf[0] = uint8_to_bcd(now.second);
|
||||
buf[1] = uint8_to_bcd(now.minute);
|
||||
buf[2] = uint8_to_bcd(now.hour);
|
||||
buf[3] = 1 << (now.day_of_week - 1);
|
||||
buf[4] = uint8_to_bcd(now.day_of_month);
|
||||
buf[5] = uint8_to_bcd(now.month);
|
||||
buf[6] = uint8_to_bcd(static_cast<uint8_t>(now.year - 2000));
|
||||
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->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS)) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
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 false;
|
||||
if (stop) {
|
||||
control |= RX8111_CTRL_STOP;
|
||||
} else {
|
||||
control &= ~RX8111_CTRL_STOP;
|
||||
}
|
||||
return this->write_byte(RX8111_REG_CONTROL, control);
|
||||
}
|
||||
|
||||
} // namespace esphome::rx8111
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome::rx8111 {
|
||||
|
||||
class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void read_time();
|
||||
void write_time();
|
||||
|
||||
protected:
|
||||
bool set_stop_bit_(bool stop);
|
||||
};
|
||||
|
||||
} // namespace esphome::rx8111
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Epson RX8111CE time platform."""
|
||||
|
||||
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_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@techdregs"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
rx8111_ns = cg.esphome_ns.namespace("rx8111")
|
||||
RX8111Component = rx8111_ns.class_("RX8111Component", time.RealTimeClock, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(RX8111Component)}
|
||||
).extend(i2c.i2c_device_schema(0x32))
|
||||
|
||||
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()")
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"rx8111.read_time", RX8111_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 i2c.register_i2c_device(var, config)
|
||||
await time.register_time(var, config)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Load the RX8111 time platform's dependencies in the C++ test build."""
|
||||
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.dependencies = manifest.dependencies + ["i2c", "time"]
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- rx8111.read_time: rtc
|
||||
- rx8111.write_time: rtc
|
||||
|
||||
time:
|
||||
- platform: rx8111
|
||||
id: rtc
|
||||
i2c_id: i2c_bus
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
|
||||
rx8111: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
rx8111: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
rx8111: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
rx8111: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
rx8111: !include common.yaml
|
||||
@@ -0,0 +1,160 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/rx8111/rx8111.h"
|
||||
|
||||
namespace esphome::rx8111::testing {
|
||||
|
||||
static constexpr uint8_t ADDRESS = 0x32;
|
||||
static constexpr uint8_t SEC = 0x10;
|
||||
static constexpr uint8_t WEEK = 0x13;
|
||||
static constexpr uint8_t DAY = 0x14;
|
||||
static constexpr uint8_t MONTH = 0x15;
|
||||
static constexpr uint8_t FLAG = 0x1E;
|
||||
static constexpr uint8_t CONTROL = 0x1F;
|
||||
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
std::array<uint8_t, 256> regs{};
|
||||
std::vector<uint8_t> writes;
|
||||
int fail_read{-1};
|
||||
int fail_write{-1};
|
||||
bool latch_event_before_flag_write{false};
|
||||
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *out, size_t out_size, uint8_t *in,
|
||||
size_t in_size) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
EXPECT_GT(out_size, 0);
|
||||
if (out_size == 0)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
const uint8_t reg = out[0];
|
||||
if (in_size > 0) {
|
||||
if (reg == this->fail_read)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
for (size_t i = 0; i < in_size; i++)
|
||||
in[i] = this->regs[reg + i];
|
||||
} else {
|
||||
if (reg == this->fail_write)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
this->writes.push_back(reg);
|
||||
if (reg == FLAG) {
|
||||
if (this->latch_event_before_flag_write)
|
||||
this->regs[FLAG] |= 0x04; // EVF latches immediately before FLAG write.
|
||||
// RX8111 flags are write-zero-to-clear; writing one leaves the bit unchanged.
|
||||
this->regs[FLAG] &= out[1];
|
||||
} else {
|
||||
for (size_t i = 1; i < out_size; i++)
|
||||
this->regs[reg + i - 1] = out[i];
|
||||
}
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
};
|
||||
|
||||
class RX8111Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->rtc_.set_i2c_bus(&this->bus_);
|
||||
this->rtc_.set_i2c_address(ADDRESS);
|
||||
this->rtc_.add_on_time_sync_callback([this]() { this->syncs_++; });
|
||||
}
|
||||
|
||||
RegisterBus bus_;
|
||||
RX8111Component rtc_;
|
||||
unsigned syncs_{0};
|
||||
};
|
||||
|
||||
TEST_F(RX8111Test, WritesCalendarAndPreservesEventFlagsWithoutReadModifyWrite) {
|
||||
this->bus_.regs[FLAG] = 0xB8; // POR + existing UF/TF/AF.
|
||||
this->bus_.regs[CONTROL] = 0x5C;
|
||||
this->bus_.latch_event_before_flag_write = true;
|
||||
const auto before = ESPTime::from_epoch_utc(::time(nullptr));
|
||||
this->rtc_.write_time();
|
||||
const auto after = ESPTime::from_epoch_utc(::time(nullptr));
|
||||
EXPECT_EQ(this->bus_.regs[CONTROL], 0x5C);
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0x3C); // The newly latched EVF survives too.
|
||||
auto bcd = [](int value) { return (value / 10) * 16 + value % 10; };
|
||||
EXPECT_TRUE((this->bus_.regs[SEC] == bcd(before.second) && this->bus_.regs[DAY] == bcd(before.day_of_month)) ||
|
||||
(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}));
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, RejectsInvalidFlagsBCDWeekdayAndCalendar) {
|
||||
this->bus_.regs[FLAG] = 0x83;
|
||||
this->rtc_.write_time();
|
||||
EXPECT_EQ(this->bus_.regs[FLAG], 0);
|
||||
this->bus_.regs[FLAG] = 0x83;
|
||||
this->bus_.writes.clear();
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
this->bus_.regs[FLAG] = 0;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
const uint8_t valid_weekday = this->bus_.regs[WEEK];
|
||||
const uint8_t valid_day = this->bus_.regs[DAY];
|
||||
const uint8_t valid_month = this->bus_.regs[MONTH];
|
||||
const uint8_t wrong_weekday = valid_weekday == 1 ? 2 : 1;
|
||||
for (uint8_t weekday : {uint8_t{0x00}, uint8_t{0x03}, uint8_t{0x80}, wrong_weekday}) {
|
||||
this->bus_.regs[WEEK] = weekday;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1) << "weekday " << unsigned(weekday);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
}
|
||||
this->bus_.regs[WEEK] = valid_weekday;
|
||||
this->bus_.regs[DAY] = 0x32;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.regs[DAY] = 0x31;
|
||||
this->bus_.regs[MONTH] = 0x04; // April 31 is always invalid.
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.regs[DAY] = valid_day;
|
||||
this->bus_.regs[MONTH] = valid_month;
|
||||
this->bus_.regs[SEC] = 0x6A;
|
||||
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->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 1);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.regs[SEC] = 0x00;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 2);
|
||||
EXPECT_FALSE(this->rtc_.status_has_warning());
|
||||
}
|
||||
|
||||
TEST_F(RX8111Test, I2CFailuresDoNotSyncOrLeaveTheCounterStopped) {
|
||||
this->bus_.fail_read = FLAG;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
EXPECT_TRUE(this->rtc_.status_has_warning());
|
||||
this->bus_.fail_read = SEC;
|
||||
this->rtc_.read_time();
|
||||
EXPECT_EQ(this->syncs_, 0);
|
||||
this->bus_.fail_read = -1;
|
||||
this->bus_.fail_write = CONTROL;
|
||||
this->rtc_.write_time();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace esphome::rx8111::testing
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Basic RX8111 configuration regressions."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.config import validate_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def configuration(tmp_path: Path, actions: list | None = None) -> dict:
|
||||
return {
|
||||
"esphome": {"name": "rx8111-clock-test", "build_path": str(tmp_path / "build")},
|
||||
"esp32": {"board": "esp32dev", "framework": {"type": "esp-idf"}},
|
||||
"logger": {},
|
||||
"i2c": {"sda": 21, "scl": 22, "scan": False},
|
||||
"time": [{"platform": "rx8111", "id": "rtc"}],
|
||||
"script": [
|
||||
{
|
||||
"id": "clock_actions",
|
||||
"then": actions or [{"rx8111.read_time": {"id": "rtc"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def validate_rx8111(tmp_path: Path, config: dict):
|
||||
CORE.reset()
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
return validate_config(config, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", ["rx8111.read_time", "rx8111.write_time"])
|
||||
def test_actions(tmp_path: Path, action: str) -> None:
|
||||
for payload in ({}, {"id": "rtc"}):
|
||||
assert not validate_rx8111(
|
||||
tmp_path, configuration(tmp_path, [{action: payload}])
|
||||
).errors
|
||||
Reference in New Issue
Block a user