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
433 434 435 436 437 438 439 |
# File 'lib/postrunner/FitFileStore.rb', line 433 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.
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/postrunner/FitFileStore.rb', line 217 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 133 134 135 136 137 |
# File 'lib/postrunner/FitFileStore.rb', line 82 def add_fit_file(fit_file_name, fit_entity = nil, overwrite = false) md5sum = FitFileStore.calc_md5_sum(fit_file_name) if @store['fit_file_md5sums'].include?(md5sum) # The FIT file is already stored in the DB. return nil unless overwrite end # If we 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 ].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.
186 187 188 189 190 191 192 193 194 |
# File 'lib/postrunner/FitFileStore.rb', line 186 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.
325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/postrunner/FitFileStore.rb', line 325 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
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/postrunner/FitFileStore.rb', line 377 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.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/postrunner/FitFileStore.rb', line 143 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.
212 213 214 |
# File 'lib/postrunner/FitFileStore.rb', line 212 def devices @store['devices'] end |
#find(query) ⇒ Object
Find a specific subset of the activities based on their index.
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 317 318 319 320 321 |
# File 'lib/postrunner/FitFileStore.rb', line 282 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.
202 203 204 205 206 207 |
# File 'lib/postrunner/FitFileStore.rb', line 202 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
344 345 346 |
# File 'lib/postrunner/FitFileStore.rb', line 344 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.
239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/postrunner/FitFileStore.rb', line 239 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
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/postrunner/FitFileStore.rb', line 415 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.
272 273 274 275 276 277 278 |
# File 'lib/postrunner/FitFileStore.rb', line 272 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.
255 256 257 258 259 |
# File 'lib/postrunner/FitFileStore.rb', line 255 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.
162 163 164 165 166 |
# File 'lib/postrunner/FitFileStore.rb', line 162 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.
172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/postrunner/FitFileStore.rb', line 172 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.
350 351 352 353 354 355 356 357 |
# File 'lib/postrunner/FitFileStore.rb', line 350 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.
338 339 340 341 342 |
# File 'lib/postrunner/FitFileStore.rb', line 338 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
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/postrunner/FitFileStore.rb', line 359 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.
263 264 265 266 267 268 |
# File 'lib/postrunner/FitFileStore.rb', line 263 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
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 |
# File 'lib/postrunner/FitFileStore.rb', line 394 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 |