diff --git a/emfit/__init__.py b/emfit/__init__.py index e69de29..a7e6077 100644 --- a/emfit/__init__.py +++ b/emfit/__init__.py @@ -0,0 +1,138 @@ +from kython import json_load +from datetime import datetime +from os.path import join +from functools import lru_cache + +from datetime import timedelta, datetime +from typing import List, Dict, Iterator + +fromts = datetime.fromtimestamp + +def hhmm(minutes): + return '{:02d}:{:02d}'.format(*divmod(minutes, 60)) + +PATH = "/L/backups/emfit/" + +AWAKE = 4 + +class Emfit: + def __init__(self, jj): + self.jj = jj + + @property + def hrv_morning(self): + return self.jj['hrv_rmssd_morning'] + + @property + def hrv_evening(self): + return self.jj['hrv_rmssd_evening'] + + @property + def date(self): + return self.end.date() + + @property + def start(self): + return fromts(self.jj['time_start']) + + @property + def end(self): + return fromts(self.jj['time_end']) + + @property + def epochs(self): + return self.jj['sleep_epoch_datapoints'] + + @property + @lru_cache() + def sleep_start(self) -> datetime: + for [ts, e] in self.epochs: + if e == AWAKE: + continue + return fromts(ts) + raise RuntimeError + + @property + @lru_cache() + def sleep_end(self) -> datetime: + for [ts, e] in reversed(self.epochs): + if e == AWAKE: + continue + return fromts(ts) + raise RuntimeError +# 'sleep_epoch_datapoints' +# [[timestamp, number]] + + # so it's actual sleep, without awake + # ok, so I need time_asleep + @property + def sleep_minutes(self): + return self.jj['sleep_duration'] // 60 + + @property + def hrv_lf(self): + return self.jj['hrv_lf'] + + @property + def hrv_hf(self): + return self.jj['hrv_hf'] + + @property + def summary(self): + return f"for {hhmm(self.sleep_minutes)} hrv: [{self.hrv_morning:.0f} {self.hrv_evening:.0f} {self.hrv_morning - self.hrv_evening:3.0f} {self.hrv_lf}/{self.hrv_hf}]" + + +# measured_datapoints +# [[timestamp, pulse, breath?, ??? hrv?]] # every 4 seconds? + @property + def sleep_hr(self): + tss = [] + res = [] + for ll in self.jj['measured_datapoints']: + [ts, pulse, br, activity] = ll + # TODO what the fuck is whaat?? It can't be HRV, it's about 500 ms on average + # act in csv.. so it must be activity? wonder how is it measured. + # but I guess makes sense. yeah, "measured_activity_avg": 595, about that + # makes even more sense given tossturn datapoints only have timestamp + if self.sleep_start < fromts(ts) < self.sleep_end: + tss.append(ts) + res.append(pulse) + return tss, res + + @property + def hrv(self): + tss = [] + res = [] + for ll in self.jj['hrv_rmssd_datapoints']: + [ts, rmssd, _, _, almost_always_zero, _] = ll + # timestamp,rmssd,tp,lfn,hfn,r_hrv + # TP is total_power?? + # erm. looks like there is a discrepancy between csv and json data. + # right, so web is using api v 1. what if i use v1?? + # definitely a discrepancy between v1 and v4. have no idea how to resolve it :( + # also if one of them is indeed tp value, it must have been rounded. + # TODO what is the meaning of the rest??? + # they don't look like HR data. + tss.append(ts) + res.append(rmssd) + return tss, res + + @property + def sleep_hr_coverage(self): + tss, hrs = self.sleep_hr + covered_sec = len([h for h in hrs if h is not None]) + expected_sec = self.sleep_minutes * 60 / 4 + return covered_sec / expected_sec * 100 + +def iter_datas() -> Iterator[Emfit]: + import os + for f in sorted(os.listdir(PATH)): + if not f.endswith('.json'): + continue + + with open(join(PATH, f), 'r') as fo: + ef = Emfit(json_load(fo)) + yield ef + +def get_datas() -> List[Emfit]: + return list(sorted(list(iter_datas()), key=lambda e: e.start)) diff --git a/plot.py b/plot.py new file mode 100644 index 0000000..f6e3139 --- /dev/null +++ b/plot.py @@ -0,0 +1,128 @@ +import matplotlib.dates as md # type: ignore +import numpy as np # type: ignore +import seaborn as sns # type: ignore +import matplotlib.pyplot as plt + + +def plot_file(jj: str): + pts = jj['sleep_epoch_datapoints'] + + + tss = [datetime.fromtimestamp(p[0]) for p in pts] + vals = [p[1] for p in pts] + + plt.figure(figsize=(20,10)) + plt.plot(tss, vals) + + + xformatter = md.DateFormatter('%H:%M') + xlocator = md.MinuteLocator(interval = 15) + + ## Set xtick labels to appear every 15 minutes + plt.gcf().axes[0].xaxis.set_major_locator(xlocator) + + ## Format xtick labels as HH:MM + plt.gcf().axes[0].xaxis.set_major_formatter(xformatter) + + + + plt.xlabel('time') + plt.ylabel('phase') + plt.title('Sleep phases') + plt.grid(True) + plt.savefig(f"{f}.png") + plt.close() # TODO + # plt.show() + + pass + + +def plot_all(): + for jj in iter_datas(): + plot_file(jj) + + +# def stats(): +# for jj in iter_datas(): +# # TODO fimezone?? +# # TODOgetinterval on 16 aug -- err it's pretty stupid. I shouldn't count bed exit interval... +# st = fromts(jj['time_start']) +# en = fromts(jj['time_end']) +# tfmt = "%Y-%m-%d %a" +# tot_mins = 0 +# res = [] +# res.append(f"{st.strftime(tfmt)} -- {en.strftime(tfmt)}") +# for cls in ['rem', 'light', 'deep']: +# mins = jj[f'sleep_class_{cls}_duration'] // 60 +# res += [cls, hhmm(mins)] +# tot_mins += mins +# res += ["total", hhmm(tot_mins)] +# print(*res) + + +def stats(): + datas = get_datas() + cur = datas[0].date + for jj in datas: + # import ipdb; ipdb.set_trace() + while cur < jj.date: + cur += timedelta(days=1) + if cur.weekday() == 0: + print("---") + if cur != jj.date: + print(" ") + # cur = jj.date + print(f"{jj.date.strftime('%m.%d %a')} {jj.hrv_morning:.0f} {jj.hrv_evening:.0f} {jj.hrv_morning - jj.hrv_evening:3.0f} {hhmm(jj.sleep_minutes)} {jj.hrv_lf}/{jj.hrv_hf} {jj.sleep_hr_coverage:3.0f}") + + +def plot_recovery_vs_hr_percentage(): + sns.set(color_codes=True) + xs = [] + ys = [] + for jj in get_datas(): + xs.append(jj.hrv_morning - jj.hrv_evening) + ys.append(jj.sleep_hr_coverage) + ax = sns.regplot(x=xs, y=ys) # "recovery", y="percentage", data=pdata) + ax.set(xlabel='recovery', ylabel='percentage') + plt.show() + + +def plot_hr(): + jj = get_datas()[-1] + tss, uu = jj.sleep_hr + tss = tss[::10] + uu = uu[::10] + plt.figure(figsize=(15,4)) + ax = sns.pointplot(tss, uu, markers=" ") + ax.set(ylim=(None, 1000)) + + plt.show() + +# TODO ok, would be nice to have that every morning in timeline +# also timeline should have dynamic filters? maybe by tags +# then I could enable emfit feed and slog feed (pulled from all org notes) and see the correlation? also could pull workouts provider (and wlog) -- actually wlog processing could be moved to timeline too + +# TODO could plot 'recovery' thing and see if it is impacted by workouts + + +# TODO time_start, time_end + +# plot_hrv() +# stats() +# plot_recovery_vs_hr_percentage() +# stats() +# import matplotlib +# matplotlib.use('Agg') + + +# TODO maybe rmssd should only be computed if we have a reasonable chunk of datas +# also, trust it only if it's stable + +# plot_timestamped([p[0] for p in pts], [p[1] for p in pts], mavgs=[]).savefig('res.png') +# TODO X axes: show hours and only two dates +# TODO 4 is awake, 3 REM, 2 light, 1 deep + + + + +# deviartion beyond 25-75 or 75-25 is bad??