···4646 devices : device list;
4747 }
48484949- let empty_owntracks = {
5050- topic = None;
5151- default_device = None;
5252- recorder = empty_recorder;
5353- devices = [];
5454- }
4949+ let empty_owntracks =
5050+ {
5151+ topic = None;
5252+ default_device = None;
5353+ recorder = empty_recorder;
5454+ devices = [];
5555+ }
55565657 let owntracks_codec : owntracks Tomlt.t =
5758 Tomlt.(
5859 Table.(
5960 obj (fun topic default_device recorder devices ->
6060- { topic; default_device;
6161+ {
6262+ topic;
6363+ default_device;
6164 recorder = Option.value ~default:empty_recorder recorder;
6262- devices = Option.value ~default:[] devices })
6565+ devices = Option.value ~default:[] devices;
6666+ })
6367 |> opt_mem "topic" string ~enc:(fun c -> c.topic)
6468 |> opt_mem "default_device" string ~enc:(fun c -> c.default_device)
6565- |> opt_mem "recorder" recorder_codec
6666- ~enc:(fun c -> if c.recorder = empty_recorder then None else Some c.recorder)
6767- |> opt_mem "devices" (list device_codec)
6868- ~enc:(fun c -> match c.devices with [] -> None | ds -> Some ds)
6969+ |> opt_mem "recorder" recorder_codec ~enc:(fun c ->
7070+ if c.recorder = empty_recorder then None else Some c.recorder)
7171+ |> opt_mem "devices" (list device_codec) ~enc:(fun c ->
7272+ match c.devices with [] -> None | ds -> Some ds)
6973 |> finish))
70747175 type t = {
···8892 {
8993 owntracks = Option.value ~default:empty_owntracks owntracks;
9094 mqtt =
9191- Option.value ~default:Mqtte_cmd.Config_file.empty_mqtt_config mqtt;
9595+ Option.value ~default:Mqtte_cmd.Config_file.empty_mqtt_config
9696+ mqtt;
9297 pool =
9393- Option.value ~default:Mqtte_cmd.Config_file.empty_pool_config pool;
9898+ Option.value ~default:Mqtte_cmd.Config_file.empty_pool_config
9999+ pool;
94100 })
9595- |> opt_mem "owntracks" owntracks_codec
9696- ~enc:(fun c ->
9797- if c.owntracks = empty_owntracks then None else Some c.owntracks)
9898- |> opt_mem "mqtt" Mqtte_cmd.Config_file.mqtt_codec
9999- ~enc:(fun c ->
100100- if c.mqtt = Mqtte_cmd.Config_file.empty_mqtt_config then None
101101- else Some c.mqtt)
102102- |> opt_mem "pool" Mqtte_cmd.Config_file.pool_codec
103103- ~enc:(fun c ->
104104- if c.pool = Mqtte_cmd.Config_file.empty_pool_config then None
105105- else Some c.pool)
101101+ |> opt_mem "owntracks" owntracks_codec ~enc:(fun c ->
102102+ if c.owntracks = empty_owntracks then None else Some c.owntracks)
103103+ |> opt_mem "mqtt" Mqtte_cmd.Config_file.mqtt_codec ~enc:(fun c ->
104104+ if c.mqtt = Mqtte_cmd.Config_file.empty_mqtt_config then None
105105+ else Some c.mqtt)
106106+ |> opt_mem "pool" Mqtte_cmd.Config_file.pool_codec ~enc:(fun c ->
107107+ if c.pool = Mqtte_cmd.Config_file.empty_pool_config then None
108108+ else Some c.pool)
106109 |> finish))
107110108111 let load xdg =
109109- match Xdge.find_config_file xdg "owntracks.toml" with
110110- | Some path ->
111111- (try Some (Tomlt_eio.decode_file_exn codec path)
112112- with exn ->
113113- Logs.warn (fun m ->
114114- m "Failed to parse owntracks.toml: %s" (Printexc.to_string exn));
115115- None)
112112+ match Xdge.config_file xdg "owntracks.toml" with
113113+ | Some path -> (
114114+ try Some (Tomlt_eio.decode_file_exn codec path)
115115+ with exn ->
116116+ Logs.warn (fun m ->
117117+ m "Failed to parse owntracks.toml: %s" (Printexc.to_string exn));
118118+ None)
116119 | None -> None
117120118121 let resolve_device_name config device_id =
···121124 config.owntracks.devices
122125 |> Option.value ~default:device_id
123126124124- let to_mqtt_config t : Mqtte_cmd.Config_file.t = { mqtt = t.mqtt; pool = t.pool }
127127+ let to_mqtt_config t : Mqtte_cmd.Config_file.t =
128128+ { mqtt = t.mqtt; pool = t.pool }
125129126130 let default_toml =
127131 {|# OwnTracks Configuration
···162166 | Ok ot_msg -> (
163167 match Owntracks.Mqtt.message ot_msg with
164168 | Owntracks.Message.Location loc ->
165165- let user = Option.value ~default:"unknown" (Owntracks.Mqtt.user ot_msg) in
166166- let device = Option.value ~default:"unknown" (Owntracks.Mqtt.device ot_msg) in
169169+ let user =
170170+ Option.value ~default:"unknown" (Owntracks.Mqtt.user ot_msg)
171171+ in
172172+ let device =
173173+ Option.value ~default:"unknown" (Owntracks.Mqtt.device ot_msg)
174174+ in
167175 let device_name = Config.resolve_device_name config device in
168176 Format.printf "@[<v 0>-------------------------------------------@,";
169177 Format.printf " Device: %s" device_name;
170178 if device_name <> device then Format.printf " (%s)" device;
171179 Format.printf "@, User: %s@," user;
172172- Format.printf " Time: %s@," (Owntracks.Location.format_timestamp (Owntracks.Location.tst loc));
173173- Format.printf " Location: %.6f, %.6f@," (Owntracks.Location.lat loc) (Owntracks.Location.lon loc);
174174- Option.iter (fun v -> Format.printf " Altitude: %.1f m@," v) (Owntracks.Location.alt loc);
175175- Option.iter (fun v -> Format.printf " Accuracy: +/- %.0f m@," v) (Owntracks.Location.acc loc);
176176- Option.iter (fun v -> Format.printf " Speed: %.1f km/h@," v) (Owntracks.Location.vel loc);
177177- Option.iter (fun v -> Format.printf " Battery: %d%%@," v) (Owntracks.Location.batt loc);
180180+ Format.printf " Time: %s@,"
181181+ (Owntracks.Location.format_timestamp (Owntracks.Location.tst loc));
182182+ Format.printf " Location: %.6f, %.6f@,"
183183+ (Owntracks.Location.lat loc)
184184+ (Owntracks.Location.lon loc);
185185+ Option.iter
186186+ (fun v -> Format.printf " Altitude: %.1f m@," v)
187187+ (Owntracks.Location.alt loc);
188188+ Option.iter
189189+ (fun v -> Format.printf " Accuracy: +/- %.0f m@," v)
190190+ (Owntracks.Location.acc loc);
191191+ Option.iter
192192+ (fun v -> Format.printf " Speed: %.1f km/h@," v)
193193+ (Owntracks.Location.vel loc);
194194+ Option.iter
195195+ (fun v -> Format.printf " Battery: %d%%@," v)
196196+ (Owntracks.Location.batt loc);
178197 Format.printf "-------------------------------------------@]@."
179198 | Owntracks.Message.Transition tr ->
180180- let device = Option.value ~default:"unknown" (Owntracks.Mqtt.device ot_msg) in
199199+ let device =
200200+ Option.value ~default:"unknown" (Owntracks.Mqtt.device ot_msg)
201201+ in
181202 let device_name = Config.resolve_device_name config device in
182203 Format.printf "[%s] %s region: %s@." device_name
183204 (String.uppercase_ascii (Owntracks.Transition.event tr))
···188209(** {1 Commands} *)
189210190211let listen_cmd ~fs =
191191- let xdg = Xdge.create fs app_name in
212212+ let xdg = Xdge.v fs app_name in
192213 let config = Config.load xdg |> Option.value ~default:Config.empty in
193214 let mqtt_config = Config.to_mqtt_config config in
194215···203224 match topic with
204225 | Some t -> t
205226 | None ->
206206- Option.value ~default:Owntracks.Mqtt.default_topic config.owntracks.topic
227227+ Option.value ~default:Owntracks.Mqtt.default_topic
228228+ config.owntracks.topic
207229 in
208230209231 Logs.info (fun m -> m "OwnTracks Location Listener");
···222244 let clock = Eio.Stdenv.clock env in
223245224246 let pool =
225225- Mqtte_cmd.create_pool ~sw ~net ~clock ~tls:conn.tls ~insecure:conn.insecure
226226- ~pool_config:mqtt.pool_config ()
247247+ Mqtte_cmd.create_pool ~sw ~net ~clock ~tls:conn.tls
248248+ ~insecure:conn.insecure ~pool_config:mqtt.pool_config ()
227249 in
228250 let endpoint = Mqtte_cmd.endpoint conn in
229251···248270 let topic =
249271 let doc = "MQTT topic (supports wildcards). Default: owntracks/#" in
250272 let env = Cmd.Env.info "OWNTRACKS_TOPIC" ~doc in
251251- Arg.(value & opt (some string) None & info [ "t"; "topic" ] ~docv:"TOPIC" ~doc ~env)
273273+ Arg.(
274274+ value
275275+ & opt (some string) None
276276+ & info [ "t"; "topic" ] ~docv:"TOPIC" ~doc ~env)
252277 in
253278 let term =
254279 Term.(
···272297let devices_cmd ~fs:_ =
273298 let run () =
274299 Eio_main.run @@ fun env ->
275275- let xdg = Xdge.create (Eio.Stdenv.fs env) app_name in
300300+ let xdg = Xdge.v (Eio.Stdenv.fs env) app_name in
276301 (match Config.load xdg with
277302 | Some config when config.owntracks.devices <> [] ->
278303 Format.printf "Configured devices:@.@.";
···282307 config.owntracks.default_device = Some d.id
283308 || config.owntracks.default_device = Some d.name
284309 in
285285- Format.printf " %s%s@. ID: %s@."
286286- d.name
310310+ Format.printf " %s%s@. ID: %s@." d.name
287311 (if is_default then " (default)" else "")
288312 d.id)
289313 config.owntracks.devices
···307331308332 (** Decode string list, trying results wrapper first then plain array. *)
309333 let decode_string_list body =
310310- match Jsont_bytesrw.decode_string Owntracks.Recorder.string_list_results_jsont body with
334334+ match
335335+ Jsont_bytesrw.decode_string Owntracks.Recorder.string_list_results_jsont
336336+ body
337337+ with
311338 | Ok items -> items
312312- | Error _ ->
313313- match Jsont_bytesrw.decode_string Owntracks.Recorder.string_list_jsont body with
339339+ | Error _ -> (
340340+ match
341341+ Jsont_bytesrw.decode_string Owntracks.Recorder.string_list_jsont body
342342+ with
314343 | Ok items -> items
315315- | Error _ -> []
344344+ | Error _ -> [])
316345317346 (** Decode locations, trying array first then data wrapper. *)
318347 let decode_locations body =
319319- match Jsont_bytesrw.decode_string Owntracks.Recorder.locations_jsont body with
348348+ match
349349+ Jsont_bytesrw.decode_string Owntracks.Recorder.locations_jsont body
350350+ with
320351 | Ok locs -> locs
321321- | Error _ ->
322322- match Jsont_bytesrw.decode_string Owntracks.Recorder.locations_data_jsont body with
352352+ | Error _ -> (
353353+ match
354354+ Jsont_bytesrw.decode_string Owntracks.Recorder.locations_data_jsont
355355+ body
356356+ with
323357 | Ok locs -> locs
324324- | Error _ -> []
358358+ | Error _ -> [])
325359326360 let list_users ~sw env ~verbose_http ~recorder_url ?auth () : string list =
327361 let url = Printf.sprintf "%s/api/0/list" recorder_url in
···329363 Logs.set_level (Some Logs.Debug);
330364 Logs.set_reporter (Logs_fmt.reporter ())
331365 end;
332332- let session = Requests.create ~sw env in
333333- let headers = match auth with
366366+ let session = Requests.v ~sw env in
367367+ let headers =
368368+ match auth with
334369 | Some (username, password) ->
335370 Requests.Headers.empty |> Requests.Headers.basic ~username ~password
336371 | None -> Requests.Headers.empty
···339374 if Requests.Response.ok response then begin
340375 let body = Requests.Response.body response |> Eio.Flow.read_all in
341376 decode_string_list body
342342- end else begin
377377+ end
378378+ else begin
343379 Format.eprintf "HTTP error: %d@." (Requests.Response.status_code response);
344380 []
345381 end
346382347347- let list_devices ~sw env ~verbose_http ~recorder_url ~user ?auth () : string list =
348348- let url = Printf.sprintf "%s/api/0/list?user=%s" recorder_url (Uri.pct_encode user) in
383383+ let list_devices ~sw env ~verbose_http ~recorder_url ~user ?auth () :
384384+ string list =
385385+ let url =
386386+ Printf.sprintf "%s/api/0/list?user=%s" recorder_url (Uri.pct_encode user)
387387+ in
349388 if verbose_http then begin
350389 Logs.set_level (Some Logs.Debug);
351390 Logs.set_reporter (Logs_fmt.reporter ())
352391 end;
353353- let session = Requests.create ~sw env in
354354- let headers = match auth with
392392+ let session = Requests.v ~sw env in
393393+ let headers =
394394+ match auth with
355395 | Some (username, password) ->
356396 Requests.Headers.empty |> Requests.Headers.basic ~username ~password
357397 | None -> Requests.Headers.empty
···360400 if Requests.Response.ok response then begin
361401 let body = Requests.Response.body response |> Eio.Flow.read_all in
362402 decode_string_list body
363363- end else begin
403403+ end
404404+ else begin
364405 Format.eprintf "HTTP error: %d@." (Requests.Response.status_code response);
365406 []
366407 end
367408368368- let fetch_locations ~sw env ~verbose_http ~recorder_url ~user ~device ~from_date ~to_date ?auth () : Owntracks.Location.t list =
369369- let url = Printf.sprintf "%s/api/0/locations?user=%s&device=%s&from=%s&to=%s"
370370- recorder_url
371371- (Uri.pct_encode user)
372372- (Uri.pct_encode device)
373373- from_date
374374- to_date
409409+ let fetch_locations ~sw env ~verbose_http ~recorder_url ~user ~device
410410+ ~from_date ~to_date ?auth () : Owntracks.Location.t list =
411411+ let url =
412412+ Printf.sprintf "%s/api/0/locations?user=%s&device=%s&from=%s&to=%s"
413413+ recorder_url (Uri.pct_encode user) (Uri.pct_encode device) from_date
414414+ to_date
375415 in
376416 Format.eprintf "Fetching from %s...@." url;
377417 (* Set up verbose logging if requested *)
···379419 Logs.set_level (Some Logs.Debug);
380420 Logs.set_reporter (Logs_fmt.reporter ())
381421 end;
382382- let session = Requests.create ~sw env in
383383- let headers = match auth with
422422+ let session = Requests.v ~sw env in
423423+ let headers =
424424+ match auth with
384425 | Some (username, password) ->
385426 Requests.Headers.empty |> Requests.Headers.basic ~username ~password
386427 | None -> Requests.Headers.empty
···389430 if Requests.Response.ok response then begin
390431 let body = Requests.Response.body response |> Eio.Flow.read_all in
391432 decode_locations body
392392- end else begin
433433+ end
434434+ else begin
393435 Format.eprintf "HTTP error: %d@." (Requests.Response.status_code response);
394436 []
395437 end
396438end
397439398440let geojson_cmd ~fs =
399399- let xdg = Xdge.create fs app_name in
441441+ let xdg = Xdge.v fs app_name in
400442 let config = Config.load xdg |> Option.value ~default:Config.empty in
401443 let mqtt_config = Config.to_mqtt_config config in
402444···404446 let today () =
405447 let now = Unix.gettimeofday () in
406448 let tm = Unix.gmtime now in
407407- Printf.sprintf "%04d-%02d-%02d" (tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
449449+ Printf.sprintf "%04d-%02d-%02d" (tm.Unix.tm_year + 1900)
450450+ (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
408451 in
409452410410- let run parsed topic device duration track from_date to_date user recorder_url recorder_user recorder_password verbose_http =
411411- let device = match device with Some _ -> device | None -> config.owntracks.default_device in
412412- let recorder_url = match recorder_url with Some _ -> recorder_url | None -> config.owntracks.recorder.url in
413413- let recorder_user = match recorder_user with Some _ -> recorder_user | None -> config.owntracks.recorder.user in
414414- let recorder_password = match recorder_password with Some _ -> recorder_password | None -> config.owntracks.recorder.password in
453453+ let run parsed topic device duration track from_date to_date user recorder_url
454454+ recorder_user recorder_password verbose_http =
455455+ let device =
456456+ match device with
457457+ | Some _ -> device
458458+ | None -> config.owntracks.default_device
459459+ in
460460+ let recorder_url =
461461+ match recorder_url with
462462+ | Some _ -> recorder_url
463463+ | None -> config.owntracks.recorder.url
464464+ in
465465+ let recorder_user =
466466+ match recorder_user with
467467+ | Some _ -> recorder_user
468468+ | None -> config.owntracks.recorder.user
469469+ in
470470+ let recorder_password =
471471+ match recorder_password with
472472+ | Some _ -> recorder_password
473473+ | None -> config.owntracks.recorder.password
474474+ in
415475416476 (* If --from is specified, use HTTP API instead of MQTT *)
417477 match from_date with
418418- | Some from_date ->
478478+ | Some from_date -> (
419479 let to_date = Option.value to_date ~default:(today ()) in
420480 (* Default user from config recorder.user, then "owntracks" *)
421421- let user = match user with
481481+ let user =
482482+ match user with
422483 | Some u -> u
423423- | None -> Option.value config.owntracks.recorder.user ~default:"owntracks"
484484+ | None ->
485485+ Option.value config.owntracks.recorder.user ~default:"owntracks"
424486 in
425487 (* Default device from config default_device, then "phone" *)
426426- let device = Option.value device ~default:(Option.value config.owntracks.default_device ~default:"phone") in
427427- (match recorder_url with
428428- | None ->
429429- Format.eprintf "Error: --recorder-url or recorder_url config required for historical queries@.";
430430- 1
431431- | Some recorder_url ->
432432- let auth = match (recorder_user, recorder_password) with
433433- | (Some u, Some p) -> Some (u, p)
434434- | _ -> None
435435- in
436436- Eio_main.run @@ fun env ->
437437- Mirage_crypto_rng_unix.use_default ();
438438- Eio.Switch.run @@ fun sw ->
439439- let locations = Recorder.fetch_locations ~sw env ~verbose_http ~recorder_url ~user ~device ~from_date ~to_date ?auth () in
440440- match locations with
441441- | [] ->
442442- Format.eprintf "No locations found for %s/%s from %s to %s@." user device from_date to_date;
443443- 1
444444- | locs ->
445445- let device_name = Config.resolve_device_name config device in
446446- let json = Owntracks.Geojson.linestring_feature ~device_name locs in
447447- print_endline (Owntracks.Geojson.to_string json);
448448- 0)
488488+ let device =
489489+ Option.value device
490490+ ~default:
491491+ (Option.value config.owntracks.default_device ~default:"phone")
492492+ in
493493+ match recorder_url with
494494+ | None ->
495495+ Format.eprintf
496496+ "Error: --recorder-url or recorder_url config required for \
497497+ historical queries@.";
498498+ 1
499499+ | Some recorder_url -> (
500500+ let auth =
501501+ match (recorder_user, recorder_password) with
502502+ | Some u, Some p -> Some (u, p)
503503+ | _ -> None
504504+ in
505505+ Eio_main.run @@ fun env ->
506506+ Mirage_crypto_rng_unix.use_default ();
507507+ Eio.Switch.run @@ fun sw ->
508508+ let locations =
509509+ Recorder.fetch_locations ~sw env ~verbose_http ~recorder_url ~user
510510+ ~device ~from_date ~to_date ?auth ()
511511+ in
512512+ match locations with
513513+ | [] ->
514514+ Format.eprintf "No locations found for %s/%s from %s to %s@."
515515+ user device from_date to_date;
516516+ 1
517517+ | locs ->
518518+ let device_name = Config.resolve_device_name config device in
519519+ let json =
520520+ Owntracks.Geojson.linestring_feature ~device_name locs
521521+ in
522522+ print_endline (Owntracks.Geojson.to_string json);
523523+ 0))
449524 | None ->
450525 (* Use MQTT for real-time data *)
451526 let mqtt = parsed.Mqtte_cmd.mqtt in
452527 let conn = mqtt.connection in
453528 let topic =
454529 Option.value topic
455455- ~default:(Option.value ~default:Owntracks.Mqtt.default_topic config.owntracks.topic)
530530+ ~default:
531531+ (Option.value ~default:Owntracks.Mqtt.default_topic
532532+ config.owntracks.topic)
456533 in
457534 let result = ref None in
458535 let track_points = ref [] in
···466543467544 let on_message msg =
468545 let open Mqtte_eio.Client in
469469- match Owntracks.Mqtt.of_mqtt ~topic:msg.topic ~payload:msg.payload with
546546+ match
547547+ Owntracks.Mqtt.of_mqtt ~topic:msg.topic ~payload:msg.payload
548548+ with
470549 | Ok ot_msg -> (
471550 match Owntracks.Mqtt.message ot_msg with
472551 | Owntracks.Message.Location loc ->
473473- let msg_device = Option.value ~default:"unknown" (Owntracks.Mqtt.device ot_msg) in
474474- let matches = match device with
552552+ let msg_device =
553553+ Option.value ~default:"unknown"
554554+ (Owntracks.Mqtt.device ot_msg)
555555+ in
556556+ let matches =
557557+ match device with
475558 | None -> true
476476- | Some d -> d = msg_device || d = Config.resolve_device_name config msg_device
559559+ | Some d ->
560560+ d = msg_device
561561+ || d = Config.resolve_device_name config msg_device
477562 in
478563 if matches then begin
479479- let device_name = Config.resolve_device_name config msg_device in
564564+ let device_name =
565565+ Config.resolve_device_name config msg_device
566566+ in
480567 if track then begin
481568 track_device_name := device_name;
482569 track_points := loc :: !track_points
483483- end else if Option.is_none !result then
484484- result := Some (Owntracks.Geojson.point_feature ~device_name loc)
570570+ end
571571+ else if Option.is_none !result then
572572+ result :=
573573+ Some (Owntracks.Geojson.point_feature ~device_name loc)
485574 end
486575 | _ -> ())
487576 | Error _ -> ()
···489578 let on_disconnect () = () in
490579491580 let pool =
492492- Mqtte_cmd.create_pool ~sw ~net ~clock ~tls:conn.tls ~insecure:conn.insecure
493493- ~pool_config:mqtt.pool_config ()
581581+ Mqtte_cmd.create_pool ~sw ~net ~clock ~tls:conn.tls
582582+ ~insecure:conn.insecure ~pool_config:mqtt.pool_config ()
494583 in
495584 let endpoint = Mqtte_cmd.endpoint conn in
496585497586 let client =
498498- Mqtte_eio.Client.connect_with_pool ~sw ~clock ~on_message ~on_disconnect
499499- ~config:mqtt.config ~pool ~endpoint ()
587587+ Mqtte_eio.Client.connect_with_pool ~sw ~clock ~on_message
588588+ ~on_disconnect ~config:mqtt.config ~pool ~endpoint ()
500589 in
501590502591 Mqtte_eio.Client.subscribe ~qos:`At_least_once [ topic ] client;
···505594 (* Track mode: collect points for the full duration *)
506595 let deadline = Eio.Time.now clock +. duration in
507596 Format.eprintf "Collecting track for %.0f seconds...@." duration;
508508- while Eio.Time.now clock < deadline && Mqtte_eio.Client.is_connected client do
597597+ while
598598+ Eio.Time.now clock < deadline
599599+ && Mqtte_eio.Client.is_connected client
600600+ do
509601 Eio.Time.sleep clock 0.5
510602 done;
511603 Mqtte_eio.Client.disconnect client;
512604 match !track_points with
513605 | [] ->
514514- Format.eprintf "No locations received within %.0f seconds@." duration;
606606+ Format.eprintf "No locations received within %.0f seconds@."
607607+ duration;
515608 1
516609 | points ->
517517- let json = Owntracks.Geojson.linestring_feature ~device_name:!track_device_name points in
610610+ let json =
611611+ Owntracks.Geojson.linestring_feature
612612+ ~device_name:!track_device_name points
613613+ in
518614 print_endline (Owntracks.Geojson.to_string json);
519615 0
520520- end else begin
616616+ end
617617+ else begin
521618 (* Single point mode: wait for first location then exit immediately *)
522619 let deadline = Eio.Time.now clock +. duration in
523523- while Option.is_none !result && Eio.Time.now clock < deadline && Mqtte_eio.Client.is_connected client do
620620+ while
621621+ Option.is_none !result
622622+ && Eio.Time.now clock < deadline
623623+ && Mqtte_eio.Client.is_connected client
624624+ do
524625 Eio.Time.sleep clock 0.1
525626 done;
526627 Mqtte_eio.Client.disconnect client;
···529630 print_endline (Owntracks.Geojson.to_string json);
530631 0
531632 | None ->
532532- Format.eprintf "No location received within %.0f seconds@." duration;
633633+ Format.eprintf "No location received within %.0f seconds@."
634634+ duration;
533635 1
534636 end
535637 in
536638 let topic =
537639 let doc = "MQTT topic (supports wildcards). Default: owntracks/#" in
538538- Arg.(value & opt (some string) None & info [ "t"; "topic" ] ~docv:"TOPIC" ~doc)
640640+ Arg.(
641641+ value & opt (some string) None & info [ "t"; "topic" ] ~docv:"TOPIC" ~doc)
539642 in
540643 let device =
541541- let doc = "Filter by device name or ID. Defaults to default_device from config." in
542542- Arg.(value & opt (some string) None & info [ "d"; "device" ] ~docv:"DEVICE" ~doc)
644644+ let doc =
645645+ "Filter by device name or ID. Defaults to default_device from config."
646646+ in
647647+ Arg.(
648648+ value
649649+ & opt (some string) None
650650+ & info [ "d"; "device" ] ~docv:"DEVICE" ~doc)
543651 in
544652 let duration =
545545- let doc = "Duration in seconds to wait for location (single point) or collect track data (with --track)" in
653653+ let doc =
654654+ "Duration in seconds to wait for location (single point) or collect \
655655+ track data (with --track)"
656656+ in
546657 Arg.(value & opt float 30.0 & info [ "duration" ] ~docv:"SECONDS" ~doc)
547658 in
548659 let track =
549549- let doc = "Collect a track (LineString) instead of a single point. \
550550- Use --duration to set collection duration (e.g., --duration 86400 for 24h)." in
660660+ let doc =
661661+ "Collect a track (LineString) instead of a single point. Use --duration \
662662+ to set collection duration (e.g., --duration 86400 for 24h)."
663663+ in
551664 Arg.(value & flag & info [ "track" ] ~doc)
552665 in
553666 let from_date =
554554- let doc = "Start date for historical query (YYYY-MM-DD). Uses HTTP API instead of MQTT." in
667667+ let doc =
668668+ "Start date for historical query (YYYY-MM-DD). Uses HTTP API instead of \
669669+ MQTT."
670670+ in
555671 Arg.(value & opt (some string) None & info [ "from" ] ~docv:"DATE" ~doc)
556672 in
557673 let to_date =
558558- let doc = "End date for historical query (YYYY-MM-DD). Defaults to today." in
674674+ let doc =
675675+ "End date for historical query (YYYY-MM-DD). Defaults to today."
676676+ in
559677 Arg.(value & opt (some string) None & info [ "to" ] ~docv:"DATE" ~doc)
560678 in
561679 let user =
···563681 Arg.(value & opt (some string) None & info [ "user" ] ~docv:"USER" ~doc)
564682 in
565683 let recorder_url =
566566- let doc = "OwnTracks Recorder URL (e.g., https://recorder.example.com). \
567567- Can also be set in config as [owntracks.recorder] url." in
568568- Arg.(value & opt (some string) None & info [ "recorder-url" ] ~docv:"URL" ~doc)
684684+ let doc =
685685+ "OwnTracks Recorder URL (e.g., https://recorder.example.com). Can also \
686686+ be set in config as [owntracks.recorder] url."
687687+ in
688688+ Arg.(
689689+ value & opt (some string) None & info [ "recorder-url" ] ~docv:"URL" ~doc)
569690 in
570691 let recorder_user =
571571- let doc = "User for HTTP Basic Auth to OwnTracks Recorder. \
572572- Can also be set in config as [owntracks.recorder] user." in
573573- Arg.(value & opt (some string) None & info [ "recorder-user" ] ~docv:"USER" ~doc)
692692+ let doc =
693693+ "User for HTTP Basic Auth to OwnTracks Recorder. Can also be set in \
694694+ config as [owntracks.recorder] user."
695695+ in
696696+ Arg.(
697697+ value
698698+ & opt (some string) None
699699+ & info [ "recorder-user" ] ~docv:"USER" ~doc)
574700 in
575701 let recorder_password =
576576- let doc = "Password for HTTP Basic Auth to OwnTracks Recorder. \
577577- Can also be set in config as [owntracks.recorder] password." in
578578- Arg.(value & opt (some string) None & info [ "recorder-password" ] ~docv:"PASS" ~doc)
702702+ let doc =
703703+ "Password for HTTP Basic Auth to OwnTracks Recorder. Can also be set in \
704704+ config as [owntracks.recorder] password."
705705+ in
706706+ Arg.(
707707+ value
708708+ & opt (some string) None
709709+ & info [ "recorder-password" ] ~docv:"PASS" ~doc)
579710 in
580711 let verbose_http =
581581- let doc = "Enable verbose HTTP-level logging for debugging recorder requests." in
712712+ let doc =
713713+ "Enable verbose HTTP-level logging for debugging recorder requests."
714714+ in
582715 Arg.(value & flag & info [ "verbose-http" ] ~doc)
583716 in
584717 let term =
585718 Term.(
586719 const run
587720 $ Mqtte_cmd.term_with_config ~app_name ~fs ~config:mqtt_config ()
588588- $ topic $ device $ duration $ track $ from_date $ to_date $ user $ recorder_url
589589- $ recorder_user $ recorder_password $ verbose_http)
721721+ $ topic $ device $ duration $ track $ from_date $ to_date $ user
722722+ $ recorder_url $ recorder_user $ recorder_password $ verbose_http)
590723 in
591724 let doc = "Output device location as GeoJSON" in
592725 let man =
593726 [
594727 `S Manpage.s_description;
595728 `P "Connects to MQTT and outputs location data as GeoJSON.";
596596- `P "By default, outputs the first location as a GeoJSON Feature with Point geometry. \
597597- With $(b,--track), collects locations for the specified duration and outputs a \
598598- LineString showing the movement path.";
599599- `P "With $(b,--from), queries the OwnTracks Recorder HTTP API for historical data \
600600- instead of using MQTT. Requires $(b,--recorder-url) or recorder_url in config.";
729729+ `P
730730+ "By default, outputs the first location as a GeoJSON Feature with \
731731+ Point geometry. With $(b,--track), collects locations for the \
732732+ specified duration and outputs a LineString showing the movement \
733733+ path.";
734734+ `P
735735+ "With $(b,--from), queries the OwnTracks Recorder HTTP API for \
736736+ historical data instead of using MQTT. Requires $(b,--recorder-url) \
737737+ or recorder_url in config.";
601738 `S Manpage.s_examples;
602739 `Pre " owntracks geojson";
603740 `Pre " owntracks geojson -d 'My Phone' --duration 60";
604741 `Pre " owntracks geojson --track --duration 3600 # 1 hour track";
605742 `Pre " owntracks geojson --from 2024-01-12 --to 2024-01-13 # historical";
606606- `Pre " owntracks geojson --from 2024-01-12 --recorder-url https://recorder.example.com";
743743+ `Pre
744744+ " owntracks geojson --from 2024-01-12 --recorder-url \
745745+ https://recorder.example.com";
607746 ]
608747 in
609748 Cmd.v (Cmd.info "geojson" ~doc ~man) term
610749611750let recorder_cmd ~fs =
612612- let xdg = Xdge.create fs app_name in
751751+ let xdg = Xdge.v fs app_name in
613752 let config = Config.load xdg |> Option.value ~default:Config.empty in
614753615754 let run recorder_url recorder_user recorder_password user verbose_http =
616616- let recorder_url = match recorder_url with Some _ -> recorder_url | None -> config.owntracks.recorder.url in
617617- let recorder_user = match recorder_user with Some _ -> recorder_user | None -> config.owntracks.recorder.user in
618618- let recorder_password = match recorder_password with Some _ -> recorder_password | None -> config.owntracks.recorder.password in
755755+ let recorder_url =
756756+ match recorder_url with
757757+ | Some _ -> recorder_url
758758+ | None -> config.owntracks.recorder.url
759759+ in
760760+ let recorder_user =
761761+ match recorder_user with
762762+ | Some _ -> recorder_user
763763+ | None -> config.owntracks.recorder.user
764764+ in
765765+ let recorder_password =
766766+ match recorder_password with
767767+ | Some _ -> recorder_password
768768+ | None -> config.owntracks.recorder.password
769769+ in
619770620771 match recorder_url with
621772 | None ->
622622- Format.eprintf "Error: --recorder-url or [owntracks.recorder] url config required@.";
773773+ Format.eprintf
774774+ "Error: --recorder-url or [owntracks.recorder] url config required@.";
623775 1
624624- | Some recorder_url ->
625625- let auth = match (recorder_user, recorder_password) with
626626- | (Some u, Some p) -> Some (u, p)
776776+ | Some recorder_url -> (
777777+ let auth =
778778+ match (recorder_user, recorder_password) with
779779+ | Some u, Some p -> Some (u, p)
627780 | _ -> None
628781 in
629782 Eio_main.run @@ fun env ->
630783 Mirage_crypto_rng_unix.use_default ();
631784 Eio.Switch.run @@ fun sw ->
632785 match user with
633633- | None ->
786786+ | None -> (
634787 (* List all users *)
635635- let users = Recorder.list_users ~sw env ~verbose_http ~recorder_url ?auth () in
636636- (match users with
637637- | [] ->
638638- Format.printf "No users found (or unable to access recorder).@.";
639639- 1
640640- | users ->
641641- Format.printf "Users on recorder:@.";
642642- List.iter (fun u -> Format.printf " %s@." u) users;
643643- 0)
644644- | Some user ->
788788+ let users =
789789+ Recorder.list_users ~sw env ~verbose_http ~recorder_url ?auth ()
790790+ in
791791+ match users with
792792+ | [] ->
793793+ Format.printf "No users found (or unable to access recorder).@.";
794794+ 1
795795+ | users ->
796796+ Format.printf "Users on recorder:@.";
797797+ List.iter (fun u -> Format.printf " %s@." u) users;
798798+ 0)
799799+ | Some user -> (
645800 (* List devices for user *)
646646- let devices = Recorder.list_devices ~sw env ~verbose_http ~recorder_url ~user ?auth () in
647647- (match devices with
648648- | [] ->
649649- Format.printf "No devices found for user '%s'.@." user;
650650- 1
651651- | devices ->
652652- Format.printf "Devices for user '%s':@." user;
653653- List.iter (fun d -> Format.printf " %s@." d) devices;
654654- 0)
801801+ let devices =
802802+ Recorder.list_devices ~sw env ~verbose_http ~recorder_url ~user
803803+ ?auth ()
804804+ in
805805+ match devices with
806806+ | [] ->
807807+ Format.printf "No devices found for user '%s'.@." user;
808808+ 1
809809+ | devices ->
810810+ Format.printf "Devices for user '%s':@." user;
811811+ List.iter (fun d -> Format.printf " %s@." d) devices;
812812+ 0))
655813 in
656814 let recorder_url =
657815 let doc = "OwnTracks Recorder URL (e.g., https://recorder.example.com)." in
658658- Arg.(value & opt (some string) None & info [ "recorder-url" ] ~docv:"URL" ~doc)
816816+ Arg.(
817817+ value & opt (some string) None & info [ "recorder-url" ] ~docv:"URL" ~doc)
659818 in
660819 let recorder_user =
661820 let doc = "User for HTTP Basic Auth to OwnTracks Recorder." in
662662- Arg.(value & opt (some string) None & info [ "recorder-user" ] ~docv:"USER" ~doc)
821821+ Arg.(
822822+ value
823823+ & opt (some string) None
824824+ & info [ "recorder-user" ] ~docv:"USER" ~doc)
663825 in
664826 let recorder_password =
665827 let doc = "Password for HTTP Basic Auth to OwnTracks Recorder." in
666666- Arg.(value & opt (some string) None & info [ "recorder-password" ] ~docv:"PASS" ~doc)
828828+ Arg.(
829829+ value
830830+ & opt (some string) None
831831+ & info [ "recorder-password" ] ~docv:"PASS" ~doc)
667832 in
668833 let user =
669834 let doc = "List devices for this user. If omitted, lists all users." in
···674839 Arg.(value & flag & info [ "verbose-http" ] ~doc)
675840 in
676841 let term =
677677- Term.(const run $ recorder_url $ recorder_user $ recorder_password $ user $ verbose_http)
842842+ Term.(
843843+ const run $ recorder_url $ recorder_user $ recorder_password $ user
844844+ $ verbose_http)
678845 in
679846 let doc = "List users and devices from OwnTracks Recorder" in
680847 let man =
681848 [
682849 `S Manpage.s_description;
683683- `P "Query the OwnTracks Recorder HTTP API to list available users and devices.";
684684- `P "Without $(b,--user), lists all users. With $(b,--user), lists devices for that user.";
850850+ `P
851851+ "Query the OwnTracks Recorder HTTP API to list available users and \
852852+ devices.";
853853+ `P
854854+ "Without $(b,--user), lists all users. With $(b,--user), lists devices \
855855+ for that user.";
685856 `S Manpage.s_examples;
686857 `Pre " owntracks recorder # list all users";
687687- `Pre " owntracks recorder --user avsm # list devices for user 'avsm'";
858858+ `Pre
859859+ " owntracks recorder --user avsm # list devices for user 'avsm'";
688860 ]
689861 in
690862 Cmd.v (Cmd.info "recorder" ~doc ~man) term
···692864let init_cmd ~fs:_ =
693865 let run force =
694866 Eio_main.run @@ fun env ->
695695- let xdg = Xdge.create (Eio.Stdenv.fs env) app_name in
867867+ let xdg = Xdge.v (Eio.Stdenv.fs env) app_name in
696868 let path = Eio.Path.(Xdge.config_dir xdg / "owntracks.toml") in
697869 let exists = Eio.Path.is_file path in
698870 if exists && not force then begin
···708880 0
709881 end
710882 in
711711- let force = Arg.(value & flag & info [ "f"; "force" ] ~doc:"Overwrite existing") in
883883+ let force =
884884+ Arg.(value & flag & info [ "f"; "force" ] ~doc:"Overwrite existing")
885885+ in
712886 let doc = "Create default configuration file" in
713887 Cmd.v (Cmd.info "init" ~doc) Term.(const run $ force)
714888···722896 `S Manpage.s_description;
723897 `P "Subscribe to OwnTracks location updates over MQTT.";
724898 `S "CONFIGURATION";
725725- `P (Printf.sprintf "Settings are stored in ~/.config/%s/owntracks.toml:" app_name);
899899+ `P
900900+ (Printf.sprintf "Settings are stored in ~/.config/%s/owntracks.toml:"
901901+ app_name);
726902 `Pre
727903 {|[owntracks]
728904topic = "owntracks/#"
···740916 ]
741917 in
742918 let default = Term.(ret (const (`Help (`Pager, None)))) in
743743- Cmd.group (Cmd.info app_name ~version:"0.1.0" ~doc ~man) ~default
744744- [ listen_cmd ~fs; geojson_cmd ~fs; recorder_cmd ~fs; devices_cmd ~fs; init_cmd ~fs ]
919919+ Cmd.group
920920+ (Cmd.info app_name ~version:"0.1.0" ~doc ~man)
921921+ ~default
922922+ [
923923+ listen_cmd ~fs;
924924+ geojson_cmd ~fs;
925925+ recorder_cmd ~fs;
926926+ devices_cmd ~fs;
927927+ init_cmd ~fs;
928928+ ]
745929 in
746746- Eio_main.run @@ fun env ->
747747- exit (Cmd.eval' (main_cmd ~fs:(Eio.Stdenv.fs env)))
930930+ Eio_main.run @@ fun env -> exit (Cmd.eval' (main_cmd ~fs:(Eio.Stdenv.fs env)))
···2424 let jsont = float_array_jsont ~kind:"Bbox"
2525end
26262727-(** Position coordinates [longitude, latitude] or [longitude, latitude, altitude]. *)
2727+(** Position coordinates [longitude, latitude] or
2828+ [longitude, latitude, altitude]. *)
2829module Position = struct
2930 type t = float_array
3031···7374module Multi_point = struct
7475 type t = Position.t garray
75767676- let jsont =
7777- Geojson_object.geometry ~kind:"MultiPoint" (garray Position.jsont)
7777+ let jsont = Geojson_object.geometry ~kind:"MultiPoint" (garray Position.jsont)
7878end
79798080(** LineString geometry. *)
8181module Line_string = struct
8282 type t = Position.t garray
83838484- let jsont =
8585- Geojson_object.geometry ~kind:"LineString" (garray Position.jsont)
8686-8484+ let jsont = Geojson_object.geometry ~kind:"LineString" (garray Position.jsont)
8785 let v positions = Geojson_object.make positions None (Jsont.Json.object' [])
8886end
8987···171169 let feature_id_jsont =
172170 let number =
173171 let dec = Jsont.Base.dec (fun n -> `Number n) in
174174- let enc = Jsont.Base.enc (function `Number n -> n | _ -> assert false) in
172172+ let enc =
173173+ Jsont.Base.enc (function `Number n -> n | _ -> assert false)
174174+ in
175175 Jsont.Base.number (Jsont.Base.map ~enc ~dec ())
176176 in
177177 let string =
178178 let dec = Jsont.Base.dec (fun n -> `String n) in
179179- let enc = Jsont.Base.enc (function `String n -> n | _ -> assert false) in
179179+ let enc =
180180+ Jsont.Base.enc (function `String n -> n | _ -> assert false)
181181+ in
180182 Jsont.Base.string (Jsont.Base.map ~enc ~dec ())
181183 in
182184 let enc = function `Number _ -> number | `String _ -> string in
···185187 let case_map obj dec = Jsont.Object.Case.map (Jsont.kind obj) obj ~dec
186188187189 let rec geometry_jsont =
188188- lazy
189189- begin
190190- let case_point = case_map Point.jsont point in
191191- let case_multi_point = case_map Multi_point.jsont multi_point in
192192- let case_line_string = case_map Line_string.jsont line_string in
193193- let case_multi_line_string =
194194- case_map Multi_line_string.jsont multi_line_string
195195- in
196196- let case_polygon = case_map Polygon.jsont polygon in
197197- let case_multi_polygon = case_map Multi_polygon.jsont multi_polygon in
198198- let case_geometry_collection =
199199- case_map (Lazy.force geometry_collection_jsont) geometry_collection
200200- in
201201- let enc_case = function
202202- | `Point v -> Jsont.Object.Case.value case_point v
203203- | `Multi_point v -> Jsont.Object.Case.value case_multi_point v
204204- | `Line_string v -> Jsont.Object.Case.value case_line_string v
205205- | `Multi_line_string v ->
206206- Jsont.Object.Case.value case_multi_line_string v
207207- | `Polygon v -> Jsont.Object.Case.value case_polygon v
208208- | `Multi_polygon v -> Jsont.Object.Case.value case_multi_polygon v
209209- | `Geometry_collection v ->
210210- Jsont.Object.Case.value case_geometry_collection v
211211- in
212212- let cases =
213213- Jsont.Object.Case.
214214- [
215215- make case_point;
216216- make case_multi_point;
217217- make case_line_string;
218218- make case_multi_line_string;
219219- make case_polygon;
220220- make case_multi_polygon;
221221- make case_geometry_collection;
222222- ]
223223- in
224224- Jsont.Object.map ~kind:"Geometry object" Fun.id
225225- |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
226226- ~tag_to_string:Fun.id ~tag_compare:String.compare
227227- |> Jsont.Object.finish
228228- end
190190+ lazy begin
191191+ let case_point = case_map Point.jsont point in
192192+ let case_multi_point = case_map Multi_point.jsont multi_point in
193193+ let case_line_string = case_map Line_string.jsont line_string in
194194+ let case_multi_line_string =
195195+ case_map Multi_line_string.jsont multi_line_string
196196+ in
197197+ let case_polygon = case_map Polygon.jsont polygon in
198198+ let case_multi_polygon = case_map Multi_polygon.jsont multi_polygon in
199199+ let case_geometry_collection =
200200+ case_map (Lazy.force geometry_collection_jsont) geometry_collection
201201+ in
202202+ let enc_case = function
203203+ | `Point v -> Jsont.Object.Case.value case_point v
204204+ | `Multi_point v -> Jsont.Object.Case.value case_multi_point v
205205+ | `Line_string v -> Jsont.Object.Case.value case_line_string v
206206+ | `Multi_line_string v ->
207207+ Jsont.Object.Case.value case_multi_line_string v
208208+ | `Polygon v -> Jsont.Object.Case.value case_polygon v
209209+ | `Multi_polygon v -> Jsont.Object.Case.value case_multi_polygon v
210210+ | `Geometry_collection v ->
211211+ Jsont.Object.Case.value case_geometry_collection v
212212+ in
213213+ let cases =
214214+ Jsont.Object.Case.
215215+ [
216216+ make case_point;
217217+ make case_multi_point;
218218+ make case_line_string;
219219+ make case_multi_line_string;
220220+ make case_polygon;
221221+ make case_multi_polygon;
222222+ make case_geometry_collection;
223223+ ]
224224+ in
225225+ Jsont.Object.map ~kind:"Geometry object" Fun.id
226226+ |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
227227+ ~tag_to_string:Fun.id ~tag_compare:String.compare
228228+ |> Jsont.Object.finish
229229+ end
229230230231 and feature_jsont : Feature.t object' Jsont.t Lazy.t =
231231- lazy
232232- begin
233233- let case_feature = case_map (Lazy.force case_feature_jsont) Fun.id in
234234- let enc_case v = Jsont.Object.Case.value case_feature v in
235235- let cases = Jsont.Object.Case.[ make case_feature ] in
236236- Jsont.Object.map ~kind:"Feature" Fun.id
237237- |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
238238- ~tag_to_string:Fun.id ~tag_compare:String.compare
239239- |> Jsont.Object.finish
240240- end
232232+ lazy begin
233233+ let case_feature = case_map (Lazy.force case_feature_jsont) Fun.id in
234234+ let enc_case v = Jsont.Object.Case.value case_feature v in
235235+ let cases = Jsont.Object.Case.[ make case_feature ] in
236236+ Jsont.Object.map ~kind:"Feature" Fun.id
237237+ |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
238238+ ~tag_to_string:Fun.id ~tag_compare:String.compare
239239+ |> Jsont.Object.finish
240240+ end
241241242242 and case_feature_jsont : Feature.t object' Jsont.t Lazy.t =
243243- lazy
244244- begin
245245- Jsont.Object.map ~kind:"Feature" Feature.make_geojson_object
246246- |> Jsont.Object.opt_mem "id" feature_id_jsont ~enc:(fun o ->
247247- Feature.id (Geojson_object.type' o))
248248- |> Jsont.Object.mem "geometry"
249249- (Jsont.option (Jsont.rec' geometry_jsont))
250250- ~enc:(fun o -> Feature.geometry (Geojson_object.type' o))
251251- |> Jsont.Object.mem "properties" (Jsont.option Jsont.json_object)
252252- ~enc:(fun o -> Feature.properties (Geojson_object.type' o))
253253- |> Geojson_object.finish_jsont
254254- end
243243+ lazy begin
244244+ Jsont.Object.map ~kind:"Feature" Feature.make_geojson_object
245245+ |> Jsont.Object.opt_mem "id" feature_id_jsont ~enc:(fun o ->
246246+ Feature.id (Geojson_object.type' o))
247247+ |> Jsont.Object.mem "geometry"
248248+ (Jsont.option (Jsont.rec' geometry_jsont))
249249+ ~enc:(fun o -> Feature.geometry (Geojson_object.type' o))
250250+ |> Jsont.Object.mem "properties" (Jsont.option Jsont.json_object)
251251+ ~enc:(fun o -> Feature.properties (Geojson_object.type' o))
252252+ |> Geojson_object.finish_jsont
253253+ end
255254256255 and geometry_collection_jsont =
257257- lazy
258258- begin
259259- Jsont.Object.map ~kind:"GeometryCollection" Geojson_object.make
260260- |> Jsont.Object.mem "geometries"
261261- (Jsont.list (Jsont.rec' geometry_jsont))
262262- ~enc:Geojson_object.type'
263263- |> Geojson_object.finish_jsont
264264- end
256256+ lazy begin
257257+ Jsont.Object.map ~kind:"GeometryCollection" Geojson_object.make
258258+ |> Jsont.Object.mem "geometries"
259259+ (Jsont.list (Jsont.rec' geometry_jsont))
260260+ ~enc:Geojson_object.type'
261261+ |> Geojson_object.finish_jsont
262262+ end
265263266264 and feature_collection_json =
267267- lazy
268268- begin
269269- Jsont.Object.map ~kind:"FeatureCollection" Geojson_object.make
270270- |> Jsont.Object.mem "features"
271271- Jsont.(list (Jsont.rec' feature_jsont))
272272- ~enc:Geojson_object.type'
273273- |> Geojson_object.finish_jsont
274274- end
265265+ lazy begin
266266+ Jsont.Object.map ~kind:"FeatureCollection" Geojson_object.make
267267+ |> Jsont.Object.mem "features"
268268+ Jsont.(list (Jsont.rec' feature_jsont))
269269+ ~enc:Geojson_object.type'
270270+ |> Geojson_object.finish_jsont
271271+ end
275272276273 and jsont : t Jsont.t Lazy.t =
277277- lazy
278278- begin
279279- let case_point = case_map Point.jsont point in
280280- let case_multi_point = case_map Multi_point.jsont multi_point in
281281- let case_line_string = case_map Line_string.jsont line_string in
282282- let case_multi_line_string =
283283- case_map Multi_line_string.jsont multi_line_string
284284- in
285285- let case_polygon = case_map Polygon.jsont polygon in
286286- let case_multi_polygon = case_map Multi_polygon.jsont multi_polygon in
287287- let case_geometry_collection =
288288- case_map (Lazy.force geometry_collection_jsont) geometry_collection
289289- in
290290- let case_feature =
291291- case_map (Lazy.force case_feature_jsont) feature
292292- in
293293- let case_feature_collection =
294294- case_map (Lazy.force feature_collection_json) feature_collection
295295- in
296296- let enc_case = function
297297- | `Point v -> Jsont.Object.Case.value case_point v
298298- | `Multi_point v -> Jsont.Object.Case.value case_multi_point v
299299- | `Line_string v -> Jsont.Object.Case.value case_line_string v
300300- | `Multi_line_string v ->
301301- Jsont.Object.Case.value case_multi_line_string v
302302- | `Polygon v -> Jsont.Object.Case.value case_polygon v
303303- | `Multi_polygon v -> Jsont.Object.Case.value case_multi_polygon v
304304- | `Geometry_collection v ->
305305- Jsont.Object.Case.value case_geometry_collection v
306306- | `Feature v -> Jsont.Object.Case.value case_feature v
307307- | `Feature_collection v ->
308308- Jsont.Object.Case.value case_feature_collection v
309309- in
310310- let cases =
311311- Jsont.Object.Case.
312312- [
313313- make case_point;
314314- make case_multi_point;
315315- make case_line_string;
316316- make case_multi_line_string;
317317- make case_polygon;
318318- make case_multi_polygon;
319319- make case_geometry_collection;
320320- make case_feature;
321321- make case_feature_collection;
322322- ]
323323- in
324324- Jsont.Object.map ~kind:"GeoJSON" Fun.id
325325- |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
326326- ~tag_to_string:Fun.id ~tag_compare:String.compare
327327- |> Jsont.Object.finish
328328- end
274274+ lazy begin
275275+ let case_point = case_map Point.jsont point in
276276+ let case_multi_point = case_map Multi_point.jsont multi_point in
277277+ let case_line_string = case_map Line_string.jsont line_string in
278278+ let case_multi_line_string =
279279+ case_map Multi_line_string.jsont multi_line_string
280280+ in
281281+ let case_polygon = case_map Polygon.jsont polygon in
282282+ let case_multi_polygon = case_map Multi_polygon.jsont multi_polygon in
283283+ let case_geometry_collection =
284284+ case_map (Lazy.force geometry_collection_jsont) geometry_collection
285285+ in
286286+ let case_feature = case_map (Lazy.force case_feature_jsont) feature in
287287+ let case_feature_collection =
288288+ case_map (Lazy.force feature_collection_json) feature_collection
289289+ in
290290+ let enc_case = function
291291+ | `Point v -> Jsont.Object.Case.value case_point v
292292+ | `Multi_point v -> Jsont.Object.Case.value case_multi_point v
293293+ | `Line_string v -> Jsont.Object.Case.value case_line_string v
294294+ | `Multi_line_string v ->
295295+ Jsont.Object.Case.value case_multi_line_string v
296296+ | `Polygon v -> Jsont.Object.Case.value case_polygon v
297297+ | `Multi_polygon v -> Jsont.Object.Case.value case_multi_polygon v
298298+ | `Geometry_collection v ->
299299+ Jsont.Object.Case.value case_geometry_collection v
300300+ | `Feature v -> Jsont.Object.Case.value case_feature v
301301+ | `Feature_collection v ->
302302+ Jsont.Object.Case.value case_feature_collection v
303303+ in
304304+ let cases =
305305+ Jsont.Object.Case.
306306+ [
307307+ make case_point;
308308+ make case_multi_point;
309309+ make case_line_string;
310310+ make case_multi_line_string;
311311+ make case_polygon;
312312+ make case_multi_polygon;
313313+ make case_geometry_collection;
314314+ make case_feature;
315315+ make case_feature_collection;
316316+ ]
317317+ in
318318+ Jsont.Object.map ~kind:"GeoJSON" Fun.id
319319+ |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
320320+ ~tag_to_string:Fun.id ~tag_compare:String.compare
321321+ |> Jsont.Object.finish
322322+ end
329323330324 let jsont = Lazy.force jsont
331325
+58-44
lib/geojson.mli
···11111212 {1:overview Overview}
13131414- GeoJSON is a format for encoding geographic data structures. It supports
1515- the following geometry types:
1414+ GeoJSON is a format for encoding geographic data structures. It supports the
1515+ following geometry types:
1616 - {!Point} - a single position
1717 - {!Line_string} - an array of positions forming a line
1818 - {!Polygon} - an array of linear rings (closed line strings)
···26262727 Creating a simple point feature:
2828 {[
2929- let pos = Position.v ~lng:(-122.4194) ~lat:37.7749 () in
3030- let point = Point.v pos in
3131- let feature = Geojson.Feature.v (`Point point) in
3232- let geojson = `Feature feature in
3333- Geojson.to_string geojson
2929+ let pos = Position.v ~lng:(-122.4194) ~lat:37.7749 () in
3030+ let point = Point.v pos in
3131+ let feature = Geojson.Feature.v (`Point point) in
3232+ let geojson = `Feature feature in
3333+ Geojson.to_string geojson
3434 ]} *)
35353636(** {1:primitives Primitive Types} *)
···3838(** Bounding box as an array of coordinates.
39394040 A bounding box is represented as an array of 4 or 6 numbers:
4141- - 2D: [\[west, south, east, north\]]
4242- - 3D: [\[west, south, min-altitude, east, north, max-altitude\]] *)
4141+ - 2D: [[west, south, east, north]]
4242+ - 3D: [[west, south, min-altitude, east, north, max-altitude]] *)
4343module Bbox : sig
4444 type t = float array
4545 (** The type for bounding boxes. *)
···5151(** Geographic position coordinates.
52525353 Positions are represented as arrays of 2 or 3 numbers:
5454- - 2D: [\[longitude, latitude\]]
5555- - 3D: [\[longitude, latitude, altitude\]]
5454+ - 2D: [[longitude, latitude]]
5555+ - 3D: [[longitude, latitude, altitude]]
56565757- Note that the order is longitude first, then latitude, following the
5858- GeoJSON specification (which differs from the common lat/lon convention). *)
5757+ Note that the order is longitude first, then latitude, following the GeoJSON
5858+ specification (which differs from the common lat/lon convention). *)
5959module Position : sig
6060 type t = float array
6161 (** The type for positions. *)
···6464 (** [jsont] is a JSON codec for positions. *)
65656666 val v : ?altitude:float -> lng:float -> lat:float -> unit -> t
6767- (** [v ~lng ~lat ()] creates a 2D position.
6868- [v ~altitude ~lng ~lat ()] creates a 3D position with altitude. *)
6767+ (** [v ~lng ~lat ()] creates a 2D position. [v ~altitude ~lng ~lat ()] creates
6868+ a 3D position with altitude. *)
69697070 val lng : t -> float
7171 (** [lng t] returns the longitude (first element). *)
···8888 (** The type for GeoJSON objects wrapping a value of type ['a]. *)
89899090 val type' : 'a t -> 'a
9191- (** [type' o] returns the wrapped value (the geometry coordinates or
9292- feature data). *)
9191+ (** [type' o] returns the wrapped value (the geometry coordinates or feature
9292+ data). *)
93939494 val bbox : 'a t -> Bbox.t option
9595 (** [bbox o] returns the optional bounding box. *)
···124124125125(** LineString geometry - an ordered sequence of positions forming a line.
126126127127- A LineString must have at least two positions. It represents a path
128128- through coordinate space. *)
127127+ A LineString must have at least two positions. It represents a path through
128128+ coordinate space. *)
129129module Line_string : sig
130130 type t = Position.t array
131131 (** The type for linestring coordinates. *)
···150150151151(** Polygon geometry - an area bounded by linear rings.
152152153153- A polygon is represented as an array of linear rings. The first ring
154154- is the exterior boundary; subsequent rings are holes. Each ring must
155155- have at least four positions, with the first and last being identical
156156- (a closed ring). *)
153153+ A polygon is represented as an array of linear rings. The first ring is the
154154+ exterior boundary; subsequent rings are holes. Each ring must have at least
155155+ four positions, with the first and last being identical (a closed ring). *)
157156module Polygon : sig
158157 type t = Line_string.t array
159158 (** The type for polygon coordinates (array of linear rings). *)
···177176178177(** Main GeoJSON types including features and geometry collections.
179178180180- This module provides the complete GeoJSON type hierarchy as defined
181181- in {{:https://datatracker.ietf.org/doc/html/rfc7946}RFC 7946}. *)
179179+ This module provides the complete GeoJSON type hierarchy as defined in
180180+ {{:https://datatracker.ietf.org/doc/html/rfc7946}RFC 7946}. *)
182181module Geojson : sig
183183-184182 (** {1:types Type Definitions} *)
185183186184 type 'a object' = 'a Geojson_object.t
187185 (** Alias for GeoJSON object wrapper. *)
188186189189- (** All geometry types as a polymorphic variant. *)
190187 type geometry =
191188 [ `Point of Point.t object'
192189 | `Multi_point of Multi_point.t object'
···195192 | `Polygon of Polygon.t object'
196193 | `Multi_polygon of Multi_polygon.t object'
197194 | `Geometry_collection of geometry_collection object' ]
195195+ (** All geometry types as a polymorphic variant. *)
198196199197 and geometry_collection = geometry list
200198 (** A collection of geometry objects. *)
201199202200 (** {1:features Features}
203201204204- A Feature object represents a spatially bounded entity with
205205- associated properties. *)
202202+ A Feature object represents a spatially bounded entity with associated
203203+ properties. *)
206204 module Feature : sig
207207-208205 type id = [ `Number of float | `String of string ]
209206 (** Feature identifiers can be either numbers or strings. *)
210207···215212 (** [id f] returns the optional feature identifier. *)
216213217214 val geometry : t -> geometry option
218218- (** [geometry f] returns the optional geometry. A feature may have
219219- null geometry. *)
215215+ (** [geometry f] returns the optional geometry. A feature may have null
216216+ geometry. *)
220217221218 val properties : t -> Jsont.json option
222222- (** [properties f] returns the optional properties JSON object.
223223- Properties can be any JSON object. *)
219219+ (** [properties f] returns the optional properties JSON object. Properties
220220+ can be any JSON object. *)
224221225222 type collection = t object' list
226223 (** A FeatureCollection is a list of features. *)
227224228225 val v : ?properties:Jsont.json -> geometry -> t object'
229229- (** [v ?properties geometry] creates a Feature with the given geometry
230230- and optional properties JSON object. *)
226226+ (** [v ?properties geometry] creates a Feature with the given geometry and
227227+ optional properties JSON object. *)
231228 end
232229233230 (** {1:toplevel Top-level GeoJSON Type} *)
···246243 These constructors wrap geometry objects in the appropriate variant. *)
247244248245 val point : Point.t object' -> [> `Point of Point.t object' ]
249249- val multi_point : Multi_point.t object' -> [> `Multi_point of Multi_point.t object' ]
250250- val line_string : Line_string.t object' -> [> `Line_string of Line_string.t object' ]
251251- val multi_line_string : Multi_line_string.t object' -> [> `Multi_line_string of Multi_line_string.t object' ]
246246+247247+ val multi_point :
248248+ Multi_point.t object' -> [> `Multi_point of Multi_point.t object' ]
249249+250250+ val line_string :
251251+ Line_string.t object' -> [> `Line_string of Line_string.t object' ]
252252+253253+ val multi_line_string :
254254+ Multi_line_string.t object' ->
255255+ [> `Multi_line_string of Multi_line_string.t object' ]
256256+252257 val polygon : Polygon.t object' -> [> `Polygon of Polygon.t object' ]
253253- val multi_polygon : Multi_polygon.t object' -> [> `Multi_polygon of Multi_polygon.t object' ]
254254- val geometry_collection : geometry_collection object' -> [> `Geometry_collection of geometry_collection object' ]
258258+259259+ val multi_polygon :
260260+ Multi_polygon.t object' -> [> `Multi_polygon of Multi_polygon.t object' ]
261261+262262+ val geometry_collection :
263263+ geometry_collection object' ->
264264+ [> `Geometry_collection of geometry_collection object' ]
265265+255266 val feature : Feature.t object' -> [> `Feature of Feature.t object' ]
256256- val feature_collection : Feature.collection object' -> [> `Feature_collection of Feature.collection object' ]
267267+268268+ val feature_collection :
269269+ Feature.collection object' ->
270270+ [> `Feature_collection of Feature.collection object' ]
257271258272 (** {1:codec Encoding and Decoding} *)
259273260274 val jsont : t Jsont.t
261261- (** [jsont] is a JSON codec for GeoJSON objects. Handles all GeoJSON
262262- types including features, feature collections, and all geometry types. *)
275275+ (** [jsont] is a JSON codec for GeoJSON objects. Handles all GeoJSON types
276276+ including features, feature collections, and all geometry types. *)
263277264278 val to_string : t -> string
265279 (** [to_string t] encodes [t] as a minified JSON string.
+21-19
lib/owntracks.mli
···25252626 Decoding a location message using jsont_bytesrw:
2727 {[
2828- let json = {|{"_type":"location","lat":51.5,"lon":-0.1,"tst":1234567890}|} in
2929- match Jsont_bytesrw.decode_string Owntracks.Message.jsont json with
3030- | Ok (Location loc) ->
3131- Printf.printf "Location: %.4f, %.4f\n"
3232- (Owntracks.Location.lat loc) (Owntracks.Location.lon loc)
3333- | Ok _ -> print_endline "Other message type"
3434- | Error e -> Printf.printf "Error: %s\n" e
2828+ let json =
2929+ {|{"_type":"location","lat":51.5,"lon":-0.1,"tst":1234567890}|}
3030+ in
3131+ match Jsont_bytesrw.decode_string Owntracks.Message.jsont json with
3232+ | Ok (Location loc) ->
3333+ Printf.printf "Location: %.4f, %.4f\n"
3434+ (Owntracks.Location.lat loc)
3535+ (Owntracks.Location.lon loc)
3636+ | Ok _ -> print_endline "Other message type"
3737+ | Error e -> Printf.printf "Error: %s\n" e
3538 ]}
36393737- See {{:https://owntracks.org/booklet/tech/json/}OwnTracks JSON format}
3838- for the complete specification.
4040+ See {{:https://owntracks.org/booklet/tech/json/}OwnTracks JSON format} for
4141+ the complete specification.
39424043 {1:modules Module Structure}
4144···56595760(** {1:types Message Types} *)
58615959-(** Location message - the primary OwnTracks message type. *)
6062module Location = Owntracks_location
6363+(** Location message - the primary OwnTracks message type. *)
61646565+module Transition = Owntracks_transition
6266(** Transition event - region entry/exit. *)
6363-module Transition = Owntracks_transition
64676868+module Waypoint = Owntracks_waypoint
6569(** Waypoint definition - monitored circular region. *)
6666-module Waypoint = Owntracks_waypoint
67706868-(** Card message - user information for display. *)
6971module Card = Owntracks_card
7272+(** Card message - user information for display. *)
70737171-(** LWT (Last Will and Testament) message. *)
7274module Lwt = Owntracks_lwt
7575+(** LWT (Last Will and Testament) message. *)
73767777+module Message = Owntracks_message
7478(** All OwnTracks message types as a variant. *)
7575-module Message = Owntracks_message
76797780(** {1:integration Integration Modules} *)
78817979-(** MQTT integration for OwnTracks messages. *)
8082module Mqtt = Owntracks_mqtt
8383+(** MQTT integration for OwnTracks messages. *)
81848282-(** OwnTracks Recorder HTTP API codecs. *)
8385module Recorder = Owntracks_recorder
8686+(** OwnTracks Recorder HTTP API codecs. *)
84878888+module Geojson = Owntracks_geojson_output
8589(** Convert OwnTracks locations to GeoJSON format. *)
8686-module Geojson = Owntracks_geojson_output
8787-
+4-12
lib/owntracks_card.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-type t = {
77- name : string option;
88- face : string option;
99- tid : string option;
1010-}
66+type t = { name : string option; face : string option; tid : string option }
117128let v ?name ?face ?tid () = { name; face; tid }
1313-149let name t = t.name
1510let face t = t.face
1611let tid t = t.tid
···2116 |> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun c -> c.name)
2217 |> Jsont.Object.opt_mem "face" Jsont.string ~enc:(fun c -> c.face)
2318 |> Jsont.Object.opt_mem "tid" Jsont.string ~enc:(fun c -> c.tid)
2424- |> Jsont.Object.skip_unknown
2525- |> Jsont.Object.finish
1919+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
26202721let jsont : t Jsont.t =
2822 let make _type name face tid =
···3428 |> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun c -> c.name)
3529 |> Jsont.Object.opt_mem "face" Jsont.string ~enc:(fun c -> c.face)
3630 |> Jsont.Object.opt_mem "tid" Jsont.string ~enc:(fun c -> c.tid)
3737- |> Jsont.Object.skip_unknown
3838- |> Jsont.Object.finish
3131+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
39324033let pp ppf card =
4141- Format.fprintf ppf "Card: %s"
4242- (Option.value ~default:"(no name)" card.name)
3434+ Format.fprintf ppf "Card: %s" (Option.value ~default:"(no name)" card.name)
+10-16
lib/owntracks_card.mli
···7788 @canonical Owntracks.Card
991010- Provides user information for display. Cards allow users to share
1111- their name and photo with others tracking their location. The
1212- tracker ID must match the location message's tid to associate the
1313- card with the correct user. *)
1010+ Provides user information for display. Cards allow users to share their name
1111+ and photo with others tracking their location. The tracker ID must match the
1212+ location message's tid to associate the card with the correct user. *)
14131514type t
1615(** The type for card messages. *)
17161817(** {1 Constructors} *)
19182020-val v :
2121- ?name:string ->
2222- ?face:string ->
2323- ?tid:string ->
2424- unit ->
2525- t
1919+val v : ?name:string -> ?face:string -> ?tid:string -> unit -> t
2620(** [v ()] creates a card message with optional fields. *)
27212822(** {1 Accessors} *)
···3125(** [name card] returns the full name of the user, if present. *)
32263327val face : t -> string option
3434-(** [face card] returns the Base64-encoded image (typically JPEG or PNG),
3535- if present. *)
2828+(** [face card] returns the Base64-encoded image (typically JPEG or PNG), if
2929+ present. *)
36303731val tid : t -> string option
3838-(** [tid card] returns the tracker ID that this card belongs to. Must
3939- match the tid in location messages to be associated correctly. *)
3232+(** [tid card] returns the tracker ID that this card belongs to. Must match the
3333+ tid in location messages to be associated correctly. *)
40344135(** {1 JSON Codec} *)
42364337val jsont : t Jsont.t
4444-(** [jsont] is a JSON codec for card messages.
4545- Expects the ["_type"] field to be ["card"]. *)
3838+(** [jsont] is a JSON codec for card messages. Expects the ["_type"] field to be
3939+ ["card"]. *)
46404741val jsont_bare : t Jsont.t
4842(** [jsont_bare] is a JSON codec that doesn't require the ["_type"] field. *)
+49-22
lib/owntracks_geojson_output.ml
···1212 ~lat:(Owntracks_location.lat loc)
1313 ()
14141515-let props ~device_name ~timestamp ~time ?accuracy ?speed ?battery ?tracker_id () =
1515+let props ~device_name ~timestamp ~time ?accuracy ?speed ?battery ?tracker_id ()
1616+ =
1617 let open Jsont.Json in
1717- let add n f opt acc = match opt with Some v -> (n, f v) :: acc | None -> acc in
1818+ let add n f opt acc =
1919+ match opt with Some v -> (n, f v) :: acc | None -> acc
2020+ in
1821 [
1922 ("name", string device_name);
2023 ("timestamp", int timestamp);
2121- ("time", string time)
2424+ ("time", string time);
2225 ]
2326 |> add "accuracy" number accuracy
2424- |> add "speed" number speed
2525- |> add "battery" int battery
2727+ |> add "speed" number speed |> add "battery" int battery
2628 |> add "tracker_id" string tracker_id
2727- |> fun mems -> Jsont.Json.object' (List.map (fun (n, v) -> Jsont.Json.mem (Jsont.Json.name n) v) mems)
2929+ |> fun mems ->
3030+ Jsont.Json.object'
3131+ (List.map (fun (n, v) -> Jsont.Json.mem (Jsont.Json.name n) v) mems)
28322933let point_feature ~device_name loc : Geojson.t =
3034 let point = Geometry.Point.v (pos_of_loc loc) in
3135 let geom : Geojson.geometry = `Point point in
3236 let tst = Owntracks_location.tst loc in
3333- let properties = Some (props ~device_name ~timestamp:tst
3434- ~time:(Owntracks_location.format_timestamp tst)
3535- ?accuracy:(Owntracks_location.acc loc)
3636- ?speed:(Owntracks_location.vel loc)
3737- ?battery:(Owntracks_location.batt loc)
3838- ?tracker_id:(Owntracks_location.tid loc) ()) in
3737+ let properties =
3838+ Some
3939+ (props ~device_name ~timestamp:tst
4040+ ~time:(Owntracks_location.format_timestamp tst)
4141+ ?accuracy:(Owntracks_location.acc loc)
4242+ ?speed:(Owntracks_location.vel loc)
4343+ ?battery:(Owntracks_location.batt loc)
4444+ ?tracker_id:(Owntracks_location.tid loc)
4545+ ())
4646+ in
3947 let feature = Geojson.Feature.v ?properties geom in
4048 `Feature feature
41494250let linestring_feature ~device_name locs : Geojson.t =
4343- let sorted = List.sort (fun a b ->
4444- Int.compare (Owntracks_location.tst a) (Owntracks_location.tst b)) locs in
5151+ let sorted =
5252+ List.sort
5353+ (fun a b ->
5454+ Int.compare (Owntracks_location.tst a) (Owntracks_location.tst b))
5555+ locs
5656+ in
4557 let positions = Array.of_list (List.map pos_of_loc sorted) in
4658 let line = Geometry.LineString.v positions in
4759 let geom : Geojson.geometry = `Line_string line in
4848- let start_time = match sorted with [] -> 0 | h :: _ -> Owntracks_location.tst h in
4949- let end_time = match List.rev sorted with [] -> 0 | h :: _ -> Owntracks_location.tst h in
5050- let properties = Some (Jsont.Json.object' [
5151- Jsont.Json.mem (Jsont.Json.name "name") (Jsont.Json.string device_name);
5252- Jsont.Json.mem (Jsont.Json.name "points") (Jsont.Json.int (List.length sorted));
5353- Jsont.Json.mem (Jsont.Json.name "start_time") (Jsont.Json.string (Owntracks_location.format_timestamp start_time));
5454- Jsont.Json.mem (Jsont.Json.name "end_time") (Jsont.Json.string (Owntracks_location.format_timestamp end_time));
5555- ]) in
6060+ let start_time =
6161+ match sorted with [] -> 0 | h :: _ -> Owntracks_location.tst h
6262+ in
6363+ let end_time =
6464+ match List.rev sorted with [] -> 0 | h :: _ -> Owntracks_location.tst h
6565+ in
6666+ let properties =
6767+ Some
6868+ (Jsont.Json.object'
6969+ [
7070+ Jsont.Json.mem (Jsont.Json.name "name")
7171+ (Jsont.Json.string device_name);
7272+ Jsont.Json.mem (Jsont.Json.name "points")
7373+ (Jsont.Json.int (List.length sorted));
7474+ Jsont.Json.mem
7575+ (Jsont.Json.name "start_time")
7676+ (Jsont.Json.string
7777+ (Owntracks_location.format_timestamp start_time));
7878+ Jsont.Json.mem
7979+ (Jsont.Json.name "end_time")
8080+ (Jsont.Json.string (Owntracks_location.format_timestamp end_time));
8181+ ])
8282+ in
5683 let feature = Geojson.Feature.v ?properties geom in
5784 `Feature feature
5885
+14-12
lib/owntracks_geojson_output.mli
···88 @canonical Owntracks.Geojson
991010 This module provides functions to convert location data into
1111- {{:https://geojson.org/}GeoJSON} Point and LineString features
1212- for use in mapping applications.
1111+ {{:https://geojson.org/}GeoJSON} Point and LineString features for use in
1212+ mapping applications.
13131414- The output is compatible with tools like Leaflet, MapLibre, QGIS,
1515- and geojson.io. *)
1414+ The output is compatible with tools like Leaflet, MapLibre, QGIS, and
1515+ geojson.io. *)
16161717-val point_feature : device_name:string -> Owntracks_location.t -> Geojson.Geojson.t
1818-(** [point_feature ~device_name loc] creates a GeoJSON Feature with
1919- Point geometry from a single location.
1717+val point_feature :
1818+ device_name:string -> Owntracks_location.t -> Geojson.Geojson.t
1919+(** [point_feature ~device_name loc] creates a GeoJSON Feature with Point
2020+ geometry from a single location.
20212122 The feature properties include:
2223 - [name]: the device name
···2728 - [battery]: battery percentage (if available)
2829 - [tracker_id]: tracker ID (if available) *)
29303030-val linestring_feature : device_name:string -> Owntracks_location.t list -> Geojson.Geojson.t
3131-(** [linestring_feature ~device_name locs] creates a GeoJSON Feature
3232- with LineString geometry from a list of locations.
3131+val linestring_feature :
3232+ device_name:string -> Owntracks_location.t list -> Geojson.Geojson.t
3333+(** [linestring_feature ~device_name locs] creates a GeoJSON Feature with
3434+ LineString geometry from a list of locations.
33353434- Locations are sorted by timestamp before creating the line. The
3535- feature properties include:
3636+ Locations are sorted by timestamp before creating the line. The feature
3737+ properties include:
3638 - [name]: the device name
3739 - [points]: number of positions in the line
3840 - [start_time]: formatted timestamp of first point
+112-62
lib/owntracks_location.ml
···25252626let v ?tid ~tst ~lat ~lon ?alt ?acc ?vel ?cog ?batt ?bs ?conn ?t ?m ?poi
2727 ?(inregions = []) ?addr ?topic () =
2828- { tid; tst; lat; lon; alt; acc; vel; cog; batt; bs; conn; t; m; poi;
2929- inregions; addr; topic }
2828+ {
2929+ tid;
3030+ tst;
3131+ lat;
3232+ lon;
3333+ alt;
3434+ acc;
3535+ vel;
3636+ cog;
3737+ batt;
3838+ bs;
3939+ conn;
4040+ t;
4141+ m;
4242+ poi;
4343+ inregions;
4444+ addr;
4545+ topic;
4646+ }
30473148let tid t = t.tid
3249let tst t = t.tst
···4562let inregions t = t.inregions
4663let addr t = t.addr
4764let topic t = t.topic
4848-4965let with_topic topic t = { t with topic = Some topic }
50665167let jsont : t Jsont.t =
5252- let make _type tid tst lat lon alt acc vel cog batt bs conn t m poi
5353- inregions addr topic =
6868+ let make _type tid tst lat lon alt acc vel cog batt bs conn t m poi inregions
6969+ addr topic =
5470 ignore _type;
5555- { tid; tst; lat; lon; alt; acc; vel; cog; batt; bs; conn; t; m; poi;
5656- inregions = Option.value ~default:[] inregions; addr; topic }
7171+ {
7272+ tid;
7373+ tst;
7474+ lat;
7575+ lon;
7676+ alt;
7777+ acc;
7878+ vel;
7979+ cog;
8080+ batt;
8181+ bs;
8282+ conn;
8383+ t;
8484+ m;
8585+ poi;
8686+ inregions = Option.value ~default:[] inregions;
8787+ addr;
8888+ topic;
8989+ }
5790 in
5891 Jsont.Object.map ~kind:"location" make
5992 |> Jsont.Object.mem "_type" Jsont.string ~enc:(fun _ -> "location")
···71104 |> Jsont.Object.opt_mem "t" Jsont.string ~enc:(fun l -> l.t)
72105 |> Jsont.Object.opt_mem "m" Jsont.int ~enc:(fun l -> l.m)
73106 |> Jsont.Object.opt_mem "poi" Jsont.string ~enc:(fun l -> l.poi)
7474- |> Jsont.Object.opt_mem "inregions" (Jsont.list Jsont.string)
7575- ~enc:(fun l -> match l.inregions with [] -> None | xs -> Some xs)
107107+ |> Jsont.Object.opt_mem "inregions" (Jsont.list Jsont.string) ~enc:(fun l ->
108108+ match l.inregions with [] -> None | xs -> Some xs)
76109 |> Jsont.Object.opt_mem "addr" Jsont.string ~enc:(fun l -> l.addr)
77110 |> Jsont.Object.opt_mem "topic" Jsont.string ~enc:(fun l -> l.topic)
7878- |> Jsont.Object.skip_unknown
7979- |> Jsont.Object.finish
111111+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
8011281113let jsont_bare : t Jsont.t =
8282- let make tid tst lat lon alt acc vel cog batt bs conn t m poi
8383- inregions addr topic =
8484- { tid; tst; lat; lon; alt; acc; vel; cog; batt; bs; conn; t; m; poi;
8585- inregions = Option.value ~default:[] inregions; addr; topic }
114114+ let make tid tst lat lon alt acc vel cog batt bs conn t m poi inregions addr
115115+ topic =
116116+ {
117117+ tid;
118118+ tst;
119119+ lat;
120120+ lon;
121121+ alt;
122122+ acc;
123123+ vel;
124124+ cog;
125125+ batt;
126126+ bs;
127127+ conn;
128128+ t;
129129+ m;
130130+ poi;
131131+ inregions = Option.value ~default:[] inregions;
132132+ addr;
133133+ topic;
134134+ }
86135 in
87136 Jsont.Object.map ~kind:"location" make
88137 |> Jsont.Object.opt_mem "tid" Jsont.string ~enc:(fun l -> l.tid)
···99148 |> Jsont.Object.opt_mem "t" Jsont.string ~enc:(fun l -> l.t)
100149 |> Jsont.Object.opt_mem "m" Jsont.int ~enc:(fun l -> l.m)
101150 |> Jsont.Object.opt_mem "poi" Jsont.string ~enc:(fun l -> l.poi)
102102- |> Jsont.Object.opt_mem "inregions" (Jsont.list Jsont.string)
103103- ~enc:(fun l -> match l.inregions with [] -> None | xs -> Some xs)
151151+ |> Jsont.Object.opt_mem "inregions" (Jsont.list Jsont.string) ~enc:(fun l ->
152152+ match l.inregions with [] -> None | xs -> Some xs)
104153 |> Jsont.Object.opt_mem "addr" Jsont.string ~enc:(fun l -> l.addr)
105154 |> Jsont.Object.opt_mem "topic" Jsont.string ~enc:(fun l -> l.topic)
106106- |> Jsont.Object.skip_unknown
107107- |> Jsont.Object.finish
155155+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
108156109157let format_timestamp tst =
110158 let t = Unix.gmtime (float_of_int tst) in
111111- Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC"
112112- (t.Unix.tm_year + 1900) (t.Unix.tm_mon + 1) t.Unix.tm_mday
113113- t.Unix.tm_hour t.Unix.tm_min t.Unix.tm_sec
159159+ Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC" (t.Unix.tm_year + 1900)
160160+ (t.Unix.tm_mon + 1) t.Unix.tm_mday t.Unix.tm_hour t.Unix.tm_min
161161+ t.Unix.tm_sec
114162115163let pp_code_map ~unknown codes ppf = function
116164 | Some s ->
117117- let display = List.assoc_opt s codes |> Option.value ~default:s in
118118- Format.pp_print_string ppf display
165165+ let display = List.assoc_opt s codes |> Option.value ~default:s in
166166+ Format.pp_print_string ppf display
119167 | None -> Format.pp_print_string ppf unknown
120168121169let pp_conn =
122170 pp_code_map ~unknown:"Unknown"
123123- ["w", "WiFi"; "m", "Mobile"; "o", "Offline"]
171171+ [ ("w", "WiFi"); ("m", "Mobile"); ("o", "Offline") ]
124172125173let pp_trigger =
126174 pp_code_map ~unknown:"Unknown"
127127- ["p", "Ping"; "c", "Circular region"; "b", "Beacon"; "r", "Response";
128128- "u", "Manual"; "t", "Timer"; "v", "Monitoring"]
175175+ [
176176+ ("p", "Ping");
177177+ ("c", "Circular region");
178178+ ("b", "Beacon");
179179+ ("r", "Response");
180180+ ("u", "Manual");
181181+ ("t", "Timer");
182182+ ("v", "Monitoring");
183183+ ]
129184130185let parse_topic topic =
131186 match String.split_on_char '/' topic with
···137192 Format.fprintf ppf "-------------------------------------------@,";
138193 begin match loc.topic with
139194 | Some topic ->
140140- begin match parse_topic topic with
141141- | Some (user, device) ->
142142- Format.fprintf ppf " User: %s / %s" user device;
143143- Option.iter (fun tid -> Format.fprintf ppf " [%s]" tid) loc.tid;
144144- Format.fprintf ppf "@,"
145145- | None ->
146146- Format.fprintf ppf " Topic: %s@," topic
147147- end
195195+ begin match parse_topic topic with
196196+ | Some (user, device) ->
197197+ Format.fprintf ppf " User: %s / %s" user device;
198198+ Option.iter (fun tid -> Format.fprintf ppf " [%s]" tid) loc.tid;
199199+ Format.fprintf ppf "@,"
200200+ | None -> Format.fprintf ppf " Topic: %s@," topic
201201+ end
148202 | None ->
149149- Option.iter (fun tid ->
150150- Format.fprintf ppf " Tracker: %s@," tid
151151- ) loc.tid
203203+ Option.iter
204204+ (fun tid -> Format.fprintf ppf " Tracker: %s@," tid)
205205+ loc.tid
152206 end;
153207 Format.fprintf ppf " Time: %s@," (format_timestamp loc.tst);
154208 Format.fprintf ppf " Location: %.6f, %.6f@," loc.lat loc.lon;
155155- Option.iter (fun alt ->
156156- Format.fprintf ppf " Altitude: %.1f m@," alt
157157- ) loc.alt;
158158- Option.iter (fun acc ->
159159- Format.fprintf ppf " Accuracy: +/- %.0f m@," acc
160160- ) loc.acc;
161161- Option.iter (fun vel ->
162162- Format.fprintf ppf " Speed: %.1f km/h@," vel
163163- ) loc.vel;
164164- Option.iter (fun cog ->
165165- Format.fprintf ppf " Heading: %.0f deg@," cog
166166- ) loc.cog;
167167- Option.iter (fun batt ->
168168- Format.fprintf ppf " Battery: %d%%@," batt
169169- ) loc.batt;
209209+ Option.iter
210210+ (fun alt -> Format.fprintf ppf " Altitude: %.1f m@," alt)
211211+ loc.alt;
212212+ Option.iter
213213+ (fun acc -> Format.fprintf ppf " Accuracy: +/- %.0f m@," acc)
214214+ loc.acc;
215215+ Option.iter
216216+ (fun vel -> Format.fprintf ppf " Speed: %.1f km/h@," vel)
217217+ loc.vel;
218218+ Option.iter
219219+ (fun cog -> Format.fprintf ppf " Heading: %.0f deg@," cog)
220220+ loc.cog;
221221+ Option.iter
222222+ (fun batt -> Format.fprintf ppf " Battery: %d%%@," batt)
223223+ loc.batt;
170224 Format.fprintf ppf " Conn: %a@," pp_conn loc.conn;
171171- Option.iter (fun _ ->
172172- Format.fprintf ppf " Trigger: %a@," pp_trigger loc.t
173173- ) loc.t;
174174- Option.iter (fun poi ->
175175- Format.fprintf ppf " POI: %s@," poi
176176- ) loc.poi;
225225+ Option.iter
226226+ (fun _ -> Format.fprintf ppf " Trigger: %a@," pp_trigger loc.t)
227227+ loc.t;
228228+ Option.iter (fun poi -> Format.fprintf ppf " POI: %s@," poi) loc.poi;
177229 if loc.inregions <> [] then
178230 Format.fprintf ppf " Regions: %s@," (String.concat ", " loc.inregions);
179179- Option.iter (fun addr ->
180180- Format.fprintf ppf " Address: %s@," addr
181181- ) loc.addr;
231231+ Option.iter (fun addr -> Format.fprintf ppf " Address: %s@," addr) loc.addr;
182232 Format.fprintf ppf "-------------------------------------------@]"
+26-26
lib/owntracks_location.mli
···7788 @canonical Owntracks.Location
991010- The primary OwnTracks message type, published when the device reports
1111- its location. Contains GPS coordinates, accuracy, altitude, speed,
1212- heading, and various device state information.
1010+ The primary OwnTracks message type, published when the device reports its
1111+ location. Contains GPS coordinates, accuracy, altitude, speed, heading, and
1212+ various device state information.
13131414- Required fields are latitude, longitude, and timestamp. All other
1515- fields are optional and may not be present depending on device
1616- capabilities and settings. *)
1414+ Required fields are latitude, longitude, and timestamp. All other fields are
1515+ optional and may not be present depending on device capabilities and
1616+ settings. *)
17171818type t
1919(** The type for location messages. *)
···4040 ?topic:string ->
4141 unit ->
4242 t
4343-(** [v ~tst ~lat ~lon ()] creates a location with the required fields.
4444- Optional fields can be provided as labeled arguments. *)
4343+(** [v ~tst ~lat ~lon ()] creates a location with the required fields. Optional
4444+ fields can be provided as labeled arguments. *)
45454646(** {1 Accessors} *)
4747···5050 characters) configured in the app. *)
51515252val tst : t -> int
5353-(** [tst loc] returns the timestamp as Unix epoch (seconds since
5454- 1970-01-01 00:00:00 UTC). *)
5353+(** [tst loc] returns the timestamp as Unix epoch (seconds since 1970-01-01
5454+ 00:00:00 UTC). *)
55555656val lat : t -> float
5757(** [lat loc] returns the latitude in decimal degrees. Range: -90 to +90. *)
···6969(** [vel loc] returns the velocity (speed) in km/h, if present. *)
70707171val cog : t -> float option
7272-(** [cog loc] returns the course over ground (heading) in degrees from
7373- true north (0-360), if present. *)
7272+(** [cog loc] returns the course over ground (heading) in degrees from true
7373+ north (0-360), if present. *)
74747575val batt : t -> int option
7676(** [batt loc] returns the battery level as percentage (0-100), if present. *)
···106106 - [3] = Move mode (frequent updates) *)
107107108108val poi : t -> string option
109109-(** [poi loc] returns the Point of Interest name if the device is
110110- currently at a defined location. *)
109109+(** [poi loc] returns the Point of Interest name if the device is currently at a
110110+ defined location. *)
111111112112val inregions : t -> string list
113113-(** [inregions loc] returns the list of region names the device is
114114- currently inside. May be empty. *)
113113+(** [inregions loc] returns the list of region names the device is currently
114114+ inside. May be empty. *)
115115116116val addr : t -> string option
117117-(** [addr loc] returns the reverse-geocoded address, if present.
118118- Typically added by the OwnTracks Recorder server. *)
117117+(** [addr loc] returns the reverse-geocoded address, if present. Typically added
118118+ by the OwnTracks Recorder server. *)
119119120120val topic : t -> string option
121121-(** [topic loc] returns the MQTT topic this message was published to,
122122- if present. Added during parsing. *)
121121+(** [topic loc] returns the MQTT topic this message was published to, if
122122+ present. Added during parsing. *)
123123124124(** {1 Modifiers} *)
125125···129129(** {1 JSON Codec} *)
130130131131val jsont : t Jsont.t
132132-(** [jsont] is a JSON codec for location messages.
133133- Expects the ["_type"] field to be ["location"]. *)
132132+(** [jsont] is a JSON codec for location messages. Expects the ["_type"] field
133133+ to be ["location"]. *)
134134135135val jsont_bare : t Jsont.t
136136-(** [jsont_bare] is a JSON codec that doesn't require the ["_type"] field.
137137- Use this for parsing recorder API responses which omit the type field. *)
136136+(** [jsont_bare] is a JSON codec that doesn't require the ["_type"] field. Use
137137+ this for parsing recorder API responses which omit the type field. *)
138138139139(** {1 Pretty Printing} *)
140140···142142(** [pp ppf loc] pretty-prints a location message. *)
143143144144val format_timestamp : int -> string
145145-(** [format_timestamp tst] formats a Unix timestamp as an ISO 8601 string
146146- in UTC timezone. *)
145145+(** [format_timestamp tst] formats a Unix timestamp as an ISO 8601 string in UTC
146146+ timezone. *)
+2-5
lib/owntracks_lwt.ml
···66type t = { tst : int }
7788let v ~tst = { tst }
99-109let tst t = t.tst
11101211let jsont_bare : t Jsont.t =
1312 let make tst = { tst } in
1413 Jsont.Object.map ~kind:"lwt" make
1514 |> Jsont.Object.mem "tst" Jsont.int ~enc:(fun l -> l.tst)
1616- |> Jsont.Object.skip_unknown
1717- |> Jsont.Object.finish
1515+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
18161917let jsont : t Jsont.t =
2018 let make _type tst =
···2422 Jsont.Object.map ~kind:"lwt" make
2523 |> Jsont.Object.mem "_type" Jsont.string ~enc:(fun _ -> "lwt")
2624 |> Jsont.Object.mem "tst" Jsont.int ~enc:(fun l -> l.tst)
2727- |> Jsont.Object.skip_unknown
2828- |> Jsont.Object.finish
2525+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
29263027let pp ppf lwt =
3128 Format.fprintf ppf "LWT: client disconnected at %s"
+2-2
lib/owntracks_lwt.mli
···2727(** {1 JSON Codec} *)
28282929val jsont : t Jsont.t
3030-(** [jsont] is a JSON codec for LWT messages.
3131- Expects the ["_type"] field to be ["lwt"]. *)
3030+(** [jsont] is a JSON codec for LWT messages. Expects the ["_type"] field to be
3131+ ["lwt"]. *)
32323333val jsont_bare : t Jsont.t
3434(** [jsont_bare] is a JSON codec that doesn't require the ["_type"] field. *)
+17-11
lib/owntracks_message.ml
···2222 Jsont.Object.Case.map "location" Owntracks_location.jsont_bare ~dec:location
2323 in
2424 let case_transition =
2525- Jsont.Object.Case.map "transition" Owntracks_transition.jsont_bare ~dec:transition
2525+ Jsont.Object.Case.map "transition" Owntracks_transition.jsont_bare
2626+ ~dec:transition
2627 in
2728 let case_waypoint =
2829 Jsont.Object.Case.map "waypoint" Owntracks_waypoint.jsont_bare ~dec:waypoint
2930 in
3031 let case_waypoints =
3131- Jsont.Object.Case.map "waypoints" Owntracks_waypoint.jsont_bare ~dec:waypoint
3232+ Jsont.Object.Case.map "waypoints" Owntracks_waypoint.jsont_bare
3333+ ~dec:waypoint
3234 in
3335 let case_card =
3436 Jsont.Object.Case.map "card" Owntracks_card.jsont_bare ~dec:card
···4446 | Lwt l -> Jsont.Object.Case.value case_lwt l
4547 | Unknown _ -> assert false (* Cannot encode Unknown *)
4648 in
4747- let cases = Jsont.Object.Case.[
4848- make case_location; make case_transition;
4949- make case_waypoint; make case_waypoints;
5050- make case_card; make case_lwt
5151- ] in
4949+ let cases =
5050+ Jsont.Object.Case.
5151+ [
5252+ make case_location;
5353+ make case_transition;
5454+ make case_waypoint;
5555+ make case_waypoints;
5656+ make case_card;
5757+ make case_lwt;
5858+ ]
5959+ in
5260 Jsont.Object.map ~kind:"message" Fun.id
5361 |> Jsont.Object.case_mem "_type" Jsont.string ~enc:Fun.id ~enc_case cases
5454- |> Jsont.Object.skip_unknown
5555- |> Jsont.Object.finish
6262+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
56635764let pp ppf = function
5865 | Location loc -> Owntracks_location.pp ppf loc
···6067 | Waypoint wp -> Owntracks_waypoint.pp ppf wp
6168 | Card c -> Owntracks_card.pp ppf c
6269 | Lwt l -> Owntracks_lwt.pp ppf l
6363- | Unknown typ ->
6464- Format.fprintf ppf "Unknown message type: %s" typ
7070+ | Unknown typ -> Format.fprintf ppf "Unknown message type: %s" typ
+8-13
lib/owntracks_message.mli
···88 @canonical Owntracks.Message
991010 All OwnTracks message types as a single variant. Use {!jsont} with
1111- {{:https://erratique.ch/software/jsont}jsont_bytesrw} to decode
1212- messages from JSON strings. *)
1111+ {{:https://erratique.ch/software/jsont}jsont_bytesrw} to decode messages
1212+ from JSON strings. *)
13131414+(** The type for OwnTracks messages. *)
1415type t =
1515- | Location of Owntracks_location.t
1616- (** A location update from the device. *)
1717- | Transition of Owntracks_transition.t
1818- (** A region entry/exit event. *)
1919- | Waypoint of Owntracks_waypoint.t
2020- (** A waypoint/region definition. *)
2121- | Card of Owntracks_card.t
2222- (** User information card. *)
2323- | Lwt of Owntracks_lwt.t
2424- (** Client disconnection notification. *)
1616+ | Location of Owntracks_location.t (** A location update from the device. *)
1717+ | Transition of Owntracks_transition.t (** A region entry/exit event. *)
1818+ | Waypoint of Owntracks_waypoint.t (** A waypoint/region definition. *)
1919+ | Card of Owntracks_card.t (** User information card. *)
2020+ | Lwt of Owntracks_lwt.t (** Client disconnection notification. *)
2521 | Unknown of string
2622 (** Unknown message type. Contains the ["_type"] value. *)
2727-(** The type for OwnTracks messages. *)
28232924(** {1 JSON Codec} *)
3025
+9-10
lib/owntracks_mqtt.ml
···4040 if String.length payload > 0 && payload.[0] = '{' then
4141 let topic_json = Printf.sprintf "{\"topic\":%S," msg.topic in
4242 topic_json ^ String.sub payload 1 (String.length payload - 1)
4343- else
4444- payload
4343+ else payload
4544 in
4646- match Jsont_bytesrw.decode_string Owntracks_message.jsont payload_with_topic with
4545+ match
4646+ Jsont_bytesrw.decode_string Owntracks_message.jsont payload_with_topic
4747+ with
4748 | Ok message -> Ok { topic = msg.topic; user; device; message }
4849 | Error e -> Error e
49505051let of_mqtt ~topic ~payload : (t, string) result =
5151- of_mqtt_message { Mqtt_message.topic; payload; qos = `At_least_once; retain = false }
5252+ of_mqtt_message
5353+ { Mqtt_message.topic; payload; qos = `At_least_once; retain = false }
52545355let default_topic = "owntracks/#"
5454-5556let user_topic user = Printf.sprintf "owntracks/%s/#" user
5656-5757let device_topic ~user ~device = Printf.sprintf "owntracks/%s/%s" user device
58585959let pp ppf msg =
6060 Format.fprintf ppf "@[<v 0>";
6161- begin match msg.user, msg.device with
6161+ begin match (msg.user, msg.device) with
6262 | Some user, Some device ->
6363- Format.fprintf ppf "User: %s / Device: %s@," user device
6464- | _ ->
6565- Format.fprintf ppf "Topic: %s@," msg.topic
6363+ Format.fprintf ppf "User: %s / Device: %s@," user device
6464+ | _ -> Format.fprintf ppf "Topic: %s@," msg.topic
6665 end;
6766 Owntracks_message.pp ppf msg.message;
6867 Format.fprintf ppf "@]"
+14-14
lib/owntracks_mqtt.mli
···7788 @canonical Owntracks.Mqtt
991010- This module provides helpers for parsing MQTT messages into OwnTracks
1111- types and constructing MQTT topic patterns for subscriptions.
1010+ This module provides helpers for parsing MQTT messages into OwnTracks types
1111+ and constructing MQTT topic patterns for subscriptions.
12121313 {1 Topic Format}
1414···5555(** [of_mqtt_message msg] parses an MQTT message into an OwnTracks message.
56565757 Extracts user and device from the topic if it follows the OwnTracks
5858- convention ([owntracks/user/device]). The topic is also injected into
5959- the message payload for location messages.
5858+ convention ([owntracks/user/device]). The topic is also injected into the
5959+ message payload for location messages.
60606161 Returns [Error] if the payload is not valid OwnTracks JSON. *)
62626363val of_mqtt : topic:string -> payload:string -> (t, string) result
6464-(** [of_mqtt ~topic ~payload] is a convenience function for parsing
6565- MQTT messages without constructing an {!Mqtt_message.t} record.
6464+(** [of_mqtt ~topic ~payload] is a convenience function for parsing MQTT
6565+ messages without constructing an {!Mqtt_message.t} record.
66666767- Equivalent to calling {!of_mqtt_message} with default QoS and
6868- retain settings. *)
6767+ Equivalent to calling {!of_mqtt_message} with default QoS and retain
6868+ settings. *)
69697070(** {1 Topic Helpers} *)
71717272val default_topic : string
7373-(** [default_topic] is ["owntracks/#"], a wildcard topic that matches
7474- all OwnTracks messages from all users and devices. *)
7373+(** [default_topic] is ["owntracks/#"], a wildcard topic that matches all
7474+ OwnTracks messages from all users and devices. *)
75757676val user_topic : string -> string
7777-(** [user_topic user] returns ["owntracks/{user}/#"], matching all
7878- devices for a specific user. *)
7777+(** [user_topic user] returns ["owntracks/{user}/#"], matching all devices for a
7878+ specific user. *)
79798080val device_topic : user:string -> device:string -> string
8181-(** [device_topic ~user ~device] returns ["owntracks/{user}/{device}"],
8282- matching a specific device. *)
8181+(** [device_topic ~user ~device] returns ["owntracks/{user}/{device}"], matching
8282+ a specific device. *)
83838484val parse_topic : string -> (string * string) option
8585(** [parse_topic topic] extracts the user and device from an OwnTracks topic.
+3-6
lib/owntracks_recorder.ml
···1818 let make data = data in
1919 Jsont.Object.map ~kind:"data_response" make
2020 |> Jsont.Object.mem "data" locations_jsont ~enc:Fun.id
2121- |> Jsont.Object.skip_unknown
2222- |> Jsont.Object.finish
2121+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
23222424-let string_list_jsont : string list Jsont.t =
2525- Jsont.list Jsont.string
2323+let string_list_jsont : string list Jsont.t = Jsont.list Jsont.string
26242725let string_list_results_jsont : string list Jsont.t =
2826 let make results = results in
2927 Jsont.Object.map ~kind:"results_response" make
3028 |> Jsont.Object.mem "results" string_list_jsont ~enc:Fun.id
3131- |> Jsont.Object.skip_unknown
3232- |> Jsont.Object.finish
2929+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
+8-9
lib/owntracks_recorder.mli
···7788 @canonical Owntracks.Recorder
991010- The {{:https://github.com/owntracks/recorder}OwnTracks Recorder} is a
1111- server that stores location history and provides an HTTP API for
1212- querying it.
1010+ The {{:https://github.com/owntracks/recorder}OwnTracks Recorder} is a server
1111+ that stores location history and provides an HTTP API for querying it.
13121414- This module provides codecs for parsing JSON responses from the
1515- Recorder API. Use these with jsont_bytesrw for decoding.
1313+ This module provides codecs for parsing JSON responses from the Recorder
1414+ API. Use these with jsont_bytesrw for decoding.
16151716 {1 API Endpoints}
18171918 The Recorder provides these endpoints:
2019 - [GET /api/0/list] - List all users
2120 - [GET /api/0/list?user=USER] - List devices for a user
2222- - [GET /api/0/locations?user=USER&device=DEVICE&from=DATE&to=DATE] -
2323- Fetch location history *)
2121+ - [GET /api/0/locations?user=USER&device=DEVICE&from=DATE&to=DATE] - Fetch
2222+ location history *)
24232524(** {1 Types} *)
2625···4241(** {1 JSON Codecs} *)
43424443val locations_jsont : Owntracks_location.t list Jsont.t
4545-(** Codec for a JSON array of location objects (without ["_type"] field).
4646- Use with the [/api/0/locations] endpoint when it returns an array. *)
4444+(** Codec for a JSON array of location objects (without ["_type"] field). Use
4545+ with the [/api/0/locations] endpoint when it returns an array. *)
47464847val locations_data_jsont : Owntracks_location.t list Jsont.t
4948(** Codec for [{data: [...]}] response format from some recorder endpoints. *)
···7788 @canonical Owntracks.Transition
991010- Published when entering or leaving a monitored region. Transitions
1111- are triggered by geofences (circular regions) or beacons configured
1212- in the OwnTracks app. *)
1010+ Published when entering or leaving a monitored region. Transitions are
1111+ triggered by geofences (circular regions) or beacons configured in the
1212+ OwnTracks app. *)
13131414type t
1515(** The type for transition events. *)
···5454(** [desc tr] returns the description/name of the region, if present. *)
55555656val wtst : t -> int option
5757-(** [wtst tr] returns the timestamp of the waypoint definition that
5858- triggered this transition, if present. *)
5757+(** [wtst tr] returns the timestamp of the waypoint definition that triggered
5858+ this transition, if present. *)
59596060(** {1 JSON Codec} *)
61616262val jsont : t Jsont.t
6363-(** [jsont] is a JSON codec for transition messages.
6464- Expects the ["_type"] field to be ["transition"]. *)
6363+(** [jsont] is a JSON codec for transition messages. Expects the ["_type"] field
6464+ to be ["transition"]. *)
65656666val jsont_bare : t Jsont.t
6767(** [jsont_bare] is a JSON codec that doesn't require the ["_type"] field. *)
+5-14
lib/owntracks_waypoint.ml
···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-type t = {
77- tst : int;
88- lat : float;
99- lon : float;
1010- rad : int;
1111- desc : string;
1212-}
66+type t = { tst : int; lat : float; lon : float; rad : int; desc : string }
137148let v ~tst ~lat ~lon ~rad ~desc = { tst; lat; lon; rad; desc }
1515-169let tst t = t.tst
1710let lat t = t.lat
1811let lon t = t.lon
···2720 |> Jsont.Object.mem "lon" Jsont.number ~enc:(fun w -> w.lon)
2821 |> Jsont.Object.mem "rad" Jsont.int ~enc:(fun w -> w.rad)
2922 |> Jsont.Object.mem "desc" Jsont.string ~enc:(fun w -> w.desc)
3030- |> Jsont.Object.skip_unknown
3131- |> Jsont.Object.finish
2323+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
32243325let jsont : t Jsont.t =
3426 let make _type tst lat lon rad desc =
···4234 |> Jsont.Object.mem "lon" Jsont.number ~enc:(fun w -> w.lon)
4335 |> Jsont.Object.mem "rad" Jsont.int ~enc:(fun w -> w.rad)
4436 |> Jsont.Object.mem "desc" Jsont.string ~enc:(fun w -> w.desc)
4545- |> Jsont.Object.skip_unknown
4646- |> Jsont.Object.finish
3737+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
47384839let pp ppf wp =
4949- Format.fprintf ppf "Waypoint: %s at (%.6f, %.6f) radius %dm"
5050- wp.desc wp.lat wp.lon wp.rad
4040+ Format.fprintf ppf "Waypoint: %s at (%.6f, %.6f) radius %dm" wp.desc wp.lat
4141+ wp.lon wp.rad
+7-13
lib/owntracks_waypoint.mli
···7788 @canonical Owntracks.Waypoint
991010- Describes a monitored circular region. Waypoints define geofences
1111- that trigger transition events when the device enters or leaves them. *)
1010+ Describes a monitored circular region. Waypoints define geofences that
1111+ trigger transition events when the device enters or leaves them. *)
12121313type t
1414(** The type for waypoint definitions. *)
15151616(** {1 Constructors} *)
17171818-val v :
1919- tst:int ->
2020- lat:float ->
2121- lon:float ->
2222- rad:int ->
2323- desc:string ->
2424- t
1818+val v : tst:int -> lat:float -> lon:float -> rad:int -> desc:string -> t
2519(** [v ~tst ~lat ~lon ~rad ~desc] creates a waypoint definition. *)
26202721(** {1 Accessors} *)
28222923val tst : t -> int
3030-(** [tst wp] returns the timestamp when the waypoint was created or
3131- last modified. *)
2424+(** [tst wp] returns the timestamp when the waypoint was created or last
2525+ modified. *)
32263327val lat : t -> float
3428(** [lat wp] returns the latitude of the region center. *)
···4539(** {1 JSON Codec} *)
46404741val jsont : t Jsont.t
4848-(** [jsont] is a JSON codec for waypoint messages.
4949- Expects the ["_type"] field to be ["waypoint"]. *)
4242+(** [jsont] is a JSON codec for waypoint messages. Expects the ["_type"] field
4343+ to be ["waypoint"]. *)
50445145val jsont_bare : t Jsont.t
5246(** [jsont_bare] is a JSON codec that doesn't require the ["_type"] field. *)