Merge pull request #328 from Freika/fix/support-owntracks-rec-file

Fix/support owntracks rec file
This commit is contained in:
Evgenii Burmakin 2024-10-16 11:11:33 +02:00 committed by GitHub
commit 6aaeb35ad5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 148 additions and 396 deletions

View file

@ -1 +1 @@
0.15.3
0.15.4

View file

@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# 0.15.4 - 2024-10-15
### Added
- Owntracks' .rec files now can be imported to Dawarich. The import process is the same as for other kinds of files, just select the .rec file and choose "owntracks" as a source.
### Removed
- Owntracks' .json files are no longer supported for import as Owntracks itself does not export to this format anymore.
# 0.15.3 - 2024-10-05
To expose the watcher functionality to the user, a new directory `/tmp/imports/watched/` was created. Add new volume to the `docker-compose.yml` file to expose this directory to the host machine.

File diff suppressed because one or more lines are too long

View file

@ -30,7 +30,12 @@ class ImportsController < ApplicationController
file = File.read(file)
raw_data = params[:import][:source] == 'gpx' ? Hash.from_xml(file) : JSON.parse(file)
raw_data =
case params[:import][:source]
when 'gpx' then Hash.from_xml(file)
when 'owntracks' then OwnTracks::RecParser.new(file).call
else JSON.parse(file)
end
import.update(raw_data:)
import.id

View file

@ -1,16 +1,16 @@
# frozen_string_literal: true
class OwnTracks::ExportParser
attr_reader :import, :json, :user_id
attr_reader :import, :data, :user_id
def initialize(import, user_id)
@import = import
@json = import.raw_data
@data = import.raw_data
@user_id = user_id
end
def call
points_data = parse_json
points_data = parse_data
points_data.each do |point_data|
next if Point.exists?(
@ -31,15 +31,9 @@ class OwnTracks::ExportParser
private
def parse_json
points = []
def parse_data
json = OwnTracks::RecParser.new(data).call
json.each_key do |user|
json[user].each_key do |devise|
json[user][devise].each { |point| points << OwnTracks::Params.new(point).call }
end
end
points
json.map { |point| OwnTracks::Params.new(point).call }
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class OwnTracks::RecParser
attr_reader :file
def initialize(file)
@file = file
end
def call
file.split("\n").map do |line|
JSON.parse(line.split("\t* \t")[1])
end
end
end

View file

@ -19,7 +19,7 @@
<%= form.radio_button :source, :owntracks, class: "radio radio-primary" %>
<span class="label-text">Owntracks</span>
</label>
<p class="text-sm mt-2">A JSON file you exported by pressing Download button in top right corner of OwnTracks web interface</p>
<p class="text-sm mt-2">A .REC file you could find in your volumes/owntracks-recorder/store/rec/USER/TOPIC directory</p>
</div>
</div>
<div class="card bordered shadow-lg p-3 hover:shadow-blue-500/50">

View file

@ -4,7 +4,7 @@ FactoryBot.define do
factory :import do
user
name { 'APRIL_2013.json' }
source { 1 }
raw_data { JSON.parse(File.read('spec/fixtures/files/owntracks/export.json')) }
source { Import.sources[:owntracks] }
raw_data { File.read('spec/fixtures/files/owntracks/2024-03.rec') }
end
end

View file

@ -0,0 +1,10 @@
2024-03-01T09:03:09Z * {"bs":2,"p":100.266,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.332,"vel":0,"t":"p","BSSID":"b0:f2:8:45:94:33","SSID":"Home Wifi","conn":"w","vac":4,"acc":10,"tst":1709283789,"lat":52.225,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T17:46:02Z * {"bs":1,"p":100.28,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.333,"t":"p","vel":0,"BSSID":"b0:f2:8:45:94:33","conn":"w","SSID":"Home Wifi","vac":3,"cog":98,"acc":9,"tst":1709315162,"lat":52.226,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T18:26:55Z * {"lon":13.334,"acc":5,"wtst":1696359532,"event":"leave","rid":"5f1d1b","desc":"home","topic":"owntracks/test/iPhone 12 Pro/event","lat":52.227,"t":"c","tst":1709317615,"tid":"RO","_type":"transition","_http":true}
2024-03-01T18:26:55Z * {"cog":40,"batt":85,"lon":13.335,"acc":5,"bs":1,"p":100.279,"vel":3,"vac":3,"lat":52.228,"topic":"owntracks/test/iPhone 12 Pro","t":"c","conn":"m","m":1,"tst":1709317615,"alt":36,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:28:30Z * {"cog":38,"batt":85,"lon":13.336,"acc":5,"bs":1,"p":100.349,"vel":3,"vac":3,"lat":52.229,"topic":"owntracks/test/iPhone 12 Pro","t":"v","conn":"m","m":1,"tst":1709317710,"alt":35,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:33:03Z * {"cog":18,"batt":85,"lon":13.337,"acc":5,"bs":1,"p":100.347,"vel":4,"vac":3,"lat":52.230,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709317983,"alt":36,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:40:11Z * {"cog":43,"batt":85,"lon":13.338,"acc":5,"bs":1,"p":100.348,"vel":6,"vac":3,"lat":52.231,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709318411,"alt":37,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:42:57Z * {"cog":320,"batt":85,"lon":13.339,"acc":5,"bs":1,"p":100.353,"vel":3,"vac":3,"lat":52.232,"topic":"owntracks/test/iPhone 12 Pro","t":"v","conn":"m","m":1,"tst":1709318577,"alt":37,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:40:11Z * {"cog":43,"batt":85,"lon":13.338,"acc":5,"bs":1,"p":100.348,"vel":6,"vac":3,"lat":52.231,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709318411,"alt":37,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:40:11Z * {"cog":43,"batt":85,"lon":13.341,"acc":5,"bs":1,"p":100.348,"created_at":1709318940,"vel":6,"vac":3,"lat":52.234,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709318411,"alt":37,"_type":"location","tid":"RO","_http":true}

View file

@ -1,275 +0,0 @@
{
"test": {
"iphone-12-pro": [
{
"batt": 85,
"lon": -74.0060,
"acc": 8,
"bs": 2,
"inrids": [
"5f1d1b"
],
"BSSID": "b0:f2:8:45:94:33",
"SSID": "Home Wifi",
"vac": 3,
"inregions": [
"home"
],
"lat": 40.7128,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "p",
"conn": "w",
"m": 1,
"tst": 1706965203,
"alt": 41,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d773",
"isorcv": "2024-02-03T13:00:03Z",
"isotst": "2024-02-03T13:00:03Z",
"disptst": "2024-02-03 13:00:03"
},
{
"batt": 89,
"lon": -0.1278,
"acc": 8,
"bs": 1,
"inrids": [
"5f1d1b"
],
"p": 101.233,
"BSSID": "b0:f2:8:45:94:33",
"SSID": "Home Wifi",
"vac": 15,
"inregions": [
"home"
],
"lat": 51.5074,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "p",
"conn": "w",
"m": 1,
"tst": 1706967037,
"alt": 36,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d773",
"isorcv": "2024-02-03T13:30:37Z",
"isotst": "2024-02-03T13:30:37Z",
"disptst": "2024-02-03 13:30:37"
},
{
"cog": 11,
"batt": 94,
"lon": 139.6917,
"acc": 5,
"bs": 1,
"p": 101.318,
"vel": 3,
"vac": 3,
"lat": 35.6895,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "v",
"conn": "m",
"m": 1,
"tst": 1706971835,
"alt": 35,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d77d",
"isorcv": "2024-02-03T14:50:35Z",
"isotst": "2024-02-03T14:50:35Z",
"disptst": "2024-02-03 14:50:35"
},
{
"cog": 11,
"batt": 94,
"lon": 151.2093,
"acc": 5,
"bs": 1,
"p": 101.318,
"vel": 3,
"vac": 3,
"lat": -33.8688,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "c",
"conn": "m",
"m": 1,
"tst": 1706971835,
"alt": 35,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d77d",
"isorcv": "2024-02-03T14:50:35Z",
"isotst": "2024-02-03T14:50:35Z",
"disptst": "2024-02-03 14:50:35"
},
{
"cog": 47,
"batt": 94,
"lon": 2.3522,
"acc": 4,
"bs": 1,
"inrids": [
"5f1d1b"
],
"p": 101.326,
"vel": 4,
"vac": 4,
"inregions": [
"home"
],
"lat": 48.8566,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "v",
"conn": "m",
"m": 1,
"tst": 1706974182,
"alt": 42,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d773",
"isorcv": "2024-02-03T15:29:42Z",
"isotst": "2024-02-03T15:29:42Z",
"disptst": "2024-02-03 15:29:42"
},
{
"batt": 94,
"lon": -43.1729,
"acc": 8,
"bs": 1,
"inrids": [
"5f1d1b"
],
"p": 101.253,
"BSSID": "b0:f2:8:45:94:33",
"SSID": "Home Wifi",
"vac": 11,
"inregions": [
"home"
],
"lat": -22.9068,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "p",
"conn": "w",
"m": 1,
"tst": 1706974302,
"alt": 37,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d773",
"isorcv": "2024-02-03T15:31:42Z",
"isotst": "2024-02-03T15:31:42Z",
"disptst": "2024-02-03 15:31:42"
},
{
"batt": 94,
"lon": -43.1729,
"acc": 8,
"bs": 1,
"inrids": [
"5f1d1b"
],
"p": 101.256,
"BSSID": "b0:f2:8:45:94:33",
"SSID": "Home Wifi",
"vac": 17,
"inregions": [
"home"
],
"lat": -22.9068,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "p",
"conn": "w",
"m": 1,
"tst": 1706977057,
"alt": 36,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d773",
"isorcv": "2024-02-03T16:17:37Z",
"isotst": "2024-02-03T16:17:37Z",
"disptst": "2024-02-03 16:17:37"
},
{
"cog": 43,
"batt": 89,
"lon": 37.6176,
"acc": 5,
"bs": 1,
"p": 101.349,
"vel": 5,
"vac": 3,
"lat": 55.7558,
"topic": "owntracks/test/iPhone 12 Pro",
"t": "v",
"conn": "m",
"m": 1,
"tst": 1706981399,
"alt": 38,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d7ku",
"isorcv": "2024-02-03T17:29:59Z",
"isotst": "2024-02-03T17:29:59Z",
"disptst": "2024-02-03 17:29:59"
},
{
"cog": 53,
"batt": 89,
"lon": 37.6176,
"acc": 5,
"bs": 1,
"p": 101.353,
"vel": 5,
"vac": 3,
"lat": 55.7558,
"topic": "owntracks/test/iPhone 12 Pro",
"conn": "m",
"m": 1,
"tst": 1706981451,
"alt": 37,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d7kv",
"isorcv": "2024-02-03T17:30:51Z",
"isotst": "2024-02-03T17:30:51Z",
"disptst": "2024-02-03 17:30:51"
},
{
"cog": 53,
"batt": 89,
"lon": 37.6176,
"acc": 5,
"bs": 1,
"p": 101.359,
"created_at": 1706981777,
"vel": 5,
"vac": 3,
"lat": 55.7558,
"topic": "owntracks/test/iPhone 12 Pro",
"conn": "m",
"m": 1,
"tst": 1706981451,
"alt": 37,
"_type": "location",
"tid": "RO",
"_http": true,
"ghash": "u33d7kv",
"isorcv": "2024-02-03T17:30:51Z",
"isotst": "2024-02-03T17:30:51Z",
"disptst": "2024-02-03 17:30:51"
}
]
}
}

View file

@ -41,7 +41,7 @@ RSpec.describe 'Imports', type: :request do
before { sign_in user }
context 'when importing owntracks data' do
let(:file) { fixture_file_upload('owntracks/export.json', 'application/json') }
let(:file) { fixture_file_upload('owntracks/2024-03.rec', 'text/plain') }
it 'queues import job' do
expect do

View file

@ -18,21 +18,21 @@ RSpec.describe OwnTracks::ExportParser do
parser
expect(Point.first.attributes).to include(
'latitude' => 40.7128,
'longitude' => -74.006,
'latitude' => 52.225,
'longitude' => 13.332,
'battery_status' => 'charging',
'battery' => 85,
'ping' => nil,
'altitude' => 41,
'accuracy' => 8,
'vertical_accuracy' => 3,
'velocity' => nil,
'battery' => 94,
'ping' => '100.266',
'altitude' => 36,
'accuracy' => 10,
'vertical_accuracy' => 4,
'velocity' => '0',
'connection' => 'wifi',
'ssid' => 'Home Wifi',
'bssid' => 'b0:f2:8:45:94:33',
'trigger' => 'background_event',
'tracker_id' => 'RO',
'timestamp' => 1_706_965_203,
'timestamp' => 1709283789,
'inrids' => ['5f1d1b'],
'in_regions' => ['home'],
'topic' => 'owntracks/test/iPhone 12 Pro',
@ -40,29 +40,27 @@ RSpec.describe OwnTracks::ExportParser do
'user_id' => user.id,
'country' => nil,
'raw_data' => {
'batt' => 85,
'lon' => -74.006,
'acc' => 8,
'bs' => 2,
'inrids' => ['5f1d1b'],
'BSSID' => 'b0:f2:8:45:94:33',
'SSID' => 'Home Wifi',
'vac' => 3,
'inregions' => ['home'],
'lat' => 40.7128,
'topic' => 'owntracks/test/iPhone 12 Pro',
't' => 'p',
'conn' => 'w',
'm' => 1,
'tst' => 1_706_965_203,
'alt' => 41,
'_type' => 'location',
'tid' => 'RO',
'_http' => true,
'ghash' => 'u33d773',
'isorcv' => '2024-02-03T13:00:03Z',
'isotst' => '2024-02-03T13:00:03Z',
'disptst' => '2024-02-03 13:00:03'
'm'=>1,
'p'=>100.266,
't'=>'p',
'bs'=>2,
'acc'=>10,
'alt'=>36,
'lat'=>52.225,
'lon'=>13.332,
'tid'=>'RO',
'tst'=>1709283789,
'vac'=>4,
'vel'=>0,
'SSID'=>'Home Wifi',
'batt'=>94,
'conn'=>'w',
'BSSID'=>'b0:f2:8:45:94:33',
'_http'=>true,
'_type'=>'location',
'topic'=>'owntracks/test/iPhone 12 Pro',
'inrids'=>['5f1d1b'],
'inregions'=>['home']
}
)
end

View file

@ -6,58 +6,52 @@ RSpec.describe OwnTracks::Params do
describe '#call' do
subject(:params) { described_class.new(raw_point_params).call }
let(:file_path) { 'spec/fixtures/files/owntracks/export.json' }
let(:file) { File.open(file_path) }
let(:json) { JSON.parse(file.read) }
let(:user) { json.keys.first }
let(:topic) { json[user].keys.first }
let(:raw_point_params) { json[user][topic].first }
let(:file_path) { 'spec/fixtures/files/owntracks/2024-03.rec' }
let(:file) { File.read(file_path) }
let(:json) { OwnTracks::RecParser.new(file).call }
let(:raw_point_params) { json.first }
let(:expected_json) do
{
latitude: 40.7128,
longitude: -74.006,
battery_status: 'charging',
battery: 85,
ping: nil,
altitude: 41,
accuracy: 8,
vertical_accuracy: 3,
velocity: nil,
connection: 'wifi',
latitude: 52.225,
longitude: 13.332,
battery: 94,
ping: 100.266,
altitude: 36,
accuracy: 10,
vertical_accuracy: 4,
velocity: 0,
ssid: 'Home Wifi',
bssid: 'b0:f2:8:45:94:33',
trigger: 'background_event',
tracker_id: 'RO',
timestamp: 1_706_965_203,
timestamp: 1_709_283_789,
inrids: ['5f1d1b'],
in_regions: ['home'],
topic: 'owntracks/test/iPhone 12 Pro',
raw_data: {
'batt' => 85,
'lon' => -74.006,
'acc' => 8,
'bs' => 2,
'inrids' => ['5f1d1b'],
'BSSID' => 'b0:f2:8:45:94:33',
'SSID' => 'Home Wifi',
'vac' => 3,
'inregions' => ['home'],
'lat' => 40.7128,
'topic' => 'owntracks/test/iPhone 12 Pro',
't' => 'p',
'conn' => 'w',
'm' => 1,
'tst' => 1_706_965_203,
'alt' => 41,
battery_status: 'charging',
connection: 'wifi',
trigger: 'background_event',
raw_data: { 'bs' => 2,
'p' => 100.266,
'batt' => 94,
'_type' => 'location',
'tid' => 'RO',
'_http' => true,
'ghash' => 'u33d773',
'isorcv' => '2024-02-03T13:00:03Z',
'isotst' => '2024-02-03T13:00:03Z',
'disptst' => '2024-02-03 13:00:03'
}
'topic' => 'owntracks/test/iPhone 12 Pro',
'alt' => 36,
'lon' => 13.332,
'vel' => 0,
't' => 'p',
'BSSID' => 'b0:f2:8:45:94:33',
'SSID' => 'Home Wifi',
'conn' => 'w',
'vac' => 4,
'acc' => 10,
'tst' => 1_709_283_789,
'lat' => 52.225,
'm' => 1,
'inrids' => ['5f1d1b'],
'inregions' => ['home'],
'_http' => true }
}
end

View file

@ -4,23 +4,24 @@ require 'rails_helper'
RSpec.describe Visits::Prepare do
describe '#call' do
let(:static_time) { Time.zone.local(2021, 1, 1, 0, 0, 0) }
let(:points) do
[
build(:point, latitude: 0, longitude: 0, timestamp: 1.day.ago),
build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: 1.day.ago + 5.minutes),
build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: 1.day.ago + 10.minutes),
build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: 1.day.ago + 15.minutes),
build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: 1.day.ago + 20.minutes),
build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: 1.day.ago + 25.minutes),
build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: 1.day.ago + 30.minutes),
build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: 1.day.ago + 35.minutes),
build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: 1.day.ago + 40.minutes),
build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: 1.day.ago + 45.minutes),
build(:point, latitude: 0.0001, longitude: 0.0001, timestamp: 1.day.ago + 50.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 55.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 95.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 100.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 105.minutes)
build(:point, latitude: 0, longitude: 0, timestamp: static_time),
build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: static_time + 5.minutes),
build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: static_time + 10.minutes),
build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: static_time + 15.minutes),
build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: static_time + 20.minutes),
build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: static_time + 25.minutes),
build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: static_time + 30.minutes),
build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: static_time + 35.minutes),
build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: static_time + 40.minutes),
build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: static_time + 45.minutes),
build(:point, latitude: 0.0001, longitude: 0.0001, timestamp: static_time + 50.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 55.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 95.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 100.minutes),
build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 105.minutes)
]
end
@ -29,7 +30,7 @@ RSpec.describe Visits::Prepare do
it 'returns correct visits' do
expect(subject).to eq [
{
date: 1.day.ago.to_date.to_s,
date: static_time.to_date.to_s,
visits: [
{
latitude: 0.0,
@ -37,8 +38,8 @@ RSpec.describe Visits::Prepare do
radius: 10,
points:,
duration: 105,
started_at: 1.day.ago.to_s,
ended_at: (1.day.ago + 105.minutes).to_s
started_at: static_time.to_s,
ended_at: (static_time + 105.minutes).to_s
}
]
}

View file

@ -24,7 +24,7 @@ describe 'OwnTracks Points API', type: :request do
't': 'p',
'conn': 'w',
'm': 1,
'tst': 1706965203,
'tst': 1_706_965_203,
'alt': 41,
'_type': 'location',
'tid': 'RO',
@ -69,20 +69,20 @@ describe 'OwnTracks Points API', type: :request do
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
response '200', 'Point created' do
let(:file_path) { 'spec/fixtures/files/owntracks/export.json' }
let(:file) { File.open(file_path) }
let(:json) { JSON.parse(file.read) }
let(:point) { json['test']['iphone-12-pro'].first }
let(:file_path) { 'spec/fixtures/files/owntracks/2024-03.rec' }
let(:file) { File.read(file_path) }
let(:json) { OwnTracks::RecParser.new(file).call }
let(:point) { json.first }
let(:api_key) { create(:user).api_key }
run_test!
end
response '401', 'Unauthorized' do
let(:file_path) { 'spec/fixtures/files/owntracks/export.json' }
let(:file) { File.open(file_path) }
let(:json) { JSON.parse(file.read) }
let(:point) { json['test']['iphone-12-pro'].first }
let(:file_path) { 'spec/fixtures/files/owntracks/2024-03.rec' }
let(:file) { File.read(file_path) }
let(:json) { OwnTracks::RecParser.new(file).call }
let(:point) { json.first }
let(:api_key) { nil }
run_test!