postrunner 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = SleepCycle.rb -- PostRunner - Manage the data from your Garmin sport devices.
5
+ #
6
+ # Copyright (c) 2016 by Chris Schlaeger <cs@taskjuggler.org>
7
+ #
8
+ # This program is free software; you can redistribute it and/or modify
9
+ # it under the terms of version 2 of the GNU General Public License as
10
+ # published by the Free Software Foundation.
11
+ #
12
+
13
+ module PostRunner
14
+
15
+ # A SleepPhase is a segment of a sleep cycle. It captures the start and
16
+ # end time as well as the kind of phase.
17
+ class SleepPhase
18
+
19
+ attr_reader :from_time, :to_time, :phase
20
+
21
+ # Create a new sleep phase.
22
+ # @param from_time [Time] Start time of the phase
23
+ # @param to_time [Time] End time of the phase
24
+ # @param phase [Symbol] The kind of phase [ :rem, :nrem1, :nrem2, :nrem3 ]
25
+ def initialize(from_time, to_time, phase)
26
+ @from_time = from_time
27
+ @to_time = to_time
28
+ @phase = phase
29
+ end
30
+
31
+ # Duration of the phase in seconds.
32
+ # @return [Fixnum] duration
33
+ def duration
34
+ @to_time - @from_time
35
+ end
36
+
37
+ end
38
+
39
+ # A sleep cycle consists of several sleep phases. This class is used to
40
+ # gather and store the relevant data of a sleep cycle. Data is analzyed and
41
+ # stored with a one minute granularity. Time values are stored as minutes
42
+ # past the zero_idx_time.
43
+ class SleepCycle
44
+
45
+ attr_reader :total_seconds, :totals
46
+ attr_accessor :start_idx, :end_idx,
47
+ :high_low_trans_idx, :low_high_trans_idx,
48
+ :prev_cycle, :next_cycle
49
+
50
+ # Create a new SleepCycle record.
51
+ # @param zero_idx_time [Time] This is the time of the 0-th minute. All
52
+ # time values are stored as minutes past this time.
53
+ # @param start_idx [Fixnum] Time when the sleep cycle starts. We may start
54
+ # with an appromated value that gets fine tuned later on.
55
+ # @param prev_cycle [SleepCycle] A reference to the preceding sleep cycle
56
+ # or nil if this is the first cycle of the analyzed period.
57
+ def initialize(zero_idx_time, start_idx, prev_cycle = nil)
58
+ @zero_idx_time = zero_idx_time
59
+ @start_idx = start_idx
60
+ # These values will be determined later.
61
+ @end_idx = nil
62
+ # Every sleep cycle has at most one high/low heart rate transition and
63
+ # one low/high transition. These variables store the time of these
64
+ # transitions or nil if the transition does not exist. Every cycle must
65
+ # have at least one of these transitions to be a valid cycle.
66
+ @high_low_trans_idx = @low_high_trans_idx = nil
67
+ @prev_cycle = prev_cycle
68
+ # Register this cycle as successor of the previous cycle.
69
+ prev_cycle.next_cycle = self if prev_cycle
70
+ @next_cycle = nil
71
+ # Array holding the sleep phases of this cycle
72
+ @phases = []
73
+ # A hash with the total durations (in secods) of the various sleep
74
+ # phases.
75
+ @total_seconds = Hash.new(0)
76
+ end
77
+
78
+ # The start time of the cycle as Time object
79
+ # @return [Time]
80
+ def from_time
81
+ idx_to_time(@start_idx)
82
+ end
83
+
84
+ # The end time of the cycle as Time object.
85
+ # @return [Time]
86
+ def to_time
87
+ idx_to_time(@end_idx + 1)
88
+ end
89
+
90
+ # Remove this cycle from the cycle chain.
91
+ def unlink
92
+ @prev_cycle.next_cycle = @next_cycle if @prev_cycle
93
+ @next_cycle.prev_cycle = @prev_cycle if @next_cycle
94
+ end
95
+
96
+ # Initially, we use the high/low heart rate transition to mark the end
97
+ # of the cycle. But it's really the end of the REM phase that marks the
98
+ # end of a sleep cycle. If we find a REM phase, we use its end to adjust
99
+ # the sleep cycle boundaries.
100
+ # @param phases [Array] List of symbols that describe the sleep phase at
101
+ # at the minute corresponding to the Array index.
102
+ def adjust_cycle_boundaries(phases)
103
+ end_of_rem_phase_idx = nil
104
+ @start_idx.upto(@end_idx) do |i|
105
+ end_of_rem_phase_idx = i if phases[i] == :rem
106
+ end
107
+ if end_of_rem_phase_idx
108
+ # We have found a REM phase. Adjust the end_idx of this cycle
109
+ # accordingly.
110
+ @end_idx = end_of_rem_phase_idx
111
+ if @next_cycle
112
+ # If we have a successor phase, we also adjust the start.
113
+ @next_cycle.start_idx = end_of_rem_phase_idx + 1
114
+ end
115
+ end
116
+ end
117
+
118
+ # Gather a list of SleepPhase objects that describe the sequence of sleep
119
+ # phases in the provided Array.
120
+ # @param phases [Array] List of symbols that describe the sleep phase at
121
+ # at the minute corresponding to the Array index.
122
+ def detect_phases(phases)
123
+ @phases = []
124
+ current_phase = phases[0]
125
+ current_phase_start = @start_idx
126
+
127
+ @start_idx.upto(@end_idx) do |i|
128
+ if (current_phase && current_phase != phases[i]) || i == @end_idx
129
+ # We found a transition in the sequence. Create a SleepPhase object
130
+ # that describes the prepvious segment and add it to the @phases
131
+ # list.
132
+ @phases << (p = SleepPhase.new(idx_to_time(current_phase_start),
133
+ idx_to_time(i == @end_idx ? i + 1 : i),
134
+ current_phase))
135
+ # Add the duration of the phase to the corresponding sum in the
136
+ # @total_seconds Hash.
137
+ @total_seconds[current_phase] += p.duration
138
+
139
+ # Update the variables that track the start and kind of the
140
+ # currently read phase.
141
+ current_phase_start = i
142
+ current_phase = phases[i]
143
+ end
144
+ end
145
+ end
146
+
147
+ # Check if this cycle is really a sleep cycle or not. A sleep cycle must
148
+ # have at least one deep sleep phase or must be part of a directly
149
+ # attached series of cycles that contain a deep sleep phase.
150
+ # @return [Boolean] True if not a sleep cycle, false otherwise.
151
+ def is_wake_cycle?
152
+ !has_deep_sleep_phase? && !has_leading_deep_sleep_phase? &&
153
+ !has_trailing_deep_sleep_phase?
154
+ end
155
+
156
+
157
+ # Check if the cycle has a deep sleep phase.
158
+ # @return [Boolean] True of one of the phases is NREM3 phase. False
159
+ # otherwise.
160
+ def has_deep_sleep_phase?
161
+ # A real deep sleep phase must be at least 10 minutes long.
162
+ @phases.each do |p|
163
+ return true if p.phase == :nrem3 && p.duration > 10 * 60
164
+ end
165
+
166
+ false
167
+ end
168
+
169
+ # Check if any of the previous cycles that are directly attached have a
170
+ # deep sleep cycle.
171
+ # @return [Boolean] True if it has a leading sleep cycle.
172
+ def has_leading_deep_sleep_phase?
173
+ return false if @prev_cycle.nil? || @start_idx != @prev_cycle.end_idx + 1
174
+
175
+ @prev_cycle.has_deep_sleep_phase? ||
176
+ @prev_cycle.has_leading_deep_sleep_phase?
177
+ end
178
+
179
+ # Check if any of the trailing cycles that are directly attached have a
180
+ # deep sleep cycle.
181
+ # @return [Boolean] True if it has a trailing sleep cycle.
182
+ def has_trailing_deep_sleep_phase?
183
+ return false if @next_cycle.nil? || @end_idx + 1 != @next_cycle.start_idx
184
+
185
+ @next_cycle.has_deep_sleep_phase? ||
186
+ @next_cycle.has_trailing_deep_sleep_phase?
187
+ end
188
+
189
+ private
190
+
191
+ def idx_to_time(idx)
192
+ return nil unless idx
193
+ @zero_idx_time + 60 * idx
194
+ end
195
+
196
+ end
197
+
198
+ end
@@ -11,5 +11,5 @@
11
11
  #
12
12
 
13
13
  module PostRunner
14
- VERSION = "0.3.0"
14
+ VERSION = "0.4.0"
15
15
  end
@@ -9,7 +9,15 @@ GEM_SPEC = Gem::Specification.new do |spec|
9
9
  spec.authors = ["Chris Schlaeger"]
10
10
  spec.email = ["cs@taskjuggler.org"]
11
11
  spec.summary = %q{Application to manage and analyze Garmin FIT files.}
12
- spec.description = %q{This application will allow you to manage and analyze .FIT files such as those generated by Garmin GPS devices. The application was developed and tested with the Garmin Forerunner 620 and Fenix 3. Other devices may work as well. They need to export the data as USB Mass Storage devices. It is an offline alternative to Garmin Connect.}
12
+ spec.description = %q{PostRunner is an application to manage FIT files
13
+ such as those produced by Garmin products like the Forerunner 620 (FR620) and
14
+ Fenix 3 or Fenix 3HR. It allows you to import the files from the device and
15
+ analyze the data. In addition to the common features like plotting pace, heart
16
+ rates, elevation and other captured values it also provides a heart rate
17
+ variability (HRV) analysis. It can also update satellite orbit prediction
18
+ (EPO) data on the device to speed-up GPS fix times. It is an offline alternative
19
+ to Garmin Connect. The software has been developed and tested on Linux but
20
+ should work on other operating systems as well.}
13
21
  spec.homepage = 'https://github.com/scrapper/postrunner'
14
22
  spec.license = "GNU GPL version 2"
15
23
 
@@ -19,7 +27,7 @@ GEM_SPEC = Gem::Specification.new do |spec|
19
27
  spec.require_paths = ["lib"]
20
28
  spec.required_ruby_version = '>=2.0'
21
29
 
22
- spec.add_dependency 'fit4ruby', '~> 1.0.0'
30
+ spec.add_dependency 'fit4ruby', '~> 1.1.0'
23
31
  spec.add_dependency 'perobs', '~> 2.2'
24
32
  spec.add_dependency 'nokogiri', '~> 1.6'
25
33
 
@@ -209,5 +209,13 @@ describe PostRunner::Main do
209
209
  expect(list.index(File.basename(@file3))).to be_nil
210
210
  end
211
211
 
212
+ it 'should support the daily command' do
213
+ postrunner([ 'daily' ])
214
+ end
215
+
216
+ it 'should supoprt the monthly command' do
217
+ postrunner([ 'monthly' ])
218
+ end
219
+
212
220
  end
213
221
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postrunner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Schlaeger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-17 00:00:00.000000000 Z
11
+ date: 2016-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fit4ruby
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.0.0
19
+ version: 1.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.0.0
26
+ version: 1.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: perobs
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -108,11 +108,16 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: 0.8.7
111
- description: This application will allow you to manage and analyze .FIT files such
112
- as those generated by Garmin GPS devices. The application was developed and tested
113
- with the Garmin Forerunner 620 and Fenix 3. Other devices may work as well. They
114
- need to export the data as USB Mass Storage devices. It is an offline alternative
115
- to Garmin Connect.
111
+ description: |-
112
+ PostRunner is an application to manage FIT files
113
+ such as those produced by Garmin products like the Forerunner 620 (FR620) and
114
+ Fenix 3 or Fenix 3HR. It allows you to import the files from the device and
115
+ analyze the data. In addition to the common features like plotting pace, heart
116
+ rates, elevation and other captured values it also provides a heart rate
117
+ variability (HRV) analysis. It can also update satellite orbit prediction
118
+ (EPO) data on the device to speed-up GPS fix times. It is an offline alternative
119
+ to Garmin Connect. The software has been developed and tested on Linux but
120
+ should work on other operating systems as well.
116
121
  email:
117
122
  - cs@taskjuggler.org
118
123
  executables:
@@ -135,6 +140,7 @@ files:
135
140
  - lib/postrunner/ActivityView.rb
136
141
  - lib/postrunner/BackedUpFile.rb
137
142
  - lib/postrunner/ChartView.rb
143
+ - lib/postrunner/DailyMonitoringAnalyzer.rb
138
144
  - lib/postrunner/DailySleepAnalyzer.rb
139
145
  - lib/postrunner/DataSources.rb
140
146
  - lib/postrunner/DeviceList.rb
@@ -152,6 +158,7 @@ files:
152
158
  - lib/postrunner/Log.rb
153
159
  - lib/postrunner/Main.rb
154
160
  - lib/postrunner/MonitoringDB.rb
161
+ - lib/postrunner/MonitoringStatistics.rb
155
162
  - lib/postrunner/NavButtonRow.rb
156
163
  - lib/postrunner/PagingButtons.rb
157
164
  - lib/postrunner/Percentiles.rb
@@ -160,7 +167,7 @@ files:
160
167
  - lib/postrunner/RecordListPageView.rb
161
168
  - lib/postrunner/RuntimeConfig.rb
162
169
  - lib/postrunner/Schema.rb
163
- - lib/postrunner/SleepStatistics.rb
170
+ - lib/postrunner/SleepCycle.rb
164
171
  - lib/postrunner/TrackView.rb
165
172
  - lib/postrunner/UserProfileView.rb
166
173
  - lib/postrunner/View.rb
@@ -1,117 +0,0 @@
1
- #!/usr/bin/env ruby -w
2
- # encoding: UTF-8
3
- #
4
- # = SleepStatistics.rb -- PostRunner - Manage the data from your Garmin sport devices.
5
- #
6
- # Copyright (c) 2016 by Chris Schlaeger <cs@taskjuggler.org>
7
- #
8
- # This program is free software; you can redistribute it and/or modify
9
- # it under the terms of version 2 of the GNU General Public License as
10
- # published by the Free Software Foundation.
11
- #
12
-
13
- require 'fit4ruby'
14
-
15
- require 'postrunner/DailySleepAnalyzer'
16
- require 'postrunner/FlexiTable'
17
-
18
- module PostRunner
19
-
20
- # This class can be used to generate reports for sleep data. It uses the
21
- # DailySleepAnalyzer class to compute the data and generates the report for
22
- # a certain time period.
23
- class SleepStatistics
24
-
25
- include Fit4Ruby::Converters
26
-
27
- # Create a new SleepStatistics object.
28
- # @param monitoring_files [Array of Fit4Ruby::Monitoring_B] FIT files
29
- def initialize(monitoring_files)
30
- @monitoring_files = monitoring_files
31
- end
32
-
33
- # Generate a report for a certain day.
34
- # @param day [String] Date of the day as YYYY-MM-DD string.
35
- def daily(day)
36
- analyzer = DailySleepAnalyzer.new(@monitoring_files, day)
37
-
38
- if analyzer.sleep_intervals.empty?
39
- return 'No sleep data available for this day'
40
- end
41
-
42
- ti = FlexiTable.new
43
- ti.head
44
- ti.row([ 'From', 'To', 'Sleep phase' ])
45
- ti.body
46
- utc_offset = analyzer.utc_offset
47
- analyzer.sleep_intervals.each do |i|
48
- ti.cell(i[:from_time].localtime(utc_offset).strftime('%H:%M'))
49
- ti.cell(i[:to_time].localtime(utc_offset).strftime('%H:%M'))
50
- ti.cell(i[:phase])
51
- ti.new_row
52
- end
53
-
54
- tt = FlexiTable.new
55
- tt.head
56
- tt.row([ 'Total Sleep', 'Deep Sleep', 'Light Sleep' ])
57
- tt.body
58
- tt.cell(secsToHM(analyzer.total_sleep), { :halign => :right })
59
- tt.cell(secsToHM(analyzer.deep_sleep), { :halign => :right })
60
- tt.cell(secsToHM(analyzer.light_sleep), { :halign => :right })
61
- tt.new_row
62
-
63
- "Sleep Statistics for #{day}\n\n#{ti}\n#{tt}"
64
- end
65
-
66
- def monthly(day)
67
- day_as_time = Time.parse(day)
68
- year = day_as_time.year
69
- month = day_as_time.month
70
- last_day_of_month = Date.new(year, month, -1).day
71
-
72
- t = FlexiTable.new
73
- left = { :halign => :left }
74
- right = { :halign => :right }
75
- t.set_column_attributes([ left, right, right, right ])
76
- t.head
77
- t.row([ 'Date', 'Total Sleep', 'Deep Sleep', 'Light Sleep' ])
78
- t.body
79
- totals = Hash.new(0)
80
- counted_days = 0
81
-
82
- 1.upto(last_day_of_month).each do |dom|
83
- day_str = Time.new(year, month, dom).strftime('%Y-%m-%d')
84
- t.cell(day_str)
85
-
86
- analyzer = DailySleepAnalyzer.new(@monitoring_files, day_str)
87
-
88
- if analyzer.sleep_intervals.empty?
89
- t.cell('-')
90
- t.cell('-')
91
- t.cell('-')
92
- else
93
- totals[:total_sleep] += analyzer.total_sleep
94
- totals[:deep_sleep] += analyzer.deep_sleep
95
- totals[:light_sleep] += analyzer.light_sleep
96
- counted_days += 1
97
-
98
- t.cell(secsToHM(analyzer.total_sleep))
99
- t.cell(secsToHM(analyzer.deep_sleep))
100
- t.cell(secsToHM(analyzer.light_sleep))
101
- end
102
- t.new_row
103
- end
104
- t.foot
105
- t.cell('Averages')
106
- t.cell(secsToHM(totals[:total_sleep] / counted_days))
107
- t.cell(secsToHM(totals[:deep_sleep] / counted_days))
108
- t.cell(secsToHM(totals[:light_sleep] / counted_days))
109
- t.new_row
110
-
111
- "Sleep Statistics for #{day_as_time.strftime('%B')} #{year}\n\n#{t}"
112
- end
113
-
114
- end
115
-
116
- end
117
-