My agentic slop goes here. Not intended for anyone else!

own slop

+1
rec-converter/.gitignore
···
+
_build
+4
rec-converter/bin/dune
···
+
(executable
+
(public_name owntracks2clickhouse)
+
(name main)
+
(libraries owntracks_to_clickhouse cmdliner))
+106
rec-converter/bin/main.ml
···
+
open Cmdliner
+
open Owntracks_to_clickhouse
+
+
let process_file input_file output_file =
+
try
+
let records = Owntracks_parser.parse_file input_file in
+
match output_file with
+
| Some out_path ->
+
Clickhouse_formatter.write_jsonl_file out_path records;
+
Printf.printf "Converted %d records from %s to %s\n"
+
(List.length records) input_file out_path
+
| None ->
+
Clickhouse_formatter.print_jsonl records
+
with
+
| Sys_error msg ->
+
Printf.eprintf "Error: %s\n" msg;
+
exit 1
+
| e ->
+
Printf.eprintf "Unexpected error: %s\n" (Printexc.to_string e);
+
exit 1
+
+
let process_directory dir_path output_file recursive =
+
let rec find_rec_files dir =
+
let entries = Sys.readdir dir in
+
Array.fold_left (fun acc entry ->
+
let full_path = Filename.concat dir entry in
+
if Sys.is_directory full_path then
+
if recursive then
+
acc @ find_rec_files full_path
+
else
+
acc
+
else if Filename.check_suffix entry ".rec" then
+
full_path :: acc
+
else
+
acc
+
) [] entries
+
in
+
+
let rec_files = find_rec_files dir_path in
+
let all_records = List.fold_left (fun acc file ->
+
try
+
let records = Owntracks_parser.parse_file file in
+
Printf.printf "Processed %s: %d records\n" file (List.length records);
+
acc @ records
+
with e ->
+
Printf.eprintf "Warning: Failed to process %s: %s\n"
+
file (Printexc.to_string e);
+
acc
+
) [] rec_files in
+
+
match output_file with
+
| Some out_path ->
+
Clickhouse_formatter.write_jsonl_file out_path all_records;
+
Printf.printf "Total: Converted %d records from %d files to %s\n"
+
(List.length all_records) (List.length rec_files) out_path
+
| None ->
+
Clickhouse_formatter.print_jsonl all_records
+
+
let main input output recursive =
+
if Sys.is_directory input then
+
process_directory input output recursive
+
else
+
process_file input output
+
+
let input_arg =
+
let doc = "Input OwnTracks .rec file or directory containing .rec files" in
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"INPUT" ~doc)
+
+
let output_arg =
+
let doc = "Output JSON lines file (if not specified, outputs to stdout)" in
+
Arg.(value & opt (some string) None & info ["o"; "output"] ~docv:"OUTPUT" ~doc)
+
+
let recursive_arg =
+
let doc = "Recursively process directories for .rec files" in
+
Arg.(value & flag & info ["r"; "recursive"] ~doc)
+
+
let main_term =
+
Term.(const main $ input_arg $ output_arg $ recursive_arg)
+
+
let info =
+
let doc = "Convert OwnTracks .rec files to ClickHouse JSON lines" in
+
let man = [
+
`S Manpage.s_description;
+
`P "$(tname) converts OwnTracks recorder files (.rec) to JSON lines format suitable for importing into ClickHouse with geo data types.";
+
`P "Each location record is converted to a JSON object with the following fields:";
+
`P "- timestamp: ISO 8601 formatted timestamp";
+
`P "- timestamp_epoch: Unix timestamp";
+
`P "- point: [longitude, latitude] array for ClickHouse Point type";
+
`P "- latitude, longitude: Individual coordinate fields";
+
`P "- altitude, accuracy, battery: Optional fields from OwnTracks";
+
`P "- tracker_id: Device identifier from OwnTracks";
+
`S Manpage.s_examples;
+
`P "Convert a single file to stdout:";
+
`Pre " $(tname) path/to/file.rec";
+
`P "Convert a single file to output file:";
+
`Pre " $(tname) path/to/file.rec -o output.jsonl";
+
`P "Process all .rec files in a directory:";
+
`Pre " $(tname) path/to/directory -o output.jsonl";
+
`P "Process directory recursively:";
+
`Pre " $(tname) path/to/directory -r -o output.jsonl";
+
] in
+
Cmd.info "owntracks2clickhouse" ~version:"1.0.0" ~doc ~man
+
+
let cmd = Cmd.v info main_term
+
+
let () = exit (Cmd.eval cmd)
+14
rec-converter/dune-project
···
+
(lang dune 3.0)
+
(name owntracks_to_clickhouse)
+
+
(generate_opam_files true)
+
+
(package
+
(name owntracks_to_clickhouse)
+
(synopsis "Convert OwnTracks .rec files to ClickHouse JSON lines")
+
(description "A library and CLI tool to convert OwnTracks recorder files to JSON lines suitable for ClickHouse import with geo data types")
+
(depends
+
ocaml
+
dune
+
ezjsonm
+
cmdliner))
+54
rec-converter/example.sh
···
+
#!/bin/bash
+
+
# Build the project
+
dune build
+
+
# Example 1: Convert a single .rec file to stdout
+
echo "Converting single file to stdout:"
+
dune exec owntracks2clickhouse avsm/avsm-ip15/2025-08.rec | head -5
+
+
# Example 2: Convert a single .rec file to output file
+
echo -e "\nConverting single file to output.jsonl:"
+
dune exec owntracks2clickhouse avsm/avsm-ip15/2025-08.rec -o single_output.jsonl
+
echo "Created single_output.jsonl"
+
+
# Example 3: Process all .rec files in a directory recursively
+
echo -e "\nProcessing all .rec files recursively:"
+
dune exec owntracks2clickhouse avsm -r -o all_records.jsonl
+
+
# Example 4: Create ClickHouse table and import data
+
cat << 'EOF'
+
+
To import into ClickHouse, create a table like this:
+
+
CREATE TABLE owntracks_locations (
+
timestamp DateTime64(3),
+
timestamp_epoch UInt32,
+
point Point,
+
latitude Float64,
+
longitude Float64,
+
altitude Nullable(Float64),
+
accuracy Nullable(Float64),
+
battery Nullable(UInt8),
+
tracker_id Nullable(String)
+
) ENGINE = MergeTree()
+
ORDER BY (tracker_id, timestamp);
+
+
Then import the JSON lines file:
+
+
clickhouse-client --query="INSERT INTO owntracks_locations FORMAT JSONEachRow" < all_records.jsonl
+
+
Or using clickhouse-local for testing:
+
+
clickhouse-local --query="
+
SELECT
+
tracker_id,
+
toDate(timestamp) as date,
+
count() as points,
+
round(avg(battery), 2) as avg_battery
+
FROM file('all_records.jsonl', 'JSONEachRow')
+
GROUP BY tracker_id, date
+
ORDER BY date DESC
+
LIMIT 10
+
"
+
EOF
+43
rec-converter/lib/clickhouse_formatter.ml
···
+
open Owntracks_parser
+
+
let option_to_json = function
+
| Some v -> v
+
| None -> `Null
+
+
let location_to_clickhouse_json record =
+
let point = `A [`Float record.lon; `Float record.lat] in
+
let timestamp_epoch = record.tst in
+
let timestamp_iso = record.timestamp in
+
+
`O [
+
("timestamp", `String timestamp_iso);
+
("timestamp_epoch", `Float (float_of_int timestamp_epoch));
+
("point", point);
+
("latitude", `Float record.lat);
+
("longitude", `Float record.lon);
+
("altitude", option_to_json (Option.map (fun x -> `Float x) record.alt));
+
("accuracy", option_to_json (Option.map (fun x -> `Float x) record.acc));
+
("battery", option_to_json (Option.map (fun x -> `Float (float_of_int x)) record.batt));
+
("tracker_id", option_to_json (Option.map (fun x -> `String x) record.tid));
+
]
+
+
let to_jsonl records =
+
List.map (fun record ->
+
let json = location_to_clickhouse_json record in
+
Ezjsonm.to_string json
+
) records
+
+
let write_jsonl_file path records =
+
let oc = open_out path in
+
List.iter (fun record ->
+
let json = location_to_clickhouse_json record in
+
output_string oc (Ezjsonm.to_string json);
+
output_char oc '\n'
+
) records;
+
close_out oc
+
+
let print_jsonl records =
+
List.iter (fun record ->
+
let json = location_to_clickhouse_json record in
+
print_endline (Ezjsonm.to_string json)
+
) records
+4
rec-converter/lib/dune
···
+
(library
+
(public_name owntracks_to_clickhouse)
+
(name owntracks_to_clickhouse)
+
(libraries ezjsonm))
+78
rec-converter/lib/owntracks_parser.ml
···
+
type location_record = {
+
timestamp: string;
+
lat: float;
+
lon: float;
+
alt: float option;
+
acc: float option;
+
batt: int option;
+
tid: string option;
+
tst: int;
+
}
+
+
let parse_timestamp line =
+
match String.split_on_char '\t' line with
+
| timestamp :: _ -> Some timestamp
+
| [] -> None
+
+
let parse_json_payload line =
+
match String.split_on_char '\t' line with
+
| _ :: _ :: json :: _ ->
+
(try Some (Ezjsonm.from_string json) with _ -> None)
+
| _ -> None
+
+
let get_string json key =
+
try Some (Ezjsonm.get_string (Ezjsonm.find json [key]))
+
with _ -> None
+
+
let get_float json key =
+
try Some (Ezjsonm.get_float (Ezjsonm.find json [key]))
+
with _ -> None
+
+
let get_int json key =
+
try Some (Ezjsonm.get_int (Ezjsonm.find json [key]))
+
with _ -> None
+
+
let extract_location_from_json json =
+
try
+
let dict = Ezjsonm.get_dict json in
+
match List.assoc_opt "_type" dict with
+
| Some (`String "location") ->
+
let lat = Ezjsonm.get_float (List.assoc "lat" dict) in
+
let lon = Ezjsonm.get_float (List.assoc "lon" dict) in
+
let tst = Ezjsonm.get_int (List.assoc "tst" dict) in
+
Some {
+
timestamp = "";
+
lat;
+
lon;
+
alt = get_float json "alt";
+
acc = get_float json "acc";
+
batt = get_int json "batt";
+
tid = get_string json "tid";
+
tst;
+
}
+
| _ -> None
+
with _ -> None
+
+
let parse_line line =
+
match parse_timestamp line, parse_json_payload line with
+
| Some timestamp, Some json ->
+
(match extract_location_from_json json with
+
| Some record -> Some { record with timestamp }
+
| None -> None)
+
| _ -> None
+
+
let parse_file path =
+
let ic = open_in path in
+
let rec read_lines acc =
+
try
+
let line = input_line ic in
+
let acc' = match parse_line line with
+
| Some record -> record :: acc
+
| None -> acc
+
in
+
read_lines acc'
+
with End_of_file ->
+
close_in ic;
+
List.rev acc
+
in
+
read_lines []
+26
rec-converter/owntracks_to_clickhouse.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "Convert OwnTracks .rec files to ClickHouse JSON lines"
+
description:
+
"A library and CLI tool to convert OwnTracks recorder files to JSON lines suitable for ClickHouse import with geo data types"
+
depends: [
+
"ocaml"
+
"dune" {>= "3.0"}
+
"ezjsonm"
+
"cmdliner"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]