DS3232 RTC crystal support + test of functionality

* Basic RTC function (like DS1307)
* Two hardware-based alarms / Stable 1Hz oscillator
* NVRAM storage for numeric variables (limited experimental support)
* Internal temperature sensor support
This commit is contained in:
Vladimir Kozhevnikov 2024-02-05 21:33:00 +03:00
parent 5e9741f51c
commit 4e14a0b8c0
8 changed files with 2291 additions and 0 deletions

View file

View file

@ -0,0 +1,660 @@
#include "ds3232.h"
#include "ds3232_alarm.h"
#include "esphome/core/log.h"
// Datasheet:
// - https://www.analog.com/media/en/technical-documentation/data-sheets/ds3232m.pdf
namespace esphome {
namespace ds3232 {
namespace ds3232alarms {} // namespace ds3232alarms
static const char *const TAG = "ds3232";
static const char *const DOW_TEXT = "day of week";
static const char *const DOM_TEXT = "day of month";
/// Factor to calculate temperature of DS3232
static const float TEMPERATURE_FACTOR = 0.25;
static const uint8_t I2C_REG_RTC = 0x00;
static const uint8_t I2C_REG_ALARM_1 = 0x07;
static const uint8_t I2C_REG_ALARM_2 = 0x0B;
static const uint8_t I2C_REG_CONTROL = 0x0E;
static const uint8_t I2C_REG_STATUS = 0x0F;
static const uint8_t I2C_REG_AGING = 0x10;
static const uint8_t I2C_REG_TEMPERATURE = 0x11;
static const uint8_t I2C_REG_START = I2C_REG_RTC;
void DS3232Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up DS3232...");
this->power_state_.next(DS3232PowerState::UNKNOWN);
if (!this->read_data_()) {
ESP_LOGE(TAG, "Unable to read data from crystal.");
this->mark_failed();
} else {
ESP_LOGV(TAG, "Checking for oscillator state.");
this->reinit_osf_();
}
this->nvram_available_ = this->read_bytes(SVC_NVRAM_ADDRESS, this->nvram_info_.raw, sizeof(this->nvram_info_.raw));
if (!this->nvram_available_) {
ESP_LOGE(TAG, "NVRAM: Unable to access NVRAM. Marking NVRAM functionality as failed.");
this->nvram_state_ = DS3232NVRAMState::FAIL;
this->variable_fail_callback_.call();
} else {
if ((this->nvram_info_.info.magix_1 == ds3232::MAGIX_CONTROL_1) &&
(this->nvram_info_.info.magix_2 == ds3232::MAGIX_CONTROL_2)) {
ESP_LOGI(TAG, "NVRAM usage has been detected.");
if (!this->nvram_info_.info.is_initialized) {
ESP_LOGI(TAG, "NVRAM will be written with initial values.");
this->plan_reset_nvram_();
} else {
ESP_LOGI(TAG, "Found NVRAM. Data version: %.2u.%.1u", this->nvram_info_.info.maj_version,
this->nvram_info_.info.min_version);
if ((this->nvram_info_.info.maj_version * 10 + this->nvram_info_.info.min_version) >
ds3232::NVRAM_DATA_VERSION) {
ESP_LOGW(TAG,
"Supported version (%.2u.%.1u) lower than stored version. Behaviour could be unpredictable. "
"Consider to reset NVRAM.",
ds3232::NVRAM_DATA_MAJ_VERSION, ds3232::NVRAM_DATA_MIN_VERSION);
}
this->nvram_state_ = DS3232NVRAMState::OK;
}
} else {
ESP_LOGI(TAG,
"Non-complaint data in NVRAM has been detected. NVRAM will be initialized. This will take some time.");
this->plan_clear_nvram_();
};
ESP_LOGD(TAG, "DS3232 NVRAM initialization complete");
this->late_startup_ = false;
}
// Workaround for states when alarms fired when ESPHome device was unpowered but DS3232 was online.
// There is a problem with A1F / A2F registers - INT signal will not be issued when this registers already set to 1.
// So there is a need to reset them to 0 on startup.
if (this->reg_data_.reg.alarm_1_match || this->reg_data_.reg.alarm_2_match) {
if (this->fire_alarms_on_startup_) {
process_alarms_();
} else {
reg_data_.reg.alarm_1_match = false;
reg_data_.reg.alarm_2_match = false;
this->write_bytes(I2C_REG_STATUS, reg_data_.raw_blocks.status_raw, sizeof(reg_data_.raw_blocks.status_raw));
}
}
}
void DS3232Component::update() {
if(this->late_startup_)
return;
this->read_time();
this->read_temperature_();
}
// Need to initialize afted i2c bus.
// I don't know why but on my ESP32 board i2c bus initialized and
// ready for connection too late. As a result sometimes
// component marked as failed.
float DS3232Component::get_setup_priority() const { return setup_priority::DATA - 200; }
void DS3232Component::reinit_osf_() {
this->read_data_();
if(!this->reg_data_.reg.osf_bit)
return;
ESP_LOGD(TAG, "Found disabled oscillator. Restarting it.");
this->reg_data_.reg.osf_bit = false;
if(!this->write_byte(I2C_REG_STATUS, this->reg_data_.raw_blocks.status_raw[0]))
{
ESP_LOGE(TAG, "Unable to restart oscillator.");
return;
}
}
void DS3232Component::dump_config() {
ESP_LOGCONFIG(TAG, "DS3232");
LOG_PIN(" Interrupt / Heartbeat pin: ", this->int_pin_);
LOG_PIN(" Reset / Power pin: ", this->rst_pin_);
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with DS3232 failed!");
} else {
this->read_data_();
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
ESP_LOGCONFIG(TAG, " Internal config from chip:");
ESP_LOGCONFIG(TAG, " Ocsillation active: %s", TRUEFALSE(!this->reg_data_.reg.osf_bit));
ESP_LOGCONFIG(TAG, " Heartbeat mode: %s", ONOFF(!this->reg_data_.reg.int_enable));
ESP_LOGCONFIG(TAG, " Alarms mode: %s", ONOFF(this->reg_data_.reg.int_enable));
ds3232_alarm::DS3232Alarm alarm;
alarm = this->get_alarm_one();
if (alarm.enabled) {
ESP_LOGCONFIG(TAG, " Alarm 1: %s", this->reg_data_.reg.int_enable ? "ACTIVE" : "SUSPENDED");
ESP_LOGCONFIG(TAG, " Start on %s", alarm.to_string().c_str());
ESP_LOGCONFIG(TAG, " Matched: %s", TRUEFALSE(this->reg_data_.reg.alarm_1_match));
} else {
ESP_LOGCONFIG(TAG, " Alarm 1: INACTIVE");
};
alarm = this->get_alarm_two();
if (alarm.enabled) {
ESP_LOGCONFIG(TAG, " Alarm 2: %s", this->reg_data_.reg.int_enable ? "ACTIVE" : "SUSPENDED");
ESP_LOGCONFIG(TAG, " Start on %s", alarm.to_string().c_str());
ESP_LOGCONFIG(TAG, " Matched: %s", TRUEFALSE(this->reg_data_.reg.alarm_2_match));
} else {
ESP_LOGCONFIG(TAG, " Alarm 2: INACTIVE");
}
}
}
void DS3232Component::loop() {
auto pow_pin_state = DS3232PowerState::UNKNOWN;
if (this->rst_pin_ != nullptr) {
pow_pin_state = this->rst_pin_->digital_read() ? DS3232PowerState::ONLINE : DS3232PowerState::BATTERY;
}
if (this->power_state_.next(pow_pin_state)) {
ESP_LOGI(TAG, "Power source changed: %u", pow_pin_state);
this->power_state_callback_.call(pow_pin_state);
}
if (this->int_pin_ != nullptr) {
auto pin_state = this->int_pin_->digital_read();
if ((this->pin_state_ != pin_state) && !pin_state) {
ESP_LOGD(TAG, "Got active low on INT pin.");
this->process_interrupt_();
}
this->pin_state_ = pin_state;
}
switch (this->nvram_state_)
{
case DS3232NVRAMState::NEED_INITIALIZATION:
if(this->busy_) {
ESP_LOGI(TAG, "Performing planned NVRAM full reset...");
clear_nvram_();
}
break;
case DS3232NVRAMState::NEED_RESET:
if(this->busy_) {
ESP_LOGI(TAG, "Performing planned NVRAM factory reset...");
this->nvram_state_ = DS3232NVRAMState::OK;
this->variable_init_callback_.call();
if(this->nvram_state_ != DS3232NVRAMState::OK)
{
ESP_LOGE(TAG, "NVRAM: Failed to reset to factory defaults.");
}
this->busy_ = false;
}
break;
default:
break;
}
}
void DS3232Component::plan_clear_nvram_() {
this->busy_ = true;
this->nvram_state_ = DS3232NVRAMState::NEED_INITIALIZATION;
}
void DS3232Component::plan_reset_nvram_() {
this->busy_ = true;
this->nvram_state_ = DS3232NVRAMState::NEED_RESET;
}
void DS3232Component::clear_nvram_() {
const uint8_t write_len = 8;
this->nvram_state_ = DS3232NVRAMState::INITIALIZATION;
ESP_LOGD(TAG, "NVRAM: Begin to clear memory.");
uint8_t total_size = MAX_NVRAM_ADDRESS - MIN_NVRAM_ADDRESS + 1;
ESP_LOGD(TAG, "NVRAM: Total size to clear: %.2d byte(s)", total_size);
uint8_t step = 0;
bool state = true;
std::vector<uint8_t> zeroes;
zeroes.resize(write_len_, 0);
while (step < total_size)
{
bool res = false;
if((total_size - step) < write_len) {
res_ = this->write_bytes(MIN_NVRAM_ADDRESS + step_, zeroes.data(), total_size - step_);
step += (total_size - step);
} else {
res_ = this->write_bytes(MIN_NVRAM_ADDRESS + step_, zeroes);
step += write_len;
}
ESP_LOGV(TAG, "NVRAM: Trying to set %#02x register to 0x00: %s", MIN_NVRAM_ADDRESS + step, YESNO(res));
state &= res;
}
if(!state)
{
ESP_LOGE(TAG, "NVRAM: Unable to clear memory. Marking as failed.");
this->nvram_state_ = DS3232NVRAMState::FAIL;
} else {
ESP_LOGD(TAG, "NVRAM: Memory has been cleared. Writing service information.");
this->nvram_info_.info.magix_1 = MAGIX_CONTROL_1;
this->nvram_info_.info.magix_2 = MAGIX_CONTROL_2;
this->nvram_info_.info.is_initialized = false;
this->nvram_info_.info.maj_version = NVRAM_DATA_MAJ_VERSION;
this->nvram_info_.info.min_version = NVRAM_DATA_MIN_VERSION;
if(!this->write_bytes(SVC_NVRAM_ADDRESS, this->nvram_info_.raw, sizeof(this->nvram_info_.raw)))
{
ESP_LOGE(TAG, "NVRAM: Unable to write service NVRAM information. Marking as failed.");
this->nvram_state_ = DS3232NVRAMState::FAIL;
}
ESP_LOGD(TAG, "NVRAM: Initializing variables with initial values.");
this->variable_init_callback_.call();
ESP_LOGD(TAG, "NVRAM: Variables has been initialized. Saving state.");
this->nvram_info_.info.is_initialized = true;
if(!this->write_bytes(SVC_NVRAM_ADDRESS, this->nvram_info_.raw, sizeof(this->nvram_info_.raw)))
{
ESP_LOGE(TAG, "NVRAM: Unable to write service NVRAM information. Marking as failed.");
this->nvram_state_ = DS3232NVRAMState::FAIL;
}
this->nvram_state_ = DS3232NVRAMState::OK;
ESP_LOGI(TAG, "NVRAM has been cleared.");
}
this->busy_ = false;
}
bool DS3232Component::validate_mem_(uint8_t reg_id, uint8_t size, bool ignore_empty) {
bool validator = true;
validator &= (ignore_empty || this->nvram_available_);
validator &= reg_id >= ds3232::MIN_NVRAM_ADDRESS;
validator &= (reg_id + size - 1) <= ds3232::MAX_NVRAM_ADDRESS;
return validator;
}
void DS3232Component::reset_memory() {
if(this->nvram_state_ == DS3232NVRAMState::INITIALIZATION) {
ESP_LOGW(TAG, "NVRAM: Another memory reset process in progress already.");
return;
}
ESP_LOGI(TAG, "Resetting NVRAM has been requested.");
this->plan_clear_nvram_();
}
void DS3232Component::reset_to_factory() {
if(this->nvram_state_ == DS3232NVRAMState::INITIALIZATION) {
ESP_LOGW(TAG, "NVRAM: Another memory reset process in progress already.");
return;
}
ESP_LOGI(TAG, "Resetting config variables to their initial values.");
this->plan_reset_nvram_();
}
bool DS3232Component::read_memory(uint8_t reg_id, std::vector<uint8_t> &data) {
if(this->nvram_state_ == DS3232NVRAMState::INITIALIZATION) {
ESP_LOGW(TAG, "NVRAM: Memory reset process in progress. Try later.");
return false;
}
if(data.size() == 0) {
ESP_LOGW(TAG, "NVRAM: Nothing to write to memory.");
return true;
}
if (!this->validate_mem_(reg_id, data.size())) {
ESP_LOGE(TAG, "Invalid NVRAM memory mapping.");
return false;
} else {
if(!this->read_bytes(reg_id, data.data(), data.size())) {
ESP_LOGE(TAG, "NVRAM: Unable to read from %#02x register with size %u.", reg_id, data.size());
this->nvram_state_ = DS3232NVRAMState::FAIL;
return false;
};
ESP_LOGD(TAG, "NVRAM: Value read from %#02x register with size %u.", reg_id, data.size());
return true;
}
}
bool DS3232Component::write_memory(const uint8_t reg_id, const std::vector<uint8_t> &data)
{
if(data.size() == 0) {
ESP_LOGW(TAG, "NVRAM: Nothing to write to memory.");
return true;
}
if(this->nvram_state_ == DS3232NVRAMState::INITIALIZATION) {
ESP_LOGW(TAG, "NVRAM: Memory reset process in progress. Try later.");
return false;
}
if(!this->validate_mem_(reg_id, data.size(), true))
{
ESP_LOGE(TAG, "Invalid NVRAM memory mapping.");
return false;
}
if(!this->write_bytes(reg_id, data))
{
ESP_LOGE(TAG, "NVRAM: Unable to write to %#02x register with size %u.", reg_id, data.size());
this->nvram_state_ = DS3232NVRAMState::FAIL;
return false;
} else {
ESP_LOGD(TAG, "NVRAM: Value written to %#02x register with size %u.", reg_id, data.size());
return true;
}
}
void DS3232Component::process_interrupt_() {
if (this->read_data_()) {
if (!reg_data_.reg.int_enable) {
ESP_LOGD(TAG, "1Hz Heartbeat fired.");
this->heartbeat_callback_.call();
} else {
process_alarms_();
}
}
}
void DS3232Component::process_alarms_() {
auto firing_time = this->reg_to_time_();
if (reg_data_.reg.alarm_1_enable && reg_data_.reg.alarm_1_match) {
reg_data_.reg.alarm_1_match = false;
ESP_LOGI(TAG, "Alarm 1 has been fired.");
this->alarm_one_callback_.call(firing_time);
};
if (reg_data_.reg.alarm_2_enable && reg_data_.reg.alarm_2_match) {
reg_data_.reg.alarm_2_match = false;
ESP_LOGI(TAG, "Alarm 2 has been fired.");
this->alarm_two_callback_.call(firing_time);
};
this->write_bytes(I2C_REG_STATUS, reg_data_.raw_blocks.status_raw, sizeof(reg_data_.raw_blocks.status_raw));
}
/// @brief Reads i2c registers from chip
/// @return True - if data has been read; otherwise - false
bool DS3232Component::read_data_() {
if (!this->read_bytes(I2C_REG_START, this->reg_data_.raw, sizeof(this->reg_data_.raw))) {
ESP_LOGE(TAG, "Unable to read I2C data.");
return false;
}
ESP_LOGV(TAG,
"Read registers:\nRTC: %0u%0u:%0u%0u:%0u%0u 2%0u%0u%0u-%0u%0u-%0u%0u\nAlarm 1 (%s): 0b%0u%0u%0u%0u "
"%0u%0u:%0u%0u:%0u%0u / %0u%0u (as %s)\nAlarm 2 (%s): 0b%0u%0u%0u0 %0u%0u:%0u%0u:00 / %0u%0u (as "
"%s)\nTemperature: (%d + 0.25*%0u) °C\nEOSC:%s BBSQW:%s CONV:%s INTCN:%s\nOSCF:%s BB32KHZ:%s EN32KHZ:%s "
"BSY:%s A2F:%0u A1F:%0u",
reg_data_.reg.hour_10, reg_data_.reg.hour, reg_data_.reg.minute_10, reg_data_.reg.minute,
reg_data_.reg.second_10, reg_data_.reg.second, reg_data_.reg.century, reg_data_.reg.year_10,
reg_data_.reg.year, reg_data_.reg.month_10, reg_data_.reg.month, reg_data_.reg.day_10, reg_data_.reg.day,
ONOFF(reg_data_.reg.alarm_1_enable), reg_data_.reg.alarm_1_mode_4, reg_data_.reg.alarm_1_mode_3,
reg_data_.reg.alarm_1_mode_2, reg_data_.reg.alarm_1_mode_1, reg_data_.reg.alarm_1_hour_10,
reg_data_.reg.alarm_1_hour, reg_data_.reg.alarm_1_minute_10, reg_data_.reg.alarm_1_minute,
reg_data_.reg.alarm_1_second_10, reg_data_.reg.alarm_1_second, reg_data_.reg.alarm_1_day_10,
reg_data_.reg.alarm_1_day, (reg_data_.reg.alarm_1_use_day_of_week ? DOW_TEXT : DOM_TEXT),
ONOFF(reg_data_.reg.alarm_2_enable), reg_data_.reg.alarm_2_mode_4, reg_data_.reg.alarm_2_mode_3,
reg_data_.reg.alarm_2_mode_2, reg_data_.reg.alarm_2_hour_10, reg_data_.reg.alarm_2_hour,
reg_data_.reg.alarm_2_minute_10, reg_data_.reg.alarm_2_minute, reg_data_.reg.alarm_2_day_10,
reg_data_.reg.alarm_2_day, (reg_data_.reg.alarm_2_use_day_of_week ? DOW_TEXT : DOM_TEXT),
reg_data_.reg.temperature_integral, reg_data_.reg.temperature_fractional, ONOFF(reg_data_.reg.EOSC),
ONOFF(reg_data_.reg.enable_battery_square_signal), ONOFF(reg_data_.reg.convert_temp),
ONOFF(reg_data_.reg.int_enable), ONOFF(reg_data_.reg.osf_bit),
ONOFF(reg_data_.reg.enable_hf_output_on_battery_mode), ONOFF(reg_data_.reg.enable_hf_output),
ONOFF(reg_data_.reg.device_busy), reg_data_.reg.alarm_2_match, reg_data_.reg.alarm_1_match);
return true;
}
/// @brief Reads temperature from internal chip sensor and updates sensor component on data change
void DS3232Component::read_temperature_() {
float temperature = reg_data_.reg.temperature_integral + TEMPERATURE_FACTOR * reg_data_.reg.temperature_fractional;
ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature);
if (this->temperature_sensor_ != nullptr) {
if (this->temperature_state_.next(temperature)) {
this->temperature_sensor_->publish_state(temperature);
};
};
}
/// @brief Creates ESPTime struct from registers data
/// @return ESPTime struct that contains time stored in RTC registers.
ESPTime DS3232Component::reg_to_time_() {
return ESPTime{
.second = uint8_t(reg_data_.reg.second + 10 * reg_data_.reg.second_10),
.minute = uint8_t(reg_data_.reg.minute + 10u * reg_data_.reg.minute_10),
.hour = uint8_t(reg_data_.reg.hour + 10u * reg_data_.reg.hour_10),
.day_of_week = uint8_t(reg_data_.reg.weekday),
.day_of_month = uint8_t(reg_data_.reg.day + 10u * reg_data_.reg.day_10),
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
.month = uint8_t(reg_data_.reg.month + 10u * reg_data_.reg.month_10),
.year = uint16_t(reg_data_.reg.year + 10u * reg_data_.reg.year_10 + 100u * reg_data_.reg.century + 2000)};
}
/// @brief Reads time from chip
void DS3232Component::read_time() {
if (!this->read_data_()) {
return;
}
if (reg_data_.reg.osf_bit) {
ESP_LOGW(TAG, "RTC halted, not syncing to system clock.");
return;
}
if (reg_data_.reg.device_busy) {
ESP_LOGW(TAG, "RTC busy, not syncing to system clock.");
return;
}
ESPTime current_rtc_time = this->reg_to_time_();
current_rtc_time.recalc_timestamp_utc(false);
if (!current_rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
time::RealTimeClock::synchronize_epoch_(current_rtc_time.timestamp);
}
/// @brief Writes current time to chip
void DS3232Component::write_time() {
auto now = time::RealTimeClock::utcnow();
if (!now.is_valid()) {
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
return;
}
reg_data_.reg.year = (now.year >= 2100) ? ((now.year - 2100) % 10) : ((now.year - 2000) % 10);
reg_data_.reg.year_10 = ((now.year >= 2100) ? ((now.year - 2100) / 10) : ((now.year - 2000) / 10)) % 10;
reg_data_.reg.century = (now.year >= 2100) ? 1 : 0;
reg_data_.reg.month = now.month % 10;
reg_data_.reg.month_10 = now.month / 10;
reg_data_.reg.day = now.day_of_month % 10;
reg_data_.reg.day_10 = now.day_of_month / 10;
reg_data_.reg.weekday = now.day_of_week;
reg_data_.reg.hour = now.hour % 10;
reg_data_.reg.hour_10 = now.hour / 10;
reg_data_.reg.minute = now.minute % 10;
reg_data_.reg.minute_10 = now.minute / 10;
reg_data_.reg.second = now.second % 10;
reg_data_.reg.second_10 = now.second / 10;
reg_data_.reg.osf_bit = true;
reg_data_.reg.EOSC = false;
this->write_bytes(I2C_REG_RTC, reg_data_.raw_blocks.rtc_raw, sizeof(reg_data_.raw_blocks.rtc_raw));
this->write_bytes(I2C_REG_CONTROL, reg_data_.raw_blocks.control_raw, sizeof(reg_data_.raw_blocks.control_raw));
this->write_bytes(I2C_REG_STATUS, reg_data_.raw_blocks.status_raw, sizeof(reg_data_.raw_blocks.status_raw));
}
/// @brief Gets alarm one state from chip
/// @return A DS3232Alarm struct that contains detailed information about alarm
ds3232_alarm::DS3232Alarm DS3232Component::get_alarm_one() {
if (!this->read_data_()) {
return {};
}
ds3232_alarm::DS3232Alarm alarm = {};
alarm.enabled = reg_data_.reg.alarm_1_enable;
alarm.seconds_supported = true;
alarm.mode = ds3232_alarm::DS3232Alarm::alarm_mode(
reg_data_.reg.alarm_1_mode_1,
reg_data_.reg.alarm_1_mode_2,
reg_data_.reg.alarm_1_mode_3,
reg_data_.reg.alarm_1_mode_4,
reg_data_.reg.alarm_1_use_day_of_week);
alarm.second = reg_data_.reg.alarm_1_second + 10u * reg_data_.reg.alarm_1_second_10;
alarm.minute = reg_data_.reg.alarm_1_minute + 10u * reg_data_.reg.alarm_1_minute_10;
alarm.hour = reg_data_.reg.alarm_1_hour + 10u * reg_data_.reg.alarm_1_hour_10;
alarm.day_of_week = reg_data_.reg.alarm_1_day;
alarm.day_of_month = reg_data_.reg.alarm_1_day + 10u * reg_data_.reg.alarm_1_day_10;
alarm.is_fired = reg_data_.reg.alarm_1_match;
if (!alarm.is_valid()) {
ESP_LOGW(TAG, "Stored 1st alarm data is invalid.");
}
return alarm;
}
/// @brief Gets alarm two state from chip
/// @return A DS3232Alarm struct that contains detailed information about alarm
ds3232_alarm::DS3232Alarm DS3232Component::get_alarm_two() {
if (!this->read_data_()) {
return {};
}
ds3232_alarm::DS3232Alarm alarm = {};
alarm.enabled = reg_data_.reg.alarm_2_enable;
alarm.seconds_supported = false;
alarm.mode = ds3232_alarm::DS3232Alarm::alarm_mode(
false,
reg_data_.reg.alarm_2_mode_2,
reg_data_.reg.alarm_2_mode_3,
reg_data_.reg.alarm_2_mode_4,
reg_data_.reg.alarm_2_use_day_of_week);
alarm.second = 0;
alarm.minute = reg_data_.reg.alarm_2_minute + 10u * reg_data_.reg.alarm_2_minute_10;
alarm.hour = reg_data_.reg.alarm_2_hour + 10u * reg_data_.reg.alarm_2_hour_10;
alarm.day_of_week = reg_data_.reg.alarm_2_day;
alarm.day_of_month = reg_data_.reg.alarm_2_day + 10u * reg_data_.reg.alarm_2_day_10;
alarm.is_fired = reg_data_.reg.alarm_2_match;
if (!alarm.is_valid()) {
ESP_LOGW(TAG, "Stored 2nd alarm data is invalid.");
}
return alarm;
}
/// @brief Resets alarm one data to initial settings and disables it.
void DS3232Component::clear_alarm_one() {
ds3232_alarm::DS3232Alarm alarm = this->get_alarm_one();
alarm.enabled = false;
alarm.second = 0;
alarm.minute = 0;
alarm.hour = 0;
alarm.day_of_month = 1;
alarm.day_of_week = 1;
alarm.mode = ds3232_alarm::DS3232Alarm::alarm_mode(ds3232_alarm::AlarmMode::EVERY_TIME);
this->set_alarm_one(alarm);
}
/// @brief Resets alarm two data to initial settings and disables it.
void DS3232Component::clear_alarm_two() {
ds3232_alarm::DS3232Alarm alarm = this->get_alarm_two();
alarm.enabled = false;
alarm.second = 0;
alarm.minute = 0;
alarm.hour = 0;
alarm.day_of_month = 1;
alarm.day_of_week = 1;
alarm.mode = ds3232_alarm::DS3232Alarm::alarm_mode(ds3232_alarm::AlarmMode::EVERY_TIME);
this->set_alarm_two(alarm);
}
/// @brief Sets the 1st alarm
/// @param alarm Struct that contains detailed information about alarm to set.
void DS3232Component::set_alarm_one(const ds3232_alarm::DS3232Alarm &alarm) {
if (!alarm.is_valid()) {
ESP_LOGW(TAG, "Unable to set alarm one to invalid state.");
return;
}
if (!this->read_data_()) {
return;
}
reg_data_.reg.alarm_1_enable = alarm.enabled;
reg_data_.reg.alarm_1_second = alarm.second % 10;
reg_data_.reg.alarm_1_second_10 = alarm.second / 10;
reg_data_.reg.alarm_1_minute = alarm.minute % 10;
reg_data_.reg.alarm_1_minute_10 = alarm.minute / 10;
reg_data_.reg.alarm_1_hour = alarm.hour % 10;
reg_data_.reg.alarm_1_hour_10 = alarm.hour / 10;
reg_data_.reg.alarm_1_day = alarm.mode.bits.use_weekdays ? alarm.day_of_week : alarm.day_of_month % 10;
reg_data_.reg.alarm_1_day_10 = alarm.mode.bits.use_weekdays ? 0 : alarm.day_of_month / 10;
reg_data_.reg.alarm_1_mode_1 = alarm.mode.bits.match_seconds;
reg_data_.reg.alarm_1_mode_2 = alarm.mode.bits.match_minutes;
reg_data_.reg.alarm_1_mode_3 = alarm.mode.bits.match_hours;
reg_data_.reg.alarm_1_mode_4 = alarm.mode.bits.match_days;
reg_data_.reg.alarm_1_use_day_of_week = alarm.mode.bits.use_weekdays;
reg_data_.reg.alarm_2_match = false;
reg_data_.reg.int_enable =
(reg_data_.reg.alarm_1_enable || reg_data_.reg.alarm_2_enable) && !this->produce_stable_frequency_;
if (!this->write_bytes(I2C_REG_ALARM_1, reg_data_.raw_blocks.alarm_1_raw, sizeof(reg_data_.raw_blocks.alarm_1_raw))) {
return;
}
this->write_bytes(I2C_REG_CONTROL, reg_data_.raw_blocks.control_raw, sizeof(reg_data_.raw_blocks.control_raw));
this->write_bytes(I2C_REG_STATUS, reg_data_.raw_blocks.status_raw, sizeof(reg_data_.raw_blocks.status_raw));
}
/// @brief Sets the 2nd alarm
/// @param alarm Struct that contains detailed information about alarm to set.
void DS3232Component::set_alarm_two(const ds3232_alarm::DS3232Alarm &alarm) {
if (!alarm.is_valid()) {
ESP_LOGW(TAG, "Unable to set alarm one to invalid state.");
return;
}
if (!this->read_data_()) {
return;
}
reg_data_.reg.alarm_2_enable = alarm.enabled;
reg_data_.reg.alarm_2_minute = alarm.minute % 10;
reg_data_.reg.alarm_2_minute_10 = alarm.minute / 10;
reg_data_.reg.alarm_2_hour = alarm.hour % 10;
reg_data_.reg.alarm_2_hour_10 = alarm.hour / 10;
reg_data_.reg.alarm_2_day = alarm.mode.bits.use_weekdays ? alarm.day_of_week : alarm.day_of_month % 10;
reg_data_.reg.alarm_2_day_10 = alarm.mode.bits.use_weekdays ? 0 : alarm.day_of_month / 10;
// reg_data_.reg.alarm_2_mode_1 = alarm.mode.bits.match_seconds; - there is no mode_1 flag for alarm 2.
reg_data_.reg.alarm_2_mode_2 = alarm.mode.bits.match_minutes;
reg_data_.reg.alarm_2_mode_3 = alarm.mode.bits.match_hours;
reg_data_.reg.alarm_2_mode_4 = alarm.mode.bits.match_days;
reg_data_.reg.alarm_2_use_day_of_week = alarm.mode.bits.use_weekdays;
reg_data_.reg.alarm_2_match = false;
reg_data_.reg.int_enable =
(reg_data_.reg.alarm_1_enable || reg_data_.reg.alarm_2_enable) && !this->produce_stable_frequency_;
if (!this->write_bytes(I2C_REG_ALARM_2, reg_data_.raw_blocks.alarm_2_raw, sizeof(reg_data_.raw_blocks.alarm_2_raw))) {
return;
}
this->write_bytes(I2C_REG_CONTROL, reg_data_.raw_blocks.control_raw, sizeof(reg_data_.raw_blocks.control_raw));
this->write_bytes(I2C_REG_STATUS, reg_data_.raw_blocks.status_raw, sizeof(reg_data_.raw_blocks.status_raw));
}
/// @brief Gets heartbeat state
/// @return If true then 1Hz signal output on INT pin enabled.
/// If false then INT pin goes low on alarm firing event.
bool DS3232Component::is_heartbeat_enabled() {
if (!this->read_data_()) {
return false;
}
return !reg_data_.reg.int_enable;
}
/// @brief Enables 1Hz output and disables alarms.
void DS3232Component::enable_heartbeat() {
if (!this->read_data_()) {
return;
}
reg_data_.reg.int_enable = false;
reg_data_.reg.alarm_1_match = false;
reg_data_.reg.alarm_2_match = false;
this->write_bytes(I2C_REG_STATUS, reg_data_.raw_blocks.status_raw, sizeof(reg_data_.raw_blocks.status_raw));
this->write_bytes(I2C_REG_CONTROL, reg_data_.raw_blocks.control_raw, sizeof(reg_data_.raw_blocks.control_raw));
this->produce_stable_frequency_ = true;
}
/// @brief Enables alarms functionality and disables 1Hz output.
void DS3232Component::enable_alarms() {
if (!this->read_data_()) {
return;
}
reg_data_.reg.int_enable = true;
reg_data_.reg.alarm_1_match = false;
reg_data_.reg.alarm_2_match = false;
this->write_bytes(I2C_REG_STATUS, reg_data_.raw_blocks.status_raw, sizeof(reg_data_.raw_blocks.status_raw));
this->write_bytes(I2C_REG_CONTROL, reg_data_.raw_blocks.control_raw, sizeof(reg_data_.raw_blocks.control_raw));
this->produce_stable_frequency_ = false;
}
} // namespace ds3232
} // namespace esphome

View file

@ -0,0 +1,352 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/hal.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/time/real_time_clock.h"
#include "esphome/core/automation.h"
#include "ds3232_alarm.h"
namespace esphome {
namespace ds3232 {
/// @brief Power state of DS3232 chip
enum DS3232PowerState {
/// @brief Power state is unknown, i.e. rst_pin is not connected.
UNKNOWN = 0b00,
/// @brief Vcc >= Vbattery, i.e. power is online.
ONLINE = 0b01,
/// @brief Vbat >= Vcc, i.e. no supplied power provided, battery is used.
BATTERY = 0b10
};
/// @brief State of NVRAM functionality
enum DS3232NVRAMState {
/// @brief State is unknown
UNCERTAIN = 0b0000,
/// @brief NVRAM should be initialized before usage.
NEED_INITIALIZATION = 0b0001,
/// @brief NVRAM initialization process in progress.
INITIALIZATION = 0b0010,
/// @brief NVRAM should be initialized with variables default values.
NEED_RESET = 0b0100,
/// @brief NVRAM is in normal state.
OK = 0b1000,
/// @brief NVRAM found but its state is faulty.
FAIL = 0b1111
};
/// @brief Basic register to store service information
static const uint8_t SVC_NVRAM_ADDRESS = 0x14;
/// @brief Minimal number of NVRAM register available to user variables.
static const uint8_t MIN_NVRAM_ADDRESS = 0x20;
/// @brief Maximal number of NVRAM register available to user variables.
static const uint8_t MAX_NVRAM_ADDRESS = 0xFF;
/// @brief Magic value 1 to control validity of NVRAM.
static const uint8_t MAGIX_CONTROL_1 = 0b00000011;
/// @brief Magic value 2 to control validity of NVRAM.
static const uint8_t MAGIX_CONTROL_2 = 0b00101101;
/// @brief Major version of NVRAM storage logic
static const uint8_t NVRAM_DATA_MAJ_VERSION = 0;
/// @brief Minor version of NVRAM storage logic
static const uint8_t NVRAM_DATA_MIN_VERSION = 1;
/// @brief NVRAM storage version
static const uint8_t NVRAM_DATA_VERSION = NVRAM_DATA_MAJ_VERSION * 10 + NVRAM_DATA_MIN_VERSION;
/// @brief Component that implements ds3232mz+ crystal capabilities
class DS3232Component : public time::RealTimeClock, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
void loop() override;
float get_setup_priority() const override;
/// @brief Gets current state of NVRAM functionality.
/// @return One of values from DS3232NVRAMState
/// @see DS3232NVRAMState
DS3232NVRAMState get_nvram_state() { return this->busy_ ? DS3232NVRAMState::UNCERTAIN : this->nvram_state_; }
void read_time(); // Action
void write_time(); // Action
bool is_heartbeat_enabled(); // Condition
void enable_heartbeat(); // Action
void enable_alarms(); // Action
/// @brief Perform full memory reinitialization. All available
/// memory registers will be set to zeroes.
/// This could help if memory structure is corrupted or faulty.
void reset_memory(); // Action
/// @brief Sets all 'configuration' variables to their initial values.
void reset_to_factory(); // Action
void set_alarm_one(const ds3232_alarm::DS3232Alarm &alarm); // Action
void set_alarm_two(const ds3232_alarm::DS3232Alarm &alarm); // Action
ds3232_alarm::DS3232Alarm get_alarm_one();
ds3232_alarm::DS3232Alarm get_alarm_two();
void clear_alarm_one();
void clear_alarm_two();
/// @brief Gets power source state.
/// @return One of values from DS3232PowerState
DS3232PowerState get_power_state();
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_interrupt_pin(GPIOPin *pin) { int_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { rst_pin_ = pin; }
void set_fire_alarms_on_startup(bool value) { fire_alarms_on_startup_ = value; }
void add_on_alarm_one_callback(std::function<void(ESPTime)> &&func) {
this->alarm_one_callback_.add(std::move(func));
}
void add_on_alarm_two_callback(std::function<void(ESPTime)> &&func) {
this->alarm_two_callback_.add(std::move(func));
}
void add_on_heartbeat_callback(std::function<void()> &&func) { this->heartbeat_callback_.add(std::move(func)); }
void add_on_power_change_callback(std::function<void(DS3232PowerState)> &&func) {
this->power_state_callback_.add(std::move(func));
}
void add_on_variable_init_required_callback(std::function<void()> &&func) { this->variable_init_callback_.add(std::move(func)); }
void add_on_variable_fail_callback(std::function<void()> &&func) { this->variable_fail_callback_.add(std::move(func)); }
bool read_memory(uint8_t reg_id, std::vector<uint8_t> &data);
bool write_memory(uint8_t reg_id, const std::vector<uint8_t> &data);
bool is_valid_nvram(const uint8_t reg_id, uint8_t size) { return this->validate_mem_(reg_id, size, true); }
protected:
void reinit_osf_();
bool read_data_();
void clear_nvram_();
void plan_clear_nvram_();
void plan_reset_nvram_();
bool fire_alarms_on_startup_{false};
void read_temperature_();
void process_interrupt_();
void process_alarms_();
bool pin_state_{false};
bool alarm_1_firing_{false};
bool alarm_2_firing_{false};
bool nvram_available_{false};
bool late_startup_{true};
DS3232NVRAMState nvram_state_{DS3232NVRAMState::UNCERTAIN};
bool validate_mem_(uint8_t reg_id, uint8_t size, bool ignore_empty = false);
ESPTime reg_to_time_();
bool produce_stable_frequency_{false};
bool busy_{false};
sensor::Sensor *temperature_sensor_{nullptr};
GPIOPin *int_pin_{nullptr};
GPIOPin *rst_pin_{nullptr};
CallbackManager<void(ESPTime)> alarm_one_callback_{};
CallbackManager<void(ESPTime)> alarm_two_callback_{};
CallbackManager<void()> heartbeat_callback_{};
CallbackManager<void(DS3232PowerState)> power_state_callback_{};
CallbackManager<void()> variable_init_callback_{};
CallbackManager<void()> variable_fail_callback_{};
Deduplicator<DS3232PowerState> power_state_;
Deduplicator<float> temperature_state_;
union DS3232NVRAMData {
struct {
uint8_t magix_1 : 2; //Always should be 0b11;
uint8_t magix_2 : 6; //Always should be 'E' char.
bool is_initialized : 1; //If set to 1 then it means that nvram has initialized values.
uint8_t min_version : 3;
uint8_t maj_version : 4;
} info;
mutable uint8_t raw[sizeof(info)];
} nvram_info_;
union DS3232RegData {
struct {
// RTC regs (0x00)
uint8_t second : 4;
uint8_t second_10 : 3;
uint8_t unused_0 : 1;
uint8_t minute : 4;
uint8_t minute_10 : 3;
uint8_t unused_1 : 1;
uint8_t hour : 4;
uint8_t hour_10 : 2;
uint8_t unused_2 : 2; // Always store in 24h mode
uint8_t weekday : 3;
uint8_t unused_3 : 5;
uint8_t day : 4;
uint8_t day_10 : 2;
uint8_t unused_4 : 2;
uint8_t month : 4;
uint8_t month_10 : 1;
uint8_t unused_5 : 2;
uint8_t century : 1;
uint8_t year : 4;
uint8_t year_10 : 4;
// Alarm 1 regs (0x07)
uint8_t alarm_1_second : 4;
uint8_t alarm_1_second_10 : 3;
bool alarm_1_mode_1 : 1;
uint8_t alarm_1_minute : 4;
uint8_t alarm_1_minute_10 : 3;
bool alarm_1_mode_2 : 1;
uint8_t alarm_1_hour : 4;
uint8_t alarm_1_hour_10 : 2;
uint8_t unused_6 : 1; // Always store in 24h mode
bool alarm_1_mode_3 : 1;
uint8_t alarm_1_day : 4;
uint8_t alarm_1_day_10 : 2;
bool alarm_1_use_day_of_week : 1;
bool alarm_1_mode_4 : 1;
// Alarm 2 regs (0x0B)
uint8_t alarm_2_minute : 4;
uint8_t alarm_2_minute_10 : 3;
bool alarm_2_mode_2 : 1;
uint8_t alarm_2_hour : 4;
uint8_t alarm_2_hour_10 : 2;
uint8_t unused_7 : 1; // Always store in 24h mode
bool alarm_2_mode_3 : 1;
uint8_t alarm_2_day : 4;
uint8_t alarm_2_day_10 : 2;
bool alarm_2_use_day_of_week : 1;
bool alarm_2_mode_4 : 1;
// Control register (0x0E)
bool alarm_1_enable : 1;
bool alarm_2_enable : 1;
bool int_enable : 1;
uint8_t unused_8 : 2;
bool convert_temp : 1;
bool enable_battery_square_signal : 1;
bool EOSC : 1;
// Status register (0x0F)
bool alarm_1_match : 1;
bool alarm_2_match : 1;
bool device_busy : 1;
bool enable_hf_output : 1;
uint8_t unused_9 : 2;
bool enable_hf_output_on_battery_mode : 1;
bool osf_bit : 1;
// Aging register (0x10)
uint8_t unused_10 : 8;
// Temperature data (read-only, 0x11)
uint8_t temperature_integral : 8;
uint8_t unused_11 : 6;
uint8_t temperature_fractional : 2;
} reg;
mutable struct {
mutable uint8_t rtc_raw[7];
mutable uint8_t alarm_1_raw[4];
mutable uint8_t alarm_2_raw[3];
mutable uint8_t control_raw[1];
mutable uint8_t status_raw[1];
mutable uint8_t aging_raw[1];
mutable uint8_t temperature_raw[2];
} raw_blocks;
mutable uint8_t raw[sizeof(reg)];
} reg_data_;
};
class HeartbeatEvent : public Trigger<> {
public:
explicit HeartbeatEvent(DS3232Component *parent) {
parent->add_on_heartbeat_callback([this]() { this->trigger(); });
}
};
class AlarmFiredEvent : public Trigger<ESPTime> {
public:
explicit AlarmFiredEvent(DS3232Component *parent, bool is_first = true) {
if (is_first) {
parent->add_on_alarm_one_callback([this](ESPTime fire_time) { this->trigger(fire_time); });
} else {
parent->add_on_alarm_two_callback([this](ESPTime fire_time) { this->trigger(fire_time); });
};
};
};
class PowerStateEvent : public Trigger<DS3232PowerState> {
public:
explicit PowerStateEvent(DS3232Component *parent) {
parent->add_on_power_change_callback([this](DS3232PowerState power_state) { this->trigger(power_state); });
}
};
template<typename... Ts> class FactoryResetNVRAMAction : public Action<Ts...>, public Parented<DS3232Component> {
public:
void play(Ts... x) override { this->parent_->reset_to_factory(); }
};
template<typename... Ts> class EraseNVRAMAction : public Action<Ts...>, public Parented<DS3232Component> {
public:
void play(Ts... x) override { this->parent_->reset_memory(); }
};
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<DS3232Component> {
public:
void play(Ts... x) override { this->parent_->write_time(); }
};
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<DS3232Component> {
public:
void play(Ts... x) override { this->parent_->read_time(); }
};
template<typename... Ts> class EnableHeartbeatAction : public Action<Ts...>, public Parented<DS3232Component> {
public:
void play(Ts... x) override { this->parent_->enable_heartbeat(); }
};
template<typename... Ts> class EnableAlarmsAction : public Action<Ts...>, public Parented<DS3232Component> {
public:
void play(Ts... x) override { this->parent_->enable_alarms(); }
};
template<typename... Ts> class SetAlarmAction : public Action<Ts...> {
public:
SetAlarmAction(DS3232Component *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(ds3232_alarm::DS3232Alarm, alarm_data)
TEMPLATABLE_VALUE(uint8_t, alarm_id)
void play(Ts... x) {
if (!alarm_data_.has_value())
return;
switch (alarm_id_.value(x...)) {
case 1:
this->parent_->set_alarm_one(alarm_data_.value(x...));
case 2:
this->parent_->set_alarm_two(alarm_data_.value(x...));
default:
break;
}
}
protected:
DS3232Component *parent_;
};
template<typename... Ts> class IsHeartbeatEnabledCondition : public Condition<Ts...>, public Parented<DS3232Component> {
public:
bool check(Ts... x) override { return this->parent_->is_heartbeat_enabled(); }
};
} // namespace ds3232
} // namespace esphome

View file

@ -0,0 +1,168 @@
#include "ds3232_alarm.h"
#include "esphome/core/log.h"
#include <array>
namespace esphome {
namespace ds3232 {
namespace ds3232_alarm {
static const char *const TAG = "ds3232_alarm";
static const char *const DAYS_OF_WEEK[] = {"", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static const char *const INVALID = "invalid";
static const char *const FORMAT_STRING_EVERY_TIME_S = "every second";
static const char *const FORMAT_STRING_EVERY_TIME_M = "every minute";
static const char *const FORMAT_STRING_EVERY_MINUTE = "every minute on %.2u%s second";
static const char *const FORMAT_STRING_EVERY_HOUR = "every hour on %.2u:%.2u";
static const char *const FORMAT_STRING_EVERY_DAY = "every day on %.2u:%.2u:%.2u";
static const char *const FORMAT_STRING_EVERY_WEEK = "every %s on %.2u:%.2u:%.2u";
static const char *const FORMAT_STRING_EVERY_MONTH = "every %u%s day of month on %.2u:%.2u:%.2u";
static const char *const FORMAT_STRING_INVALID = INVALID;
// Possible output formatted strings for DS3232Alarm::to_string():
// every 31st day of month on 23:59:59 35 symbols
// every Mon on 23:59:59 21 symbols
// every day on 23:59:59 21 symbols
// every hour on 59:59 19 symbols
// every minute on 59th second 27 symbols
// every minute 12 symbols
// every second 12 symbols
// disabled 8 symbols
// So, the recommended formatted_string length initialization: 128 symbols (35 * 3 (105) growed to 2^x(128))
static const size_t DS3232_ALARM_FORMAT_STRING_LENGTH = 128;
/// Converts alarm match bits to mode
// static AlarmMode bits_to_mode(bool use_week, bool match_day, bool match_hour, bool match_min, bool match_sec = false) {
// uint8_t mode_bits =
// 0x00001 * match_sec + 0x00010 * match_min + 0x00100 * match_hour + 0x01000 * match_day + 0x10000 * use_week;
// return static_cast<AlarmMode>(mode_bits);
// };
DS3232AlarmMode DS3232Alarm::alarm_mode(const AlarmMode mode) {
DS3232AlarmMode am = {};
am.alarm_mode = mode;
return am;
};
DS3232AlarmMode DS3232Alarm::alarm_mode(const bool bit_seconds, const bool bit_minutes, const bool bit_hours,
const bool bit_days, const bool use_weekdays) {
bool weekdays_bit = use_weekdays && !(bit_seconds || bit_minutes || bit_hours);
uint8_t mode_bits = 0b00001 * bit_seconds + 0b00010 * bit_minutes + 0b00100 * bit_hours + 0b01000 * bit_days +
0b10000 * weekdays_bit;
ESP_LOGVV(TAG, "Converted alarm mode from %.1u, %.1u, %.1u, %.1u, %.1u to bits %.2u",
bit_seconds, bit_minutes, bit_hours, bit_days, use_weekdays, mode_bits);
return DS3232Alarm::alarm_mode(static_cast<AlarmMode>(mode_bits));
};
bool DS3232Alarm::is_valid() const {
bool result = true;
result &= this->seconds_supported ? (this->second >= 0 && this->second < 60) : true;
result &= this->minute >= 0 && this->minute < 60;
result &= this->hour >= 0 && this->hour < 24;
result &= this->day_of_week > 0 && this->day_of_week <= 7;
result &= this->day_of_month > 0 && this->day_of_month <= 31;
return result;
}
DS3232Alarm DS3232Alarm::create(const bool is_enabled, const DS3232AlarmMode mode, const bool use_weekdays,
const uint8_t day, const uint8_t hour, const uint8_t minute,
const uint8_t second, const bool is_fired,
const bool is_seconds_supported)
{
DS3232Alarm alarm = {};
// .enabled = is_enabled,
// .seconds_supported = is_seconds_supported,
// .mode = mode,
// .second = second,
// .minute = minute,
// .hour = hour,
// .day_of_week = (use_weekdays ? day : 1),
// .day_of_month = (use_weekdays ? 1 : day),
// .is_fired = is_fired
// };
ESP_LOGVV(TAG, "Initializing new instance of alarm:");
alarm.enabled = is_enabled;
ESP_LOGVV(TAG, " Alarm state: %s", ONOFF(alarm.enabled));
alarm.seconds_supported = is_seconds_supported;
ESP_LOGVV(TAG, " Seconds supported: %s", YESNO(alarm.seconds_supported));
alarm.mode = mode;
ESP_LOGVV(TAG, " Mode set to: %.2u (%.2u)", alarm.mode, mode);
alarm.second = second;
ESP_LOGVV(TAG, " Seconds set to: %.2u (%.2u)", alarm.second, second);
alarm.minute = minute;
ESP_LOGVV(TAG, " Minutes set to: %.2u (%.2u)", alarm.minute, minute);
alarm.hour = hour;
ESP_LOGVV(TAG, " Hours set to: %.2u (%.2u)", alarm.hour, hour);
alarm.day_of_week = use_weekdays ? day : 1;
ESP_LOGVV(TAG, " Day of month set to: %.2u (%.2u)", alarm.day_of_week, use_weekdays ? day : 1);
alarm.day_of_month = use_weekdays ? 1 : day;
ESP_LOGVV(TAG, " Day of week set to: %.2u (%.2u)", alarm.day_of_month, use_weekdays ? 1 : day);
alarm.is_fired = is_fired;
ESP_LOGVV(TAG, " Alarm is fired", YESNO(alarm.is_fired));
return alarm;
}
std::string DS3232Alarm::to_string() const {
if (!this->is_valid())
return std::string(INVALID);
char formatted_string[DS3232_ALARM_FORMAT_STRING_LENGTH];
size_t len;
switch (this->mode.alarm_mode) {
case AlarmMode::EVERY_TIME:
case 31:
return std::string(this->seconds_supported ? FORMAT_STRING_EVERY_TIME_S : FORMAT_STRING_EVERY_TIME_M);
case AlarmMode::MATCH_SECONDS:
case 30:
if (!this->seconds_supported)
return std::string(FORMAT_STRING_EVERY_TIME_M);
len = snprintf(formatted_string, sizeof(formatted_string), FORMAT_STRING_EVERY_MINUTE, this->second,
ORDINAL_SUFFIX(this->second));
if (len < 0) {
ESP_LOGE(TAG, "Unable to format alarm data.");
return std::string(INVALID);
}
return std::string(formatted_string);
case AlarmMode::MATCH_MINUTES_SECONDS:
case 28:
len =
snprintf(formatted_string, sizeof(formatted_string), FORMAT_STRING_EVERY_HOUR, this->minute, this->second);
if (len < 0) {
ESP_LOGE(TAG, "Unable to format alarm data.");
return std::string(INVALID);
}
return std::string(formatted_string);
case AlarmMode::MATCH_TIME:
case 24:
len = snprintf(formatted_string, sizeof(formatted_string), FORMAT_STRING_EVERY_DAY, this->hour, this->minute,
this->second);
if (len < 0) {
ESP_LOGE(TAG, "Unable to format alarm data.");
return std::string(INVALID);
}
return std::string(formatted_string);
case AlarmMode::MATCH_DAY_OF_WEEK_AND_TIME:
len = snprintf(formatted_string, sizeof(formatted_string), FORMAT_STRING_EVERY_WEEK,
DAYS_OF_WEEK[this->day_of_week], this->hour, this->minute, this->second);
if (len < 0) {
ESP_LOGE(TAG, "Unable to format alarm data.");
return std::string(INVALID);
}
return std::string(formatted_string);
case AlarmMode::MATCH_DATE_AND_TIME:
len = snprintf(formatted_string, sizeof(formatted_string), FORMAT_STRING_EVERY_MONTH, this->day_of_month,
ORDINAL_SUFFIX(this->day_of_month), this->hour, this->minute, this->second);
if (len < 0) {
ESP_LOGE(TAG, "Unable to format alarm data.");
return std::string(INVALID);
}
return std::string(formatted_string);
default:
return INVALID;
break;
}
}
} // namespace ds3232_alarm
} // namespace ds3232
} // namespace esphome

View file

@ -0,0 +1,112 @@
#pragma once
#include <string>
namespace esphome {
namespace ds3232 {
namespace ds3232_alarm {
#define ORDINAL_SUFFIX(NUMBER) \
((((NUMBER) % 10) == 1) && (NUMBER) != 11) ? "st" \
: (((((NUMBER) % 10) == 2) && (NUMBER) != 12) ? "nd" \
: (((((NUMBER) % 10) == 3) && (NUMBER) != 13)) ? "rd" \
: "th")
/// @brief Mode of the alarm
enum AlarmMode {
/// Fire alarm once a second for
/// alarm one and once per minute for
/// alarm two.
EVERY_TIME = 0b01111,
/// Alarm when seconds match
/// If seconds are not supported by alarm then
/// it will be fired on every 00 seconds i.e. every minute.
MATCH_SECONDS = 0b01110,
/// Alarm when minutes and optionally seconds match
MATCH_MINUTES_SECONDS = 0b01100,
/// Alarm when hours, minutes, and optionally seconds match
MATCH_TIME = 0b01000,
/// Alarm when date, hours, minutes, and optionally seconds match
MATCH_DATE_AND_TIME = 0b00000,
/// Alarm when day, hours, minutes, and optionally seconds match
MATCH_DAY_OF_WEEK_AND_TIME = 0b10000,
};
/// static AlarmMode bits_to_mode(bool use_week, bool match_day, bool match_hour, bool match_min, bool match_sec =
/// false);
/// Mode of alarm (firing pattern)
union DS3232AlarmMode {
mutable struct {
bool match_seconds : 1;
bool match_minutes : 1;
bool match_hours : 1;
bool match_days : 1;
bool use_weekdays : 1;
uint8_t unused : 3;
} bits;
mutable AlarmMode alarm_mode;
mutable uint8_t raw;
};
/// @brief Alarm description
struct DS3232Alarm {
/// Indicates whether this alarm is enabled or not
bool enabled = false;
/// Determines whether seconds matching is supported
/// by alarm. Default is true.
bool seconds_supported = true;
/// Mode of alarm (firing pattern)
DS3232AlarmMode mode = alarm_mode(AlarmMode::MATCH_DATE_AND_TIME);
/// On which second alarm should be fired
uint8_t second = 0;
/// On which minute alarm should be fired
uint8_t minute = 0;
/// On which hour alarm should be fired
uint8_t hour = 0;
/// On which day of week (1 is Sunday, 7 is Saturday) alarm should be fired
uint8_t day_of_week = 1;
/// On which day of month alarm should be fired.
uint8_t day_of_month = 1;
/// Indicates whether alarm has been fired
bool is_fired = false;
/// Checks whether alarm configuration is valid.
/// @return true is alarm contains valid configuration; otherwise - false.
bool is_valid() const;
/// Formats alarm into human-readable text.
/// @return Textual description of alarm.
std::string to_string() const;
/// @brief Sets target alarm mode
/// @param target_mode Mode of alarm to set
void set_mode(AlarmMode target_mode) { mode.alarm_mode = target_mode; }
static DS3232AlarmMode alarm_mode(AlarmMode mode);
static DS3232AlarmMode alarm_mode(bool bit_seconds = false, bool bit_minutes = false,
bool bit_hours = false, bool bit_days = false,
bool use_weekdays = false);
static DS3232Alarm create(bool is_enabled, DS3232AlarmMode mode, bool use_weekdays = true,
uint8_t day = 1, uint8_t hour = 0, uint8_t minute = 0,
uint8_t second = 0, bool is_fired = false,
bool is_seconds_supported = false);
};
} // namespace ds3232_alarm
} // namespace ds3232
} // namespace esphome

View file

@ -0,0 +1,198 @@
#pragma once
#include <string>
#include "ds3232.h"
#include "esphome/core/log.h"
#include "esphome/components/logger/logger.h"
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace ds3232 {
namespace ds3232_variables {
static const char *const TAG = "ds3232_nvram";
template<typename T> class DS3232Variable : public Component {
public:
using value_type = T;
explicit DS3232Variable(DS3232Component *parent, const uint8_t reg_id) {
reg_ = reg_id;
parent_ = parent;
}
explicit DS3232Variable(DS3232Component *parent, const uint8_t reg_id, T initial_value) : initial_value_(initial_value) {
reg_ = reg_id;
parent_ = parent;
}
/// @brief Access to variable value.
/// @return Variable
T &value() { return this->value_; }
void setup() override {
if (this->parent_ == nullptr) {
this->mark_failed_();
return;
}
this->parent_->add_on_variable_init_required_callback([=]() { this->set_to_default_(); });
this->parent_->add_on_variable_fail_callback([=]() { this->mark_failed_(); });
if (!this->parent_->is_valid_nvram(this->reg_, sizeof(T))) {
this->mark_failed_();
return;
}
DS3232NVRAMState nvram_state_ = this->parent_->get_nvram_state();
while(nvram_state_ != DS3232NVRAMState::OK)
{
if(nvram_state_ == DS3232NVRAMState::FAIL)
{
this->log_error_("Unable to init with failed NVRAM.");
this->mark_failed();
break;
}
if(nvram_state_ == DS3232NVRAMState::NEED_RESET)
{
this->set_to_default_();
}
delay_microseconds_safe(10);
nvram_state_ = this->parent_->get_nvram_state();
}
if(!this->try_read_()) {
this->mark_failed_();
} else {
this->is_initialized_ = true;
}
}
float get_setup_priority() const override { return this->parent_->get_setup_priority() - 0.5f; } //Ensure that variables will be initialized after ds3232 component.
void loop() override {
if(!this->try_write_())
this->mark_failed_();
}
void on_shutdown() override { this->try_write_(); }
bool is_initialized() { return this->is_initialized_; }
protected:
uint8_t reg_;
T initial_value_{};
T value_{};
T prev_value_{};
bool need_init_{false};
bool need_to_wait_{false};
bool is_initialized_{false};
std::va_list llist_{};
void log_error_(const char* log_str) {
#ifdef USE_LOGGER
auto *log = logger::global_logger;
if (log == nullptr)
return;
log->log_vprintf_(ESPHOME_LOG_LEVEL_ERROR, TAG, __LINE__, log_str, this->llist_);
#endif
}
void mark_failed_() {
this->need_to_wait_ = false;
this->need_init_ = false;
this->is_initialized_ = false;
this->mark_failed();
}
void set_to_default_() {
std::vector<uint8_t> buffer;
buffer.resize(sizeof(T));
memcpy(buffer.data(), &this->initial_value_, sizeof(T));
if(!this->parent_->write_memory(this->reg_, buffer))
{
this->mark_failed_();
} else {
memcpy(&this->value_, &this->initial_value_, sizeof(T));
memcpy(&this->prev_value_, &this->initial_value_, sizeof(T));
};
}
bool try_write_() {
if (this->parent_ == nullptr) {
return false;
}
if (this->parent_->get_nvram_state() == ds3232::DS3232NVRAMState::FAIL)
return false;
int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T));
if (diff != 0) {
std::vector<uint8_t> buffer;
buffer.resize(sizeof(T));
memcpy(buffer.data(), &this->value_, sizeof(T));
if (!this->parent_->write_memory(this->reg_, buffer)) {
this->mark_failed_();
this->log_error_("Unable to write new value to NVRAM.");
return false;
} else {
memcpy(&this->prev_value_, &this->value_, sizeof(T));
return true;
};
} else {
return true;
};
return false;
}
bool try_read_() {
if (this->parent_ == nullptr) {
this->log_error_("No crystal defined. Read from NVRAM failed.");
return false;
}
if (this->parent_->get_nvram_state() != ds3232::DS3232NVRAMState::OK)
{
this->log_error_("Invalid NVRAM state. Unable to read data.");
return false;
}
std::vector<uint8_t> buffer;
buffer.resize(sizeof(T));
if (this->parent_->read_memory(this->reg_, buffer)) {
memcpy(&this->value_, buffer.data(), buffer.size());
memcpy(&this->prev_value_, &this->value_, sizeof(T));
return true;
}
this->log_error_("Failed to read data.");
return false;
}
DS3232Component *parent_{nullptr};
};
template<typename T> T &id(DS3232Variable<T> *value) {
value->try_read_();
return value->value();
}
template<class C, typename... Ts> class DS3232VariableSetAction : public Action<Ts...> {
public:
explicit DS3232VariableSetAction(C *parent) : parent_(parent) {}
using T = typename C::value_type;
TEMPLATABLE_VALUE(T, value);
void play(Ts... x) override {
this->parent_->value() = this->value_.value(x...);
}
protected:
C *parent_;
};
} // namespace ds3232_variables
} // namespace ds3232
} // namespace esphome

View file

@ -0,0 +1,647 @@
import re
from esphome import config_validation as cv, automation, pins
from esphome import codegen as cg
from esphome.components import i2c, time, sensor
from esphome.const import (
CONF_TEMPERATURE,
CONF_ID,
CONF_ACTIVE,
CONF_TRIGGER_ID,
CONF_HOUR,
CONF_MINUTE,
CONF_SECOND,
CONF_INTERRUPT_PIN,
CONF_RESET_PIN,
CONF_TIME,
CONF_MODE,
CONF_TYPE,
CONF_VALUE,
CONF_INITIAL_VALUE,
CONF_SIZE,
UNIT_CELSIUS,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from esphome.core import coroutine_with_priority
CODEOWNERS = ["@ViKozh"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensor"]
# Fix for ESPTime wrong location
ESPTime = cg.esphome_ns.struct("ESPTime")
ds3232_ns = cg.esphome_ns.namespace("ds3232")
DS3232Component = ds3232_ns.class_("DS3232Component", time.RealTimeClock, i2c.I2CDevice)
DS3232PowerState = ds3232_ns.enum("DS3232PowerState")
ReadAction = ds3232_ns.class_("ReadAction", automation.Action)
WriteAction = ds3232_ns.class_("WriteAction", automation.Action)
EnableHeartbeatAction = ds3232_ns.class_("EnableHeartbeatAction", automation.Action)
EnableAlarmsAction = ds3232_ns.class_("EnableAlarmsAction", automation.Action)
SetAlarmAction = ds3232_ns.class_("SetAlarmAction", automation.Action)
FactoryResetAction = ds3232_ns.class_("FactoryResetNVRAMAction", automation.Action)
EraseMemoryAction = ds3232_ns.class_("EraseNVRAMAction", automation.Action)
PowerStateEventTrigger = ds3232_ns.class_(
"PowerStateEvent",
automation.Trigger.template(DS3232PowerState),
)
HeartBeatEventTrigger = ds3232_ns.class_(
"HeartbeatEvent", automation.Trigger.template()
)
AlarmFiredEventTrigger = ds3232_ns.class_(
"AlarmFiredEvent",
automation.Trigger.template(ESPTime),
)
IsHeartbeatEnabledCondition = ds3232_ns.class_(
"IsHeartbeatEnabledCondition", automation.Condition
)
ds3232_alarm_ns = ds3232_ns.namespace("ds3232_alarm")
DS3232Alarm = ds3232_alarm_ns.struct("DS3232Alarm")
DS3232AlarmMode = ds3232_alarm_ns.enum("AlarmMode")
ds3232_variables_ns = ds3232_ns.namespace("ds3232_variables")
DS3232Variable = ds3232_variables_ns.class_("DS3232Variable", cg.Component)
DS3232VariableSetAction = ds3232_variables_ns.class_(
"DS3232VariableSetAction", automation.Action
)
ALARM_MODES = {
"EVERY_TIME": DS3232AlarmMode.EVERY_TIME,
"MATCH_SECONDS": DS3232AlarmMode.MATCH_SECONDS,
"MATCH_MINUTES_SECONDS": DS3232AlarmMode.MATCH_MINUTES_SECONDS,
"MATCH_TIME": DS3232AlarmMode.MATCH_TIME,
"MATCH_DATE_AND_TIME": DS3232AlarmMode.MATCH_DATE_AND_TIME,
"MATCH_DAY_OF_WEEK_AND_TIME": DS3232AlarmMode.MATCH_DAY_OF_WEEK_AND_TIME,
}
WEEKDAYS = {
"SUN": 1,
"SUNDAY": 1,
"MON": 2,
"MONDAY": 2,
"TUE": 3,
"TUESDAY": 3,
"WEDNESDAY": 4,
"WED": 4,
"THURSDAY": 5,
"THU": 5,
"FRIDAY": 6,
"FRI": 6,
"SATURDAY": 7,
"SAT": 7,
}
PERSISTENT_VARIABLE_TYPES = {
"BOOL": ["bool", 1],
"UINT": ["uint32_t", 4],
"UINT64": ["uint64_t", 8],
"UINT32": ["uint32_t", 4],
"UINT16": ["uint16_t", 2],
"UINT8": ["uint8_t", 1],
"INT": ["int32_t", 4],
"INT64": ["int64_t", 8],
"INT32": ["int32_t", 4],
"INT16": ["int16_t", 2],
"INT8": ["int8_t", 1],
"FLOAT": ["float", 4],
"DOUBLE": ["double", 8],
}
CONF_USE_HEARTBEAT = "use_heartbeat"
CONF_FIRE_ALARMS_ON_STARTUP = "fire_alarms_on_startup"
CONF_FIRST_ALARM = "first_alarm"
CONF_SECOND_ALARM = "second_alarm"
CONF_ON_POWER_CHANGE = "on_power_change"
CONF_ON_HEARTBEAT = "on_heartbeat"
CONF_ON_FIRST_ALARM = "on_first_alarm"
CONF_ON_SECOND_ALARM = "on_second_alarm"
CONF_TIME_PATTERN = "time_pattern"
CONF_DAY_OF_MONTH = "day_of_month"
CONF_DAY_OF_WEEK = "day_of_week"
CONF_REGISTER = "register"
CONF_PERSISTENT_STORAGE = "persistent_storage"
CONF_CPP_TYPE = "cpp_type"
MIN_REGISTER_VALUE = 0x16
MAX_REGISTER_VALUE = 0xFF
def validate_variable_definition_(conf):
size_ = int(PERSISTENT_VARIABLE_TYPES[str.upper(conf[CONF_TYPE])][1])
end_reg = int(MAX_REGISTER_VALUE) - (size_ - 1)
if int(MIN_REGISTER_VALUE) <= int(conf[CONF_REGISTER]) <= end_reg:
conf[CONF_CPP_TYPE] = PERSISTENT_VARIABLE_TYPES[str.upper(conf[CONF_TYPE])][0]
conf[CONF_SIZE] = size_
else:
raise cv.Invalid(
f"Unable to allocate memory DS3232 in NVRAM for variable {conf[CONF_ID]}"
)
return conf
def validate_persistent_storage_(conf):
memory_map = {x: True for x in range(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE)}
for var_conf in conf:
start_reg = int(var_conf[CONF_REGISTER])
end_reg = start_reg + int(var_conf[CONF_SIZE]) - 1
subrange = range(start_reg, end_reg)
if all(memory_map[x] for x in subrange):
for x in subrange:
memory_map[x] = False
else:
raise cv.Invalid(
f"Overlapping DS3232 NVRAM addresses found: {hex(start_reg)} to {hex(end_reg)}"
)
return conf
PERSISTENT_VARIABLE_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(DS3232Variable),
cv.Required(CONF_TYPE): cv.enum(PERSISTENT_VARIABLE_TYPES, upper=True),
cv.Required(CONF_REGISTER): cv.hex_int_range(
min=MIN_REGISTER_VALUE, max=MAX_REGISTER_VALUE
),
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
}
).extend(cv.COMPONENT_SCHEMA),
validate_variable_definition_,
)
VALIDATE_ALARM_REGEX_ = r"^((?P<weekday>\w{3,})|(?P<day>(\d{1,2})|\*)){0,1}\s+(?P<hour>(\d{1,2})|\*):(?P<minute>(\d{1,2})|\*):(?P<second>(\d{1,2})|\*)$"
ALARM_REGEX_PLACEHOLDER_ = "*"
def validate_alarm_(conf):
value_seconds = 0
value_minutes = 0
value_hours = 0
value_days_of_week = 1
value_days_of_month = 1
if CONF_TIME in conf:
value_seconds = conf[CONF_TIME][CONF_SECOND]
value_minutes = conf[CONF_TIME][CONF_MINUTE]
value_hours = conf[CONF_TIME][CONF_HOUR]
if CONF_DAY_OF_WEEK in conf[CONF_TIME]:
value_days_of_week = WEEKDAYS.get(
str.upper(conf[CONF_TIME][CONF_DAY_OF_WEEK])
)
if CONF_DAY_OF_MONTH in conf[CONF_TIME]:
value_days_of_month = conf[CONF_TIME][CONF_DAY_OF_MONTH]
elif CONF_TIME_PATTERN in conf:
parse_result = re.fullmatch(
VALIDATE_ALARM_REGEX_,
conf[CONF_TIME_PATTERN],
flags=re.IGNORECASE | re.UNICODE,
)
if parse_result:
values = parse_result.groupdict()
sec = values.get("second", ALARM_REGEX_PLACEHOLDER_)
if sec != ALARM_REGEX_PLACEHOLDER_ and sec is not None:
value_seconds = int(sec)
min = values.get("minute", ALARM_REGEX_PLACEHOLDER_)
if min != ALARM_REGEX_PLACEHOLDER_ and min is not None:
value_minutes = int(min)
hr = values.get("hour", ALARM_REGEX_PLACEHOLDER_)
if hr != ALARM_REGEX_PLACEHOLDER_ and hr is not None:
value_hours = int(hr)
day_m = values.get("day", ALARM_REGEX_PLACEHOLDER_)
if day_m != ALARM_REGEX_PLACEHOLDER_ and day_m is not None:
value_days_of_month = int(day_m)
weekday = values.get("weekday", ALARM_REGEX_PLACEHOLDER_)
if weekday != ALARM_REGEX_PLACEHOLDER_ and weekday is not None:
value_days_of_week = WEEKDAYS.get(str.upper(weekday))
else:
raise cv.Invalid(
"Invalid alarm match pattern. See documentation for reference."
)
elif conf[CONF_MODE] != DS3232AlarmMode.EVERY_TIME:
raise cv.Invalid(
"Alarms without time settings only possible with EVERY_TIME mode."
)
if not (0 <= value_seconds <= 59):
raise cv.Invalid("Seconds should be in range between 0 and 59.")
if not (0 <= value_minutes <= 59):
raise cv.Invalid("Minutes should be in range between 0 and 59.")
if not (0 <= value_hours <= 59):
raise cv.Invalid("Hours should be in range between 0 and 23.")
if not (1 <= value_days_of_month <= 31):
raise cv.Invalid("Day of month should be in range from 1 to 31.")
if not (1 <= value_days_of_week <= 7):
raise cv.Invalid(
"Day of week should be in range from 1 (Sunday) to 7 (Saturday)."
)
conf[CONF_SECOND] = value_seconds
conf[CONF_MINUTE] = value_minutes
conf[CONF_HOUR] = value_hours
conf[CONF_DAY_OF_WEEK] = value_days_of_week
conf[CONF_DAY_OF_MONTH] = value_days_of_month
return conf
def validate_(conf):
if CONF_INTERRUPT_PIN not in conf:
if CONF_ON_FIRST_ALARM in conf:
raise cv.Invalid(
f"'{CONF_INTERRUPT_PIN}' should be defined and properly connected to use '{CONF_ON_FIRST_ALARM}' trigger."
)
if CONF_ON_SECOND_ALARM in conf:
raise cv.Invalid(
f"'{CONF_INTERRUPT_PIN}' should be defined and properly connected to use '{CONF_ON_SECOND_ALARM}' trigger."
)
if CONF_ON_HEARTBEAT in conf:
raise cv.Invalid(
f"'{CONF_INTERRUPT_PIN}' should be defined and properly connected to use '{CONF_ON_HEARTBEAT}' trigger."
)
if CONF_RESET_PIN not in conf:
if CONF_ON_POWER_CHANGE in conf:
raise cv.Invalid(
f"'{CONF_RESET_PIN}' should be defined and properly connected to use '{CONF_ON_POWER_CHANGE}' trigger."
)
return conf
alarm_day_err_msg_ = "Alarm cannot be set both for day of month and day of week."
ALARM_TIME_PATTERN_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SECOND, default=0): cv.Range(
min=0, min_included=True, max=59, max_included=True
),
cv.Optional(CONF_MINUTE, default=0): cv.Range(
min=0, min_included=True, max=59, max_included=True
),
cv.Optional(CONF_HOUR, default=0): cv.Range(
min=0, min_included=True, max=23, max_included=True
),
cv.Exclusive(CONF_DAY_OF_WEEK, "alarm_day", msg=alarm_day_err_msg_): cv.enum(
WEEKDAYS, upper=True
),
cv.Exclusive(CONF_DAY_OF_MONTH, "alarm_day", msg=alarm_day_err_msg_): cv.Range(
min=1, min_included=True, max=31, max_included=True
),
}
)
alarm_pattern_err_msg_ = "Alarm should be specified either by time match patterns or time elements. See docs for examples."
ALARM_SCHEMA = cv.Schema(
{
cv.Required(CONF_MODE): cv.enum(ALARM_MODES, upper=True),
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.Exclusive(
CONF_TIME_PATTERN, "alarm_pattern", msg=alarm_pattern_err_msg_
): cv.string_strict,
cv.Exclusive(
CONF_TIME, "alarm_pattern", msg=alarm_pattern_err_msg_
): ALARM_TIME_PATTERN_SCHEMA,
}
)
DS3232_ALARM_SCHEMA = cv.All(ALARM_SCHEMA, validate_alarm_)
CONFIG_SCHEMA = cv.All(
time.TIME_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DS3232Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_USE_HEARTBEAT, default=True): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): pins.gpio_input_pullup_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_FIRE_ALARMS_ON_STARTUP, default=True): cv.boolean,
cv.Optional(CONF_FIRST_ALARM): DS3232_ALARM_SCHEMA,
cv.Optional(CONF_SECOND_ALARM): DS3232_ALARM_SCHEMA,
cv.Optional(CONF_ON_HEARTBEAT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HeartBeatEventTrigger
),
}
),
cv.Optional(CONF_ON_FIRST_ALARM): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
AlarmFiredEventTrigger
),
}
),
cv.Optional(CONF_ON_SECOND_ALARM): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
AlarmFiredEventTrigger
),
}
),
cv.Optional(CONF_ON_POWER_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
PowerStateEventTrigger
),
}
),
cv.Optional(CONF_PERSISTENT_STORAGE, default={}): cv.All(
[PERSISTENT_VARIABLE_SCHEMA],
cv.ensure_list(),
validate_persistent_storage_,
),
}
).extend(i2c.i2c_device_schema(0x68)),
validate_,
)
@automation.register_action(
"ds3232.factory_reset",
FactoryResetAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DS3232Component),
}
),
)
async def ds3232_factory_reset(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(
"ds3232.erase_memory",
EraseMemoryAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DS3232Component),
}
),
)
async def ds3232_erase_memory(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(
"ds3232.write_time",
WriteAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DS3232Component),
}
),
)
async def ds3232_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(
"ds3232.read_time",
ReadAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DS3232Component),
}
),
)
async def ds3232_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
@automation.register_action(
"ds3232.enable_heartbeat",
EnableHeartbeatAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DS3232Component),
}
),
)
async def ds3232_enable_heartbeat_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(
"ds3232.enable_alarms",
EnableAlarmsAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DS3232Component),
}
),
)
async def ds3232_enable_alarms_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_condition(
"ds3232.is_heartbeat_enabled",
IsHeartbeatEnabledCondition,
automation.maybe_simple_id({cv.GenerateID(): cv.use_id(DS3232Component)}),
)
async def ds3232_is_hb_enabled_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def alarm(config, is_second_alarm=False):
is_enabled = cg.bool_(config[CONF_ACTIVE])
is_seconds_supported = cg.bool_(not is_second_alarm)
is_fired = cg.bool_(False)
use_weekdays = None
day = None
hour = cg.uint8(config.get(CONF_HOUR, 0))
minute = cg.uint8(config.get(CONF_MINUTE, 0))
second = cg.uint8(config.get(CONF_SECOND, 0))
cur_op = DS3232Alarm.op
DS3232Alarm.op = "::"
mode = DS3232Alarm.alarm_mode(
ALARM_MODES[config.get(CONF_MODE, "MATCH_DAY_OF_WEEK_AND_TIME")]
)
DS3232Alarm.op = cur_op
if config[CONF_MODE] == DS3232AlarmMode.MATCH_DAY_OF_WEEK_AND_TIME:
use_weekdays = cg.bool_(True)
day = cg.uint8(config.get(CONF_DAY_OF_WEEK, 1))
else:
use_weekdays = cg.bool_(False)
day = cg.uint8(config.get(CONF_DAY_OF_MONTH, 1))
cur_op = DS3232Alarm.op
DS3232Alarm.op = "::"
expr = DS3232Alarm.create(
is_enabled,
mode,
use_weekdays,
day,
hour,
minute,
second,
is_fired,
is_seconds_supported,
)
DS3232Alarm.op = cur_op
return expr
SET_ALARM_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(DS3232Component),
}
).extend(ALARM_SCHEMA)
SET_ALARM_VALID_SCHEMA = cv.All(
SET_ALARM_SCHEMA,
validate_alarm_,
)
# Run with low priority so that namespaces are registered first
@coroutine_with_priority(-100.0)
async def variable_to_code(conf, parent_id):
var_type = cg.RawExpression(PERSISTENT_VARIABLE_TYPES[conf[CONF_TYPE]][0])
paren = await cg.get_variable(parent_id)
template_args = cg.TemplateArguments(var_type)
register = conf[CONF_REGISTER]
res_type = DS3232Variable.template(template_args)
var_instance = None
if CONF_INITIAL_VALUE in conf:
var_instance = DS3232Variable.new(
template_args, paren, register, cg.RawExpression(conf[CONF_INITIAL_VALUE])
)
else:
var_instance = DS3232Variable.new(template_args, paren, register)
var_persistent = cg.Pvariable(conf[CONF_ID], var_instance, res_type)
await cg.register_component(var_persistent, conf)
@automation.register_action(
"ds3232.set_alarm_one", SetAlarmAction, SET_ALARM_VALID_SCHEMA
)
async def ds3232_set_alarm_one(config, action_id, template_args, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_args, paren)
cg.add(var.set_alarm_id(1))
alarm_ = await alarm(config)
cg.add(var.set_alarm_data(alarm_))
return var
@automation.register_action(
"ds3232.set_alarm_two", SetAlarmAction, SET_ALARM_VALID_SCHEMA
)
async def ds3232_set_alarm_two(config, action_id, template_args, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_args, paren)
cg.add(var.set_alarm_id(2))
alarm_ = await alarm(config, True)
cg.add(var.set_alarm_data(alarm_))
return var
@automation.register_action(
"ds3232_nvram.set",
DS3232VariableSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(DS3232Variable),
cv.Required(CONF_VALUE): cv.templatable(cv.string_strict),
}
),
)
async def ds3232_nvram_set_to_code(config, action_id, template_arg, args):
full_id, parent = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
variable = cg.new_Pvariable(action_id, template_arg, parent)
templat = await cg.templatable(
config[CONF_VALUE], args, None, to_exp=cg.RawExpression
)
cg.add(variable.set_value(templat))
return variable
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)
for variable in config[CONF_PERSISTENT_STORAGE]:
await variable_to_code(variable, config[CONF_ID])
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))
if CONF_FIRE_ALARMS_ON_STARTUP in config:
cg.add(var.set_fire_alarms_on_startup(config[CONF_FIRE_ALARMS_ON_STARTUP]))
if CONF_INTERRUPT_PIN in config:
int_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
cg.add(var.set_interrupt_pin(int_pin))
for conf in config.get(CONF_ON_FIRST_ALARM, []):
paren = await cg.get_variable(config[CONF_ID])
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], paren, True)
await automation.build_automation(
trigger,
[
(ESPTime, "fire_time"),
],
conf,
)
for conf in config.get(CONF_ON_SECOND_ALARM, []):
paren = await cg.get_variable(config[CONF_ID])
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], paren, False)
await automation.build_automation(
trigger,
[
(ESPTime, "fire_time"),
],
conf,
)
if CONF_FIRST_ALARM in config:
cg.add(
var.set_alarm_one(
await alarm(config[CONF_FIRST_ALARM], is_second_alarm=False)
)
)
if CONF_SECOND_ALARM in config:
cg.add(
var.set_alarm_two(
await alarm(config[CONF_SECOND_ALARM], is_second_alarm=True)
)
)
for conf in config.get(CONF_ON_HEARTBEAT, []):
paren = await cg.get_variable(config[CONF_ID])
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], paren)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_POWER_CHANGE, []):
paren = await cg.get_variable(config[CONF_ID])
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], paren)
await automation.build_automation(
trigger, [(DS3232PowerState, "power_state")], conf
)

154
tests/test1.2.yaml Normal file
View file

@ -0,0 +1,154 @@
---
substitutions:
devicename: test1_2
sensorname: my
textname: template
roomname: living_room
esphome:
name: test1_2
name_add_mac_suffix: true
platform: ESP32
board: nodemcu-32s
build_path: build/test1
i2c:
sda: GPIO21
scl: GPIO22
id: i2c_bus
frequency: 400kHz
logger:
level: VERBOSE
api:
wifi:
ssid: "ssid"
time:
- platform: ds3232
id: ds3232_chip
i2c_id: i2c_bus
temperature:
name: "Chip Temperature"
use_heartbeat: false
interrupt_pin:
number: GPIO18
mode:
input: true
pullup: true
reset_pin: GPIO23
fire_alarms_on_startup: true
first_alarm:
mode: MATCH_DAY_OF_WEEK_AND_TIME
time_pattern: "Wed 12:04:23"
second_alarm:
mode: MATCH_TIME
time:
minute: 10
hour: 21
on_first_alarm:
then:
- lambda: |-
if(!id(counter_one)->is_initialized()) {
ESP_LOGD("TEST", "NVRAM not yet ready...");
return;
};
int i = id(counter_one)->value();
i++;
id(test_snsr).publish_state(i);
id(counter_one)->value() = i;
on_heartbeat:
then:
- lambda: |-
ESP_LOGD("TEST", "1Hz heartbeat signal");
on_second_alarm:
then:
- logger.log: "Alarm 2 has been fired"
- lambda: |-
id(has_ever_fired)->value() = true;
# on_power_change:
# then:
# - lambda: |-
# id(counter_one)->value() = id(counter_one)->value() + 1;
# - if:
# condition:
# lambda: |-
# return id(has_ever_fired)->value();
# then:
# - ds3232.enable_heartbeat: ds3232_chip
# else:
# - ds3232.enable_alarms: ds3232_chip
persistent_storage:
- id: counter_one
type: uint64
register: 0x21
initial_value: '3548'
- id: has_ever_fired
type: bool
register: 0x20
sensor:
- platform: template
name: "Test"
id: test_snsr
- platform: template
name: "NVRAM Counter"
lambda: |-
if(id(counter_one)->is_initialized())
return id(counter_one)->value();
else
return NAN;
update_interval: 5s
- platform: template
name: "Has Ever Fired"
update_interval: 5s
lambda: |-
if(id(has_ever_fired)->is_initialized())
return id(has_ever_fired)->value();
else
return NAN;
button:
- platform: template
name: Reset variables
on_press:
then:
- ds3232_nvram.set:
id: has_ever_fired
value: 'false'
- ds3232_nvram.set:
id: counter_one
value: '5555555'
- platform: template
name: Factory Reset
on_press:
then:
- ds3232.factory_reset
- platform: template
name: Toggle DS3232 mode
on_press:
then:
- if:
condition:
ds3232.is_heartbeat_enabled: ds3232_chip
then:
- ds3232.enable_alarms: ds3232_chip
else:
- ds3232.enable_heartbeat: ds3232_chip
- platform: template
name: Set New Alarm
on_press:
- ds3232.set_alarm_one:
mode: MATCH_SECONDS
active: true
time_pattern: "* *:*:10"
- ds3232.set_alarm_two:
mode: MATCH_MINUTES_SECONDS
active: true
time:
minute: 15
- ds3232_nvram.set:
id: has_ever_fired
value: 'false'