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(from_version, to_version) ⇒ 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
482 483 484 485 486 487 488 |
# File 'lib/postrunner/FitFileStore.rb', line 482 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.
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/postrunner/FitFileStore.rb', line 266 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.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/postrunner/FitFileStore.rb', line 132 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. unless (id = extract_fit_file_id(fit_entity)) return nil end long_uid = "#{id[:numeric_manufacturer]}-" + "#{id[:numeric_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.
234 235 236 237 238 239 240 241 242 |
# File 'lib/postrunner/FitFileStore.rb', line 234 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.
374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/postrunner/FitFileStore.rb', line 374 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
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 |
# File 'lib/postrunner/FitFileStore.rb', line 426 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.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/postrunner/FitFileStore.rb', line 191 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.
261 262 263 |
# File 'lib/postrunner/FitFileStore.rb', line 261 def devices @store['devices'] end |
#find(query) ⇒ Object
Find a specific subset of the activities based on their index.
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/postrunner/FitFileStore.rb', line 331 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.
251 252 253 254 255 256 |
# File 'lib/postrunner/FitFileStore.rb', line 251 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(from_version, to_version) ⇒ Object
Version upgrade logic.
72 73 74 75 76 77 78 79 80 81 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 |
# File 'lib/postrunner/FitFileStore.rb', line 72 def handle_version_update(from_version, to_version) if from_version <= Gem::Version.new('0.12.0') # PostRunner up until version 0.12.0 was using a long_uid with # manufacturer name and product name. This was a bad idea since unknown # devices were resolved to their numerical ID. In case the unknown ID # was later added to the dictionary in fit4ruby version update, it # resolved to its name and the device was recognized as a new device. # Versions after 0.12.0 only use the numerical versions for the device # long_uid and directory names. uid_remap = {} @store['devices'].each do |uid, device| old_uid = uid if (first_activity = device.activities.first) first_activity.load_fit_file if (fit_activity = first_activity.fit_activity) if (device_info = fit_activity.device_infos.first) new_uid = "#{device_info.numeric_manufacturer}-" + "#{device_info.numeric_product}-#{device_info.serial_number}" uid_remap[old_uid] = new_uid puts first_activity.fit_file_name end end end end @store.transaction do pwd = Dir.pwd base_dir_name = @store['config']['devices_dir'] Dir.chdir(base_dir_name) uid_remap.each do |old_uid, new_uid| if Dir.exist?(old_uid) && !Dir.exist?(new_uid) && !File.symlink?(old_uid) # Rename the directory from the old (string) scheme to the # new numeric scheme. FileUtils.mv(old_uid, new_uid) # Create a symbolic link with that points the old name to # the new name. File.symlink(new_uid, old_uid) end # Now update the long_uid in the FFS_Device object @store['devices'][new_uid] = device = @store['devices'][old_uid] device.long_uid = new_uid @store['devices'].delete(old_uid) end Dir.chdir(pwd) end end end |
#list_activities ⇒ Object
393 394 395 |
# File 'lib/postrunner/FitFileStore.rb', line 393 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.
288 289 290 291 292 293 294 295 296 297 298 |
# File 'lib/postrunner/FitFileStore.rb', line 288 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
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 |
# File 'lib/postrunner/FitFileStore.rb', line 464 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.
321 322 323 324 325 326 327 |
# File 'lib/postrunner/FitFileStore.rb', line 321 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.
304 305 306 307 308 |
# File 'lib/postrunner/FitFileStore.rb', line 304 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.
210 211 212 213 214 |
# File 'lib/postrunner/FitFileStore.rb', line 210 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 a Hash 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.
220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/postrunner/FitFileStore.rb', line 220 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.
399 400 401 402 403 404 405 406 |
# File 'lib/postrunner/FitFileStore.rb', line 399 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.
387 388 389 390 391 |
# File 'lib/postrunner/FitFileStore.rb', line 387 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
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/postrunner/FitFileStore.rb', line 408 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.
312 313 314 315 316 317 |
# File 'lib/postrunner/FitFileStore.rb', line 312 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
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/postrunner/FitFileStore.rb', line 443 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 |