A comprehensive Python library for parsing and analyzing ResMed CPAP machine data
Project description
cpap-py
A lightweight Python library for parsing ResMed CPAP (Continuous Positive Airway Pressure) device data files. This library provides complete access to all data stored on CPAP devices including device identification, summary statistics, detailed waveform data, and configuration changes.
Features
- Zero Dependencies: Pure Python implementation using only the standard library
- Complete Data Extraction: Parses all available CPAP data including pressure settings, delivered pressures, leak rates, respiratory metrics, and event indices
- Device Identification: Parse both
.tgt(text) and.jsonformat identification files - Summary Data: Extract daily statistics from
STR.edffiles including AHI, leak, pressure, respiratory rate - Session Data: Parse detailed waveform data from
DATALOGEDF files (BRP, PLD, SAD, EVE, CSL, AEV) - Settings & Configuration: Extract device settings including pressure ranges (min/max), comfort settings, humidification
- Pure Python EDF Parser: Full implementation of European Data Format (EDF/EDF+) parser
- Multiple Device Support: Works with ResMed S9, AirSense 10, AirSense 11, and AirCurve series
- BiLevel Support: Full support for BiLevel therapy modes (S, ST, T, PAC, ASV)
- Comprehensive Testing: 97% code coverage with 188 automated tests
- Python 3.9+: Supports Python 3.9, 3.10, 3.11, and 3.12
Installation
pip install cpap-py
For development installation with test dependencies:
git clone https://github.com/dynacylabs/cpap-py.git
cd cpap-py
pip install -e ".[dev]"
See INSTALL.md for detailed installation instructions.
Quick Start
Load All CPAP Data
from cpap_py import CPAPLoader
# Load all data from a CPAP data directory
loader = CPAPLoader("path/to/cpap/data")
data = loader.load_all()
# Access device information
print(f"Device: {data.machine_info.model}")
print(f"Serial: {data.machine_info.serial}")
print(f"Model Series: {data.machine_info.series}")
# Access daily summary records
for record in data.summary_records:
print(f"Date: {record.date}")
print(f" AHI: {record.ahi:.1f} events/hour")
print(f" Duration: {record.mask_duration/3600:.1f} hours")
print(f" Leak (median): {record.leak_50:.1f} L/min")
print(f" Pressure (95th): {record.mp_95:.1f} cmH2O")
# Access detailed session data
for session in data.sessions:
print(f"Session: {session.start_time}")
print(f" Duration: {session.duration/3600:.1f} hours")
print(f" Flow rate samples: {len(session.flow_rate)}")
print(f" Pressure samples: {len(session.pressure)}")
print(f" Events recorded: {len(session.events)}")
print(f" File type: {session.file_type}")
Usage Examples
Parse Device Identification
from cpap_py import IdentificationParser
parser = IdentificationParser("path/to/data")
info = parser.parse()
if info:
print(f"Model: {info.model}")
print(f"Serial: {info.serial}")
print(f"Series: {info.series}")
print(f"Model Number: {info.model_number}")
Parse Summary Data (STR.edf)
from cpap_py import STRParser
parser = STRParser("path/to/STR.edf")
if parser.parse():
for record in parser.records:
if record.date:
print(f"{record.date}: AHI={record.ahi:.1f}, Hours={record.mask_duration/3600:.1f}")
print(f" Leak (median/95th): {record.leak_50:.1f}/{record.leak_95:.1f} L/min")
print(f" Pressure (median/95th): {record.mp_50:.1f}/{record.mp_95:.1f} cmH2O")
# Filter by date range
from datetime import date
filtered = parser.get_records_by_date_range(
date(2025, 12, 1),
date(2025, 12, 15)
)
Parse Session Data (DATALOG)
from cpap_py import DatalogParser
parser = DatalogParser("path/to/DATALOG")
# Parse all sessions
sessions = parser.parse_all_sessions()
for session in sessions:
print(f"{session.date} - {session.file_type}")
print(f" Start: {session.start_time}")
print(f" Duration: {session.duration/3600:.2f} hours")
print(f" Sample rate: {session.sample_rate} Hz")
# Access waveform data
if session.flow_rate:
print(f" Flow rate: {len(session.flow_rate)} samples")
if session.pressure:
print(f" Pressure: {len(session.pressure)} samples")
if session.spo2:
print(f" SpO2: {len(session.spo2)} samples")
# Access events
for event in session.events:
print(f" Event: {event.event_type} at {event.timestamp}s")
# Get sessions for specific date
from datetime import date
date_sessions = parser.get_sessions_by_date(date(2025, 12, 15))
Parse Settings Files
from cpap_py import SettingsParser
parser = SettingsParser("path/to/SETTINGS")
changes = parser.parse_all()
for change in changes:
print(f"{change.timestamp}: {change.setting} = {change.value}")
Parse Individual EDF Files
from cpap_py import EDFParser
edf = EDFParser("path/to/file.edf")
if edf.parse():
# Access header information
print(f"Recording date: {edf.header.start_date}")
print(f"Duration: {edf.header.duration} seconds")
print(f"Signals: {len(edf.signals)}")
# Access signals
for signal in edf.signals:
print(f" {signal.label}: {len(signal.data)} samples")
print(f" Unit: {signal.physical_dimension}")
print(f" Range: {signal.physical_min} to {signal.physical_max}")
# Find specific signal
flow_signal = edf.get_signal("Flow")
if flow_signal:
# Get physical (scaled) values
physical_values = edf.get_physical_values(flow_signal)
print(f"Flow range: {min(physical_values):.1f} to {max(physical_values):.1f} L/min")
Data Directory Structure
The library expects data in the ResMed CPAP format:
data_directory/
├── Identification.tgt # or Identification.json
├── STR.edf # Daily summary data
├── DATALOG/
│ ├── 20251126/ # Date folders (YYYYMMDD)
│ │ ├── BRP00001.edf # Breathing data
│ │ ├── PLD00001.edf # Pressure/leak data
│ │ └── EVE00001.edf # Events
│ └── 20251127/
└── SETTINGS/
├── CGL.tgt # Clinical settings
└── UGL.tgt # User settings
File Types
Identification Files
.tgt: Text format with#KEY valuepairs.json: JSON format (AirSense 11)
EDF Files
STR.edf: Daily summary statisticsBRP.edf: Breathing waveforms (flow, tidal volume, etc.)PLD.edf: Pressure and leak dataSAD.edf: Summary/advanced dataEVE.edf: Event markers (apneas, hypopneas)CSL.edf: Clinical settings logAEV.edf: Advanced events
API Reference
CPAPLoader
High-level interface for loading all CPAP data.
loader = CPAPLoader(data_path: str)
Methods:
load_all()→CPAPData: Load all data (identification, summary, sessions, settings)load_identification_only()→MachineInfo | None: Load only device identificationload_summary_only()→List[STRRecord]: Load only STR.edf summary dataload_sessions_for_date(date)→List[SessionData]: Load sessions for specific dateget_date_range()→tuple[date, date] | None: Get (start_date, end_date) of available data
IdentificationParser
Parse device identification files (.tgt or .json).
parser = IdentificationParser(data_path: str)
Methods:
parse()→MachineInfo | None: Parse identification file and return device info
Supported Files:
Identification.tgt- Text format with#KEY valuepairsIdentification.json- JSON format (AirSense 11)
STRParser
Parse STR.edf daily summary files.
parser = STRParser(filepath: str, serial_number: str = None)
Methods:
parse()→bool: Parse file and populate records listget_records_by_date_range(start: date, end: date)→List[STRRecord]: Filter records by date
Mode Constants:
MODE_UNKNOWN = 0MODE_CPAP = 1MODE_APAP = 2MODE_BILEVEL_FIXED = 3MODE_BILEVEL_AUTO = 4MODE_BILEVEL_S = 5MODE_BILEVEL_ST = 6MODE_BILEVEL_T = 7MODE_BILEVEL_PAC = 8MODE_ASV = 9
DatalogParser
Parse DATALOG session files (BRP, PLD, SAD, EVE, CSL, AEV).
parser = DatalogParser(datalog_path: str)
Methods:
scan_files()→Dict[date, List[Path]]: Scan directory and return files by dateparse_session_file(filepath: str)→SessionData | None: Parse single session fileparse_all_sessions()→List[SessionData]: Parse all session files in directoryget_sessions_by_date(target_date: date)→List[SessionData]: Get sessions for specific dateget_sessions_by_date_range(start: date, end: date)→List[SessionData]: Get sessions in range
File Types:
BRP- Breathing waveforms (flow, tidal volume, minute ventilation, respiratory rate)PLD- Pressure and leak dataSAD- Summary/advanced dataEVE- Event markers (apneas, hypopneas, flow limitations)CSL- Clinical settings logAEV- Advanced events
SettingsParser
Parse SETTINGS files (.tgt format).
parser = SettingsParser(settings_path: str)
Methods:
parse_all()→List[SettingChange]: Parse all settings filesparse_file(filepath: str)→List[SettingChange]: Parse single settings file
EDFParser
Low-level European Data Format (EDF/EDF+) parser.
parser = EDFParser(filepath: str)
Methods:
open()→bool: Open EDF fileclose(): Close fileparse()→bool: Parse entire file (header + signals + data)parse_header()→bool: Parse only file headerparse_signal_headers()→bool: Parse signal definitionsparse_data()→bool: Parse signal dataget_signal(label: str, index: int = 0)→EDFSignal | None: Find signal by labelget_physical_values(signal: EDFSignal)→List[float]: Convert digital to physical values
Data Classes
CPAPData
Complete CPAP data container.
Fields:
machine_info: MachineInfo | None- Device informationsummary_records: List[STRRecord]- Daily summary statisticssessions: List[SessionData]- Detailed session waveform datasettings_changes: List[SettingChange]- Configuration changes
MachineInfo
Device identification information.
Fields:
serial: str- Device serial numbermodel: str- Model name (e.g., "AirSense 10 AutoSet")model_number: str- Model numberseries: str- Device series (e.g., "AirSense 11", "AirSense 10", "S9")properties: Dict[str, str]- Additional device properties
STRRecord
Daily summary record from STR.edf.
Key Fields:
date: date- Record dateahi: float- Apnea-Hypopnea Index (events/hour)ai: float- Apnea Indexhi: float- Hypopnea Indexcai: float- Central Apnea Indexoai: float- Obstructive Apnea Indexmask_duration: float- Therapy duration (seconds)mask_on: List[int]- Mask-on timestampsmask_off: List[int]- Mask-off timestampsleak_50: float- Median leak (L/min)leak_95: float- 95th percentile leak (L/min)mp_50: float- Median mask pressure (cmH2O)mp_95: float- 95th percentile mask pressure (cmH2O)mode: int- Therapy modemin_pressure: float- Minimum pressure settingmax_pressure: float- Maximum pressure settingipap: float- IPAP setting (BiLevel)epap: float- EPAP setting (BiLevel)epr: int- EPR setting- And 70+ more statistics fields...
SessionData
Detailed session data from DATALOG files.
Fields:
date: date- Session datestart_time: datetime- Session start timestampduration: float- Session duration (seconds)file_type: str- File type (BRP, PLD, SAD, EVE, etc.)sample_rate: float- Sample rate (Hz)flow_rate: List[float]- Flow rate waveform (L/min)pressure: List[float]- Pressure waveform (cmH2O)mask_pressure: List[float]- Mask pressure (cmH2O)leak: List[float]- Leak rate (L/min)tidal_volume: List[float]- Tidal volume (mL)minute_vent: List[float]- Minute ventilation (L/min)resp_rate: List[float]- Respiratory rate (breaths/min)target_ipap: List[float]- Target IPAP (cmH2O)target_epap: List[float]- Target EPAP (cmH2O)spo2: List[float]- SpO2 (%)pulse: List[float]- Pulse rate (bpm)events: List[SessionEvent]- Respiratory events
SessionEvent
Event recorded during CPAP session.
Fields:
timestamp: float- Time since session start (seconds)event_type: str- Event type (e.g., "Obstructive Apnea", "Hypopnea")duration: float- Event duration (seconds)data: Dict[str, float]- Additional event data
SettingChange
Device setting change record.
Fields:
timestamp: datetime- When setting was changedsetting: str- Setting namevalue: str- New valuesource: str- Source file
EDFHeader
EDF file header information.
Fields:
version: str- EDF versionpatient_id: str- Patient identifierrecording_id: str- Recording identifierstart_date: datetime- Recording start date/timeheader_bytes: int- Header sizereserved: str- Reserved field (may contain "EDF+C" for EDF+)num_records: int- Number of data recordsduration: float- Duration of data record (seconds)num_signals: int- Number of signals
EDFSignal
EDF signal descriptor.
Fields:
label: str- Signal labeltransducer_type: str- Transducer typephysical_dimension: str- Physical unit (e.g., "L/min", "cmH2O")physical_min: float- Physical minimumphysical_max: float- Physical maximumdigital_min: int- Digital minimumdigital_max: int- Digital maximumprefiltering: str- Prefiltering infosample_count: int- Samples per data recordreserved: str- Reserved fielddata: List[int]- Raw digital samplesgain: float- Calculated gain for conversionoffset: float- Calculated offset for conversion
Project Structure
cpap-py/
├── src/
│ └── cpap_py/ # Main library package
│ ├── __init__.py # Package initialization and exports
│ ├── edf_parser.py # EDF/EDF+ file parser (pure Python)
│ ├── identification.py # Device identification parser
│ ├── str_parser.py # STR.edf summary data parser
│ ├── datalog_parser.py # DATALOG session data parser
│ ├── settings_parser.py # Settings file parser
│ ├── loader.py # High-level data loader
│ └── utils.py # Utility functions
├── tests/ # Comprehensive test suite (97% coverage)
│ ├── conftest.py # Pytest fixtures
│ ├── test_init.py # Package initialization tests
│ ├── test_identification.py # ID parser tests
│ ├── test_edf_parser.py # EDF parser tests
│ ├── test_utils.py # Utility function tests
│ ├── test_parser_core.py # Core parser functionality
│ ├── test_integration.py # Integration tests
│ ├── test_mock_scenarios.py # Mock-based tests
│ ├── test_realistic_edf_data.py # Realistic EDF data tests
│ ├── test_signal_combinations.py # Signal combination tests
│ ├── test_bilevel_modes.py # BiLevel mode tests
│ ├── test_settings_alternative_signals.py # Alternative signal tests
│ └── test_optional_signals_errors.py # Error handling tests
├── setup.py # Setup configuration
├── pyproject.toml # Project metadata and build config
├── requirements.txt # Python dependencies
├── README.md # This file
├── INSTALL.md # Installation guide
├── USAGE.md # Detailed usage guide
├── DEVELOPMENT.md # Development guide
├── CONTRIBUTING.md # Contribution guidelines
├── TEST_SUITE.md # Test suite documentation
├── TESTING_COMPLETE.md # Test coverage report
└── LICENSE # MIT License
Utility Functions
The utils module provides helper functions for CPAP data analysis:
from cpap_py.utils import (
split_sessions_by_noon,
format_duration,
calculate_ahi,
therapy_mode_name,
downsample_signal,
calculate_percentile
)
# Split session timestamps by noon boundary
sessions = split_sessions_by_noon(timestamps)
# Format duration as HH:MM:SS
duration_str = format_duration(seconds)
# Calculate AHI from event counts and hours
ahi = calculate_ahi(apneas=10, hypopneas=5, hours=7.5)
# Get therapy mode name from mode code
mode_name = therapy_mode_name(mode_code) # "CPAP", "APAP", "BiLevel S", etc.
# Downsample signal data
downsampled = downsample_signal(data, factor=10)
# Calculate percentile (50th, 95th, etc.)
p95 = calculate_percentile(data, 95)
Testing
The library includes a comprehensive test suite with 97% code coverage and 188 automated tests.
Run Tests
# Run all tests with coverage
./run_tests.sh
# Run specific test file
pytest tests/test_identification.py -v
# Run with detailed coverage
pytest tests/ --cov=cpap_py --cov-report=term-missing
See TEST_SUITE.md for detailed testing documentation.
Documentation
- README.md - This file - Quick start and API reference
- INSTALL.md - Detailed installation instructions
- USAGE.md - Comprehensive usage guide with examples
- DEVELOPMENT.md - Development setup and workflows
- CONTRIBUTING.md - How to contribute to the project
- TEST_SUITE.md - Test suite documentation
- TESTING_COMPLETE.md - Test coverage report
Requirements
- Python: 3.9 or higher
- Dependencies: None (pure Python standard library implementation)
- Development Dependencies (optional):
- pytest >= 7.0.0
- pytest-cov >= 4.0.0
- pytest-mock >= 3.10.0
- coverage >= 7.0.0
- black >= 23.0.0 (code formatting)
- ruff >= 0.1.0 (linting)
Supported Devices
- ResMed AirSense 11 - Latest generation with JSON identification
- ResMed AirSense 10 - AutoSet, Elite, CPAP, For Her
- ResMed AirCurve 10 - S, ST, VAuto, ASV
- ResMed S9 - AutoSet, Elite, CPAP, VPAP series
- Other ResMed devices using EDF format
Supported Data Files
Identification Files
Identification.tgt- Text format (#KEY value)Identification.json- JSON format (AirSense 11)
Summary Files
STR.edf- Daily summary statistics (AHI, leak, pressure, etc.)
Session Data Files (DATALOG/)
BRP*.edf- Breathing waveforms (flow rate, tidal volume, minute ventilation, respiratory rate)PLD*.edf- Pressure and leak dataSAD*.edf- Summary/advanced dataEVE*.edf- Respiratory events (apneas, hypopneas, flow limitations)CSL*.edf- Clinical settings logAEV*.edf- Advanced events
Settings Files (SETTINGS/)
*.tgt- Settings files (CGL, UGL, etc.)
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Development Workflow
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for new functionality
- Run the test suite (
./run_tests.sh) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License - see LICENSE file for details.
Acknowledgments
This library is based on the file format specifications from OSCAR (Open Source CPAP Analysis Reporter), an excellent open-source CPAP data analysis application.
Special thanks to the OSCAR development team for their comprehensive documentation of ResMed file formats.
Support
- Issues: GitHub Issues
- Documentation: GitHub Repository
- Repository: https://github.com/dynacylabs/cpap-py
Related Projects
- OSCAR - Open Source CPAP Analysis Reporter (Qt/C++ desktop application)
- SleepHQ - Online CPAP data analysis platform
- pyedflib - Python library for reading/writing EDF files
Changelog
Version 0.1.0 (Current)
- Initial release
- Complete EDF/EDF+ parser (pure Python)
- Support for all ResMed device generations (S9, AirSense 10, AirSense 11)
- Parse identification files (.tgt, .json)
- Parse STR.edf summary data
- Parse DATALOG session data (BRP, PLD, SAD, EVE, CSL, AEV)
- Parse SETTINGS files
- High-level CPAPLoader interface
- Comprehensive test suite (97% coverage, 188 tests)
- Zero external dependencies
- Python 3.9+ support
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cpap_py-1.0.0.tar.gz.
File metadata
- Download URL: cpap_py-1.0.0.tar.gz
- Upload date:
- Size: 70.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a6837a073a1f0667f86ceee11a868d8a616ec71f79e3cb21bc39068005d6b6e
|
|
| MD5 |
899b8bc363e492ff6249089d8310ac66
|
|
| BLAKE2b-256 |
cf6a3e42ad4eb1812b4a569c9fbf74b316e0c49d032a64b518d2151bb1b46371
|
Provenance
The following attestation bundles were made for cpap_py-1.0.0.tar.gz:
Publisher:
publish-to-pypi.yml on dynacylabs/cpap-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cpap_py-1.0.0.tar.gz -
Subject digest:
0a6837a073a1f0667f86ceee11a868d8a616ec71f79e3cb21bc39068005d6b6e - Sigstore transparency entry: 771538637
- Sigstore integration time:
-
Permalink:
dynacylabs/cpap-py@73c7ae7a394f3c7d208558843f9e093f2a6104a2 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/dynacylabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@73c7ae7a394f3c7d208558843f9e093f2a6104a2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file cpap_py-1.0.0-py3-none-any.whl.
File metadata
- Download URL: cpap_py-1.0.0-py3-none-any.whl
- Upload date:
- Size: 26.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
425ea467f2db3d8a5ac53b013a7de236570bbd9302ad898670f8a02b54b3d844
|
|
| MD5 |
43bf149adc3e95a4c711fb2578480ff5
|
|
| BLAKE2b-256 |
fd0807837620cddba64a974399756636f8fc59533422bc487220b98f8cb5a108
|
Provenance
The following attestation bundles were made for cpap_py-1.0.0-py3-none-any.whl:
Publisher:
publish-to-pypi.yml on dynacylabs/cpap-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cpap_py-1.0.0-py3-none-any.whl -
Subject digest:
425ea467f2db3d8a5ac53b013a7de236570bbd9302ad898670f8a02b54b3d844 - Sigstore transparency entry: 771538645
- Sigstore integration time:
-
Permalink:
dynacylabs/cpap-py@73c7ae7a394f3c7d208558843f9e093f2a6104a2 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/dynacylabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@73c7ae7a394f3c7d208558843f9e093f2a6104a2 -
Trigger Event:
release
-
Statement type: