Class: PostRunner::FitFileStore
- Inherits:
-
PEROBS::Object
- Object
- PEROBS::Object
- PostRunner::FitFileStore
- Includes:
- DirUtils
- Defined in:
- lib/postrunner/FitFileStore.rb
Overview
The FitFileStore stores all FIT file and provides access to the contained data.
Instance Attribute Summary collapse
-
#store ⇒ Object
readonly
Returns the value of attribute store.
-
#views ⇒ Object
readonly
Returns the value of attribute views.
Class Method Summary collapse
Instance Method Summary collapse
-
#activities ⇒ Array of FFS_Activity
List of stored activities.
-
#add_fit_file(fit_file_name, fit_entity = nil, overwrite = false) ⇒ FFS_Activity or FFS_Monitoring
Add a file to the store.
-
#change_unit_system ⇒ Object
Perform the necessary report updates after the unit system has been changed.
-
#check ⇒ Object
This methods checks all stored FIT files for correctness, updates all indexes and re-generates all HTML reports.
- #daily_report(day) ⇒ Object
-
#delete_activity(activity) ⇒ Object
Delete an activity from the database.
-
#devices ⇒ Array of FFS_Device
List of registered devices.
-
#find(query) ⇒ Object
Find a specific subset of the activities based on their index.
-
#fit_file_dir(fit_file_base_name, long_uid, type) ⇒ String
Determine the right directory for the given FIT file.
-
#handle_version_update ⇒ Object
Version upgrade logic.
-
#initialize(p) ⇒ FitFileStore
constructor
Create a new FIT file store.
- #list_activities ⇒ Object
-
#monitorings(start_date, end_date) ⇒ Array of Monitoring_B
Read in all Monitoring_B FIT files that overlap with the given interval.
- #monthly_report(day) ⇒ Object
-
#predecessor(activity) ⇒ Object
Return the previous Activity before the provided activity.
-
#ref_by_activity(activity) ⇒ Fixnum
Return the reference index of the given FFS_Activity.
-
#rename_activity(activity, name) ⇒ Object
Rename the specified activity and update all HTML pages that contain the name.
-
#restore ⇒ Object
Setup non-persistent variables.
-
#set_activity_attribute(activity, attribute, value) ⇒ Object
Set the specified attribute of the given activity to a new value.
-
#show_in_browser(html_file) ⇒ Object
Launch a web browser and show an HTML file.
-
#show_list_in_browser ⇒ Object
Show the activity list in a web browser.
- #show_monitoring(day) ⇒ Object
-
#successor(activity) ⇒ Object
Return the next Activity after the provided activity.
- #weekly_report(day) ⇒ Object
Methods included from DirUtils
Constructor Details
#initialize(p) ⇒ FitFileStore
Create a new FIT file store.
39 40 41 42 |
# File 'lib/postrunner/FitFileStore.rb', line 39 def initialize(p) super(p) restore end |
Instance Attribute Details
#store ⇒ Object (readonly)
Returns the value of attribute store.
35 36 37 |
# File 'lib/postrunner/FitFileStore.rb', line 35 def store @store end |
#views ⇒ Object (readonly)
Returns the value of attribute views.
35 36 37 |
# File 'lib/postrunner/FitFileStore.rb', line 35 def views @views end |
Class Method Details
.calc_md5_sum(file_name) ⇒ Object
428 429 430 431 432 433 434 |
# File 'lib/postrunner/FitFileStore.rb', line 428 def FitFileStore::calc_md5_sum(file_name) begin Digest::MD5.hexdigest File.read(file_name) rescue IOError return 0 end end |
Instance Method Details
#activities ⇒ Array of FFS_Activity
Returns List of stored activities.
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/postrunner/FitFileStore.rb', line 212 def activities list = [] @store['devices'].each do |id, device| list += device.activities end # Sort the activites by timestamps (newest to oldest). As the list is # composed from multiple devices, there is a small chance of identical # timestamps. To guarantee a stable list, we use the long UID of the # device in cases of identical timestamps. list.sort! do |a1, a2| a1. == a2. ? a1.device.long_uid <=> a2.device.long_uid : a2. <=> a1. end list end |
#add_fit_file(fit_file_name, fit_entity = nil, overwrite = false) ⇒ FFS_Activity or FFS_Monitoring
Add a file to the store.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/postrunner/FitFileStore.rb', line 82 def add_fit_file(fit_file_name, fit_entity = nil, overwrite = false) # If the file hasn't been read yet, read it in as a # Fit4Ruby::Activity or Fit4Ruby::Monitoring entity. unless fit_entity return nil unless (fit_entity = read_fit_file(fit_file_name)) end unless [ Fit4Ruby::Activity, Fit4Ruby::Monitoring_B, Fit4Ruby::Metrics ].include?(fit_entity.class) Log.fatal "Unsupported FIT file type #{fit_entity.class}" end # Generate a String that uniquely identifies the device that generated # the FIT file. id = extract_fit_file_id(fit_entity) long_uid = "#{id[:manufacturer]}-#{id[:product]}-#{id[:serial_number]}" # Make sure the device that created the FIT file is properly registered. device = register_device(long_uid) # Store the FIT entity with the device. entity = device.add_fit_file(fit_file_name, fit_entity, overwrite) # The FIT file might be already stored or invalid. In that case we # abort this method. return nil unless entity if fit_entity.is_a?(Fit4Ruby::Activity) @store['records'].scan_activity_for_records(entity) # Generate HTML file for this activity. entity.generate_html_report # The HTML activity views contain links to their predecessors and # successors. After inserting a new activity, we need to re-generate # these views as well. if (pred = predecessor(entity)) pred.generate_html_report end if (succ = successor(entity)) succ.generate_html_report end # And update the index pages generate_html_index_pages end Log.info "#{File.basename(fit_file_name)} " + 'has been successfully added to archive' entity end |
#change_unit_system ⇒ Object
Perform the necessary report updates after the unit system has been changed.
181 182 183 184 185 186 187 188 189 |
# File 'lib/postrunner/FitFileStore.rb', line 181 def change_unit_system # If we have changed the unit system we need to re-generate all HTML # reports. activities.reverse.each do |activity| activity.generate_html_report end @store['records'].generate_html_reports generate_html_index_pages end |
#check ⇒ Object
This methods checks all stored FIT files for correctness, updates all indexes and re-generates all HTML reports.
320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/postrunner/FitFileStore.rb', line 320 def check records = @store['records'] records.delete_all_records activities.reverse.each do |a| a.check records.scan_activity_for_records(a) a.purge_fit_file end records.generate_html_reports generate_html_index_pages end |
#daily_report(day) ⇒ Object
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
# File 'lib/postrunner/FitFileStore.rb', line 372 def daily_report(day) # 'day' specifies the current day. But we don't know what timezone the # watch was set to for a given date. The files are always named after # the moment of finishing the recording expressed as GMT time. # Each file contains information about the time zone for the specific # file. Recording is always flipped to a new file at midnight GMT but # there are usually multiple files per GMT day. day_as_time = Time.parse(day).gmtime # To get weekly intensity minutes we need 7 days of data prior to the # current date and 1 day after to include the following night. We add # at least 12 extra hours to accomodate time zone changes. monitoring_files = monitorings(day_as_time - 8 * 24 * 60 * 60, day_as_time + 36 * 60 * 60) puts MonitoringStatistics.new(monitoring_files).daily(day) end |
#delete_activity(activity) ⇒ Object
Delete an activity from the database. It will only delete the entry in the database. The original activity file will not be deleted from the file system.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/postrunner/FitFileStore.rb', line 138 def delete_activity(activity) pred = predecessor(activity) succ = successor(activity) activity.device.delete_activity(activity) # The HTML activity views contain links to their predecessors and # successors. After deleting an activity, we need to re-generate these # views. pred.generate_html_report if pred succ.generate_html_report if succ generate_html_index_pages end |
#devices ⇒ Array of FFS_Device
Returns List of registered devices.
207 208 209 |
# File 'lib/postrunner/FitFileStore.rb', line 207 def devices @store['devices'] end |
#find(query) ⇒ Object
Find a specific subset of the activities based on their index.
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/postrunner/FitFileStore.rb', line 277 def find(query) case query when /\A-?\d+$\z/ index = query.to_i # The UI counts the activities from 1 to N. Ruby counts from 0 - # (N-1). if index <= 0 Log.error 'Index must be larger than 0' return [] end # The UI counts the activities from 1 to N. Ruby counts from 0 - # (N-1). if (a = activities[index - 1]) return [ a ] end when /\A-?\d+--?\d+\z/ idxs = query.match(/(?<sidx>-?\d+)-(?<eidx>-?[0-9]+)/) if (sidx = idxs['sidx'].to_i) <= 0 Log.error 'Start index must be larger than 0' return [] end if (eidx = idxs['eidx'].to_i) <= 0 Log.error 'End index must be larger than 0' return [] end if eidx < sidx Log.error 'Start index must be smaller than end index' return [] end # The UI counts the activities from 1 to N. Ruby counts from 0 - # (N-1). unless (as = activities[(sidx - 1)..(eidx - 1)]).empty? return as end else Log.error "Invalid activity query: #{query}" end [] end |
#fit_file_dir(fit_file_base_name, long_uid, type) ⇒ String
Determine the right directory for the given FIT file. The resulting path looks something like /home/user/.postrunner/devices/garmin-fenix3-1234/ activity/5A.
197 198 199 200 201 202 |
# File 'lib/postrunner/FitFileStore.rb', line 197 def fit_file_dir(fit_file_base_name, long_uid, type) # The first letter of the FIT file specifies the creation year. # The second letter of the FIT file specifies the creation month. File.join(@store['config']['devices_dir'], long_uid, type, fit_file_base_name[0..1]) end |
#handle_version_update ⇒ Object
Version upgrade logic.
72 73 74 |
# File 'lib/postrunner/FitFileStore.rb', line 72 def handle_version_update # Nothing here so far. end |
#list_activities ⇒ Object
339 340 341 |
# File 'lib/postrunner/FitFileStore.rb', line 339 def list_activities puts ActivityListView.new(self).to_s end |
#monitorings(start_date, end_date) ⇒ Array of Monitoring_B
Read in all Monitoring_B FIT files that overlap with the given interval.
234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/postrunner/FitFileStore.rb', line 234 def monitorings(start_date, end_date) monitorings = [] @store['devices'].each do |id, device| monitorings += device.monitorings(start_date.gmtime, end_date.gmtime) end monitorings.reverse.map do |m| read_fit_file(File.join(fit_file_dir(m.fit_file_name, m.device.long_uid, 'monitor'), m.fit_file_name)) end end |
#monthly_report(day) ⇒ Object
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
# File 'lib/postrunner/FitFileStore.rb', line 410 def monthly_report(day) # 'day' specifies the current month. It must be in the form of # YYYY-MM-01. But we don't know what timezone the watch was set to for a # given date. The files are always named after the moment of finishing # the recording expressed as GMT time. Each file contains information # about the time zone for the specific file. Recording is always flipped # to a new file at midnight GMT but there are usually multiple files per # GMT day. day_as_time = Time.parse(day).gmtime # To get weekly intensity minutes we need 7 days of data prior to the # current month start and 1 after to inclide the following night. We add # at least 12 extra hours to accomondate time zone changes. monitoring_files = monitorings(day_as_time - 8 * 24 * 60 * 60, day_as_time + 32 * 24 * 60 * 60) puts MonitoringStatistics.new(monitoring_files).monthly(day) end |
#predecessor(activity) ⇒ Object
Return the previous Activity before the provided activity. If none is found, return nil.
267 268 269 270 271 272 273 |
# File 'lib/postrunner/FitFileStore.rb', line 267 def predecessor(activity) all_activities = activities idx = all_activities.index(activity) return nil if idx.nil? # Activities indexes are reversed. The predecessor has a higher index. all_activities[idx + 1] end |
#ref_by_activity(activity) ⇒ Fixnum
Return the reference index of the given FFS_Activity.
250 251 252 253 254 |
# File 'lib/postrunner/FitFileStore.rb', line 250 def ref_by_activity(activity) return nil unless (idx = activities.index(activity)) idx + 1 end |
#rename_activity(activity, name) ⇒ Object
Rename the specified activity and update all HTML pages that contain the name.
157 158 159 160 161 |
# File 'lib/postrunner/FitFileStore.rb', line 157 def rename_activity(activity, name) activity.set('name', name) generate_html_index_pages @store['records'].generate_html_reports if activity.has_records? end |
#restore ⇒ Object
Setup non-persistent variables.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/postrunner/FitFileStore.rb', line 45 def restore @data_dir = @store['config']['data_dir'] # Ensure that we have an Array in the store to hold all known devices. @store['devices'] = @store.new(PEROBS::Hash) unless @store['devices'] @devices_dir = File.join(@data_dir, 'devices') # It's generally not a good idea to store absolute file names in the # database. We'll make an exception here as this is the only way to # propagate this path to FFS_Activity or FFS_Monitoring objects. The # store entry is updated on each program run, so the DB can be moved # safely to another directory. @store['config']['devices_dir'] = @devices_dir create_directory(@devices_dir, 'devices') unless @store['fit_file_md5sums'] @store['fit_file_md5sums'] = @store.new(PEROBS::Array) end # Define which View objects the HTML output will consist of. This # doesn't really belong in this class but for now it's the best place # to put it. @views = ViewButtons.new([ NavButtonDef.new('activities.png', 'index.html'), NavButtonDef.new('record.png', "records-0.html") ]) end |
#set_activity_attribute(activity, attribute, value) ⇒ Object
Set the specified attribute of the given activity to a new value.
167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/postrunner/FitFileStore.rb', line 167 def set_activity_attribute(activity, attribute, value) activity.set(attribute, value) case attribute when 'norecord', 'type' # If we have changed a norecord setting or an activity type, we need # to regenerate all reports and re-collect the record list since we # don't know which Activity needs to replace the changed one. check end generate_html_index_pages end |
#show_in_browser(html_file) ⇒ Object
Launch a web browser and show an HTML file.
345 346 347 348 349 350 351 352 |
# File 'lib/postrunner/FitFileStore.rb', line 345 def show_in_browser(html_file) cmd = "#{ENV['BROWSER'] || 'firefox'} \"#{html_file}\" &" unless system(cmd) Log.fatal "Failed to execute the following shell command: #{$cmd}\n" + "#{$!}" end end |
#show_list_in_browser ⇒ Object
Show the activity list in a web browser.
333 334 335 336 337 |
# File 'lib/postrunner/FitFileStore.rb', line 333 def show_list_in_browser generate_html_index_pages @store['records'].generate_html_reports show_in_browser(File.join(@store['config']['html_dir'], 'index.html')) end |
#show_monitoring(day) ⇒ Object
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/postrunner/FitFileStore.rb', line 354 def show_monitoring(day) # 'day' specifies the current day. But we don't know what timezone the # watch was set to for a given date. The files are always named after # the moment of finishing the recording expressed as GMT time. # Each file contains information about the time zone for the specific # file. Recording is always flipped to a new file at midnight GMT but # there are usually multiple files per GMT day. day_as_time = Time.parse(day).gmtime # To get weekly intensity minutes we need 7 days of data prior to the # current date and 1 day after to include the following night. We add # at least 12 extra hours to accomodate time zone changes. monitoring_files = monitorings(day_as_time - 8 * 24 * 60 * 60, day_as_time + 36 * 60 * 60) show_in_browser(DailyMonitoringView.new(@store, day, monitoring_files). file_name) end |
#successor(activity) ⇒ Object
Return the next Activity after the provided activity. Note that this has a lower index. If none is found, return nil.
258 259 260 261 262 263 |
# File 'lib/postrunner/FitFileStore.rb', line 258 def successor(activity) all_activities = activities idx = all_activities.index(activity) return nil if idx.nil? || idx == 0 all_activities[idx - 1] end |
#weekly_report(day) ⇒ Object
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
# File 'lib/postrunner/FitFileStore.rb', line 389 def weekly_report(day) # 'day' specifies the current week. It must be in the form of # YYYY-MM-DD and references a day in the specific week. But we don't # know what timezone the watch was set to for a given date. The files # are always named after the moment of finishing the recording expressed # as GMT time. Each file contains information about the time zone for # the specific file. Recording is always flipped to a new file at # midnight GMT but there are usually multiple files per # GMT day. day_as_time = Time.parse(day).gmtime start_day = day_as_time - (24 * 60 * 60 * (day_as_time.wday - @store['config']['week_start_day'])) # To get weekly intensity minutes we need 7 days of data prior to the # current month start and 1 after to include the following night. We add # at least 12 extra hours to accomondate time zone changes. monitoring_files = monitorings(start_day - 8 * 24 * 60 * 60, start_day + 8 * 24 * 60 * 60) puts MonitoringStatistics.new(monitoring_files).weekly(start_day) end |