"""PCF8523 Real Time Clock Driver This module provides an interface to the PCF8523 RTC chip via I2C. Designed for Raspberry Pi and similar Linux boards with I2C support. SPDX-FileCopyrightText: 2016 Philip R. Moyer for Adafruit Industries SPDX-FileCopyrightText: 2016 Radomir Dopieralski for Adafruit Industries SPDX-FileCopyrightText: 2025 Converted for Python 3.14 SPDX-License-Identifier: MIT """ import time from typing import Optional try: from smbus2 import SMBus except ImportError: raise ImportError("smbus2 is required. Install with: pip install smbus2") # Power management constants STANDARD_BATTERY_SWITCHOVER_AND_DETECTION = 0b000 BATTERY_SWITCHOVER_OFF = 0b111 # I2C address for PCF8523 PCF8523_ADDRESS = 0x68 # Register addresses _CONTROL_1 = 0x00 _CONTROL_2 = 0x01 _CONTROL_3 = 0x02 _SECONDS = 0x03 _MINUTES = 0x04 _HOURS = 0x05 _DAYS = 0x06 _WEEKDAYS = 0x07 _MONTHS = 0x08 _YEARS = 0x09 _MINUTE_ALARM = 0x0A _HOUR_ALARM = 0x0B _DAY_ALARM = 0x0C _WEEKDAY_ALARM = 0x0D _OFFSET = 0x0E _TMR_CLKOUT_CTRL = 0x0F def _bcd2bin(value: int) -> int: """Convert binary coded decimal to binary.""" return value - 6 * (value >> 4) def _bin2bcd(value: int) -> int: """Convert binary to binary coded decimal.""" return value + 6 * (value // 10) class PCF8523: """Interface to the PCF8523 RTC. Args: i2c_bus: I2C bus number (typically 1 for Raspberry Pi) address: I2C address of the PCF8523 (default: 0x68) Example: >>> rtc = PCF8523(1) # Use I2C bus 1 >>> print(rtc.datetime) time.struct_time(tm_year=2025, tm_mon=11, tm_mday=9, ...) """ def __init__(self, i2c_bus: int = 1, address: int = PCF8523_ADDRESS): """Initialize the PCF8523 driver. Args: i2c_bus: I2C bus number (default: 1) address: I2C device address (default: 0x68) """ self._bus = SMBus(i2c_bus) self._address = address def _read_byte(self, register: int) -> int: """Read a single byte from a register.""" return self._bus.read_byte_data(self._address, register) def _write_byte(self, register: int, value: int) -> None: """Write a single byte to a register.""" self._bus.write_byte_data(self._address, register, value) def _read_bit(self, register: int, bit: int) -> bool: """Read a specific bit from a register.""" value = self._read_byte(register) return bool((value >> bit) & 1) def _write_bit(self, register: int, bit: int, value: bool) -> None: """Write a specific bit in a register.""" reg_value = self._read_byte(register) if value: reg_value |= (1 << bit) else: reg_value &= ~(1 << bit) self._write_byte(register, reg_value) def _read_bits(self, register: int, bit: int, length: int) -> int: """Read multiple bits from a register.""" value = self._read_byte(register) mask = (1 << length) - 1 return (value >> bit) & mask def _write_bits(self, register: int, bit: int, length: int, value: int) -> None: """Write multiple bits to a register.""" reg_value = self._read_byte(register) mask = ((1 << length) - 1) << bit reg_value = (reg_value & ~mask) | ((value << bit) & mask) self._write_byte(register, reg_value) @property def lost_power(self) -> bool: """True if the device has lost power since the time was set.""" return self._read_bit(_CONTROL_3, 7) @lost_power.setter def lost_power(self, value: bool) -> None: """Clear or set the power lost flag.""" self._write_bit(_CONTROL_3, 7, value) @property def power_management(self) -> int: """Power management state that dictates battery switchover. Defaults to BATTERY_SWITCHOVER_OFF (0b111). """ return self._read_bits(_CONTROL_3, 5, 3) @power_management.setter def power_management(self, value: int) -> None: """Set power management mode.""" self._write_bits(_CONTROL_3, 5, 3, value) @property def datetime(self) -> time.struct_time: """Get the current date and time as a time.struct_time object.""" # Read all time registers at once buffer = self._bus.read_i2c_block_data(self._address, _SECONDS, 7) # Convert BCD to binary seconds = _bcd2bin(buffer[0] & 0x7F) minutes = _bcd2bin(buffer[1] & 0x7F) hours = _bcd2bin(buffer[2] & 0x3F) days = _bcd2bin(buffer[3] & 0x3F) weekday = buffer[4] & 0x07 months = _bcd2bin(buffer[5] & 0x1F) years = _bcd2bin(buffer[6]) + 2000 return time.struct_time((years, months, days, hours, minutes, seconds, weekday, -1, -1)) @datetime.setter def datetime(self, value: time.struct_time) -> None: """Set the current date and time from a time.struct_time object. Args: value: time.struct_time with year, month, day, hour, minute, second, weekday """ # Enable battery switchover and clear lost power flag self.power_management = STANDARD_BATTERY_SWITCHOVER_AND_DETECTION # Convert to BCD and write to registers buffer = [ _bin2bcd(value.tm_sec) & 0x7F, _bin2bcd(value.tm_min) & 0x7F, _bin2bcd(value.tm_hour) & 0x3F, _bin2bcd(value.tm_mday) & 0x3F, value.tm_wday & 0x07, _bin2bcd(value.tm_mon) & 0x1F, _bin2bcd(value.tm_year - 2000) & 0xFF, ] self._bus.write_i2c_block_data(self._address, _SECONDS, buffer) # Clear the power lost flag self.lost_power = False @property def alarm_interrupt(self) -> bool: """True if the interrupt pin will output when alarm is alarming.""" return self._read_bit(_CONTROL_1, 1) @alarm_interrupt.setter def alarm_interrupt(self, value: bool) -> None: """Enable or disable alarm interrupt output.""" self._write_bit(_CONTROL_1, 1, value) @property def alarm_status(self) -> bool: """True if alarm is alarming. Set to False to reset.""" return self._read_bit(_CONTROL_2, 3) @alarm_status.setter def alarm_status(self, value: bool) -> None: """Clear alarm status flag.""" self._write_bit(_CONTROL_2, 3, value) @property def battery_low(self) -> bool: """True if the battery is low and should be replaced.""" return self._read_bit(_CONTROL_3, 2) @property def high_capacitance(self) -> bool: """True for high oscillator capacitance (12.5pF), False for lower (7pF).""" return self._read_bit(_CONTROL_1, 7) @high_capacitance.setter def high_capacitance(self, value: bool) -> None: """Set oscillator capacitance mode.""" self._write_bit(_CONTROL_1, 7, value) @property def calibration_schedule_per_minute(self) -> bool: """False to apply calibration offset every 2 hours (1 LSB = 4.340ppm); True to offset every minute (1 LSB = 4.069ppm). """ return self._read_bit(_OFFSET, 7) @calibration_schedule_per_minute.setter def calibration_schedule_per_minute(self, value: bool) -> None: """Set calibration schedule mode.""" self._write_bit(_OFFSET, 7, value) @property def calibration(self) -> int: """Calibration offset to apply, from -64 to +63.""" value = self._read_bits(_OFFSET, 0, 7) # Convert to signed integer if value > 63: value -= 128 return value @calibration.setter def calibration(self, value: int) -> None: """Set calibration offset (-64 to +63).""" if not -64 <= value <= 63: raise ValueError("Calibration must be between -64 and +63") # Convert to unsigned for register if value < 0: value += 128 self._write_bits(_OFFSET, 0, 7, value) def set_alarm(self, minute: Optional[int] = None, hour: Optional[int] = None, day: Optional[int] = None, weekday: Optional[int] = None) -> None: """Set an alarm. Args: minute: Minute to trigger (0-59) or None to disable hour: Hour to trigger (0-23) or None to disable day: Day to trigger (1-31) or None to disable weekday: Weekday to trigger (0-6) or None to disable Note: Alarms only fire at full minutes (seconds are ignored). """ alarm_regs = [0x80, 0x80, 0x80, 0x80] # All disabled by default if minute is not None: alarm_regs[0] = _bin2bcd(minute) & 0x7F if hour is not None: alarm_regs[1] = _bin2bcd(hour) & 0x3F if day is not None: alarm_regs[2] = _bin2bcd(day) & 0x3F if weekday is not None: alarm_regs[3] = weekday & 0x07 self._bus.write_i2c_block_data(self._address, _MINUTE_ALARM, alarm_regs) def clear_alarm(self) -> None: """Disable and clear the alarm.""" self.alarm_status = False self.set_alarm() # Disable all alarm fields def close(self) -> None: """Close the I2C bus connection.""" self._bus.close() def __enter__(self): """Context manager entry.""" return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit.""" self.close() return False