diff options
author | Chris Schlaeger <chris@linux.com> | 2016-05-06 20:34:55 +0200 |
---|---|---|
committer | Chris Schlaeger <chris@linux.com> | 2016-05-06 20:34:55 +0200 |
commit | 1ede5a4667a11021366e4dfdd9f4243d8989800d (patch) | |
tree | ea056f1422441ba8572c6e78947ea19478bf4904 | |
parent | dd12763bbdc141fd537ac6aabe0e2d9c2bbac06d (diff) | |
download | postrunner-1ede5a4667a11021366e4dfdd9f4243d8989800d.zip |
New: Add sleep cycle count column to monthly report.
-rw-r--r-- | lib/postrunner/DailySleepAnalyzer.rb | 105 | ||||
-rw-r--r-- | lib/postrunner/SleepStatistics.rb | 29 | ||||
-rw-r--r-- | spec/PostRunner_spec.rb | 8 |
3 files changed, 77 insertions, 65 deletions
diff --git a/lib/postrunner/DailySleepAnalyzer.rb b/lib/postrunner/DailySleepAnalyzer.rb index fd3340c..d4c04e9 100644 --- a/lib/postrunner/DailySleepAnalyzer.rb +++ b/lib/postrunner/DailySleepAnalyzer.rb @@ -53,8 +53,42 @@ module PostRunner # window to analyze against the midnight of the specified day def initialize(monitoring_files, day, window_offest_secs) @window_start_time = @window_end_time = @utc_offset = nil + + # The following activity types are known: + # [ :undefined, :running, :cycling, :transition, + # :fitness_equipment, :swimming, :walking, :unknown7, + # :resting, :unknown9 ] + @activity_type = Array.new(TIME_WINDOW_MINUTES, nil) + # The activity values in the FIT files can range from 0 to 7. + @activity_intensity = Array.new(TIME_WINDOW_MINUTES, nil) + # Wrist motion data is not very well suited to determine wake or sleep + # states. A single movement can be a turning motion, a NREM1 jerk or + # even a movement while you dream. The fewer motions are detected, the + # more likely you are really asleep. To even out single spikes, we + # average the motions over a period of time. This Array stores the + # weighted activity. + @weighted_sleep_activity = Array.new(TIME_WINDOW_MINUTES, 8) + # We classify the sleep activity into :wake, :low_activity and + # :no_activity in this Array. + @sleep_activity_classification = Array.new(TIME_WINDOW_MINUTES, nil) + + # The data from the monitoring files is stored in Arrays that cover 24 + # hours at 1 minute resolution. The algorithm currently cannot handle + # time zone or DST changes. The day is always 24 hours and the local + # time at noon the previous day is used for the whole window. + @heart_rate = Array.new(TIME_WINDOW_MINUTES, nil) + # From the wrist motion data and if available from the heart rate data, + # we try to guess the sleep phase (:wake, :rem, :nrem1, :nrem2, :nrem3). + # This Array will hold a minute-by-minute list of the guessed sleep + # phase. + @sleep_phase = Array.new(TIME_WINDOW_MINUTES, :wake) + # The DailySleepAnalzyer extracts the sleep cycles from the monitoring + # data. Each night usually has 5 - 6 sleep cycles. If we have heart rate + # data, those cycles can be identified fairly well. If we have to rely + # on wrist motion data only, we usually find more cycles than there + # actually were. Each cycle is captured as SleepCycle object. @sleep_cycles = [] - @sleep_phase = [] + # The resting heart rate. @resting_heart_rate = nil # Day as Time object. Midnight UTC. @@ -108,23 +142,13 @@ module PostRunner localtime - mi[0].timestamp end + # Load monitoring data from monitoring_b FIT files into Arrays. + # @param monitoring_files [Array of Monitoring_B] FIT files to read + # @param day [Time] Midnight UTC of the day to analyze + # @param window_offest_secs [Fixnum] Difference between midnight and the + # start of the time window to analyze. def extract_data_from_monitor_files(monitoring_files, day, window_offest_secs) - # The data from the monitoring files is stored in Arrays that cover 36 - # hours at 1 minute resolution. We store the period noon of the previous - # day to midnight the next day for the given day. The algorithm - # currently cannot handle time zone or DST changes. The day is always 24 - # hours and the local time at noon the previous day is used for the - # whole 36 hour period. - @heart_rate = Array.new(TIME_WINDOW_MINUTES, nil) - # The following activity types are known: - # [ :undefined, :running, :cycling, :transition, - # :fitness_equipment, :swimming, :walking, :unknown7, - # :resting, :unknown9 ] - @activity_type = Array.new(TIME_WINDOW_MINUTES, nil) - # The activity values in the FIT files can range from 0 to 7. - @activity_intensity = Array.new(TIME_WINDOW_MINUTES, nil) - monitoring_files.each do |mf| utc_offset = extract_utc_offset(mf) # Midnight (local time) of the requested day. @@ -219,9 +243,6 @@ module PostRunner end def categorize_sleep_activity - @weighted_sleep_activity = Array.new(TIME_WINDOW_MINUTES, 8) - @sleep_activity_classification = Array.new(TIME_WINDOW_MINUTES, nil) - delta = 7 0.upto(TIME_WINDOW_MINUTES - 1) do |i| intensity_sum = 0 @@ -324,9 +345,9 @@ module PostRunner transitions > 3 end + # Use the wrist motion data and heart rate data to guess the sleep phases + # and sleep cycles. def categorize_sleep_phase_by_hr_level - @sleep_phase = Array.new(TIME_WINDOW_MINUTES, :wake) - rem_possible = false current_hr_phase = nil cycle = nil @@ -406,14 +427,6 @@ module PostRunner end end - def delete_wake_cycles - wake_cycles = [] - @sleep_cycles.each { |c| wake_cycles << c if c.is_wake_cycle? } - - wake_cycles.each { |c| c.unlink } - @sleep_cycles.delete_if { |c| wake_cycles.include?(c) } - end - def categorize_sleep_phase_by_activity_level @sleep_phase = [] mappings = { :wake => :wake, :low_activity => :nrem1, @@ -460,6 +473,14 @@ module PostRunner end end + def delete_wake_cycles + wake_cycles = [] + @sleep_cycles.each { |c| wake_cycles << c if c.is_wake_cycle? } + + wake_cycles.each { |c| c.unlink } + @sleep_cycles.delete_if { |c| wake_cycles.include?(c) } + end + def determine_resting_heart_rate # Find the smallest heart rate. TODO: While being awake. @heart_rate.each_with_index do |heart_rate, idx| @@ -471,32 +492,6 @@ module PostRunner end end - - def trim_wake_periods_at_ends - first_deep_sleep_idx = last_deep_sleep_idx = nil - - @sleep_phase.each_with_index do |p, idx| - if p.phase == :nrem3 - first_deep_sleep_idx = idx unless first_deep_sleep_idx - last_deep_sleep_idx = idx - end - end - - return unless first_deep_sleep_idx && last_deep_sleep_idx - - while first_deep_sleep_idx > 0 && - @sleep_phase[first_deep_sleep_idx - 1].phase != :wake do - first_deep_sleep_idx -= 1 - end - while last_deep_sleep_idx < @sleep_phase.length - 1 && - @sleep_phase[last_deep_sleep_idx + 1].phase != :wake do - last_deep_sleep_idx += 1 - end - - @sleep_phase = - @sleep_phase[first_deep_sleep_idx..last_deep_sleep_idx] - end - def calculate_totals @total_sleep = @light_sleep = @deep_sleep = @rem_sleep = 0 diff --git a/lib/postrunner/SleepStatistics.rb b/lib/postrunner/SleepStatistics.rb index 16de544..ca35286 100644 --- a/lib/postrunner/SleepStatistics.rb +++ b/lib/postrunner/SleepStatistics.rb @@ -40,9 +40,12 @@ module PostRunner end "Sleep Statistics for #{day}\n\n" + - daily_sleep_cycle_table(analyzer).to_s + daily_sleep_cycle_table(analyzer).to_s + + "\nResting heart rate: #{analyzer.resting_heart_rate} BPM" end + # Generate a report for a certain month. + # @param day [String] Date of a day in that months as YYYY-MM-DD string. def monthly(day) day_as_time = Time.parse(day) year = day_as_time.year @@ -52,35 +55,40 @@ module PostRunner t = FlexiTable.new left = { :halign => :left } right = { :halign => :right } - t.set_column_attributes([ left, right, right, right, right, right ]) + t.set_column_attributes( + [ left, right, right, right, right, right, right ]) t.head - t.row([ 'Date', 'Total Sleep', 'REM Sleep', 'Deep Sleep', - 'Light Sleep', 'RHR' ]) + t.row([ 'Date', 'Total Sleep', 'Cycles', 'REM Sleep', 'Light Sleep', + 'Deep Sleep', 'RHR' ]) t.body totals = Hash.new(0) counted_days = 0 rhr_days = 0 1.upto(last_day_of_month).each do |dom| - day_str = Time.new(year, month, dom).strftime('%Y-%m-%d') + break if (time = Time.new(year, month, dom)) > Time.now + + day_str = time.strftime('%Y-%m-%d') t.cell(day_str) analyzer = DailySleepAnalyzer.new(@monitoring_files, day_str, -12 * 60 * 60) if (analyzer.sleep_cycles.empty?) - 4.times { t.cell('-') } + 5.times { t.cell('-') } else totals[:total_sleep] += analyzer.total_sleep + totals[:cycles] += analyzer.sleep_cycles.length totals[:rem_sleep] += analyzer.rem_sleep - totals[:deep_sleep] += analyzer.deep_sleep totals[:light_sleep] += analyzer.light_sleep + totals[:deep_sleep] += analyzer.deep_sleep counted_days += 1 t.cell(secsToHM(analyzer.total_sleep)) + t.cell(analyzer.sleep_cycles.length) t.cell(secsToHM(analyzer.rem_sleep)) - t.cell(secsToHM(analyzer.deep_sleep)) t.cell(secsToHM(analyzer.light_sleep)) + t.cell(secsToHM(analyzer.deep_sleep)) end if (rhr = analyzer.resting_heart_rate) && rhr > 0 @@ -96,11 +104,12 @@ module PostRunner t.cell('Averages') if counted_days > 0 t.cell(secsToHM(totals[:total_sleep] / counted_days)) + t.cell('%.1f' % (totals[:cycles] / counted_days)) t.cell(secsToHM(totals[:rem_sleep] / counted_days)) - t.cell(secsToHM(totals[:deep_sleep] / counted_days)) t.cell(secsToHM(totals[:light_sleep] / counted_days)) + t.cell(secsToHM(totals[:deep_sleep] / counted_days)) else - 3.times { t.cell('-') } + 5.times { t.cell('-') } end if rhr_days > 0 t.cell('%.1f' % (totals[:rhr] / rhr_days)) diff --git a/spec/PostRunner_spec.rb b/spec/PostRunner_spec.rb index 94fff2c..bd75974 100644 --- a/spec/PostRunner_spec.rb +++ b/spec/PostRunner_spec.rb @@ -209,5 +209,13 @@ describe PostRunner::Main do expect(list.index(File.basename(@file3))).to be_nil end + it 'should support the daily command' do + postrunner([ 'daily' ]) + end + + it 'should supoprt the monthly command' do + postrunner([ 'monthly' ]) + end + end |