postrunner 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.