summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Schlaeger <chris@linux.com>2016-05-06 20:34:55 +0200
committerChris Schlaeger <chris@linux.com>2016-05-06 20:34:55 +0200
commit1ede5a4667a11021366e4dfdd9f4243d8989800d (patch)
treeea056f1422441ba8572c6e78947ea19478bf4904
parentdd12763bbdc141fd537ac6aabe0e2d9c2bbac06d (diff)
downloadpostrunner-1ede5a4667a11021366e4dfdd9f4243d8989800d.zip
New: Add sleep cycle count column to monthly report.
-rw-r--r--lib/postrunner/DailySleepAnalyzer.rb105
-rw-r--r--lib/postrunner/SleepStatistics.rb29
-rw-r--r--spec/PostRunner_spec.rb8
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