Skip to content

Instantly share code, notes, and snippets.

@0x9900
Last active April 20, 2024 15:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 0x9900/0e0994d4b8e6adafc0ae73dd7eb79dd4 to your computer and use it in GitHub Desktop.
Save 0x9900/0e0994d4b8e6adafc0ae73dd7eb79dd4 to your computer and use it in GitHub Desktop.
iHealth heart rate monitor
#!/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