Now reads config.yaml.

Configuration now comes from config.yaml.  Command line flags override
config.yaml values.

Added --config and --secrets to specify the path to config.yaml and
secrets.yaml.
This commit is contained in:
John Wiseman 2020-01-20 21:30:50 -08:00
parent 807b5d7d44
commit 074f7c77c3
3 changed files with 115 additions and 70 deletions

View File

@ -60,7 +60,7 @@
(let [url (->> [url (let [url (->> [url
"lat" lat "lat" lat
"lon" lon "lon" lon
"dist" radius-nm] "dist" (.toFixed radius-nm 1)]
(map str) (map str)
(string/join "/"))] (string/join "/"))]
(p/let [http-result (util/http-get url {:headers {:api-auth api-key}})] (p/let [http-result (util/http-get url {:headers {:api-auth api-key}})]
@ -69,15 +69,10 @@
result)))) result))))
;; We keep position reports going back this far.
(def max-history-age-ms (* 25 60 1000))
;; Given a vector of position history, removes old entries. ;; Given a vector of position history, removes old entries.
(defn prune-history [history now] (defn prune-history [history now config]
(let [h (filterv #(< (- now (:time %)) max-history-age-ms) history)] (let [h (filterv #(< (- now (:time %)) (:max-history-age-ms config)) history)]
h)) h))
@ -109,9 +104,9 @@
updated-db)) updated-db))
(defn prune-histories [db now] (defn prune-histories [db now config]
(reduce-kv (fn [m k v] (reduce-kv (fn [m k v]
(assoc m k (update v :history prune-history now))) (assoc m k (update v :history prune-history now config)))
{} {}
db)) db))
@ -144,10 +139,10 @@
(first args)) (first args))
(defn update-history-db [db new-data now] (defn update-history-db [db new-data now config]
(-> db (-> db
(update-history-db-add-new-data new-data now) (update-history-db-add-new-data new-data now)
(prune-histories now) (prune-histories now config)
(prune-records now))) (prune-records now)))
@ -170,12 +165,6 @@
(/ (.getTime (js/Date.)) 1)) (/ (.getTime (js/Date.)) 1))
;; This is how many degrees of turning we need to see over
;; max-history-age-ms ms to consider it a potential circling aircraft.
(def curviness-threshold-degrees 1440)
(defn ac-desc [ac] (defn ac-desc [ac]
(str (:icao ac) " " (:lat ac) " " (:lon ac) (str (:icao ac) " " (:lat ac) " " (:lon ac)
" #" (:registration ac) " " (:alt ac) " " (:curviness ac) " " " #" (:registration ac) " " (:alt ac) " " (:curviness ac) " "
@ -194,15 +183,15 @@
image-path)) image-path))
(defn circling? [ac] (defn circling? [ac config]
(and (> (geo/flight-curviness (:history ac)) curviness-threshold-degrees) (and (> (geo/flight-curviness (:history ac)) (:curviness-threshold-degrees config))
(> (:alt ac) 300))) (> (:alt ac) 300)))
;; Returns a vector of two elements, ;; Returns a vector of two elements,
;; [updated-database potentially-circling-aircraft] ;; [updated-database potentially-circling-aircraft]
(defn detect-circles [db now] (defn detect-circles [db now config]
(log-verbose "Detecting circles") (log-verbose "Detecting circles")
(loop [old-db (seq db) (loop [old-db (seq db)
new-db {} new-db {}
@ -213,7 +202,7 @@
ac (assoc ac ac (assoc ac
:curviness curviness :curviness curviness
:normalized-curviness (geo/flight-normalized-curviness (:history ac))) :normalized-curviness (geo/flight-normalized-curviness (:history ac)))
currently-circling? (circling? ac) currently-circling? (circling? ac config)
previously-circling? (:started-circling-time ac)] previously-circling? (:started-circling-time ac)]
(cond (cond
(and currently-circling? (and currently-circling?
@ -371,11 +360,6 @@
recent-hist)) recent-hist))
;; If the centroid of the aircraft's positions is less than this close
;; to an airport, then it's probably just doinf flight training.
(def minimum-airport-distance-km 2.5)
;;(def minimum-airport-distance-miles 0)
(defn process-potential-circle [ac config now] (defn process-potential-circle [ac config now]
(p/let [icao (:icao ac) (p/let [icao (:icao ac)
recent-positions (recent-history (:history ac)) recent-positions (recent-history (:history ac))
@ -393,11 +377,11 @@
(log-info "%s: Closest airport is %s, distance: %s km" (log-info "%s: Closest airport is %s, distance: %s km"
(:icao ac) (:label airport-properties) (:distance airport-properties)) (:icao ac) (:label airport-properties) (:distance airport-properties))
(log-info "%s: No airports nearby" (:icao ac))) (log-info "%s: No airports nearby" (:icao ac)))
(if (and airport-properties (<= (:distance airport-properties) minimum-airport-distance-km)) (if (and airport-properties (<= (:distance airport-properties) (:minimum-airport-distance-km config)))
(log-info "%s: Filtering out because it's %s km (minimum is %s) from %s" (log-info "%s: Filtering out because it's %s km (minimum is %s) from %s"
(:icao ac) (:icao ac)
(:distance airport-properties) (:distance airport-properties)
minimum-airport-distance-km (:minimum-airport-distance-km config)
(:label airport-properties) (:label airport-properties)
()) ())
(do (do
@ -458,55 +442,88 @@
(p/recur (rest acs)))))) (p/recur (rest acs))))))
(def history-db-path "advisory-circular.db") (def default-config
(def secrets-path "secrets.yaml") {
;; We keep position reports going back this far.
:max-history-age-ms (* 25 60 1000)
;; This is how many degrees of turning we need to see over
;; max-history-age-ms ms to consider it a potential circling
;; aircraft.
:curviness-threshold-degrees 1440
;; If the centroid of the aircraft's positions is less than this
;; close to an airport, then it's probably just doinf flight
;; training.
:minimum-airport-distance-km 2.5
:history-db-path "advisory-circular.db"
:twitter {:enabled? true}})
(defn build-config [secrets commander] (defn build-config-from-commander [commander]
(-> (merge-with merge (cond-> {}
secrets (.-adsbxUrl commander)
{:adsbx {:url (.-adsbxUrl commander)}} (assoc-in [:adsbx :url] (.-adsbxUrl commander))
{:twitter {:enabled? (.-tweeting commander)}} ;; Note that we're distinguishing from the situation where
{:pelias {:url (.-peliasUrl commander)}}) ;; --tweeting or --no-tweeting is supplied, in which case the
(assoc :basestation-sqb (.-basestationSqb commander) ;; tweeting field will be true or false, from the situation where
:lat (.-lat commander) ;; it's not suppled, in which case it will be undefined/nil.
:lon (.-lon commander) (not (nil? (.-tweeting commander)))
:radius-nm (.-radius commander)))) (assoc-in [:twitter :enabled?] (.-tweeting commander))
(.-peliasUrl commander)
(assoc-in [:pelias :url] (.-peliasUrl commander))
(.-basestationSqb commander)
(assoc :basestation-sqb (.-basestationSqb commander))
(.-lat commander)
(assoc :lat (.-lat commander))
(.-lon commander)
(assoc :lon (.-lon commander))
(.-radius commander)
(assoc :radius (.-radius commander))))
(defn build-config [config cli-config secrets]
(util/deep-merge default-config config cli-config secrets))
(defn main [& args] (defn main [& args]
(-> commander (-> commander
(.requiredOption "--lat <lat>" "Latitude of the circle of region of interest" parse-number) (.option "--lat <lat>" "Latitude of the circle of region of interest" parse-number)
(.requiredOption "--lon <lat>" "Longitude of the circle of the region of interest" parse-number) (.option "--lon <lat>" "Longitude of the circle of the region of interest" parse-number)
(.requiredOption "--adsbx-url <url>" "ADSBX API url") (.option "--adsbx-url <url>" "ADSBX API url")
(.requiredOption "--pelias-url <url>" "Base pelias geocoder URL") (.option "--pelias-url <url>" "Base pelias geocoder URL")
(.option "--radius <radius>" "Radius of the circle of interest, in nautical miles" 20 parse-number) (.option "--radius <radius>" "Radius of the circle of interest, in km" parse-number)
(.option "--basestation-sqb <path>" "Path to a basestation.sqb database file") (.option "--basestation-sqb <path>" "Path to a basestation.sqb database file")
(.option "--no-tweeting" "Do not tweet.") (.option "--tweeting" "Enables tweeting")
(.option "--no-tweeting" "Do not tweet")
(.option "--config <path>" "Path to the configuration yaml file" "config.yaml")
(.option "--secrets <path>" "Path to the secrets yaml file" "secrets.yaml")
(.parse (.-argv js/process))) (.parse (.-argv js/process)))
(let [start-time (current-time)] (let [start-time (current-time)]
(p/then (p/all [(read-history-db history-db-path) (p/let [base-config (util/read-config (.-config commander))
(util/read-config secrets-path)]) cli-config (build-config-from-commander commander)
(fn [[db secrets]] secrets (util/read-config (.-secrets commander))
(p/let [config (build-config secrets commander) config (build-config base-config cli-config secrets)
data (get-adsbexchange-live-data _1 (pprint/pprint base-config)
{:url (get-in config [:adsbx :url]) _2 (pprint/pprint cli-config)
:api-key (get-in config [:adsbx :api-key]) _3 (pprint/pprint config)
:lat (:lat config) db (read-history-db (:history-db-path config))
:lon (:lon config) data (get-adsbexchange-live-data
:radius-nm (:radius-nm config)}) {:url (get-in config [:adsbx :url])
now (current-time) :api-key (get-in config [:adsbx :api-key])
[new-db potential-circles] (-> db :lat (:lat config)
(update-history-db (:aircraft data) now) :lon (:lon config)
(detect-circles now))] :radius-nm (* (:radius-km config) 0.539957)})
(p/do now (current-time)
(when potential-circles [new-db potential-circles] (-> db
(doseq [ac potential-circles] (update-history-db (:aircraft data) now config)
(log-warn "%s: New circle detected: %s" (:icao ac) (ac-desc ac))) (detect-circles now config))]
(process-potential-circles potential-circles config now)) (p/do
(write-history-db new-db history-db-path) (when potential-circles
(doseq [ac potential-circles]
(log-warn "%s: New circle detected: %s" (:icao ac) (ac-desc ac)))
(process-potential-circles potential-circles config now))
(write-history-db new-db (:history-db-path config))
(let [end-time (current-time)] (let [end-time (current-time)]
(log-info (log-info
"Completed processing in %f seconds: tracking %s aircraft; %s potential circles" "Completed processing in %f seconds: tracking %s aircraft; %s potential circles"
(/ (- end-time start-time) 1000) (/ (- end-time start-time) 1000)
(count new-db) (count new-db)
(count potential-circles))))))))) (count potential-circles)))))))

View File

@ -17,13 +17,15 @@
(.readFile fs-promises path (clj->js options))) (.readFile fs-promises path (clj->js options)))
;; Writes a file, returns a promise that resolves to no arguments on success. ;; Writes a file, returns a promise that resolves to no arguments on
;; success.
(defn write-file [path data options] (defn write-file [path data options]
(.writeFile fs-promises path data (clj->js options))) (.writeFile fs-promises path data (clj->js options)))
;; Reads a YAML config file. Returns a promise that resolves to the parsed YAML. ;; Reads a YAML config file. Returns a promise that resolves to the
;; parsed YAML.
(defn read-config [path] (defn read-config [path]
(log-verbose "Reading config file %s" path) (log-verbose "Reading config file %s" path)
@ -48,3 +50,19 @@
(p/let [result (.get request (p/let [result (.get request
(clj->js (merge {:url (str url) :gzip true} options)))] (clj->js (merge {:url (str url) :gzip true} options)))]
result))))) result)))))
;; From https://github.com/puppetlabs/clj-kitchensink/blob/cfea4a16e4d2e15a2d391131a163b4eeb60d872e/src/puppetlabs/kitchensink/core.clj#L311-L332
(defn deep-merge
"Deeply merges maps so that nested maps are combined rather than replaced.
For example:
(deep-merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}})
;;=> {:foo {:bar :baz, :fuzz :buzz}}
;; contrast with clojure.core/merge
(merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}})
;;=> {:foo {:fuzz :quzz}} ; note how last value for :foo wins"
[& vs]
(if (every? map? vs)
(apply merge-with deep-merge vs)
(last vs)))

View File

@ -0,0 +1,10 @@
(ns lemondronor.circlebot.util-test
(:require [cljs.test :refer (deftest is testing)]
[lemondronor.circlebot.util :as util]))
(deftest deep-merge
(is (= (util/deep-merge
{:adsbx {:url "http://bar"}}
{:adsbx {:url "http://foo"}}
{:adsbx {:secret "123"}})
{:adsbx {:url "http://foo", :secret "123"}})))