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()
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.