Skip to content

Import script for fitbit data dumps #1192

@tomjelen

Description

@tomjelen

Have to close down my Fitbit account due to the recent change of requiring Google accounts to log in.

So I exported my data and wanted to import it into OpenScale. I created the following script, inspired by one of the other scripts on the wiki. So I thought maybe this script could be added to the wiki.

This script parses the weight JSON files in your export folder and extracts time/fat/weight and turns it into a CSV file for OpenScale.

#!/usr/bin/python

"""
Fitbit to OpenScale Converter
Converts Fitbit weight export JSON files to OpenScale CSV format.

This script processes all weight-*.json files from a Fitbit data export
and converts them to a single CSV file compatible with OpenScale.

Usage:
    python fitbit-to-openscale.py <fitbit_export_folder> <output_csv_file>

Example:
    python fitbit-to-openscale.py "TomJelen/Personal & Account" openscale_weight_data.csv
"""

import argparse
import csv
import datetime
import json
import os
import glob
import sys
from pathlib import Path

# OpenScale CSV header - all supported fields
OPENSCALE_HEADER = '"biceps","bone","caliper1","caliper2","caliper3","calories","chest","comment","dateTime","fat","hip","lbm","muscle","neck","thigh","visceralFat","waist","water","weight"'

def find_weight_files(fitbit_folder):
    """
    Find all weight-*.json files in the Fitbit export folder.

    Args:
        fitbit_folder (str): Path to the Fitbit export folder

    Returns:
        list: List of paths to weight JSON files
    """
    if not os.path.exists(fitbit_folder):
        raise FileNotFoundError(f"Fitbit export folder not found: {fitbit_folder}")

    # Look for weight-*.json files
    pattern = os.path.join(fitbit_folder, "weight-*.json")
    weight_files = glob.glob(pattern)

    if not weight_files:
        raise FileNotFoundError(f"No weight-*.json files found in {fitbit_folder}")

    print(f"Found {len(weight_files)} weight files")
    return sorted(weight_files)

def parse_fitbit_json(file_path):
    """
    Parse a single Fitbit weight JSON file and extract measurements.

    Args:
        file_path (str): Path to the JSON file

    Returns:
        list: List of measurement dictionaries
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        if not isinstance(data, list):
            print(f"Warning: {file_path} does not contain a list, skipping")
            return []

        measurements = []
        for entry in data:
            if not isinstance(entry, dict):
                continue

            # Extract required fields
            log_id = entry.get('logId')
            weight = entry.get('weight')
            fat = entry.get('fat')

            # Skip entries without essential data
            if log_id is None or weight is None:
                continue

            measurements.append({
                'logId': log_id,
                'weight': weight,
                'fat': fat,
                'bmi': entry.get('bmi'),
                'source': entry.get('source', 'Unknown')
            })

        return measurements

    except json.JSONDecodeError as e:
        print(f"Error parsing JSON file {file_path}: {e}")
        return []
    except Exception as e:
        print(f"Error reading file {file_path}: {e}")
        return []

def convert_timestamp(log_id):
    """
    Convert Fitbit logId (milliseconds since epoch) to OpenScale datetime format.

    Args:
        log_id (int): Fitbit logId timestamp in milliseconds

    Returns:
        str: Formatted datetime string (YYYY-MM-DD HH:MM)
    """
    try:
        # Convert milliseconds to seconds
        timestamp_seconds = log_id / 1000.0
        dt = datetime.datetime.fromtimestamp(timestamp_seconds)
        return dt.strftime('%Y-%m-%d %H:%M')
    except (ValueError, OSError) as e:
        print(f"Error converting timestamp {log_id}: {e}")
        return None

def process_all_weight_data(fitbit_folder):
    """
    Process all weight JSON files and return sorted measurements.

    Args:
        fitbit_folder (str): Path to the Fitbit export folder

    Returns:
        list: List of all measurements sorted by datetime
    """
    weight_files = find_weight_files(fitbit_folder)
    all_measurements = []

    for file_path in weight_files:
        print(f"Processing {os.path.basename(file_path)}...")
        measurements = parse_fitbit_json(file_path)
        all_measurements.extend(measurements)

    print(f"Total measurements found: {len(all_measurements)}")

    # Convert timestamps and filter out invalid entries
    valid_measurements = []
    for measurement in all_measurements:
        datetime_str = convert_timestamp(measurement['logId'])
        if datetime_str:
            measurement['dateTime'] = datetime_str
            valid_measurements.append(measurement)

    print(f"Valid measurements after timestamp conversion: {len(valid_measurements)}")

    # Sort by datetime
    valid_measurements.sort(key=lambda x: x['dateTime'])

    return valid_measurements

def write_openscale_csv(measurements, output_path):
    """
    Write measurements to OpenScale CSV format.

    Note: Fitbit exports weight data in pounds regardless of user profile settings.
    This function converts pounds to kilograms for OpenScale compatibility.

    Args:
        measurements (list): List of measurement dictionaries
        output_path (str): Path to output CSV file
    """
    # Define the field names in the order they appear in the header
    fieldnames = ['biceps', 'bone', 'caliper1', 'caliper2', 'caliper3', 'calories',
                  'chest', 'comment', 'dateTime', 'fat', 'hip', 'lbm', 'muscle',
                  'neck', 'thigh', 'visceralFat', 'waist', 'water', 'weight']

    try:
        with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
            # Write the header exactly as OpenScale expects it
            csvfile.write(f'{OPENSCALE_HEADER}\n')

            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

            for measurement in measurements:
                # Create a row with only the fields we have data for
                row = {field: '' for field in fieldnames}  # Initialize all fields as empty

                # Fill in the fields we have data for
                row['dateTime'] = measurement['dateTime']

                # Convert weight from pounds to kilograms
                # Fitbit exports weight in pounds regardless of user profile settings
                weight_lbs = float(measurement['weight'])
                weight_kg = weight_lbs / 2.20462  # Convert pounds to kilograms
                row['weight'] = f'{weight_kg:.2f}'  # Round to 2 decimal places

                # Add fat percentage if available
                if measurement.get('fat') is not None:
                    row['fat'] = measurement['fat']

                # Add a comment with source information
                source = measurement.get('source', 'Fitbit')
                row['comment'] = f'Imported from {source} (converted from lbs)'

                writer.writerow(row)

        print(f"Successfully wrote {len(measurements)} measurements to {output_path}")

    except Exception as e:
        print(f"Error writing CSV file: {e}")
        raise

def main():
    """Main function with argument parsing."""
    parser = argparse.ArgumentParser(
        description='Convert Fitbit weight export JSON files to OpenScale CSV format',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python fitbit-to-openscale.py "TomJelen/Personal & Account" openscale_weight_data.csv
  python fitbit-to-openscale.py /path/to/fitbit/export output.csv
        """
    )

    parser.add_argument('fitbit_folder',
                       help='Path to Fitbit export folder containing weight-*.json files')
    parser.add_argument('output_csv',
                       help='Path to output CSV file for OpenScale')

    args = parser.parse_args()

    try:
        print("Fitbit to OpenScale Converter")
        print("=" * 40)
        print(f"Input folder: {args.fitbit_folder}")
        print(f"Output file: {args.output_csv}")
        print()

        # Process all weight data
        measurements = process_all_weight_data(args.fitbit_folder)

        if not measurements:
            print("No valid measurements found. Exiting.")
            sys.exit(1)

        # Write to OpenScale CSV format
        write_openscale_csv(measurements, args.output_csv)

        print()
        print("Conversion completed successfully!")
        print(f"You can now import {args.output_csv} into OpenScale.")

    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == '__main__':
    main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementIndicates new feature requests

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions