Last active
April 20, 2024 15:45
-
-
Save 0x9900/0e0994d4b8e6adafc0ae73dd7eb79dd4 to your computer and use it in GitHub Desktop.
iHealth heart rate monitor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# | |
# BSD 3-Clause License | |
# | |
# Copyright (c) 2023-2024 Fred W6BSD | |
# All rights reserved. | |
# | |
__doc__ = """ | |
Read the CSV file from the iHealth heart rate monitor and generate a graph. | |
https://ihealthlabs.com/products/ihealth-track-connected-blood-pressure-monitor | |
""" | |
import argparse | |
import csv | |
import os | |
import warnings | |
from collections import defaultdict | |
from datetime import datetime, timedelta | |
import matplotlib.dates as mdates | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from matplotlib import ticker | |
DPI = 300 | |
TREND_DAYS = 7 | |
DARKBLUE = "#1d1330" | |
LIGHTBLUE = "#e1f7fa" | |
GRAY = "#4b4b4b" | |
LIGHTGRAY = "#ababab" | |
RC_PARAMS = { | |
"axes.edgecolor": GRAY, | |
"axes.facecolor": DARKBLUE, | |
"axes.facecolor": DARKBLUE, | |
"axes.labelcolor": LIGHTGRAY, | |
"figure.edgecolor": DARKBLUE, | |
"figure.facecolor": DARKBLUE, | |
"font.size": 10, | |
"lines.linewidth": 1, | |
"lines.markersize": 5, | |
"text.color": "white", | |
"xtick.color": LIGHTGRAY, | |
"xtick.color": LIGHTGRAY, | |
"xtick.labelcolor": LIGHTGRAY, | |
"ytick.color": LIGHTGRAY, | |
"ytick.color": LIGHTGRAY, | |
"ytick.labelcolor": LIGHTGRAY, | |
} | |
def read_ihealth(filename: str) -> set: | |
data = defaultdict(lambda: list([[], [], []])) | |
with open(filename, 'r', encoding='utf-8') as fdi: | |
reader = csv.reader(fdi) | |
next(reader) | |
for line in reader: | |
sdate = line[0] + ',' + line[1] | |
date = datetime.strptime(sdate, '%Y-%m-%d,%H:%M') | |
date = date.replace(minute=0) | |
data[date][0].append(float(line[2])) | |
data[date][1].append(float(line[3])) | |
data[date][2].append(float(line[4])) | |
return sorted(list(data.items())) | |
def interval(data, gnr=24): | |
delta = mdates.num2date(np.max(data)) - mdates.num2date(np.min(data)) | |
return int(int(delta.days < gnr) + delta.days / gnr) | |
def graph(data, image='/tmp/ihealth.jpg', trend_days=TREND_DAYS): | |
dates = np.array([np.datetime64(d[0]) for d in data]) | |
dstart = dates.max() - np.timedelta64(trend_days, 'D') | |
systolic = np.array([np.mean(d[1][0]) for d in data], dtype=np.int32) | |
diastolic = np.array([np.mean(d[1][1]) for d in data], dtype=np.int32) | |
hrate = np.array([np.mean(d[1][2]) for d in data], dtype=np.int32) | |
idx = dates[:] > dstart | |
dates = mdates.date2num(dates) | |
with warnings.catch_warnings(action="ignore"): | |
systolic_t = np.poly1d(np.polyfit(dates[idx], systolic[idx], 1)) | |
diastolic_t = np.poly1d(np.polyfit(dates[idx], diastolic[idx], 1)) | |
plt.rcParams.update(RC_PARAMS) | |
plt.title("Fred's BP") | |
plt.subplots_adjust(hspace=0.3) | |
fig, (axp, axr) = plt.subplots(2, 1, figsize=(12, 9), | |
gridspec_kw={'height_ratios': [2, 1]}) | |
axp.set_title('Blood Pressure', fontstyle='italic') | |
axp.plot(dates, systolic, marker='o', label='SYS', color='tab:blue') | |
axp.plot(dates, diastolic, marker='o', label='DIA', color='tab:orange') | |
axp.plot(dates[idx], systolic_t(dates[idx]), color='tab:cyan', | |
label=f'Trend ({trend_days} Days)') | |
axp.plot(dates[idx], diastolic_t(dates[idx]), color='tab:cyan') | |
axp.set_ylabel('mmHg') | |
axp.set_ylim([diastolic.min() * .70, systolic.max() * 1.1]) | |
axp.yaxis.set_minor_locator(ticker.AutoMinorLocator(2)) | |
axp.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) | |
axp.xaxis.set_major_locator(mdates.DayLocator(interval=interval(dates))) | |
axp.set_xticklabels([]) | |
axp.margins(0.025) | |
axp.legend(loc="upper left", fontsize="10") | |
axp.grid(linestyle="solid", linewidth=.5, alpha=.25) | |
axp.grid(which='minor', linestyle="dotted", linewidth=.25) | |
axp.tick_params(axis='x', labelrotation=45, labelsize=8) | |
axr.margins(4, 4) | |
axr.set_title('Heart Rate', fontstyle='italic') | |
axr.plot(dates, hrate, marker='o', label='Heart Rate', color="tab:cyan") | |
axr.set_ylabel('bpm') | |
axr.set_ylim([hrate.min() / 3, hrate.max() * 1.3]) | |
axr.yaxis.set_minor_locator(ticker.AutoMinorLocator(2)) | |
axr.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) | |
axr.xaxis.set_major_locator(mdates.DayLocator(interval=interval(dates))) | |
axr.margins(0.025) | |
axr.legend(loc="upper left", fontsize="10") | |
axr.grid(linestyle="solid", linewidth=.5, alpha=.25) | |
axr.grid(which='minor', linestyle="dotted", linewidth=.25) | |
axr.tick_params(axis='x', labelrotation=45, labelsize=8) | |
fig.savefig(image, transparent=False, dpi=DPI) | |
print(f'{image} saved') | |
def main(): | |
parser = argparse.ArgumentParser(description="iHealth heart rate plotter") | |
parser.add_argument('-f', '--file', required=True) | |
parser.add_argument('-o', '--output', required=True) | |
parser.add_argument('-t', '--trend-days', type=int, default=TREND_DAYS) | |
opts = parser.parse_args() | |
if not os.path.exists(opts.file): | |
print(f'File: {opts.file} Not found') | |
return os.EX_IOERR | |
data = read_ihealth(opts.file) | |
graph(data, opts.output, opts.trend_days) | |
return os.EX_OK | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment