diff --git a/scripts/artifacts/fitbit.py b/scripts/artifacts/fitbit.py index fd661dd7..685b9576 100644 --- a/scripts/artifacts/fitbit.py +++ b/scripts/artifacts/fitbit.py @@ -1,28 +1,49 @@ +# Module Description: Parses Fitbit data from Android (Phone) and Wear OS (Watch) + +import json +import folium +import os + +from scripts.artifact_report import ArtifactHtmlReport +from scripts.ilapfuncs import logfunc, tsv, kmlgen, timeline, open_sqlite_db_readonly, convert_ts_human_to_utc, convert_utc_human_to_timezone + __artifacts_v2__ = { "Fitbit": { - "name": "Fitbit", - "description": "Parses Fitbit activities", + "name": "Fitbit Smartphone Data", + "description": "Parses Fitbit activities from Android Smartphone app", "author": "@AlexisBrignoni", "version": "0.0.4", "date": "2021-04-23", "requirements": "none", "category": "Fitbit", - "notes": "Updated 2023-12-12 by @segumarc, wrong file_found was wrote in the 'located at' field in the html report", + "notes": "Updated 2023-12-12 by @segumarc", "paths": ('*/com.fitbit.FitbitMobile/databases/activity_db*','*/com.fitbit.FitbitMobile/databases/device_database*','*/com.fitbit.FitbitMobile/databases/exercise_db*','*/com.fitbit.FitbitMobile/databases/heart_rate_db*','*/com.fitbit.FitbitMobile/databases/sleep*','*/com.fitbit.FitbitMobile/databases/social_db*','*/com.fitbit.FitbitMobile/databases/mobile_track_db*'), "function": "get_fitbit" + }, + "FitbitWearOS": { + "name": "Fitbit Wear OS Data", + "description": "Parses User DB and Passive Stats DB from Pixel Watch/Wear OS", + "author": "Ganeshbs17", + "version": "0.0.1", + "date": "2026-01-12", + "requirements": "none", + "category": "Fitbit", + "notes": "Specific to Pixel Watch/Wear OS", + "paths": ('*/com.fitbit.FitbitMobile/databases/user.db*', '*/com.fitbit.FitbitMobile/databases/passive_stats.db*'), + "function": "get_fitbit_wearos" } } -import os -import sqlite3 -import textwrap - -from datetime import datetime, timezone -from scripts.artifact_report import ArtifactHtmlReport -from scripts.ilapfuncs import logfunc, tsv, kmlgen, timeline, is_platform_windows, open_sqlite_db_readonly, convert_ts_human_to_utc, convert_utc_human_to_timezone - -def get_fitbit(files_found, report_folder, seeker, wrap_text): +def get_fitbit(files_found, report_folder, _seeker, _wrap_text): + file_found_activity = '' + file_found_device = '' + file_found_exercise = '' + file_found_heart = '' + file_found_sleep = '' + file_found_social = '' + file_found_mobile = '' + data_list_activity = [] data_list_devices = [] data_list_exercises = [] @@ -211,7 +232,7 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): start_time = convert_utc_human_to_timezone(convert_ts_human_to_utc(row[1]),'UTC') data_list_sleep_summary.append((date_of_sleep,start_time,row[2],row[3],row[4],row[5],row[6],row[7],row[8],row[9],file_found)) db.close() - + if file_found.endswith('social_db'): file_found_social = file_found db = open_sqlite_db_readonly(file_found) @@ -299,10 +320,10 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_activity, file_found_activity) report.end_artifact_report() - tsvname = f'Fitbit Activity' + tsvname = 'Fitbit Activity' tsv(report_folder, data_headers, data_list_activity, tsvname) - tlactivity = f'Fitbit Activity' + tlactivity = 'Fitbit Activity' timeline(report_folder, tlactivity, data_list_activity, data_headers) else: logfunc('No Fitbit Activity data available') @@ -316,10 +337,10 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_devices, file_found_device) report.end_artifact_report() - tsvname = f'Fitbit Device Info' + tsvname = 'Fitbit Device Info' tsv(report_folder, data_headers, data_list_devices, tsvname) - tlactivity = f'Fitbit Device Info' + tlactivity = 'Fitbit Device Info' timeline(report_folder, tlactivity, data_list_devices, data_headers) else: logfunc('No Fitbit Device Info data available') @@ -333,13 +354,13 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_exercises, file_found_exercise) report.end_artifact_report() - tsvname = f'Fitbit Exercise' + tsvname = 'Fitbit Exercise' tsv(report_folder, data_headers, data_list_exercises, tsvname) - tlactivity = f'Fitbit Exercise' + tlactivity = 'Fitbit Exercise' timeline(report_folder, tlactivity, data_list_exercises, data_headers) else: - logfunc(f'No Fitbit - Exercise data available') + logfunc('No Fitbit - Exercise data available') if data_list_heart: report = ArtifactHtmlReport('Fitbit Heart Rate Summary') @@ -350,10 +371,10 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_heart, file_found_heart) report.end_artifact_report() - tsvname = f'Fitbit Heart Rate Summary' + tsvname = 'Fitbit Heart Rate Summary' tsv(report_folder, data_headers, data_list_heart, tsvname) - tlactivity = f'Fitbit Heart Rate Summary' + tlactivity = 'Fitbit Heart Rate Summary' timeline(report_folder, tlactivity, data_list_heart, data_headers) else: logfunc('No Fitbit Heart Rate Summary data available') @@ -367,10 +388,10 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_sleep_detail, file_found_sleep) report.end_artifact_report() - tsvname = f'Fitbit Sleep Detail' + tsvname = 'Fitbit Sleep Detail' tsv(report_folder, data_headers, data_list_sleep_detail, tsvname) - tlactivity = f'Fitbit Sleep Detail' + tlactivity = 'Fitbit Sleep Detail' timeline(report_folder, tlactivity, data_list_sleep_detail, data_headers) else: logfunc('No Fitbit Sleep Detail data available') @@ -384,7 +405,7 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_sleep_summary, file_found_sleep) report.end_artifact_report() - tsvname = f'Fitbit Sleep Summary' + tsvname = 'Fitbit Sleep Summary' tsv(report_folder, data_headers, data_list_sleep_summary, tsvname) else: logfunc('No Fitbit Sleep Summary data available') @@ -398,7 +419,7 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_friends, file_found_social) report.end_artifact_report() - tsvname = f'Fitbit Friends' + tsvname = 'Fitbit Friends' tsv(report_folder, data_headers, data_list_friends, tsvname) else: @@ -413,10 +434,10 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_user, file_found_social) report.end_artifact_report() - tsvname = f'Fitbit User Profile' + tsvname = 'Fitbit User Profile' tsv(report_folder, data_headers, data_list_user, tsvname) - tlactivity = f'Fitbit User Profile' + tlactivity = 'Fitbit User Profile' timeline(report_folder, tlactivity, data_list_user, data_headers) else: @@ -431,12 +452,459 @@ def get_fitbit(files_found, report_folder, seeker, wrap_text): report.write_artifact_data_table(data_headers, data_list_steps, file_found_mobile) report.end_artifact_report() - tsvname = f'Fitbit Steps' + tsvname = 'Fitbit Steps' tsv(report_folder, data_headers, data_list_steps, tsvname) - tlactivity = f'Fitbit Steps' + tlactivity = 'Fitbit Steps' timeline(report_folder, tlactivity, data_list_steps, data_headers) else: logfunc('No Fitbit Steps data available') - \ No newline at end of file + +# pylint: disable=broad-exception-caught +def get_fitbit_wearos(files_found, report_folder, _seeker, _wrap_text): + + for file_found in files_found: + file_found = str(file_found) + + # --- PROCESS USER.DB --- + if file_found.endswith('user.db'): + db = open_sqlite_db_readonly(file_found) + cursor = db.cursor() + + # 1. User Profile + try: + cursor.execute(''' + SELECT + fullName, + displayName, + email, + gender, + dateOfBirth, + height, + weight, + memberSince, + userId + FROM FitbitProfileEntity + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - User Profile (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - User Profile (Wear OS)','User account details including Display Name, DOB, Gender, and Join Date. Parsed from FitbitProfileEntity table.') + report.add_script() + data_headers = ('Full Name', 'Display Name', 'Email', 'Gender', 'DOB', 'Height', 'Weight', 'Member Since', 'User ID') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - User Profile (Wear OS)') + except Exception as e: + logfunc(f'Error parsing Fitbit Profile: {e}') + + # 2. Activity / Workout History + try: + cursor.execute(''' + SELECT + datetime(startTime/1000, 'unixepoch') as "Start Time", + name as "Activity Name", + duration/1000/60 as "Duration (Mins)", + distance, + distanceUnit, + steps, + calories, + averageHeartRate, + elevationGain, + activeZoneMinutes, + logId + FROM ActivityExerciseEntity + ORDER BY startTime DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Activity History (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Activity History (Wear OS)', 'Log of exercises and activities including duration, distance, and calories. Parsed from ActivityExerciseEntity table.') + report.add_script() + data_headers = ('Start Time', 'Activity Name', 'Duration (Mins)', 'Distance', 'Unit', 'Steps', 'Calories', 'Avg HR', 'Elevation', 'AZM', 'Log ID') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9], row[10])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Activity History (Wear OS)') + timeline(report_folder, 'Fitbit - Activity History (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Activity History: {e}') + + # 3. DAILY Summaries + try: + cursor.execute(''' + SELECT + date, + totalMinutesMoving, + totalMinutesSedentary, + longestDuration as "Longest Sedentary Duration", + longestStart as "Longest Sedentary Start Time" + FROM SedentaryDataEntity + ORDER BY date DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Daily Activity (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Daily Activity (Wear OS)','Daily summary of total minutes moved vs. sedentary minutes. Parsed from SedentaryDataEntity table.') + report.add_script() + data_headers = ('Date', 'Total Moving Mins', 'Total Sedentary Mins', 'Longest Sedentary Duration (Mins)', 'Longest Sedentary Start Time') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3], row[4])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Daily Activity (Wear OS)') + timeline(report_folder, 'Fitbit - Daily Activity (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Daily Summaries: {e}') + + # 4. HOURLY Steps (Flattened JSON) + try: + cursor.execute(''' + SELECT + date, + hourlyData + FROM SedentaryDataEntity + ORDER BY date DESC + ''') + all_rows = cursor.fetchall() + hourly_data_list = [] + for row in all_rows: + date_str = row[0] + json_data = row[1] + if json_data: + try: + parsed_json = json.loads(json_data) + hourly_list = parsed_json.get('hourlyData', []) + for hour_entry in hourly_list: + time_str = hour_entry.get('dateTime', '') + steps_str = hour_entry.get('steps', '0') + full_timestamp = f"{date_str} {time_str}" + hourly_data_list.append((full_timestamp, date_str, time_str, steps_str)) + except ValueError: + pass + if len(hourly_data_list) > 0: + report = ArtifactHtmlReport('Fitbit - Hourly Steps (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Hourly Steps (Wear OS)','Granular hourly step counts parsed from JSON blobs stored in the SedentaryDataEntity table.') + report.add_script() + data_headers = ('Full Timestamp', 'Date', 'Time', 'Steps') + report.write_artifact_data_table(data_headers, hourly_data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, hourly_data_list, 'Fitbit - Hourly Steps (Wear OS)') + timeline(report_folder, 'Fitbit - Hourly Steps (Wear OS)', hourly_data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Hourly Steps: {e}') + + # 5. Sleep Logs + try: + cursor.execute(''' + SELECT + datetime(startTime/1000, 'unixepoch') as "Sleep Start", + datetime(endTime/1000, 'unixepoch') as "Sleep End", + dateOfSleep, + minutesAsleep, + minutesAwake, + minutesToFallAsleep, + minutesAfterWakeup, + type as "Sleep Type", + isMainSleep + FROM FitbitSleepDateEntity + ORDER BY startTime DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Sleep Logs (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Sleep Logs (Wear OS)','Main sleep session logs including start/end times and sleep stages. Parsed from FitbitSleepDateEntity table.') + report.add_script() + data_headers = ('Sleep Start', 'Sleep End', 'Date of Sleep', 'Mins Asleep', 'Mins Awake', 'Time to Fall Asleep', 'Time After Wakeup', 'Type', 'Is Main Sleep') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Sleep Logs (Wear OS)') + timeline(report_folder, 'Fitbit - Sleep Logs (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Sleep Logs: {e}') + + db.close() + + # --- PROCESS PASSIVE_STATS.DB --- + elif file_found.endswith('passive_stats.db'): + db = open_sqlite_db_readonly(file_found) + cursor = db.cursor() + + # 1. Exercise Summaries + try: + cursor.execute(''' + SELECT + datetime(time/1000, 'unixepoch') as "Start Time", + sessionId, + exerciseTypeId as "Activity Type ID", + totalDistanceMm / 1000000.0 as "Distance (KM)", + steps, + caloriesBurned, + avgHeartRate, + elevationGainFt + FROM ExerciseSummaryEntity + ORDER BY time DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Workouts (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Workouts (Wear OS)','Workout summaries including steps, calories etc. Parsed from ExerciseSummaryEntity table.') + report.add_script() + data_headers = ('Start Time', 'Session ID', 'Activity Type ID', 'Distance (KM)', 'Steps', 'Calories', 'Avg HR', 'Elevation (ft)') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Workouts (Wear OS)') + timeline(report_folder, 'Fitbit - Workouts (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Workouts: {e}') + + # 2. Exercise GPS + try: + cursor.execute(''' + SELECT + datetime(time/1000, 'unixepoch') as "Timestamp", + latitude, + longitude, + altitude, + speed, + bearing, + estimatedPositionError + FROM ExerciseGpsEntity + ORDER BY time ASC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + # 1. Generate the standard text report first + report = ArtifactHtmlReport('Fitbit - GPS Trackpoints (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - GPS Trackpoints (Wear OS)', 'GPS Coordinates. click here to open in new tab') + report.add_script() + + data_headers = ('Timestamp', 'Latitude', 'Longitude', 'Altitude', 'Speed', 'Est. Error') + data_list = [] + points = [] + + for row in all_rows: + # Add to text report + data_list.append((row[0], row[1], row[2], row[3], row[4], row[6])) + + # Add to Map Points (Filter out valid 0.0 or nulls if needed) + if row[1] and row[2]: + points.append((row[1], row[2])) + + # --------------------------------------------------------- + # MAP GENERATION (Folium) + # --------------------------------------------------------- + if len(points) > 0: + try: + # Center map on the first point + m = folium.Map(location=points[0], zoom_start=13, tiles='OpenStreetMap') + + # Add the route line + folium.PolyLine(points, color="red", weight=2.5, opacity=1).add_to(m) + + # Add Start/End markers + folium.Marker(points[0], popup='Start', icon=folium.Icon(color='green', icon='play')).add_to(m) + folium.Marker(points[-1], popup='End', icon=folium.Icon(color='red', icon='stop')).add_to(m) + + # Save HTML map to the report folder + map_filename = 'Fitbit_GPS_Map.html' + map_path = os.path.join(report_folder, map_filename) + m.save(map_path) + + logfunc(f'Map generated: {map_path}') + except Exception as e: + logfunc(f'Error generating map: {str(e)}') + # --------------------------------------------------------- + + report.write_artifact_data_table(data_headers, data_list, file_found) + + # --- START: INJECT IFRAME AT BOTTOM --- + if len(points) > 0: + report.add_section_heading('Interactive Map Preview') + report.add_map('') + # --- END: INJECT IFRAME AT BOTTOM --- + + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - GPS Trackpoints (Wear OS)') + timeline(report_folder, 'Fitbit - GPS Trackpoints (Wear OS)', data_list, data_headers) + kmlgen(report_folder, 'Fitbit_GPS_WearOS', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit GPS: {e}') + + # 3. Heart Rate Stats + try: + cursor.execute(''' + SELECT + datetime(startTime/1000, 'unixepoch') as "Start Time", + datetime(endTime/1000, 'unixepoch') as "End Time", + value as "BPM", + accuracy + FROM HeartRateStatEntity + ORDER BY startTime DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Heart Rate Stats (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Heart Rate Stats (Wear OS)','Heart rate statistics (BPM). Parsed from HeartRateStatEntity table.') + report.add_script() + data_headers = ('Start Time', 'End Time', 'BPM', 'Accuracy') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Heart Rate Stats (Wear OS)') + timeline(report_folder, 'Fitbit - Heart Rate Stats (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit HR Stats: {e}') + + # 4. Live Pace + try: + cursor.execute(''' + SELECT + datetime(timeSeconds/1000, 'unixepoch') as "Timestamp", + sessionId, + statType, + value + FROM LivePaceEntity + ORDER BY timeSeconds DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Live Pace (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Live Pace (Wear OS)','Live pace statistics during workouts. Parsed from LivePaceEntity table.') + report.add_script() + data_headers = ('Timestamp', 'Session ID', 'Stat Type', 'Value') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Live Pace (Wear OS)') + timeline(report_folder, 'Fitbit - Live Pace (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Live Pace: {e}') + + # 5. Sleep Periods + try: + cursor.execute(''' + SELECT + datetime(sleepStartTime/1000, 'unixepoch') as "Sleep Start", + datetime(sleepEndTime/1000, 'unixepoch') as "Sleep End", + (sleepEndTime - sleepStartTime)/1000/60 as "Duration (Mins)" + FROM LocalSleepPeriodsEntity + ORDER BY sleepStartTime DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Sleep (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Sleep (Wear OS)', 'Raw sleep periods detected by device. Parsed from LocalSleepPeriodsEntity table.') + report.add_script() + data_headers = ('Sleep Start', 'Sleep End', 'Duration (Mins)') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Sleep (Wear OS)') + timeline(report_folder, 'Fitbit - Sleep (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Sleep: {e}') + + # 6. Active Zone Minutes + try: + cursor.execute(''' + SELECT + datetime(startTime/1000, 'unixepoch') as "Start Time", + datetime(endTime/1000, 'unixepoch') as "End Time", + activeZone, + value as "Points", + lastBpm + FROM PassiveAzmEntity + ORDER BY startTime DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Active Zones (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Active Zones (Wear OS)','Minutes spent in elevated heart rate zones (Fat Burn = 1x, Cardio/Peak = 2x). Indicates physical exertion intensity. Parsed from PassiveAzmEntity table.') + report.add_script() + data_headers = ('Start Time', 'End Time', 'Zone ID', 'Points', 'Last BPM') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3], row[4])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Active Zones (Wear OS)') + timeline(report_folder, 'Fitbit - Active Zones (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit AZM: {e}') + + # 7. Exercise Splits + try: + cursor.execute(''' + SELECT + datetime(time/1000, 'unixepoch') as "Split Time", + sessionId, + avgPaceMilliSecPerKm / 1000 / 60.0 as "Avg Pace (Min/Km)", + avgHeartRate, + steps, + caloriesBurned + FROM ExerciseSplitAnnotationEntity + ORDER BY time ASC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Workout Splits (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Workout Splits (Wear OS)','Performance metrics (Pace, HR, Steps). Parsed from ExerciseSplitAnnotationEntity table.') + report.add_script() + data_headers = ('Split Time', 'Session ID', 'Avg Pace (Min/Km)', 'Avg HR', 'Steps', 'Calories') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2], row[3], row[4], row[5])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Workout Splits (Wear OS)') + timeline(report_folder, 'Fitbit - Workout Splits (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Splits: {e}') + + # 8. Opaque HR + try: + cursor.execute(''' + SELECT + datetime(timestamp/1000, 'unixepoch') as "Timestamp", + baseHeartRate as "Base HR", + confidence as "Confidence (0-3)" + FROM OpaqueHeartRateEntity + ORDER BY timestamp DESC + ''') + all_rows = cursor.fetchall() + if len(all_rows) > 0: + report = ArtifactHtmlReport('Fitbit - Opaque HR (Wear OS)') + report.start_artifact_report(report_folder, 'Fitbit - Opaque HR (Wear OS)','Raw heart rate sensor readings. Parsed from OpaqueHeartRateEntity table.') + report.add_script() + data_headers = ('Timestamp', 'Base HR', 'Confidence') + data_list = [] + for row in all_rows: + data_list.append((row[0], row[1], row[2])) + report.write_artifact_data_table(data_headers, data_list, file_found) + report.end_artifact_report() + tsv(report_folder, data_headers, data_list, 'Fitbit - Opaque HR (Wear OS)') + timeline(report_folder, 'Fitbit - Opaque HR (Wear OS)', data_list, data_headers) + except Exception as e: + logfunc(f'Error parsing Fitbit Opaque HR: {e}') + + db.close() \ No newline at end of file