From 85794d9f2f4b1d0cd8db826c41c35307358102d9 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Mon, 10 Nov 2025 18:05:25 +0100 Subject: [PATCH] first commit --- .gitignore | 67 ++++++++ LICENSE | 22 +++ MANIFEST.in | 4 + README.md | 194 +++++++++++++++++++++ examples/alarm_example.py | 82 +++++++++ examples/calibration_example.py | 101 +++++++++++ examples/simple_test.py | 75 ++++++++ pypcf8523/__init__.py | 15 ++ pypcf8523/pcf8523.py | 295 ++++++++++++++++++++++++++++++++ pyproject.toml | 42 +++++ 10 files changed, 897 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 examples/alarm_example.py create mode 100644 examples/calibration_example.py create mode 100644 examples/simple_test.py create mode 100644 pypcf8523/__init__.py create mode 100644 pypcf8523/pcf8523.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c562a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Virtual environments +venv/ +ENV/ +env/ +.venv + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f437570 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2016 Adafruit Industries +Copyright (c) 2025 Duncan Tourolle (Python conversion) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..61977df --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.md +include LICENSE +include pyproject.toml +recursive-include examples *.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..56be3b3 --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +# PyPCF8523 - Python Driver for PCF8523 Real Time Clock + +A Python 3.8+ driver for the PCF8523 Real Time Clock (RTC) chip, designed for Raspberry Pi and similar Linux boards with I2C support. + +This library is a Python conversion of [Adafruit's CircuitPython PCF8523 library](https://github.com/adafruit/Adafruit_CircuitPython_PCF8523), adapted to work with standard Python and smbus2. + +## Features + +- **Battery-backed RTC**: Maintains accurate time even when main power is lost +- **Dual voltage support**: Works with 3.3V or 5V logic +- **Alarm functionality**: Set alarms with minute precision +- **Calibration support**: Adjust clock accuracy with offset calibration +- **Low battery detection**: Monitor backup battery status +- **Power management**: Configurable battery switchover modes +- **Python 3.8+ compatible**: Modern Python with type hints + +## Hardware Requirements + +- Raspberry Pi (any model with I2C) or compatible Linux board +- PCF8523 RTC module (e.g., Adafruit PCF8523 breakout) +- I2C connection to the RTC module + +## Installation + +### Prerequisites + +Enable I2C on your Raspberry Pi: +```bash +sudo raspi-config +# Navigate to: Interface Options > I2C > Enable +``` + +### Install the package + +```bash +pip install smbus2 +pip install -e /path/to/PyPCF8523 +``` + +Or install dependencies directly: +```bash +cd /path/to/PyPCF8523 +pip install -r requirements.txt +pip install -e . +``` + +## Quick Start + +### Basic Usage + +```python +import time +from pypcf8523 import PCF8523 + +# Initialize RTC on I2C bus 1 (default for Raspberry Pi) +rtc = PCF8523(i2c_bus=1) + +# Set the current time +current_time = time.localtime() +rtc.datetime = current_time + +# Read the current time +dt = rtc.datetime +print(f"Current time: {dt.tm_year}-{dt.tm_mon:02d}-{dt.tm_mday:02d} " + f"{dt.tm_hour:02d}:{dt.tm_min:02d}:{dt.tm_sec:02d}") +``` + +### Using Context Manager + +```python +from pypcf8523 import PCF8523 + +with PCF8523(1) as rtc: + print(f"RTC Time: {rtc.datetime}") + + # Check if power was lost + if rtc.lost_power: + print("Warning: RTC lost power, time may be incorrect") +``` + +### Setting an Alarm + +```python +from pypcf8523 import PCF8523 + +rtc = PCF8523(1) + +# Set alarm for 8:30 AM every day +rtc.set_alarm(minute=30, hour=8) + +# Enable alarm interrupt +rtc.alarm_interrupt = True + +# Check if alarm triggered +if rtc.alarm_status: + print("Alarm triggered!") + rtc.alarm_status = False # Clear the alarm +``` + +### Calibration + +```python +from pypcf8523 import PCF8523 + +rtc = PCF8523(1) + +# Set calibration offset (-64 to +63) +# Positive values speed up the clock, negative values slow it down +rtc.calibration = 5 + +# Set calibration schedule +rtc.calibration_schedule_per_minute = True # Apply every minute +# or +rtc.calibration_schedule_per_minute = False # Apply every 2 hours +``` + +## API Reference + +### PCF8523 Class + +#### Constructor +```python +PCF8523(i2c_bus=1, address=0x68) +``` +- `i2c_bus`: I2C bus number (default: 1) +- `address`: I2C device address (default: 0x68) + +#### Properties + +- **`datetime`** (struct_time): Get or set the current date and time +- **`lost_power`** (bool): True if device lost power since time was set +- **`power_management`** (int): Battery switchover mode (0-7) +- **`alarm_interrupt`** (bool): Enable/disable alarm interrupt output +- **`alarm_status`** (bool): Check if alarm triggered (write False to clear) +- **`battery_low`** (bool): True if backup battery is low (read-only) +- **`high_capacitance`** (bool): Oscillator capacitance mode +- **`calibration`** (int): Clock calibration offset (-64 to +63) +- **`calibration_schedule_per_minute`** (bool): Calibration schedule mode + +#### Methods + +- **`set_alarm(minute=None, hour=None, day=None, weekday=None)`**: Set alarm +- **`clear_alarm()`**: Disable and clear alarm +- **`close()`**: Close I2C bus connection + +## Pin Connections (Raspberry Pi) + +| PCF8523 | Raspberry Pi | +|---------|--------------| +| VCC | 3.3V (Pin 1) | +| GND | GND (Pin 6) | +| SDA | SDA (Pin 3) | +| SCL | SCL (Pin 5) | + +## Troubleshooting + +### I2C Device Not Found + +Check if the device is detected: +```bash +i2cdetect -y 1 +``` +You should see `68` in the output grid. + +### Permission Denied + +Add your user to the i2c group: +```bash +sudo usermod -a -G i2c $USER +``` +Then log out and back in. + +### Accuracy Issues + +The PCF8523 can drift up to 2 seconds per day. For critical timing applications: +1. Use the calibration feature to compensate for drift +2. Consider periodic synchronization with NTP +3. Monitor temperature (affects crystal oscillator) + +## Credits + +- **Original CircuitPython Library**: [Adafruit Industries](https://github.com/adafruit/Adafruit_CircuitPython_PCF8523) +- **Original Authors**: Philip R. Moyer and Radomir Dopieralski +- **Python Conversion**: Duncan Tourolle + +## License + +MIT License - See LICENSE file for details + +## Contributing + +This is a conversion of Adafruit's CircuitPython library. For core functionality improvements, please contribute to the [original repository](https://github.com/adafruit/Adafruit_CircuitPython_PCF8523). + +For Python-specific issues or improvements, feel free to submit issues or pull requests. diff --git a/examples/alarm_example.py b/examples/alarm_example.py new file mode 100644 index 0000000..91d18b8 --- /dev/null +++ b/examples/alarm_example.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Alarm example for PyPCF8523 RTC driver. + +This example demonstrates how to use the alarm functionality: +- Setting an alarm +- Checking alarm status +- Clearing an alarm + +Hardware setup: +- Connect PCF8523 to Raspberry Pi I2C bus 1 +- Optionally connect INT pin to a GPIO for interrupt handling +""" + +import time +from pypcf8523 import PCF8523 + + +def main(): + print("PCF8523 Alarm Example") + print("=" * 50) + + # Initialize the RTC + rtc = PCF8523(i2c_bus=1) + + # Get current time + current = rtc.datetime + print(f"Current time: {current.tm_hour:02d}:{current.tm_min:02d}:{current.tm_sec:02d}") + + # Set an alarm for 2 minutes from now + alarm_minute = (current.tm_min + 2) % 60 + alarm_hour = current.tm_hour + if alarm_minute < current.tm_min: # Handle hour rollover + alarm_hour = (alarm_hour + 1) % 24 + + print(f"Setting alarm for: {alarm_hour:02d}:{alarm_minute:02d}") + rtc.set_alarm(minute=alarm_minute, hour=alarm_hour) + + # Enable the alarm interrupt + rtc.alarm_interrupt = True + print("Alarm interrupt enabled") + + # Clear any existing alarm status + rtc.alarm_status = False + + print("\nWaiting for alarm... (Press Ctrl+C to stop)") + print("-" * 50) + + try: + while True: + # Read current time + current = rtc.datetime + time_str = f"{current.tm_hour:02d}:{current.tm_min:02d}:{current.tm_sec:02d}" + + # Check if alarm triggered + if rtc.alarm_status: + print(f"\nšŸ”” ALARM! Triggered at {time_str}") + + # Clear the alarm + rtc.alarm_status = False + print("Alarm cleared") + + # Optionally disable the alarm + # rtc.clear_alarm() + # print("Alarm disabled") + + break + else: + print(f"Current time: {time_str} - Waiting for alarm...", end="\r") + + time.sleep(1.0) + + except KeyboardInterrupt: + print("\n\nExample stopped by user") + finally: + # Clean up + rtc.clear_alarm() + rtc.close() + print("Alarm cleared and RTC connection closed") + + +if __name__ == "__main__": + main() diff --git a/examples/calibration_example.py b/examples/calibration_example.py new file mode 100644 index 0000000..7cf4ab6 --- /dev/null +++ b/examples/calibration_example.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Calibration example for PyPCF8523 RTC driver. + +This example demonstrates how to calibrate the RTC for better accuracy: +- Reading calibration settings +- Adjusting calibration offset +- Setting calibration schedule + +The PCF8523 can drift up to 2 seconds per day. Calibration helps +compensate for this drift. + +Calibration offset range: -64 to +63 +- Positive values speed up the clock +- Negative values slow it down + +Calibration schedule: +- Per minute: 1 LSB = 4.069 ppm +- Per 2 hours: 1 LSB = 4.340 ppm +""" + +import time +from pypcf8523 import PCF8523 + + +def main(): + print("PCF8523 Calibration Example") + print("=" * 50) + + # Initialize the RTC + rtc = PCF8523(i2c_bus=1) + + # Read current calibration settings + current_offset = rtc.calibration + per_minute = rtc.calibration_schedule_per_minute + + print(f"Current calibration offset: {current_offset}") + print(f"Calibration schedule: {'Per minute' if per_minute else 'Per 2 hours'}") + + # Calculate ppm (parts per million) offset + ppm_per_lsb = 4.069 if per_minute else 4.340 + ppm_offset = current_offset * ppm_per_lsb + print(f"Approximate offset: {ppm_offset:.2f} ppm") + + # Example: Set calibration + print("\n" + "-" * 50) + print("Example calibration adjustment:") + print("-" * 50) + + # If your RTC is running fast (gaining time), use negative offset + # If your RTC is running slow (losing time), use positive offset + + # Example: Clock gains 2 seconds per day + # 2 seconds / 86400 seconds = 23.15 ppm + # Offset needed: 23.15 / 4.069 ā‰ˆ -6 (per minute mode) + + new_offset = 0 # Change this based on your measurements + print(f"\nTo set calibration offset to {new_offset}:") + print(f" rtc.calibration = {new_offset}") + + if new_offset != 0: + print("\nUncomment the following lines to apply:") + print(" # rtc.calibration_schedule_per_minute = True") + print(f" # rtc.calibration = {new_offset}") + print(f" # This would give approximately {new_offset * 4.069:.2f} ppm offset") + + # Uncomment to actually apply calibration: + # rtc.calibration_schedule_per_minute = True + # rtc.calibration = new_offset + + # How to measure drift: + print("\n" + "=" * 50) + print("How to measure and calibrate your RTC:") + print("=" * 50) + print("1. Set the RTC to accurate time (sync with NTP)") + print("2. Wait 24-48 hours") + print("3. Compare RTC time with accurate time") + print("4. Calculate drift in seconds per day") + print("5. Convert to ppm: (drift_seconds / 86400) * 1,000,000") + print("6. Calculate offset: ppm / 4.069 (per minute mode)") + print("7. Apply opposite sign: if fast use negative, if slow use positive") + print("8. Set the calibration offset") + print("\nExample:") + print(" If RTC gains 2 seconds/day:") + print(" 2 / 86400 * 1000000 = 23.15 ppm") + print(" 23.15 / 4.069 = 5.69 ā‰ˆ 6") + print(" Use offset = -6 (negative because it's fast)") + + # Check battery status + print("\n" + "-" * 50) + if rtc.battery_low: + print("āš ļø WARNING: Backup battery is low!") + else: + print("āœ“ Backup battery is OK") + + # Clean up + rtc.close() + print("\nRTC connection closed") + + +if __name__ == "__main__": + main() diff --git a/examples/simple_test.py b/examples/simple_test.py new file mode 100644 index 0000000..3cf6fec --- /dev/null +++ b/examples/simple_test.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Simple test example for PyPCF8523 RTC driver. + +This example demonstrates basic usage of the PCF8523 RTC: +- Reading the current time +- Setting the time +- Checking power loss status + +Hardware setup: +- Connect PCF8523 to Raspberry Pi I2C bus 1 +- VCC -> 3.3V, GND -> GND, SDA -> GPIO2, SCL -> GPIO3 +""" + +import time +from pypcf8523 import PCF8523 + +# Days of the week for display +DAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") + + +def main(): + # Initialize the RTC on I2C bus 1 + print("Initializing PCF8523 RTC...") + rtc = PCF8523(i2c_bus=1) + + # Check if the RTC lost power + if rtc.lost_power: + print("WARNING: RTC lost power. Setting time to system time...") + + # Set the RTC to the current system time + # In a real application, you might want to sync with NTP first + current_time = time.localtime() + rtc.datetime = current_time + print(f"Time set to: {time.strftime('%Y-%m-%d %H:%M:%S', current_time)}") + else: + print("RTC power OK") + + # To manually set the time, uncomment and modify this section: + # ================================================================ + # import time + # # Set to a specific time: 2025-11-09 15:30:00 (Saturday) + # set_time = time.struct_time((2025, 11, 9, 15, 30, 0, 5, -1, -1)) + # rtc.datetime = set_time + # print(f"Time manually set to: {time.strftime('%Y-%m-%d %H:%M:%S', set_time)}") + # ================================================================ + + print("\nReading time from RTC (Press Ctrl+C to stop):") + print("-" * 50) + + try: + while True: + # Read the current time from the RTC + current = rtc.datetime + + # Format and display the time + day_name = DAYS[current.tm_wday] + time_str = (f"{day_name} " + f"{current.tm_year}/{current.tm_mon:02d}/{current.tm_mday:02d} " + f"{current.tm_hour:02d}:{current.tm_min:02d}:{current.tm_sec:02d}") + + print(time_str) + + # Wait one second before next read + time.sleep(1.0) + + except KeyboardInterrupt: + print("\n\nTest stopped by user") + finally: + # Clean up + rtc.close() + print("RTC connection closed") + + +if __name__ == "__main__": + main() diff --git a/pypcf8523/__init__.py b/pypcf8523/__init__.py new file mode 100644 index 0000000..f9c49f0 --- /dev/null +++ b/pypcf8523/__init__.py @@ -0,0 +1,15 @@ +"""PyPCF8523 - PCF8523 Real Time Clock Driver for Python 3.14+ + +This library provides a Python interface for the PCF8523 RTC chip on Raspberry Pi. +Converted from Adafruit's CircuitPython library for standard Python. + +Author: Converted for Python 3.14 +Original: Philip R. Moyer and Radomir Dopieralski for Adafruit Industries +License: MIT +""" + +__version__ = "1.0.0" + +from .pcf8523 import PCF8523 + +__all__ = ["PCF8523"] diff --git a/pypcf8523/pcf8523.py b/pypcf8523/pcf8523.py new file mode 100644 index 0000000..2b363d9 --- /dev/null +++ b/pypcf8523/pcf8523.py @@ -0,0 +1,295 @@ +"""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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..868afaf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pypcf8523" +version = "1.0.0" +description = "Python 3.14+ driver for the PCF8523 Real Time Clock" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +authors = [ + {name = "Duncan Tourolle", email = "duncan@tourolle.paris"} +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: POSIX :: Linux", + "Topic :: System :: Hardware", + "Topic :: Software Development :: Libraries :: Python Modules", +] +keywords = ["pcf8523", "rtc", "real-time-clock", "i2c", "raspberry-pi", "hardware"] +dependencies = [ + "smbus2>=0.4.0", +] + +[project.urls] +"Homepage" = "https://github.com/adafruit/Adafruit_CircuitPython_PCF8523" +"Original CircuitPython Library" = "https://github.com/adafruit/Adafruit_CircuitPython_PCF8523" + +[tool.setuptools] +packages = ["pypcf8523"] +package-dir = {"pypcf8523" = "pypcf8523"}