From 3f6f2d7d650feee79896b13903c7a26e98559d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 21 Nov 2025 21:28:42 +0100 Subject: [PATCH] [bm8563] Add bm8563 component (#11616) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/bm8563/__init__.py | 1 + esphome/components/bm8563/bm8563.cpp | 198 ++++++++++++++++++ esphome/components/bm8563/bm8563.h | 57 +++++ esphome/components/bm8563/time.py | 80 +++++++ tests/components/bm8563/common.yaml | 10 + tests/components/bm8563/test.esp32-ard.yaml | 4 + tests/components/bm8563/test.esp32-idf.yaml | 4 + tests/components/bm8563/test.esp8266-ard.yaml | 4 + tests/components/bm8563/test.rp2040-ard.yaml | 4 + 10 files changed, 363 insertions(+) create mode 100644 esphome/components/bm8563/__init__.py create mode 100644 esphome/components/bm8563/bm8563.cpp create mode 100644 esphome/components/bm8563/bm8563.h create mode 100644 esphome/components/bm8563/time.py create mode 100644 tests/components/bm8563/common.yaml create mode 100644 tests/components/bm8563/test.esp32-ard.yaml create mode 100644 tests/components/bm8563/test.esp32-idf.yaml create mode 100644 tests/components/bm8563/test.esp8266-ard.yaml create mode 100644 tests/components/bm8563/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index c3d8f4350..d6ec7b882 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -72,6 +72,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_nus/* @tomaszduda23 esphome/components/bluetooth_proxy/* @bdraco @jesserockz +esphome/components/bm8563/* @abmantis esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth diff --git a/esphome/components/bm8563/__init__.py b/esphome/components/bm8563/__init__.py new file mode 100644 index 000000000..20254a8b0 --- /dev/null +++ b/esphome/components/bm8563/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@abmantis"] diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp new file mode 100644 index 000000000..07831485c --- /dev/null +++ b/esphome/components/bm8563/bm8563.cpp @@ -0,0 +1,198 @@ +#include "bm8563.h" +#include "esphome/core/log.h" + +namespace esphome::bm8563 { + +static const char *const TAG = "bm8563"; + +static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00; +static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01; +static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4 +static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8 +static constexpr uint8_t TIMER_CONTROL_REG = 0x0E; +static constexpr uint8_t TIMER_VALUE_REG = 0x0F; +static constexpr uint8_t CLOCK_1_HZ = 0x82; +static constexpr uint8_t CLOCK_1_60_HZ = 0x83; +// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds +static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60; + +void BM8563::setup() { + if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) { + this->mark_failed(); + return; + } +} + +void BM8563::update() { this->read_time(); } + +void BM8563::dump_config() { + ESP_LOGCONFIG(TAG, "BM8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void BM8563::start_timer(uint32_t duration_s) { + this->clear_irq_(); + this->set_timer_irq_(duration_s); +} + +void BM8563::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + + ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour, + now.minute, now.second); + + this->set_time_(now); + this->set_date_(now); +} + +void BM8563::read_time() { + ESPTime rtc_time; + this->get_time_(rtc_time); + this->get_date_(rtc_time); + rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid + ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); + + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +uint8_t BM8563::bcd2_to_byte_(uint8_t value) { + const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10; + return tmp + (value & 0x0F); +} + +uint8_t BM8563::byte_to_bcd2_(uint8_t value) { + const uint8_t bcdhigh = value / 10; + value -= bcdhigh * 10; + return (bcdhigh << 4) | value; +} + +void BM8563::get_time_(ESPTime &time) { + uint8_t buf[3] = {0}; + this->read_register(TIME_FIRST_REG, buf, 3); + + time.second = this->bcd2_to_byte_(buf[0] & 0x7f); + time.minute = this->bcd2_to_byte_(buf[1] & 0x7f); + time.hour = this->bcd2_to_byte_(buf[2] & 0x3f); +} + +void BM8563::set_time_(const ESPTime &time) { + uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)}; + this->write_register_(TIME_FIRST_REG, buf, 3); +} + +void BM8563::get_date_(ESPTime &time) { + uint8_t buf[4] = {0}; + this->read_register(DATE_FIRST_REG, buf, sizeof(buf)); + + time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f); + time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07); + time.month = this->bcd2_to_byte_(buf[2] & 0x1f); + + uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff); + + if (buf[2] & 0x80) { + time.year = 1900 + year_byte; + } else { + time.year = 2000 + year_byte; + } +} + +void BM8563::set_date_(const ESPTime &time) { + uint8_t buf[4] = { + this->byte_to_bcd2_(time.day_of_month), + this->byte_to_bcd2_(time.day_of_week), + this->byte_to_bcd2_(time.month), + this->byte_to_bcd2_(time.year % 100), + }; + + if (time.year < 2000) { + buf[2] = buf[2] | 0x80; + } + + this->write_register_(DATE_FIRST_REG, buf, 4); +} + +void BM8563::write_byte_(uint8_t reg, uint8_t value) { + if (!this->write_byte(reg, value)) { + ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value); + } +} + +void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) { + if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len); + } +} + +optional BM8563::read_register_(uint8_t reg) { + uint8_t data; + if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to read register 0x%02X", reg); + return {}; + } + return data; +} + +void BM8563::set_timer_irq_(uint32_t duration_s) { + ESP_LOGI(TAG, "Timer Duration: %u s", duration_s); + + if (duration_s > MAX_TIMER_DURATION_S) { + ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S); + return; + } + + if (duration_s > 255) { + uint8_t duration_minutes = duration_s / 60; + this->write_byte_(TIMER_VALUE_REG, duration_minutes); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ); + } else { + this->write_byte_(TIMER_VALUE_REG, duration_s); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ); + } + + auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_ctrl_status_2.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value(); + ctrl_status_2_reg_value |= (1 << 0); + ctrl_status_2_reg_value &= ~(1 << 7); + this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value); +} + +void BM8563::clear_irq_() { + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3); +} + +void BM8563::disable_irq_() { + this->clear_irq_(); + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc); +} + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/bm8563.h b/esphome/components/bm8563/bm8563.h new file mode 100644 index 000000000..eda2d1b3c --- /dev/null +++ b/esphome/components/bm8563/bm8563.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome::bm8563 { + +class BM8563 : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void write_time(); + void read_time(); + void start_timer(uint32_t duration_s); + + private: + void get_time_(ESPTime &time); + void get_date_(ESPTime &time); + + void set_time_(const ESPTime &time); + void set_date_(const ESPTime &time); + + void set_timer_irq_(uint32_t duration_s); + void clear_irq_(); + void disable_irq_(); + + void write_byte_(uint8_t reg, uint8_t value); + void write_register_(uint8_t reg, const uint8_t *data, size_t len); + optional read_register_(uint8_t reg); + + uint8_t bcd2_to_byte_(uint8_t value); + uint8_t byte_to_bcd2_(uint8_t value); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->read_time(); } +}; + +template class TimerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto duration = this->duration_.value(x...); + this->parent_->start_timer(duration); + } +}; + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py new file mode 100644 index 000000000..2785315af --- /dev/null +++ b/esphome/components/bm8563/time.py @@ -0,0 +1,80 @@ +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_DURATION, CONF_ID + +DEPENDENCIES = ["i2c"] + +I2C_ADDR = 0x51 + +bm8563_ns = cg.esphome_ns.namespace("bm8563") +BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice) +WriteAction = bm8563_ns.class_("WriteAction", automation.Action) +ReadAction = bm8563_ns.class_("ReadAction", automation.Action) +TimerAction = bm8563_ns.class_("TimerAction", automation.Action) + +CONFIG_SCHEMA = ( + time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BM8563), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(I2C_ADDR)) +) + + +@automation.register_action( + "bm8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "bm8563.start_timer", + TimerAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(BM8563), + cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), + } + ), +) +async def bm8563_start_timer_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "bm8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + 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) diff --git a/tests/components/bm8563/common.yaml b/tests/components/bm8563/common.yaml new file mode 100644 index 000000000..ec3fdd151 --- /dev/null +++ b/tests/components/bm8563/common.yaml @@ -0,0 +1,10 @@ +esphome: + on_boot: + - bm8563.read_time + - bm8563.write_time + - bm8563.start_timer: + duration: 300s + +time: + - platform: bm8563 + i2c_id: i2c_bus diff --git a/tests/components/bm8563/test.esp32-ard.yaml b/tests/components/bm8563/test.esp32-ard.yaml new file mode 100644 index 000000000..7c503b0cc --- /dev/null +++ b/tests/components/bm8563/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp32-idf.yaml b/tests/components/bm8563/test.esp32-idf.yaml new file mode 100644 index 000000000..b47e39c38 --- /dev/null +++ b/tests/components/bm8563/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp8266-ard.yaml b/tests/components/bm8563/test.esp8266-ard.yaml new file mode 100644 index 000000000..4a98b9388 --- /dev/null +++ b/tests/components/bm8563/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.rp2040-ard.yaml b/tests/components/bm8563/test.rp2040-ard.yaml new file mode 100644 index 000000000..319a7c71a --- /dev/null +++ b/tests/components/bm8563/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml