postrunner 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a27854d18dbc179f4111f3b678e7004dbe2ef2ea
4
- data.tar.gz: 11c3167f322b25abd1b3428f5c58d8d14ba28e66
3
+ metadata.gz: ea10a2814c3c25363117c69d85a1f5e0d4c07c2c
4
+ data.tar.gz: f0c72064a97183bfee97978e11a534bd9c2d5c5e
5
5
  SHA512:
6
- metadata.gz: 91ebf4ab3b416459ac7f5f43e2f9817d7d998af7e2dcba7b8bcc13f13ad14f54d917de5f4ce5c618ce83821ca1462ae605d73cdad4c66615b4c5fc5b93ce953f
7
- data.tar.gz: 4ab49219c002a4c63ead0cc007d52b2df0c5edeb97d662f39ebfbefa7ecafda64a56007a21057a5723872651a3446724dbc11947cb9b55524336d2ad75f00f52
6
+ metadata.gz: 644edbd1db0ad2fe562e74a2d09b61ad2664942b1b0aaf31647a96d948cb9c2eb5951fd79a07302d4d2099215f76189b20524071fdd5a1b5d46d340dad851c8a
7
+ data.tar.gz: b2d3a31c489a3bc506c4d3d809abc35f4eb059edcf94fee1a8b630b52dbdc3e9e580b94825209123890ca670dc6999f28074a1e618c05342935d14a7abd4c167
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = postrunner.rb -- PostRunner - Manage the data from your Garmin sport devices.
5
5
  #
6
- # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2016 by Chris Schlaeger <cs@taskjuggler.org>
7
7
  #
8
8
  # This program is free software; you can redistribute it and/or modify
9
9
  # it under the terms of version 2 of the GNU General Public License as
@@ -21,6 +21,6 @@ require 'postrunner/Main'
21
21
 
22
22
  module PostRunner
23
23
 
24
- Main.new(ARGV)
24
+ Main.new.main(ARGV)
25
25
 
26
26
  end
@@ -75,14 +75,12 @@ module PostRunner
75
75
  { :metric => 'km', :statute => 'mi'}) ])
76
76
  end
77
77
  t.row([ 'Time:', secsToHMS(session.total_timer_time) ])
78
+ t.row([ 'Avg. Speed:',
79
+ local_value(session, 'avg_speed', '%.1f %s',
80
+ { :metric => 'km/h', :statute => 'mph' }) ])
78
81
  if @activity.sport == 'running' || @activity.sport == 'multisport'
79
82
  t.row([ 'Avg. Pace:', pace(session, 'avg_speed') ])
80
83
  end
81
- if @activity.sport != 'running'
82
- t.row([ 'Avg. Speed:',
83
- local_value(session, 'avg_speed', '%.1f %s',
84
- { :metric => 'km/h', :statute => 'mph' }) ])
85
- end
86
84
  t.row([ 'Total Ascent:',
87
85
  local_value(session, 'total_ascent', '%.0f %s',
88
86
  { :metric => 'm', :statute => 'ft' }) ])
@@ -117,8 +115,8 @@ module PostRunner
117
115
  end
118
116
  if @activity.sport == 'cycling'
119
117
  t.row([ 'Avg. Cadence:',
120
- session.avg_candence ?
121
- "#{(2 * session.avg_candence).round} rpm" : '-' ])
118
+ session.avg_cadence ?
119
+ "#{(2 * session.avg_cadence).round} rpm" : '-' ])
122
120
  end
123
121
 
124
122
  t.row([ 'Training Effect:', session.total_training_effect ?
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = DailySleepAnalzyer.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
+ module PostRunner
16
+
17
+ # This class extracts the sleep information from a set of monitoring files
18
+ # and determines when and how long the user was awake or had a light or deep
19
+ # sleep.
20
+ class DailySleepAnalyzer
21
+
22
+ # Utility class to store the interval of a sleep/wake phase.
23
+ class SleepInterval < Struct.new(:from_time, :to_time, :phase)
24
+ end
25
+
26
+ attr_reader :sleep_intervals, :utc_offset,
27
+ :total_sleep, :deep_sleep, :light_sleep
28
+
29
+ # Create a new DailySleepAnalyzer object to analyze the given monitoring
30
+ # files.
31
+ # @param monitoring_files [Array] A set of Monitoring_B objects
32
+ # @param day [String] Day to analyze as YY-MM-DD string
33
+ def initialize(monitoring_files, day)
34
+ @noon_yesterday = @noon_today = @utc_offset = nil
35
+ @sleep_intervals = []
36
+
37
+ # Day as Time object. Midnight UTC.
38
+ day_as_time = Time.parse(day + "-00:00:00+00:00").gmtime
39
+ extract_data_from_monitor_files(monitoring_files, day_as_time)
40
+ fill_sleep_activity
41
+ smoothen_sleep_activity
42
+ analyze
43
+ trim_wake_periods_at_ends
44
+ calculate_totals
45
+ end
46
+
47
+ private
48
+
49
+ def extract_utc_offset(monitoring_file)
50
+ # The monitoring files have a monitoring_info section that contains a
51
+ # timestamp in UTC and a local_time field for the same time in the local
52
+ # time. If any of that isn't present, we use an offset of 0.
53
+ if (mi = monitoring_file.monitoring_infos).nil? || mi.empty? ||
54
+ (localtime = mi[0].local_time).nil?
55
+ return 0
56
+ end
57
+
58
+ # Otherwise the delta in seconds between UTC and localtime is the
59
+ # offset.
60
+ localtime - mi[0].timestamp
61
+ end
62
+
63
+ def extract_data_from_monitor_files(monitoring_files, day)
64
+ # We use an Array with entries for every minute from noon yesterday to
65
+ # noon today.
66
+ @sleep_activity = Array.new(24 * 60, nil)
67
+ monitoring_files.each do |mf|
68
+ utc_offset = extract_utc_offset(mf)
69
+ # Noon (local time) the day before the requested day. The time object
70
+ # is UTC for the noon time in the local time zone.
71
+ noon_yesterday = day - 12 * 60 * 60 - utc_offset
72
+ # Noon (local time) of the current day
73
+ noon_today = day + 12 * 60 * 60 - utc_offset
74
+
75
+ mf.monitorings.each do |m|
76
+ # Ignore all entries outside our 24 hour window from noon the day
77
+ # before to noon the current day.
78
+ next if m.timestamp < noon_yesterday || m.timestamp >= noon_today
79
+
80
+ if @noon_yesterday.nil? && @noon_today.nil?
81
+ # The instance variables will only be set once we have found our
82
+ # first monitoring file that matches the requested day. We use the
83
+ # local time setting for this first file even if it changes in
84
+ # subsequent files.
85
+ @noon_yesterday = noon_yesterday
86
+ @noon_today = noon_today
87
+ @utc_offset = utc_offset
88
+ end
89
+
90
+ if (cati = m.current_activity_type_intensity)
91
+ activity_type = cati & 0x1F
92
+
93
+ # Compute the index in the @sleep_activity Array.
94
+ index = (m.timestamp - @noon_yesterday) / 60
95
+ if activity_type == 8
96
+ intensity = (cati >> 5) & 0x7
97
+ @sleep_activity[index] = intensity
98
+ else
99
+ @sleep_activity[index] = false
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ def fill_sleep_activity
108
+ current = nil
109
+ @sleep_activity = @sleep_activity.reverse.map do |v|
110
+ v.nil? ? current : current = v
111
+ end.reverse
112
+
113
+ if $DEBUG
114
+ File.open('sleep-data.csv', 'w') do |f|
115
+ f.puts 'Date;Value'
116
+ @sleep_activity.each_with_index do |v, i|
117
+ f.puts "#{@noon_yesterday + i * 60};#{v.is_a?(Fixnum) ? v : 8}"
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def smoothen_sleep_activity
124
+ window_size = 30
125
+
126
+ @smoothed_sleep_activity = Array.new(24 * 60, nil)
127
+ 0.upto(24 * 60 - 1).each do |i|
128
+ window_start_idx = i - window_size
129
+ window_end_idx = i
130
+ sum = 0.0
131
+ (i - window_size + 1).upto(i).each do |j|
132
+ sum += j < 0 ? 8.0 :
133
+ @sleep_activity[j].is_a?(Fixnum) ? @sleep_activity[j] : 8
134
+ end
135
+ @smoothed_sleep_activity[i] = sum / window_size
136
+ end
137
+
138
+ if $DEBUG
139
+ File.open('smoothed-sleep-data.csv', 'w') do |f|
140
+ f.puts 'Date;Value'
141
+ @smoothed_sleep_activity.each_with_index do |v, i|
142
+ f.puts "#{@noon_yesterday + i * 60};#{v}"
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def analyze
149
+ current_phase = :awake
150
+ current_phase_start = @noon_yesterday
151
+ @sleep_intervals = []
152
+
153
+ @smoothed_sleep_activity.each_with_index do |v, idx|
154
+ if v < 0.25
155
+ phase = :deep_sleep
156
+ elsif v < 1.5
157
+ phase = :light_sleep
158
+ else
159
+ phase = :awake
160
+ end
161
+
162
+ if current_phase != phase
163
+ t = @noon_yesterday + 60 * idx
164
+ @sleep_intervals << SleepInterval.new(current_phase_start, t,
165
+ current_phase)
166
+ current_phase = phase
167
+ current_phase_start = t
168
+ end
169
+ end
170
+ @sleep_intervals << SleepInterval.new(current_phase_start, @noon_today,
171
+ current_phase)
172
+ end
173
+
174
+ def trim_wake_periods_at_ends
175
+ first_deep_sleep_idx = last_deep_sleep_idx = nil
176
+
177
+ @sleep_intervals.each_with_index do |p, idx|
178
+ if p.phase == :deep_sleep ||
179
+ (p.phase == :light_sleep && ((p.to_time - p.from_time) > 15 * 60))
180
+ first_deep_sleep_idx = idx unless first_deep_sleep_idx
181
+ last_deep_sleep_idx = idx
182
+ end
183
+ end
184
+
185
+ return unless first_deep_sleep_idx && last_deep_sleep_idx
186
+
187
+ if first_deep_sleep_idx > 0 &&
188
+ @sleep_intervals[first_deep_sleep_idx - 1].phase == :light_sleep
189
+ first_deep_sleep_idx -= 1
190
+ end
191
+ if last_deep_sleep_idx < @sleep_intervals.length - 2 &&
192
+ @sleep_intervals[last_deep_sleep_idx + 1].phase == :light_sleep
193
+ last_deep_sleep_idx += 1
194
+ end
195
+
196
+ @sleep_intervals =
197
+ @sleep_intervals[first_deep_sleep_idx..last_deep_sleep_idx]
198
+ end
199
+
200
+ def calculate_totals
201
+ @total_sleep = @light_sleep = @deep_sleep = 0
202
+ @sleep_intervals.each do |p|
203
+ if p.phase != :awake
204
+ seconds = p.to_time - p.from_time
205
+ @total_sleep += seconds
206
+ if p.phase == :light_sleep
207
+ @light_sleep += seconds
208
+ else
209
+ @deep_sleep += seconds
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ # Return the begining of the current day in local time as Time object.
216
+ def begining_of_today(time = Time.now)
217
+ sec, min, hour, day, month, year = time.to_a
218
+ sec = min = hour = 0
219
+ Time.new(*[ year, month, day, hour, min, sec, 0 ]).localtime
220
+ end
221
+
222
+ end
223
+
224
+ end
225
+
@@ -31,6 +31,7 @@ module PostRunner
31
31
  'environment_sensor_legacy' => 'GPS',
32
32
  'gps' => 'GPS',
33
33
  'heart_rate' => 'Heart Rate Sensor',
34
+ 'optical_heart_rate' => 'Optical Heart Rate Sensor',
34
35
  'running_dynamics' => 'Running Dynamics',
35
36
  'stride_speed_distance' => 'Footpod'
36
37
  }
@@ -104,6 +105,15 @@ module PostRunner
104
105
  t.new_row
105
106
  end
106
107
 
108
+ if type == 'GPS' && (epo = @fit_activity.epo_data) && epo.valid == 1
109
+ t.cell('EPO Data Start:')
110
+ t.cell(epo.interval_start)
111
+ t.new_row
112
+ t.cell('EPO Data End:')
113
+ t.cell(epo.interval_end)
114
+ t.new_row
115
+ end
116
+
107
117
  if device.serial_number
108
118
  t.cell('Serial Number:')
109
119
  t.cell(device.serial_number)
@@ -157,10 +157,6 @@ module PostRunner
157
157
  load_fit_file(filter)
158
158
  end
159
159
 
160
- def dump(filter)
161
- load_fit_file(filter)
162
- end
163
-
164
160
  def query(key)
165
161
  unless @@Schemata.include?(key)
166
162
  raise ArgumentError, "Unknown key '#{key}' requested in query"
@@ -12,6 +12,7 @@
12
12
 
13
13
  require 'perobs'
14
14
  require 'postrunner/FFS_Activity'
15
+ require 'postrunner/FFS_Monitoring'
15
16
 
16
17
  module PostRunner
17
18
 
@@ -50,13 +51,12 @@ module PostRunner
50
51
  # @return [FFS_Activity or FFS_Monitoring] Corresponding entry in the
51
52
  # FitFileStore or nil if file could not be added.
52
53
  def add_fit_file(fit_file_name, fit_entity, overwrite)
53
- case fit_entity.class
54
- when Fit4Ruby::Activity.class
54
+ if fit_entity.is_a?(Fit4Ruby::Activity)
55
55
  entity = activity_by_file_name(File.basename(fit_file_name))
56
56
  entities = @activities
57
57
  type = 'activity'
58
58
  new_entity_class = FFS_Activity
59
- when Fit4Ruby::Monitoring.class
59
+ elsif fit_entity.is_a?(Fit4Ruby::Monitoring_B)
60
60
  entity = monitoring_by_file_name(File.basename(fit_file_name))
61
61
  entities = @monitorings
62
62
  type = 'monitoring'
@@ -123,6 +123,22 @@ module PostRunner
123
123
  @monitorings.find { |a| a.fit_file_name == file_name }
124
124
  end
125
125
 
126
+ # Return all monitorings that overlap with the time interval given by
127
+ # from_time and to_time.
128
+ # @param from_time [Time] start time of the interval
129
+ # @param to_time [Time] end time of the interval (not included)
130
+ # @return [Array] list of overlapping FFS_Monitoring objects.
131
+ def monitorings(from_time, to_time)
132
+ list = []
133
+ @monitorings.each do |m|
134
+ if (from_time <= m.period_start && m.period_start < to_time) ||
135
+ (from_time <= m.period_end && m.period_end < to_time)
136
+ list << m
137
+ end
138
+ end
139
+ list
140
+ end
141
+
126
142
  end
127
143
 
128
144
  end
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = FFS_Monitoring.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
+ require 'perobs'
15
+
16
+ module PostRunner
17
+
18
+ # The FFS_Monitoring objects can store a reference to the FIT file data and
19
+ # caches some frequently used values.
20
+ class FFS_Monitoring < PEROBS::Object
21
+
22
+ include DirUtils
23
+
24
+ po_attr :device, :fit_file_name, :name, :period_start, :period_end
25
+
26
+ # Create a new FFS_Monitoring object.
27
+ # @param p [PEROBS::Handle] PEROBS handle
28
+ # @param fit_file_name [String] The fully qualified file name of the FIT
29
+ # file to add
30
+ # @param fit_entity [Fit4Ruby::FitEntity] The content of the loaded FIT
31
+ # file
32
+ def initialize(p, device, fit_file_name, fit_entity)
33
+ super(p)
34
+
35
+ self.device = device
36
+ self.fit_file_name = fit_file_name ? File.basename(fit_file_name) : nil
37
+ self.name = fit_file_name ? File.basename(fit_file_name) : nil
38
+
39
+ extract_summary_values(fit_entity)
40
+ end
41
+
42
+ # Store a copy of the given FIT file in the corresponding directory.
43
+ # @param fit_file_name [String] Fully qualified name of the FIT file.
44
+ def store_fit_file(fit_file_name)
45
+ # Get the right target directory for this particular FIT file.
46
+ dir = @store['file_store'].fit_file_dir(File.basename(fit_file_name),
47
+ @device.long_uid, 'monitor')
48
+ # Create the necessary directories if they don't exist yet.
49
+ create_directory(dir, 'Device monitoring diretory')
50
+
51
+ # Copy the file into the target directory.
52
+ begin
53
+ FileUtils.cp(fit_file_name, dir)
54
+ rescue StandardError
55
+ Log.fatal "Cannot copy #{fit_file_name} into #{dir}: #{$!}"
56
+ end
57
+ end
58
+
59
+ # FFS_Monitoring objects are sorted by their start time values and then by
60
+ # their device long_uids.
61
+ def <=>(a)
62
+ @period_start == a.period_start ?
63
+ a.device.long_uid <=> self.device.long_uid :
64
+ a.period_start <=> @period_start
65
+ end
66
+
67
+ private
68
+
69
+ def extract_summary_values(fit_entity)
70
+ self.period_start = fit_entity.monitoring_infos[0].timestamp
71
+
72
+ period_end = @period_start
73
+ fit_entity.monitorings.each do |monitoring|
74
+ period_end = monitoring.timestamp if monitoring.timestamp
75
+ end
76
+ self.period_end = period_end
77
+
78
+ puts "#{@period_start} - #{@period_end}"
79
+ end
80
+
81
+ def decode_activity_type(activity_type)
82
+ types = [ :generic, :running, :cycling, :transition,
83
+ :fitness_equipment, :swimming, :walking, :unknown7,
84
+ :resting, :unknown9 ]
85
+ if (decoded_type = types[activity_type])
86
+ decoded_type
87
+ else
88
+ Log.error "Unknown activity type #{activity_type}"
89
+ :generic
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+
@@ -18,6 +18,7 @@ require 'postrunner/DirUtils'
18
18
  require 'postrunner/FFS_Device'
19
19
  require 'postrunner/ActivityListView'
20
20
  require 'postrunner/ViewButtons'
21
+ require 'postrunner/SleepStatistics'
21
22
 
22
23
  module PostRunner
23
24
 
@@ -81,8 +82,8 @@ module PostRunner
81
82
  end
82
83
 
83
84
  unless [ Fit4Ruby::Activity,
84
- Fit4Ruby::Monitoring ].include?(fit_entity.class)
85
- Log.critical "Unsupported FIT file type #{fit_entity.class}"
85
+ Fit4Ruby::Monitoring_B ].include?(fit_entity.class)
86
+ Log.fatal "Unsupported FIT file type #{fit_entity.class}"
86
87
  end
87
88
 
88
89
  # Generate a String that uniquely identifies the device that generated
@@ -318,6 +319,51 @@ module PostRunner
318
319
  end
319
320
  end
320
321
 
322
+ def daily_report(day)
323
+ monitorings = []
324
+ # 'day' specifies the current day. But we don't know what timezone the
325
+ # watch was set to for a given date. The files are always named after
326
+ # the moment of finishing the recording expressed as GMT time.
327
+ # Each file contains information about the time zone for the specific
328
+ # file. Recording is always flipped to a new file at midnight GMT but
329
+ # there are usually multiple files per GMT day.
330
+ day_as_time = Time.parse(day).gmtime
331
+ @store['devices'].each do |id, device|
332
+ # We are looking for all files that potentially overlap with our
333
+ # localtime day.
334
+ monitorings += device.monitorings(day_as_time - 36 * 60 * 60,
335
+ day_as_time + 36 * 60 * 60)
336
+ end
337
+ monitoring_files = monitorings.map do |m|
338
+ read_fit_file(File.join(fit_file_dir(m.fit_file_name, m.device.long_uid,
339
+ 'monitor'), m.fit_file_name))
340
+ end
341
+ puts SleepStatistics.new(monitoring_files).daily(day)
342
+ end
343
+
344
+ def monthly_report(day)
345
+ monitorings = []
346
+ # 'day' specifies the current month. It must be in the form of
347
+ # YYYY-MM-01. But we don't know what timezone the watch was set to for a
348
+ # given date. The files are always named after the moment of finishing
349
+ # the recording expressed as GMT time. Each file contains information
350
+ # about the time zone for the specific file. Recording is always flipped
351
+ # to a new file at midnight GMT but there are usually multiple files per
352
+ # GMT day.
353
+ day_as_time = Time.parse(day).gmtime
354
+ @store['devices'].each do |id, device|
355
+ # We are looking for all files that potentially overlap with our
356
+ # localtime day.
357
+ monitorings += device.monitorings(day_as_time - 36 * 60 * 60,
358
+ day_as_time + 33 * 24 * 60 * 60)
359
+ end
360
+ monitoring_files = monitorings.map do |m|
361
+ read_fit_file(File.join(fit_file_dir(m.fit_file_name, m.device.long_uid,
362
+ 'monitor'), m.fit_file_name))
363
+ end
364
+ puts SleepStatistics.new(monitoring_files).monthly(day)
365
+ end
366
+
321
367
  private
322
368
 
323
369
  def read_fit_file(fit_file_name)
@@ -330,16 +376,36 @@ module PostRunner
330
376
  end
331
377
 
332
378
  def extract_fit_file_id(fit_entity)
333
- fit_entity.device_infos.each do |di|
334
- if di.device_index == 0
335
- return {
336
- :manufacturer => di.manufacturer,
337
- :product => di.garmin_product || di.product,
338
- :serial_number => di.serial_number
339
- }
379
+ unless (fid = fit_entity.file_id)
380
+ Log.fatal 'FIT file has no file_id section'
381
+ end
382
+
383
+ if fid.manufacturer == 'garmin' &&
384
+ fid.garmin_product == 'fr920xt'
385
+ # Garmin Fenix3 with firmware before 6.80 is reporting 'fr920xt' in
386
+ # the file_id section but 'fenix3' in the first device_info section.
387
+ # To tell the Fenix3 apart from the FR920XT we need to look into the
388
+ # device_info section for all devices with a garmin_product of
389
+ # 'fr920xt'.
390
+ fit_entity.device_infos.each do |di|
391
+ if di.device_index == 0
392
+ return {
393
+ :manufacturer => di.manufacturer,
394
+ :product => di.garmin_product || di.product,
395
+ :serial_number => di.serial_number
396
+ }
397
+ end
340
398
  end
399
+ Log.fatal "Fit entity has no device info for 0"
400
+ else
401
+ # And for all properly developed devices we can just look at the
402
+ # file_id section.
403
+ return {
404
+ :manufacturer => fid.manufacturer,
405
+ :product => fid.garmin_product || fid.product,
406
+ :serial_number => fid.serial_number
407
+ }
341
408
  end
342
- Log.fatal "Fit entity has no device info for 0"
343
409
  end
344
410
 
345
411
  def register_device(long_uid)
@@ -30,15 +30,17 @@ module PostRunner
30
30
 
31
31
  include DirUtils
32
32
 
33
- def initialize(args)
33
+ def initialize
34
34
  @filter = nil
35
35
  @name = nil
36
36
  @force = false
37
37
  @attribute = nil
38
38
  @value = nil
39
39
  @db_dir = File.join(ENV['HOME'], '.postrunner')
40
+ end
40
41
 
41
- return if (args = parse_options(args)).nil?
42
+ def main(args)
43
+ return 0 if (args = parse_options(args)).nil?
42
44
 
43
45
  unless $DEBUG
44
46
  Kernel.trap('INT') do
@@ -51,15 +53,40 @@ module PostRunner
51
53
  end
52
54
 
53
55
  begin
54
- main(args)
56
+ create_directory(@db_dir, 'PostRunner data')
57
+ @db = PEROBS::Store.new(File.join(@db_dir, 'database'))
58
+ # Create a hash to store configuration data in the store unless it
59
+ # exists already.
60
+ cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
61
+ cfg['unit_system'] ||= :metric
62
+ cfg['version'] ||= VERSION
63
+ # We always override the data_dir as the user might have moved the data
64
+ # directory. The only reason we store it in the DB is to have it
65
+ # available throught the application.
66
+ cfg['data_dir'] = @db_dir
67
+ # Always update html_dir setting so that the DB directory can be moved
68
+ # around by the user.
69
+ cfg['html_dir'] = File.join(@db_dir, 'html')
70
+
71
+ setup_directories
72
+ if $DEBUG && (errors = @db.check) != 0
73
+ Log.abort "Postrunner database is corrupted: #{errors} errors found"
74
+ end
75
+ return execute_command(args)
76
+
55
77
  rescue Exception => e
56
78
  if e.is_a?(SystemExit) || e.is_a?(Interrupt)
57
79
  $stderr.puts e.backtrace.join("\n") if $DEBUG
80
+ elsif e.is_a?(Fit4Ruby::Abort)
81
+ # Programm execution error that does not warrant a backtrace to be
82
+ # printed.
83
+ return -1
58
84
  else
59
- Log.fatal("#{e}\n#{e.backtrace.join("\n")}\n\n" +
85
+ Log.error("#{e}\n#{e.backtrace.join("\n")}\n\n" +
60
86
  "#{'*' * 79}\nYou have triggered a bug in PostRunner " +
61
87
  "#{VERSION}!")
62
88
  end
89
+ return -1
63
90
  end
64
91
  end
65
92
 
@@ -71,7 +98,7 @@ module PostRunner
71
98
 
72
99
  opts.separator <<"EOT"
73
100
 
74
- Copyright (c) 2014, 2015 by Chris Schlaeger
101
+ Copyright (c) 2014, 2015, 2016 by Chris Schlaeger
75
102
 
76
103
  This program is free software; you can redistribute it and/or modify it under
77
104
  the terms of version 2 of the GNU General Public License as published by the
@@ -157,12 +184,19 @@ import [ <fit file> | <directory> ]
157
184
  file or directory is provided, the directory that was used for the
158
185
  previous import is being used.
159
186
 
187
+ daily [ <date> ]
188
+ Print a report summarizing the current day or the specified day.
189
+
160
190
  delete <ref>
161
191
  Delete the activity from the archive.
162
192
 
163
193
  list
164
194
  List all FIT files stored in the data base.
165
195
 
196
+ monthly [ <date> ]
197
+ Print a table with various statistics for each day of the specified
198
+ month.
199
+
166
200
  records
167
201
  List all personal records.
168
202
 
@@ -217,35 +251,11 @@ EOT
217
251
  begin
218
252
  parser.parse!(args)
219
253
  rescue OptionParser::InvalidOption
220
- Log.fatal "#{$!}"
254
+ Log.error "#{$!}\n" + help
255
+ return nil
221
256
  end
222
257
  end
223
258
 
224
- def main(args)
225
- create_directory(@db_dir, 'PostRunner data')
226
- @db = PEROBS::Store.new(File.join(@db_dir, 'database'))
227
- # Create a hash to store configuration data in the store unless it
228
- # exists already.
229
- cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
230
- cfg['unit_system'] ||= :metric
231
- cfg['version'] ||= VERSION
232
- # We always override the data_dir as the user might have moved the data
233
- # directory. The only reason we store it in the DB is to have it
234
- # available throught the application.
235
- cfg['data_dir'] = @db_dir
236
- # Always update html_dir setting so that the DB directory can be moved
237
- # around by the user.
238
- cfg['html_dir'] = File.join(@db_dir, 'html')
239
-
240
- setup_directories
241
- if $DEBUG && (errors = @db.check) != 0
242
- Log.fatal "Postrunner database is corrupted: #{errors} errors found"
243
- end
244
- execute_command(args)
245
-
246
- @db.sync
247
- end
248
-
249
259
  def setup_directories
250
260
  create_directory(@db['config']['html_dir'], 'HTML output')
251
261
 
@@ -255,18 +265,18 @@ EOT
255
265
  misc_dir = File.realpath(File.join(File.dirname(__FILE__),
256
266
  '..', '..', 'misc'))
257
267
  unless Dir.exists?(misc_dir)
258
- Log.fatal "Cannot find 'misc' directory under '#{misc_dir}': #{$!}"
268
+ Log.abort "Cannot find 'misc' directory under '#{misc_dir}': #{$!}"
259
269
  end
260
270
  src_dir = File.join(misc_dir, dir)
261
271
  unless Dir.exists?(src_dir)
262
- Log.fatal "Cannot find '#{src_dir}': #{$!}"
272
+ Log.abort "Cannot find '#{src_dir}': #{$!}"
263
273
  end
264
274
  dst_dir = @db['config']['html_dir']
265
275
 
266
276
  begin
267
277
  FileUtils.cp_r(src_dir, dst_dir)
268
278
  rescue IOError
269
- Log.fatal "Cannot copy auxilliary data directory '#{dst_dir}': #{$!}"
279
+ Log.abort "Cannot copy auxilliary data directory '#{dst_dir}': #{$!}"
270
280
  end
271
281
  end
272
282
  end
@@ -293,6 +303,14 @@ EOT
293
303
  else
294
304
  process_files_or_activities(args, :check)
295
305
  end
306
+ when 'daily'
307
+ # Get the date of requested day in 'YY-MM-DD' format. If no argument
308
+ # is given, use the current date.
309
+ @ffs.daily_report(day_in_localtime(args, '%Y-%m-%d'))
310
+ when 'monthly'
311
+ # Get the date of requested day in 'YY-MM-DD' format. If no argument
312
+ # is given, use the current date.
313
+ @ffs.monthly_report(day_in_localtime(args, '%Y-%m-01'))
296
314
  when 'delete'
297
315
  process_activities(args, :delete)
298
316
  when 'dump'
@@ -319,15 +337,15 @@ EOT
319
337
  puts @records.to_s
320
338
  when 'rename'
321
339
  unless (@name = args.shift)
322
- Log.fatal 'You must provide a new name for the activity'
340
+ Log.abort 'You must provide a new name for the activity'
323
341
  end
324
342
  process_activities(args, :rename)
325
343
  when 'set'
326
344
  unless (@attribute = args.shift)
327
- Log.fatal 'You must specify the attribute you want to change'
345
+ Log.abort 'You must specify the attribute you want to change'
328
346
  end
329
347
  unless (@value = args.shift)
330
- Log.fatal 'You must specify the new value for the attribute'
348
+ Log.abort 'You must specify the new value for the attribute'
331
349
  end
332
350
  process_activities(args, :set)
333
351
  when 'show'
@@ -347,12 +365,19 @@ EOT
347
365
  when 'update-gps'
348
366
  update_gps_data
349
367
  when nil
350
- Log.fatal("No command provided. " +
351
- "See 'postrunner -h' for more information.")
368
+ Log.abort("No command provided. " + help)
352
369
  else
353
- Log.fatal("Unknown command '#{cmd}'. " +
354
- "See 'postrunner -h' for more information.")
370
+ Log.abort("Unknown command '#{cmd}'. " + help)
355
371
  end
372
+
373
+ # Ensure that all updates are written to the database.
374
+ @db.sync
375
+
376
+ 0
377
+ end
378
+
379
+ def help
380
+ "See 'postrunner -h' for more information."
356
381
  end
357
382
 
358
383
  def process_files_or_activities(files_or_activities, command)
@@ -367,7 +392,7 @@ EOT
367
392
 
368
393
  def process_activities(activity_refs, command)
369
394
  if activity_refs.empty?
370
- Log.fatal("You must provide at least one activity reference.")
395
+ Log.abort("You must provide at least one activity reference.")
371
396
  end
372
397
 
373
398
  activity_refs.each do |a_ref|
@@ -379,7 +404,7 @@ EOT
379
404
  end
380
405
  activities.each { |a| process_activity(a, command) }
381
406
  else
382
- Log.fatal "Activity references must start with ':': #{a_ref}"
407
+ Log.abort "Activity references must start with ':': #{a_ref}"
383
408
  end
384
409
  end
385
410
 
@@ -388,7 +413,7 @@ EOT
388
413
 
389
414
  def process_files(files_or_dirs, command)
390
415
  if files_or_dirs.empty?
391
- Log.fatal("You must provide at least one .FIT file name.")
416
+ Log.abort("You must provide at least one .FIT file name.")
392
417
  end
393
418
 
394
419
  files_or_dirs.each do |fod|
@@ -430,10 +455,9 @@ EOT
430
455
  return false
431
456
  end
432
457
 
433
- if fit_entity.is_a?(Fit4Ruby::Activity)
458
+ if fit_entity.is_a?(Fit4Ruby::Activity) ||
459
+ fit_entity.is_a?(Fit4Ruby::Monitoring_B)
434
460
  return @ffs.add_fit_file(fit_file_name, fit_entity, @force)
435
- #elsif fit_entity.is_a?(Fit4Ruby::Monitoring_B)
436
- # return @monitoring.add(fit_file_name, fit_entity)
437
461
  else
438
462
  Log.error "#{fit_file_name} is not a recognized FIT file"
439
463
  return false
@@ -477,7 +501,7 @@ EOT
477
501
 
478
502
  def change_unit_system(args)
479
503
  if args.length != 1 || !%w( metric statute ).include?(args[0])
480
- Log.fatal("You must specify 'metric' or 'statute' as unit system.")
504
+ Log.error("You must specify 'metric' or 'statute' as unit system.")
481
505
  end
482
506
 
483
507
  if @db['config']['unit_system'].to_s != args[0]
@@ -488,7 +512,7 @@ EOT
488
512
 
489
513
  def change_html_dir(args)
490
514
  if args.length != 1
491
- Log.fatal('You must specify a directory')
515
+ Log.error('You must specify a directory')
492
516
  end
493
517
 
494
518
  if @db['config']['html_dir'] != args[0]
@@ -530,6 +554,15 @@ EOT
530
554
  end
531
555
  end
532
556
 
557
+ def day_in_localtime(args, format)
558
+ begin
559
+ (args.empty? ? Time.now : Time.parse(args[0])).
560
+ localtime.strftime(format)
561
+ rescue ArgumentError
562
+ Log.abort("#{args[0]} is not a valid date. Use YYYY-MM-DD format.")
563
+ end
564
+ end
565
+
533
566
  def handle_version_update
534
567
  if @db['config']['version'] != VERSION
535
568
  Log.warn "PostRunner version upgrade detected."
@@ -0,0 +1,117 @@
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
+
@@ -56,6 +56,7 @@ module PostRunner
56
56
  <<EOT
57
57
  .titlebar {
58
58
  width: 100%;
59
+ min-width: 1210px;
59
60
  height: 50px;
60
61
  margin: 0px;
61
62
  background: linear-gradient(#7FA1FF 0, #002EAC 50px);
@@ -11,5 +11,5 @@
11
11
  #
12
12
 
13
13
  module PostRunner
14
- VERSION = "0.2.1"
14
+ VERSION = "0.3.0"
15
15
  end
@@ -19,7 +19,7 @@ GEM_SPEC = Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
  spec.required_ruby_version = '>=2.0'
21
21
 
22
- spec.add_dependency 'fit4ruby', '~> 0.0.12'
22
+ spec.add_dependency 'fit4ruby', '~> 1.0.0'
23
23
  spec.add_dependency 'perobs', '~> 2.2'
24
24
  spec.add_dependency 'nokogiri', '~> 1.6'
25
25
 
@@ -19,13 +19,19 @@ describe PostRunner::Main do
19
19
 
20
20
  def postrunner(args)
21
21
  args = [ '--dbdir', @db_dir ] + args
22
- old_stdout = $stdout
23
- $stdout = (stdout = StringIO.new)
24
- @postrunner = nil
25
- GC.start
26
- @postrunner = PostRunner::Main.new(args)
27
- $stdout = old_stdout
28
- stdout.string
22
+ begin
23
+ old_stdout = $stdout
24
+ old_stderr = $stderr
25
+ $stdout = (stdout = StringIO.new)
26
+ $stderr = (stderr = StringIO.new)
27
+ GC.start
28
+ retval = PostRunner::Main.new.main(args)
29
+ ensure
30
+ $stdout = old_stdout
31
+ $stderr = old_stderr
32
+ end
33
+
34
+ { :retval => retval, :stdout => stdout.string, :stderr => stderr.string }
29
35
  end
30
36
 
31
37
  before(:all) do
@@ -47,11 +53,13 @@ describe PostRunner::Main do
47
53
  end
48
54
 
49
55
  it 'should abort without arguments' do
50
- expect { postrunner([]) }.to raise_error(Fit4Ruby::Error)
56
+ v = postrunner([])
57
+ expect(v[:retval]).to eql(-1)
51
58
  end
52
59
 
53
60
  it 'should abort with bad command' do
54
- expect { postrunner(%w( foobar)) }.to raise_error(Fit4Ruby::Error)
61
+ v = postrunner(%w( foobar))
62
+ expect(v[:retval]).to eql(-1)
55
63
  end
56
64
 
57
65
  it 'should support the -v option' do
@@ -79,51 +87,59 @@ describe PostRunner::Main do
79
87
  end
80
88
 
81
89
  it 'should list the imported file' do
82
- expect(postrunner(%w( list )).index(File.basename(@file1))).to be_a(Fixnum)
90
+ v = postrunner(%w( list ))
91
+ expect(v[:stdout].index(File.basename(@file1))).to be_a(Fixnum)
83
92
  end
84
93
 
85
94
  it 'should import the 2nd FIT file' do
86
95
  postrunner([ 'import', @file2 ])
87
- list = postrunner(%w( list ))
96
+ v = postrunner(%w( list ))
97
+ list = v[:stdout]
88
98
  expect(list.index(File.basename(@file1))).to be_a(Fixnum)
89
99
  expect(list.index(File.basename(@file2))).to be_a(Fixnum)
90
100
  end
91
101
 
92
102
  it 'should delete the first file' do
93
103
  postrunner(%w( delete :2 ))
94
- list = postrunner(%w( list ))
104
+ v = postrunner(%w( list ))
105
+ list = v[:stdout]
95
106
  expect(list.index(File.basename(@file1))).to be_nil
96
107
  expect(list.index(File.basename(@file2))).to be_a(Fixnum)
97
108
  end
98
109
 
99
110
  it 'should not import the deleted file again' do
100
111
  postrunner([ 'import', @file1 ])
101
- list = postrunner(%w( list ))
112
+ v = postrunner(%w( list ))
113
+ list = v[:stdout]
102
114
  expect(list.index(File.basename(@file1))).to be_nil
103
115
  expect(list.index(File.basename(@file2))).to be_a(Fixnum)
104
116
  end
105
117
 
106
118
  it 'should rename FILE2.FIT activity' do
107
119
  postrunner(%w( rename foobar :1 ))
108
- list = postrunner(%w( list ))
120
+ v = postrunner(%w( list ))
121
+ list = v[:stdout]
109
122
  expect(list.index(File.basename(@file2))).to be_nil
110
123
  expect(list.index('foobar')).to be_a(Fixnum)
111
124
  end
112
125
 
113
126
  it 'should fail when setting bad attribute' do
114
- expect { postrunner(%w( set foo bar :1)) }.to raise_error(Fit4Ruby::Error)
127
+ v = postrunner(%w( set foo bar :1))
128
+ expect(v[:retval]).to eql(-1)
115
129
  end
116
130
 
117
131
  it 'should set name for 2nd activity' do
118
132
  postrunner(%w( set name foobar :1 ))
119
- list = postrunner(%w( list ))
133
+ v = postrunner(%w( list ))
134
+ list = v[:stdout]
120
135
  expect(list.index(@file2)).to be_nil
121
136
  expect(list.index('foobar')).to be_a(Fixnum)
122
137
  end
123
138
 
124
139
  it 'should set activity type for 2nd activity' do
125
140
  postrunner(%w( set type Cycling :1 ))
126
- list = postrunner(%w( summary :1 ))
141
+ v = postrunner(%w( summary :1 ))
142
+ list = v[:stdout]
127
143
  expect(list.index('Running')).to be_nil
128
144
  expect(list.index('Cycling')).to be_a(Fixnum)
129
145
  end
@@ -137,18 +153,21 @@ describe PostRunner::Main do
137
153
  end
138
154
 
139
155
  it 'should fail when setting bad activity type' do
140
- expect { postrunner(%w( set type foobar :1)) }.to raise_error(Fit4Ruby::Error)
156
+ v = postrunner(%w( set type foobar :1))
157
+ expect(v[:retval]).to eql(-1)
141
158
  end
142
159
 
143
160
  it 'should set activity subtype for FILE2.FIT activity' do
144
161
  postrunner(%w( set subtype Road :1 ))
145
- list = postrunner(%w( summary :1 ))
162
+ v = postrunner(%w( summary :1 ))
163
+ list = v[:stdout]
146
164
  expect(list.index('Generic')).to be_nil
147
165
  expect(list.index('Road')).to be_a(Fixnum)
148
166
  end
149
167
 
150
168
  it 'should fail when setting bad activity subtype' do
151
- expect { postrunner(%w( set subtype foobar :1)) }.to raise_error(Fit4Ruby::Error)
169
+ v = postrunner(%w( set subtype foobar :1))
170
+ expect(v[:retval]).to eql(-1)
152
171
  end
153
172
 
154
173
  it 'should dump an activity from the archive' do
@@ -170,19 +189,22 @@ describe PostRunner::Main do
170
189
  it 'should list records' do
171
190
  # Add slow running activity
172
191
  postrunner([ 'import', '--force', @file1 ])
173
- list = postrunner([ 'records' ])
192
+ v = postrunner([ 'records' ])
193
+ list = v[:stdout]
174
194
  expect(list.index(File.basename(@file1))).to be_a(Fixnum)
175
195
 
176
196
  # Add fast running activity
177
197
  postrunner([ 'import', @file3 ])
178
- list = postrunner([ 'records' ])
198
+ v =postrunner([ 'records' ])
199
+ list = v[:stdout]
179
200
  expect(list.index(File.basename(@file3))).to be_a(Fixnum)
180
201
  expect(list.index(File.basename(@file1))).to be_nil
181
202
  end
182
203
 
183
204
  it 'should ignore records of an activity' do
184
205
  postrunner(%w( set norecord true :1 ))
185
- list = postrunner([ 'records' ])
206
+ v = postrunner([ 'records' ])
207
+ list = v[:stdout]
186
208
  expect(list.index(File.basename(@file1))).to be_a(Fixnum)
187
209
  expect(list.index(File.basename(@file3))).to be_nil
188
210
  end
@@ -80,6 +80,10 @@ def create_fit_activity(config)
80
80
  ts = Time.parse(config[:t])
81
81
  serial = config[:serial] || 12345890
82
82
  a = Fit4Ruby::Activity.new({ :timestamp => ts })
83
+ a.new_file_id({ :manufacturer => 'garmin',
84
+ :time_created => ts,
85
+ :garmin_product => 'fr920xt',
86
+ :serial_number => serial })
83
87
  a.total_timer_time = (config[:duration] || 10) * 60
84
88
  a.new_user_profile({ :timestamp => ts,
85
89
  :age => 33, :height => 1.78, :weight => 73.0,
@@ -28,8 +28,7 @@ task :permissions do
28
28
  # Find the bin and test directories relative to this file.
29
29
  baseDir = File.expand_path('..', File.dirname(__FILE__))
30
30
 
31
- execs = Dir.glob("#{baseDir}/bin/*") +
32
- Dir.glob("#{baseDir}/test/**/genrefs")
31
+ execs = Dir.glob("#{baseDir}/bin/*")
33
32
 
34
33
  Find.find(baseDir) do |f|
35
34
  # Ignore the whoke pkg directory as it may contain links to the other
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.2.1
4
+ version: 0.3.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-03-02 00:00:00.000000000 Z
11
+ date: 2016-04-17 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: 0.0.12
19
+ version: 1.0.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: 0.0.12
26
+ version: 1.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: perobs
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -135,6 +135,7 @@ files:
135
135
  - lib/postrunner/ActivityView.rb
136
136
  - lib/postrunner/BackedUpFile.rb
137
137
  - lib/postrunner/ChartView.rb
138
+ - lib/postrunner/DailySleepAnalyzer.rb
138
139
  - lib/postrunner/DataSources.rb
139
140
  - lib/postrunner/DeviceList.rb
140
141
  - lib/postrunner/DirUtils.rb
@@ -142,6 +143,7 @@ files:
142
143
  - lib/postrunner/EventList.rb
143
144
  - lib/postrunner/FFS_Activity.rb
144
145
  - lib/postrunner/FFS_Device.rb
146
+ - lib/postrunner/FFS_Monitoring.rb
145
147
  - lib/postrunner/FitFileStore.rb
146
148
  - lib/postrunner/FlexiTable.rb
147
149
  - lib/postrunner/HRV_Analyzer.rb
@@ -158,6 +160,7 @@ files:
158
160
  - lib/postrunner/RecordListPageView.rb
159
161
  - lib/postrunner/RuntimeConfig.rb
160
162
  - lib/postrunner/Schema.rb
163
+ - lib/postrunner/SleepStatistics.rb
161
164
  - lib/postrunner/TrackView.rb
162
165
  - lib/postrunner/UserProfileView.rb
163
166
  - lib/postrunner/View.rb
@@ -327,7 +330,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
327
330
  version: '0'
328
331
  requirements: []
329
332
  rubyforge_project:
330
- rubygems_version: 2.4.5.1
333
+ rubygems_version: 2.2.2
331
334
  signing_key:
332
335
  specification_version: 4
333
336
  summary: Application to manage and analyze Garmin FIT files.