feature-requests icon indicating copy to clipboard operation
feature-requests copied to clipboard

Support for DS3231 (I2C RTC)

Open ghost opened this issue 5 years ago • 25 comments

Describe the problem you have/What new integration you would like

I understand supporting this should not be a priority over other useful sensors, but having support for some kind of reliable I2C RTC chip would be nice in certain situations, especially if the ESPHome node is placed at a location where the wifi signal strength is poor. Please describe your use case for this integration and alternatives you've tried:

Without a bulky external antenna, GPS modules don't work that well in some buildings, sometimes it won't even get a fix at all. Additional context

ghost avatar Dec 01 '19 08:12 ghost

Hope to get support

lsaoye avatar Jan 26 '20 11:01 lsaoye

This could go very nice along with a ntp server component https://github.com/esphome/feature-requests/issues/296 :tada:

rradar avatar Jan 27 '20 10:01 rradar

hopefully there will be support for ds1307 too

e-budianto avatar Mar 06 '20 03:03 e-budianto

Sometimes electricity breaks in our location and then restores, but internet connection restoring in a long time after usually, so our ESP modules cannot understand the time for light controlling logic

a-x- avatar May 09 '20 17:05 a-x-

Just been looking for ways to have backup programming for my boiler and an RTC was a requirement - would love to see it!

Denyerec avatar Oct 06 '20 20:10 Denyerec

Me too. If there is no wi fi, I would like to show the correct time on the display.

wisesokol avatar Nov 09 '20 10:11 wisesokol

hopefully there will be support for ds1307 too

I have finally started working on that again: https://github.com/esphome/esphome/pull/1441

badbadc0ffee avatar Jan 03 '21 19:01 badbadc0ffee

The DS3231 register set is so similar to the one of the DS1307, that the DS1307 component (esphome/esphome#1441) should already work with a DS3231. I could not test this, however, because I don't have such a device at hand; and additional features such as temperature readings and alarms are obviously not supported.

I could definitely also provide a dedicated driver component for the DS3231 if I had one to test with. :-) https://www.amazon.de/hz/wishlist/ls/IKJQ9RPIUL6J

badbadc0ffee avatar Jan 04 '21 19:01 badbadc0ffee

https://github.com/esphome/esphome/pull/1441 is merged in 1.16.0b2.

@badbadc0ffee I need to find my DS3231 somewhere in the drawer so I can test it with your component! Thank's already for your work :+1:

rradar avatar Jan 11 '21 23:01 rradar

Any update on DS3231 support for ESP32? Or if someone can show us how ti write our own code porting from conventional Arduino IDE based approach to get DS3231 integrated into ESPHOME.

NarinLab avatar Feb 08 '21 03:02 NarinLab

I would like if I can wake ESP by external clock to do its job and then turn it off completely. It is not conceptually correct to have ESP sleeping and all supporting circuitry powered up if no job is needed. Everything should be powered down until its time comes.

jbrepogmailcom avatar Feb 17 '21 11:02 jbrepogmailcom

@jbrepogmailcom I think you misunderstand. It is perfectly reasonable to leave the rest of the external support circuitry powered up when calling a routine that is documented to put the processor to sleep. Switching off power to the entire board is beyond the scope of the the deep sleep routine. You could probably build a basic switch from a couple of transistors, or use a dedicated power management IC (eg. AXP192)

ckuethe avatar Feb 17 '21 19:02 ckuethe

Hello, we are probably talking about different things. I meant to switch off both circuitry and ESP32 by DS3231 during the deep sleep, not before. And after wakeup, to reprogram DS3231 for nex sleep. However, I learned in the meanwhile that DS3231 takes about 100µA during sleep, while ESP32 with RTC on takes 10µA in deep sleep and 5µA in hibernation mode. So it is better to use ESP32 itself to manage sleep time and turn on/off supporting electronics.

jbrepogmailcom avatar Feb 17 '21 20:02 jbrepogmailcom

A bit late to the discussion, but the DS1307 component in ESPHome seems to work just fine with the DS3231 module. I have it attached to a NodeMCU 32s board and it's ticking along nicely.

Edit: I'm getting two devices at the i2c scan. I'm, ofc, using 0x68. I have no idea what the other is tbh.

[22:17:45][C][i2c:028]: I2C Bus:
[22:17:45][C][i2c:029]:   SDA Pin: GPIO21
[22:17:45][C][i2c:030]:   SCL Pin: GPIO22
[22:17:45][C][i2c:031]:   Frequency: 50000 Hz
[22:17:45][I][i2c:033]: Scanning i2c bus for active devices...
[22:17:45][I][i2c:040]: Found i2c device at address 0x57
[22:17:45][I][i2c:040]: Found i2c device at address 0x68
[22:17:45][C][ds1307:022]: DS1307:
[22:17:45][C][ds1307:023]:   Address: 0x68

zenzay avatar Mar 31 '21 20:03 zenzay

Edit: I'm getting two devices at the i2c scan. I'm, ofc, using 0x68. I have no idea what the other is tbh.

The other is likely 32K of eeprom (AT24C32X), which I know can have an address of 0x57 and appears on modules like this:

image

Writing to and reading from it is not currently supported by esphome.

WeekendWarrior1 avatar Sep 15 '21 05:09 WeekendWarrior1

That seems very likely, as that's the exact module I have :-)

I currently have no use for the eeprom on the module, so it's not really something I would wish esphome to focus on anyhow.

Thanks for the information!

zenzay avatar Sep 15 '21 08:09 zenzay

Just to throw my two cents in. For my ESPHome sensors, I'm hoping to replicate the work found https://www.raspberrypi.com/news/build-low-power-clock-controlled-devices/ to greatly prolong the battery life on my sensors. It seems like this "sleep" method allows for zero current to the DS321 (via VCC) or ESP32 and very, very low current for the P-Channel Mosfet switch (< 1mA). The DS3231 runs from the coin cell while waiting for the next alarm. When that alarm happens, the DS3231 switches the mosfet, the ESP32 circuit is powered on. The ESP32 can then do it's business, set a next alarm, and when it clears the current DS3231 alarm the ESP32 circuit is shutdown (completely), along with the VCC input on the DS3231 (so it is only running from the coin-cell until it triggers the next alarm, turning the ESP32 circuit back on).

It seems like several others have more or less the same goal. Has anyone made any progress?

I first will probably just do it as an Arduino or MicroPython app and publish to Home Assistant via MQTT (via node-red probably). I'd love to then transfer what I've learned to be able to do this with ESPHome, but we'll see how far I get on that front. It's currently not clear to me how I'd want to implement this in ESPHome. Maybe as an alternative to deep sleep or via lambda scripts?

The unit I ordered for testing is https://www.amazon.com/gp/product/B07WJZ7NKM - I've read you specifically should prefer the DS3231SN versions of the chip.

kdorff avatar Oct 13 '21 19:10 kdorff

https://esphome.io/components/time.html?#ds1307-time-source

nagyrobi avatar Jul 01 '22 07:07 nagyrobi

https://esphome.io/components/time.html?#ds1307-time-source

That's an interesting device for reading/writing time. The ds3231 could enable doing a lot more. See my description above.

My goal is to be able to use the ds3231 to be able to be able to power down circuit including the esp32 for stretches of time and it can trigger turning the circuit back on to do work - and the cycle repeats.

kevindorff avatar Jul 01 '22 20:07 kevindorff

I have a Norvi NORVI-ENET-AE06-R DIN mount ESP32 that uses a DS3231, I found that the built-in ds1307 component seems to works fine for RTC only purposes.

ptr727 avatar Nov 17 '23 05:11 ptr727

okay guys got it figured out with esp32-c3 super mini anyhow. see my esphome.yaml files attached one for setting the time, the other for reading the time. ds3231-esphome-scripts.zip

htaf@mriczo:~/liberit/kyut/ds3231-rtc$ cat ds3231-set-time.yaml

esphome:
  name: ds3231-set-time

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: arduino

i2c:
  sda: GPIO20
  scl: GPIO21
  scan: true

logger:

wifi:
  output_power: 8.5dB
  networks:
    - ssid: "yourWifiNetwork"
      password: "yourPassword"

time:
  - platform: sntp
    id: sntp_time
    timezone: America/Toronto

custom_component:
  - lambda: |-
      class DS3231Component : public PollingComponent {
       public:
        DS3231Component(esphome::time::RealTimeClock *rtc) : PollingComponent(10000), rtc_(rtc) {}

        void setup() override {
          if (!Wire.requestFrom(0x68, 1)) {
            ESP_LOGE("DS3231", "RTC not found at I2C address 0x68");
            return;
          }
          ESP_LOGI("DS3231", "RTC found at I2C address 0x68");

          auto now = this->rtc_->now();
          if (now.is_valid()) {
            set_time(now.year, now.month, now.day_of_month, now.hour, now.minute, now.second, now.day_of_week);
            ESP_LOGI("DS3231", "RTC time set to %04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);
          } else {
            ESP_LOGE("DS3231", "SNTP time is not valid; cannot set RTC");
          }
        }

        void update() override {
          auto time = get_time();
          if (time.year > 0) {
            ESP_LOGI("DS3231", "Current RTC Time: %04d-%02d-%02d %02d:%02d:%02d",
                     time.year, time.month, time.day, time.hour, time.minute, time.second);
          } else {
            ESP_LOGE("DS3231", "Failed to read time from RTC");
          }
        }

        void set_time(int year, int month, int day, int hour, int minute, int second, int day_of_week) {
          Wire.beginTransmission(0x68);
          Wire.write(0);  // Start at register 0
          Wire.write(dec_to_bcd(second));        // Seconds
          Wire.write(dec_to_bcd(minute));        // Minutes
          Wire.write(dec_to_bcd(hour));          // Hours
          Wire.write(dec_to_bcd(day_of_week));   // Day of the week
          Wire.write(dec_to_bcd(day));           // Day of the month
          Wire.write(dec_to_bcd(month));         // Month
          Wire.write(dec_to_bcd(year - 2000));   // Year
          Wire.endTransmission();
        }

        struct Time {
          int year;
          int month;
          int day;
          int hour;
          int minute;
          int second;
          int day_of_week;
        };

        Time get_time() {
          Wire.beginTransmission(0x68);
          Wire.write(0);  // Start at register 0
          Wire.endTransmission();

          if (Wire.requestFrom(0x68, 7) != 7) {
            ESP_LOGE("DS3231", "Failed to read time registers");
            return Time{0, 0, 0, 0, 0, 0, 0};
          }

          uint8_t second = bcd_to_dec(Wire.read());
          uint8_t minute = bcd_to_dec(Wire.read());
          uint8_t hour = bcd_to_dec(Wire.read());
          uint8_t day_of_week = bcd_to_dec(Wire.read());
          uint8_t day = bcd_to_dec(Wire.read());
          uint8_t month = bcd_to_dec(Wire.read());
          uint16_t year = bcd_to_dec(Wire.read()) + 2000;

          return Time{year, month, day, hour, minute, second, day_of_week};
        }

       private:
        esphome::time::RealTimeClock *rtc_;

        uint8_t dec_to_bcd(int val) {
          return ((val / 10 * 16) + (val % 10));
        }

        int bcd_to_dec(uint8_t val) {
          return ((val / 16 * 10) + (val % 16));
        }
      };

      auto my_rtc = new DS3231Component(id(sntp_time));
      App.register_component(my_rtc);
      return {};

htaf@mriczo:~/liberit/kyut/ds3231-rtc$

htaf@mriczo:~/liberit/kyut/ds3231-rtc$ cat ds3231-read-time.yaml

esphome:
  name: ds3231-read-time
  platform: ESP32
  board: esp32-c3-devkitm-1

i2c:
  sda: GPIO20  # Adjust to your setup
  scl: GPIO21
  scan: true

logger:

# Use the DS1307 platform to interact with the DS3231 RTC
time:
  - platform: ds1307
    id: rtc_time
    timezone: America/Toronto
    on_time_sync:
      then:
        - logger.log: "Time synchronized with RTC."
        - lambda: |-
            auto now = id(rtc_time).now();
            ESP_LOGI("RTC", "Current time: %04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);

interval:
  - interval: 5s
    then:
      - lambda: |-
          auto now = id(rtc_time).now();
          ESP_LOGI("Time", "Current time: %04d-%02d-%02d %02d:%02d:%02d",
                   now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);

htaf@mriczo:~/liberit/kyut/ds3231-rtc$

elspru avatar Dec 30 '24 21:12 elspru

oh and hey you guys wanted the alarm via SQW pin, here is how to set it up, for every minute on the first second, check the set_alarm() code if you need a different interval:

esphome:
  name: ds3231-alarm-test
  platform: ESP32
  board: esp32-c3-devkitm-1

i2c:
  sda: GPIO20  # Adjust to your wiring
  scl: GPIO21
  scan: true

logger:

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO9  # Pin connected to SQW/INT from DS3231
      mode: INPUT_PULLUP
    name: "DS3231 Alarm Trigger"
    filters:
      - delayed_off: 10ms  # Debounce to avoid false positives
    on_press:
      - logger.log: "Alarm Detected!"  # Log when interrupt occurs

custom_component:
  - lambda: |-
      class DS3231Alarm : public Component {
       public:
        void setup() override {
          if (!Wire.requestFrom(0x68, 1)) {
            ESP_LOGE("DS3231", "RTC not found at I2C address 0x68");
            return;
          }
          ESP_LOGI("DS3231", "RTC found at I2C address 0x68");

          // Set up the alarm to trigger every 6 seconds
          set_alarm();
        }

        void set_alarm() {
          Wire.beginTransmission(0x68);
          Wire.write(0x07);        // Start at Alarm1 seconds register
          Wire.write(0x01);        // A1M1=0: Match on first second
          Wire.write(0x80);        // A1M2=1: Match any minute
          Wire.write(0x80);        // A1M3=1: Match any hour
          Wire.write(0x80);        // A1M4=1: Match any day
          Wire.endTransmission();

          // Enable Alarm1 interrupt and disable square wave
          Wire.beginTransmission(0x68);
          Wire.write(0x0E);  // Control register
          Wire.write(0x05);  // INTCN=1 (enable interrupt), A1IE=1 (enable Alarm1)
          Wire.endTransmission();

          ESP_LOGI("DS3231", "Alarm set to trigger every 6 seconds.");
        }


        void loop() override {
          // Check if Alarm1 triggered by reading the status register
          Wire.beginTransmission(0x68);
          Wire.write(0x0F);  // Status register
          Wire.endTransmission();
          Wire.requestFrom(0x68, 1);
          uint8_t status = Wire.read();

          if (status & 0x01) {  // Alarm1 flag (A1F) is set
            ESP_LOGI("DS3231", "Alarm Triggered!");

            // Clear the Alarm1 flag
            Wire.beginTransmission(0x68);
            Wire.write(0x0F);  // Status register
            Wire.write(status & ~0x01);  // Clear A1F
            Wire.endTransmission();
          }
        }
      };

      auto my_alarm = new DS3231Alarm();
      App.register_component(my_alarm);
      return {};

elspru avatar Dec 30 '24 22:12 elspru

@elspru thanks for the example, does this work with an esp32-s3 and esp-idf framework? i have this config:

substitutions:
  device_name: "v"
  friendly_name: "V"
  ip_address: 192.168.188.190
  device_description: "Project v. - IP: ${ip_address}"

esphome:
  name: ${device_name}
  comment: ${device_description}
  friendly_name: ${friendly_name}
  min_version: 2024.11.0
  name_add_mac_suffix: false
  libraries:
    # a library bundled with Arduino
    - Wire

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  reboot_timeout: 0s
  encryption:
    key: !secret api_encryption_key

# Allow Over-The-Air updates
ota:
- platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Optional manual IP
  manual_ip:
    static_ip: ${ip_address}
    gateway: 192.168.188.1
    subnet: 255.255.255.0

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: ${device_name}-FH
    password: !secret ap_password

i2c:
  sda: GPIO48
  scl: GPIO47
  scan: true
  id: bus_a

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO9  # Pin connected to SQW/INT from DS3231
      mode: INPUT_PULLUP
    name: "DS3231 Alarm Trigger"
    filters:
      - delayed_off: 10ms  # Debounce to avoid false positives
    on_press:
      - logger.log: "Alarm Detected!"  # Log when interrupt occurs

custom_component:
  - lambda: |-
      class DS3231Alarm : public Component {
       public:
        void setup() override {
          if (!Wire.requestFrom(0x68, 1)) {
            ESP_LOGE("DS3231", "RTC not found at I2C address 0x68");
            return;
          }
          ESP_LOGI("DS3231", "RTC found at I2C address 0x68");

          // Set up the alarm to trigger every 6 seconds
          set_alarm();
        }

        void set_alarm() {
          Wire.beginTransmission(0x68);
          Wire.write(0x07);        // Start at Alarm1 seconds register
          Wire.write(0x01);        // A1M1=0: Match on first second
          Wire.write(0x80);        // A1M2=1: Match any minute
          Wire.write(0x80);        // A1M3=1: Match any hour
          Wire.write(0x80);        // A1M4=1: Match any day
          Wire.endTransmission();

          // Enable Alarm1 interrupt and disable square wave
          Wire.beginTransmission(0x68);
          Wire.write(0x0E);  // Control register
          Wire.write(0x05);  // INTCN=1 (enable interrupt), A1IE=1 (enable Alarm1)
          Wire.endTransmission();

          ESP_LOGI("DS3231", "Alarm set to trigger every 6 seconds.");
        }


        void loop() override {
          // Check if Alarm1 triggered by reading the status register
          Wire.beginTransmission(0x68);
          Wire.write(0x0F);  // Status register
          Wire.endTransmission();
          Wire.requestFrom(0x68, 1);
          uint8_t status = Wire.read();

          if (status & 0x01) {  // Alarm1 flag (A1F) is set
            ESP_LOGI("DS3231", "Alarm Triggered!");

            // Clear the Alarm1 flag
            Wire.beginTransmission(0x68);
            Wire.write(0x0F);  // Status register
            Wire.write(status & ~0x01);  // Clear A1F
            Wire.endTransmission();
          }
        }
      };

      auto my_alarm = new DS3231Alarm();
      App.register_component(my_alarm);
      return {};

and when i compile the program i get this error:

Library Manager: Installing Wire
Warning! Could not find the package with 'Wire' requirements for your system 'linux_x86_64'
Library Manager: Installing Wire
Warning! Could not find the package with 'Wire' requirements for your system 'linux_x86_64'
Dependency Graph
........
........
Compiling .pioenvs/vaso-smart/console/linenoise/linenoise.c.o
/root/config/vaso-smart.yaml: In member function 'virtual void setup()::<lambda()>::DS3231Alarm::setup()':
/root/config/vaso-smart.yaml:116:16: error: 'Wire' was not declared in this scope
  116 |           if (!Wire.requestFrom(0x68, 1)) {
      |                ^~~~
/root/config/vaso-smart.yaml: In member function 'void setup()::<lambda()>::DS3231Alarm::set_alarm()':
/root/config/vaso-smart.yaml:127:11: error: 'Wire' was not declared in this scope
  127 |           Wire.beginTransmission(0x68);
      |           ^~~~
/root/config/vaso-smart.yaml: In member function 'virtual void setup()::<lambda()>::DS3231Alarm::loop()':
/root/config/vaso-smart.yaml:147:11: error: 'Wire' was not declared in this scope
  147 |           Wire.beginTransmission(0x68);
      |           ^~~~
Compiling .pioenvs/vaso-smart/console/argtable3/arg_cmd.c.o

Thanks!

dj-fiorex avatar Mar 07 '25 11:03 dj-fiorex

Hell0 i have the config

globals:
  - id: last_on_relayone_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never ON"'

  - id: last_off_relayone_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never OFF"'
  
  - id: last_on_relaytwo_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never ON"'

  - id: last_off_relaytwo_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never OFF"'
    
  - id: last_on_relaythree_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never ON"'

  - id: last_off_relaythree_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never OFF"'
    
  - id: last_on_relayfour_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never ON"'

  - id: last_off_relayfour_timestamp
    type: std::string
    restore_value: true
    initial_value: '"Never OFF"'
    
    
substitutions:
  # Default name
  name: "wateringdevice"
  # Default friendly name
  friendly_name: "wateringdevice"
  # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. Typically used for areas such as 'Lounge Room', 'Kitchen' etc
  room: ""
  # Description as appears in ESPHome & top of webserver page
  device_description: "wateringdevice"
  # Project Name
  project_name: "wateringdevice"
  # Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
  project_version: "v1.0.3"
   # Restore the relay (GPO switch) upon reboot to state:
  relay1_restore_mode: RESTORE_DEFAULT_OFF
  relay2_restore_mode: RESTORE_DEFAULT_OFF
  relay3_restore_mode: RESTORE_DEFAULT_OFF
  relay4_restore_mode: RESTORE_DEFAULT_OFF
  # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs)
  dns_domain: ".local"
  # Set timezone of the smart plug. Useful if the plug is in a location different to the HA server. Can be entered in unix Country/Area format (i.e. "Australia/Sydney")
  timezone: "Europe/Berlin"
  # Set the duration between the sntp service polling ntp.org servers for an update
  sntp_update_interval: 6h
  # Network time servers for your region, enter from lowest to highest priority. To use local servers update as per zones or countries at: https://www.ntppool.org/zone/@
  sntp_server_1: "0.pool.ntp.org"
  sntp_server_2: "1.pool.ntp.org"
  sntp_server_3: "2.pool.ntp.org"
  # Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken
  wifi_fast_connect: "false"
  # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
  log_level: "DEBUG"
  # Enable or disable the use of IPv6 networking on the device
  ipv6_enable: "false"


esphome:
  name: wateringdevice
  on_boot: 
    - priority: 90
      then: 
      
  
esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
#api:
#  password: ""

ota:
  password: ""

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  output_power: 15

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: $devicename
    password: !secret ap_password

web_server:
  port: 80
  
captive_portal:


output:
  - platform: gpio
    pin: GPIO23
    id: led
  - platform: gpio
    pin: GPIO32
    id: relay_pin_1
  - platform: gpio
    pin: GPIO33
    id: relay_pin_2
  - platform: gpio
    pin: GPIO25
    id: relay_pin_3
  - platform: gpio
    pin: GPIO26
    id: relay_pin_4

# Define the switches based on the relay pins
switch:
  - platform: output
    id: relay_1
    name: "Relay 1"
    output: relay_pin_1
    on_turn_on:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_on_relayone_timestamp) = std::string(buffer);
            id(last_on_relayone_text_sensor).publish_state(id(last_on_relayone_timestamp));
          } else {
            id(last_on_relayone_timestamp) = "Time invalid";
            id(last_on_relayone_text_sensor).publish_state("Time invalid");
          }

    on_turn_off:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_off_relayone_timestamp) = std::string(buffer);
            id(last_off_relayone_text_sensor).publish_state(id(last_off_relayone_timestamp));
          } else {
            id(last_off_relayone_timestamp) = "Time invalid";
            id(last_off_relayone_text_sensor).publish_state("Time invalid");
          }
  - platform: output
    id: relay_2
    name: "Relay 2"
    output: relay_pin_2
    on_turn_on:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_on_relaytwo_timestamp) = std::string(buffer);
            id(last_on_relaytwo_text_sensor).publish_state(id(last_on_relaytwo_timestamp));
          } else {
            id(last_on_relaytwo_timestamp) = "Time invalid";
            id(last_on_relaytwo_text_sensor).publish_state("Time invalid");
          }

    on_turn_off:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_off_relaytwo_timestamp) = std::string(buffer);
            id(last_off_relaytwo_text_sensor).publish_state(id(last_off_relaytwo_timestamp));
          } else {
            id(last_off_relaytwo_timestamp) = "Time invalid";
            id(last_off_relaytwo_text_sensor).publish_state("Time invalid");
          }
          
  - platform: output
    id: relay_3
    name: "Relay 3"
    output: relay_pin_3
    on_turn_on:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_on_relaythree_timestamp) = std::string(buffer);
            id(last_on_relaythree_text_sensor).publish_state(id(last_on_relaythree_timestamp));
          } else {
            id(last_on_relaythree_timestamp) = "Time invalid";
            id(last_on_relaythree_text_sensor).publish_state("Time invalid");
          }

    on_turn_off:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_off_relaythree_timestamp) = std::string(buffer);
            id(last_off_relaythree_text_sensor).publish_state(id(last_off_relaythree_timestamp));
          } else {
            id(last_off_relaythree_timestamp) = "Time invalid";
            id(last_off_relaythree_text_sensor).publish_state("Time invalid");
          }
          
  - platform: output
    id: relay_4
    name: "Relay 4"
    output: relay_pin_4
    on_turn_on:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_on_relayfour_timestamp) = std::string(buffer);
            id(last_on_relayfour_text_sensor).publish_state(id(last_on_relayfour_timestamp));
          } else {
            id(last_on_relayfour_timestamp) = "Time invalid";
            id(last_on_relayfour_text_sensor).publish_state("Time invalid");
          }

    on_turn_off:
      - lambda: |-
          if (id(sntp_time).now().is_valid()) {
            auto now = id(sntp_time).now();
            char buffer[32];
            snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month,
                     now.hour, now.minute, now.second);
            id(last_off_relayfour_timestamp) = std::string(buffer);
            id(last_off_relayfour_text_sensor).publish_state(id(last_off_relayfour_timestamp));
          } else {
            id(last_off_relayfour_timestamp) = "Time invalid";
            id(last_off_relayfour_text_sensor).publish_state("Time invalid");
          }
          
debug:
  update_interval: 60s
  
interval:
  - interval: 1000ms
    then:
      - output.turn_on: led
      - delay: 500ms
      - output.turn_off: led

custom_component:
  - id: tss
    lambda: |-
      class DS3231Component : public PollingComponent {
       public:
        DS3231Component(esphome::time::RealTimeClock *rtc) : PollingComponent(10000), rtc_(rtc) {}

        void setup() override {
          if (!Wire.requestFrom(0x68, 1)) {
            ESP_LOGE("DS3231", "RTC not found at I2C address 0x68");
            return;
          }
          ESP_LOGI("DS3231", "RTC found at I2C address 0x68");

          auto now = this->rtc_->now();
          if (now.is_valid()) {
            set_time(now.year, now.month, now.day_of_month, now.hour, now.minute, now.second, now.day_of_week);
            ESP_LOGI("DS3231", "RTC time set to %04d-%02d-%02d %02d:%02d:%02d",
                     now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);
          } else {
            ESP_LOGE("DS3231", "SNTP time is not valid; cannot set RTC");
          }
        }

        void update() override {
          auto time = get_time();
          if (time.year > 0) {
            ESP_LOGI("DS3231", "Current RTC Time: %04d-%02d-%02d %02d:%02d:%02d",
                     time.year, time.month, time.day, time.hour, time.minute, time.second);
          } else {
            ESP_LOGE("DS3231", "Failed to read time from RTC");
          }
        }

        void set_time(int year, int month, int day, int hour, int minute, int second, int day_of_week) {
          Wire.beginTransmission(0x68);
          Wire.write(0);  // Start at register 0
          Wire.write(dec_to_bcd(second));        // Seconds
          Wire.write(dec_to_bcd(minute));        // Minutes
          Wire.write(dec_to_bcd(hour));          // Hours
          Wire.write(dec_to_bcd(day_of_week));   // Day of the week
          Wire.write(dec_to_bcd(day));           // Day of the month
          Wire.write(dec_to_bcd(month));         // Month
          Wire.write(dec_to_bcd(year - 2000));   // Year
          Wire.endTransmission();
        }

        struct Time {
          int year;
          int month;
          int day;
          int hour;
          int minute;
          int second;
          int day_of_week;
        };

        Time get_time() {
          Wire.beginTransmission(0x68);
          Wire.write(0);  // Start at register 0
          Wire.endTransmission();

          if (Wire.requestFrom(0x68, 7) != 7) {
            ESP_LOGE("DS3231", "Failed to read time registers");
            return Time{0, 0, 0, 0, 0, 0, 0};
          }

          uint8_t second = bcd_to_dec(Wire.read());
          uint8_t minute = bcd_to_dec(Wire.read());
          uint8_t hour = bcd_to_dec(Wire.read());
          uint8_t day_of_week = bcd_to_dec(Wire.read());
          uint8_t day = bcd_to_dec(Wire.read());
          uint8_t month = bcd_to_dec(Wire.read());
          uint16_t year = bcd_to_dec(Wire.read()) + 2000;

          return Time{year, month, day, hour, minute, second, day_of_week};
        }

       private:
        esphome::time::RealTimeClock *rtc_;

        uint8_t dec_to_bcd(int val) {
          return ((val / 10 * 16) + (val % 10));
        }

        int bcd_to_dec(uint8_t val) {
          return ((val / 16 * 10) + (val % 16));
        }
      };

      auto my_rtc = new DS3231Component(id(sntp_time));
      App.register_component(my_rtc);
      return {my_rtc};

text_sensor:
  - platform: debug
    device:
      name: "Device Info"
    reset_reason:
      name: "Reset Reason"
  - platform: wifi_info
    ip_address:
      name: "IP Address"
      entity_category: diagnostic
    ssid:
      name: "Connected SSID"
      entity_category: diagnostic
    mac_address:
      name: "Mac Address"
      entity_category: diagnostic
      
  - platform: template
    name: "Last Relay one ON Time"
    id: last_on_relayone_text_sensor
    lambda: |-
      return id(last_on_relayone_timestamp);

  - platform: template
    name: "Last Relay one OFF Time"
    id: last_off_relayone_text_sensor
    lambda: |-
      return id(last_off_relayone_timestamp);
      
  - platform: template
    name: "Last Relay two ON Time"
    id: last_on_relaytwo_text_sensor
    lambda: |-
      return id(last_on_relaytwo_timestamp);

  - platform: template
    name: "Last Relay two OFF Time"
    id: last_off_relaytwo_text_sensor
    lambda: |-
      return id(last_off_relaytwo_timestamp);
      
  - platform: template
    name: "Last Relay three ON Time"
    id: last_on_relaythree_text_sensor
    lambda: |-
      return id(last_on_relaythree_timestamp);

  - platform: template
    name: "Last Relay three OFF Time"
    id: last_off_relaythree_text_sensor
    lambda: |-
      return id(last_off_relaythree_timestamp);
      
  - platform: template
    name: "Last Relay four ON Time"
    id: last_on_relayfour_text_sensor
    lambda: |-
      return id(last_on_relayfour_timestamp);

  - platform: template
    name: "Last Relay four OFF Time"
    id: last_off_relayfour_text_sensor
    lambda: |-
      return id(last_off_relayfour_timestamp);



  #  Creates a sensor showing when the device was last restarted
  - platform: template
    name: 'Last Restart'
    id: device_last_restart
    icon: mdi:clock
    entity_category: diagnostic
#    device_class: timestamp

  #  Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
  - platform: template
    name: "Uptime"
    entity_category: diagnostic
    lambda: |-
      int seconds = (id(uptime_sensor).state);
      int days = seconds / (24 * 3600);
      seconds = seconds % (24 * 3600);
      int hours = seconds / 3600;
      seconds = seconds % 3600;
      int minutes = seconds /  60;
      seconds = seconds % 60;
      if ( days > 3650 ) {
        return { "Starting up" };
      } else if ( days ) {
        return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
      } else if ( hours ) {
        return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
      } else if ( minutes ) {
        return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
      } else {
        return { (String(seconds) +"s").c_str() };
      }
    icon: mdi:clock-start

time:
  - platform: sntp
    id: sntp_time
  # Define the timezone of the device
    timezone: "${timezone}"
  # Change sync interval from default 5min to 6 hours (or as set in substitutions)
    update_interval: ${sntp_update_interval}
  # Set specific sntp servers to use
    servers:
      - "${sntp_server_1}"
      - "${sntp_server_2}"
      - "${sntp_server_3}"
      
    # Publish the time the device was last restarted
    on_time_sync:
      then:
        - lambda: |-
                  struct Time {
                  int year;
                  int month;
                  int day;
                  int hour;
                  int minute;
                  int second;
                  int day_of_week;
                  };
                   Time r = static_cast< DS3231Component*> (id(tss).get_component(0))->get_time();
        # Update last restart time, but only once.
        - if:
            condition:
              lambda: 'return id(device_last_restart).state == "";'
            then:
              - text_sensor.template.publish:
                  id: device_last_restart
                  state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'

    
binary_sensor:
  - platform: status
    name: "Status"
    entity_category: diagnostic

      
      
sensor:
  - platform: uptime
    name: "Uptime Sensor"
    id: uptime_sensor
    entity_category: diagnostic
    internal: true

  - platform: wifi_signal
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 60s
    entity_category: "diagnostic"

  - platform: copy
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
    device_class: ""

button:
  - platform: restart
    name: "Restart"
    entity_category: config

  - platform: factory_reset
    name: "Factory Reset"
    id: Reset
    entity_category: config
 
  - platform: safe_mode
    name: "Safe Mode"
    internal: false
    entity_category: config
    
  - platform: template
    name: "Relais2 fuer x% Sekunden"
    on_press:
      - switch.turn_on: relay_2
      - delay: !lambda |-
            int delay_ms = (int(id(relais_two_zeit_prozent).state) * 600); 
            // 100% * 600ms = 60s max
            return delay_ms;
      - switch.turn_off: relay_2

number:
  - platform: template
    name: "Relais2 Zeit (%)"
    id: relais_two_zeit_prozent
    min_value: 0
    max_value: 100
    step: 1
    optimistic: true
    mode: slider

i2c:
  sda: GPIO21  # Passe diese Pins an dein Setup an
  scl: GPIO22
  scan: true   # Zeigt I²C-Geräte im Log

but I get this error

watering.yaml:501:30: error: 'DS3231Component' does not name a type watering.yaml:501:45: error: expected '>' before '*' token watering.yaml:501:45: error: expected '(' before '*' token watering.yaml:501:46: error: expected primary-expression before '>' token watering.yaml:501:72: error: 'class esphome::Component' has no member named 'get_time'; did you mean 'set_timeout'? watering.yaml:501:82: error: expected ')' before ';' token *** [.pioenvs/wateringdevice/src/main.cpp.o] Error 1

how do i call the function correctly?

miloit avatar Mar 29 '25 14:03 miloit

As custom components are not a possibility anymore and I needed a to have my DS3231 fully work with for porting my led matrix project to esphome, I converted the rtc sync and get time to an external component, you just have to add this to your yaml:

external_components:
  - source: github://zubrick/zmatrix-esphome@main
    components: [ ds3231 ]
    refresh: 10min

ds3231:
  id: ds3231_time
  time_id: sntp_time
  update_interval: 240min

time:
  - platform: sntp
    id: sntp_time
    timezone: "Europe/Zurich"

the update_interval parameter lets you decide how often you want to try syncing the rtc to the ntp (or any other time source you may use)

you can then call id(ds3231_id).get_time() in your lambda to get the rtc time returned as the Time structure

Sorry, I didn't implement the alarm part, I don't have any use for that

zubrick avatar May 28 '25 05:05 zubrick