Add elprice table + JSON API (GET /api/elprice?date=...)
New `elprice` table with a simpler shape (time_dk, price_area, price_dkk) backing a JSON endpoint that returns hourly DK1 + DK2 prices for any date. Cache-miss path fetches from the same DayAheadPrices dataset as day_ahead_prices, upserts, and returns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
DROP TABLE elprice;
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE elprice (
|
||||
time_dk TIMESTAMP NOT NULL,
|
||||
price_area VARCHAR(3) NOT NULL,
|
||||
price_dkk NUMERIC(10,2) NOT NULL,
|
||||
PRIMARY KEY (time_dk, price_area)
|
||||
);
|
||||
@@ -65,3 +65,16 @@ ON CONFLICT (time_start, hour) DO NOTHING;
|
||||
INSERT INTO consumption (time_start, hour, kwh)
|
||||
VALUES (:time-start, :hour, :kwh)
|
||||
ON CONFLICT (time_start, hour) DO NOTHING;
|
||||
|
||||
-- :name insert-elprice! :! :n
|
||||
-- :doc Insert an hourly el-price row; keep first value on conflict
|
||||
INSERT INTO elprice (time_dk, price_area, price_dkk)
|
||||
VALUES (:time-dk, :price-area, :price-dkk)
|
||||
ON CONFLICT (time_dk, price_area) DO NOTHING;
|
||||
|
||||
-- :name get-elprice-for-date :? :*
|
||||
-- :doc Hourly prices for a DK local date, both areas
|
||||
SELECT time_dk, price_area, price_dkk
|
||||
FROM elprice
|
||||
WHERE time_dk::date = :date
|
||||
ORDER BY time_dk, price_area;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
(ns pmagnus.elprice.web.controllers.elprice
|
||||
(:require
|
||||
[clojure.data.json :as json]
|
||||
[clojure.tools.logging :as log])
|
||||
(:import
|
||||
(java.net
|
||||
URI
|
||||
URLEncoder)
|
||||
(java.net.http
|
||||
HttpClient
|
||||
HttpRequest
|
||||
HttpResponse$BodyHandlers)
|
||||
(java.sql
|
||||
Timestamp)
|
||||
(java.time
|
||||
LocalDate
|
||||
ZoneId)
|
||||
(java.time.format
|
||||
DateTimeFormatter)))
|
||||
|
||||
|
||||
(def ^:private dk-zone (ZoneId/of "Europe/Copenhagen"))
|
||||
(def ^:private date-fmt (DateTimeFormatter/ofPattern "yyyy-MM-dd"))
|
||||
(def ^:private eur-dkk-rate 7.46)
|
||||
|
||||
|
||||
(defn- build-url
|
||||
[^LocalDate date]
|
||||
(let [start (.format date date-fmt)
|
||||
end (.format (.plusDays date 1) date-fmt)]
|
||||
(str "https://api.energidataservice.dk/dataset/DayAheadPrices"
|
||||
"?start=" start
|
||||
"&end=" end
|
||||
"&filter=" (URLEncoder/encode (json/write-str {"PriceArea" ["DK1" "DK2"]}) "UTF-8")
|
||||
"&sort=TimeDK%20asc"
|
||||
"&limit=200")))
|
||||
|
||||
|
||||
(defn- fetch-json
|
||||
[url]
|
||||
(let [client (HttpClient/newHttpClient)
|
||||
request (-> (HttpRequest/newBuilder)
|
||||
(.uri (URI/create url))
|
||||
(.header "Accept" "application/json")
|
||||
(.GET)
|
||||
(.build))
|
||||
response (.send client request (HttpResponse$BodyHandlers/ofString))]
|
||||
(when (= 200 (.statusCode response))
|
||||
(json/read-str (.body response) :key-fn keyword))))
|
||||
|
||||
|
||||
(defn- parse-timestamp
|
||||
[s]
|
||||
(when s
|
||||
(Timestamp/valueOf (.replace ^String s "T" " "))))
|
||||
|
||||
|
||||
(defn- dkk-price
|
||||
[price-dkk price-eur]
|
||||
(or price-dkk
|
||||
(when price-eur (* price-eur eur-dkk-rate))))
|
||||
|
||||
|
||||
(defn- fetch-from-api
|
||||
[^LocalDate date]
|
||||
(let [url (build-url date)]
|
||||
(log/info "Fetching el prices for" (.format date date-fmt) "from" url)
|
||||
(:records (fetch-json url))))
|
||||
|
||||
|
||||
(defn- save-records!
|
||||
[query-fn records]
|
||||
(doseq [rec records]
|
||||
(when-let [dkk (dkk-price (:DayAheadPriceDKK rec) (:DayAheadPriceEUR rec))]
|
||||
(query-fn :insert-elprice!
|
||||
{:time-dk (parse-timestamp (:TimeDK rec))
|
||||
:price-area (:PriceArea rec)
|
||||
:price-dkk dkk}))))
|
||||
|
||||
|
||||
(defn get-or-fetch!
|
||||
"Return hourly el-price rows for the given LocalDate (DK local), fetching
|
||||
from the upstream API and caching on a miss."
|
||||
[query-fn ^LocalDate date]
|
||||
(let [sql-date (java.sql.Date/valueOf date)
|
||||
rows (query-fn :get-elprice-for-date {:date sql-date})]
|
||||
(if (seq rows)
|
||||
rows
|
||||
(let [records (fetch-from-api date)]
|
||||
(when (seq records)
|
||||
(log/info "Saving" (count records) "el-price records")
|
||||
(save-records! query-fn records))
|
||||
(query-fn :get-elprice-for-date {:date sql-date})))))
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns pmagnus.elprice.web.routes.api
|
||||
(:require
|
||||
[integrant.core :as ig]
|
||||
[pmagnus.elprice.web.controllers.elprice :as elprice]
|
||||
[pmagnus.elprice.web.controllers.health :as health]
|
||||
[pmagnus.elprice.web.middleware.exception :as exception]
|
||||
[pmagnus.elprice.web.middleware.formats :as formats]
|
||||
@@ -8,7 +9,13 @@
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||
[reitit.ring.middleware.parameters :as parameters]
|
||||
[reitit.swagger :as swagger]))
|
||||
[reitit.swagger :as swagger]
|
||||
[ring.util.http-response :as http-response])
|
||||
(:import
|
||||
(java.time
|
||||
LocalDate)
|
||||
(java.time.format
|
||||
DateTimeParseException)))
|
||||
|
||||
|
||||
(def route-data
|
||||
@@ -33,9 +40,24 @@
|
||||
exception/wrap-exception]})
|
||||
|
||||
|
||||
(defn- elprice-handler
|
||||
[query-fn {{{:keys [date]} :query} :parameters}]
|
||||
(try
|
||||
(let [d (LocalDate/parse date)
|
||||
rows (elprice/get-or-fetch! query-fn d)]
|
||||
(http-response/ok
|
||||
{:date date
|
||||
:prices (mapv (fn [r] {:time_dk (str (:time_dk r))
|
||||
:price_area (:price_area r)
|
||||
:price_dkk (:price_dkk r)})
|
||||
rows)}))
|
||||
(catch DateTimeParseException _
|
||||
(http-response/bad-request {:error "date must be YYYY-MM-DD"}))))
|
||||
|
||||
|
||||
;; Routes
|
||||
(defn api-routes
|
||||
[_opts]
|
||||
[{:keys [query-fn]}]
|
||||
[["/swagger.json"
|
||||
{:get {:no-doc true
|
||||
:swagger {:info {:title "pmagnus.elprice API"}}
|
||||
@@ -44,7 +66,11 @@
|
||||
;; note that use of the var is necessary
|
||||
;; for reitit to reload routes without
|
||||
;; restarting the system
|
||||
{:get #'health/healthcheck!}]])
|
||||
{:get #'health/healthcheck!}]
|
||||
["/elprice"
|
||||
{:get {:summary "Hourly DK1 + DK2 electricity prices for a date"
|
||||
:parameters {:query [:map [:date :string]]}
|
||||
:handler (partial elprice-handler query-fn)}}]])
|
||||
|
||||
|
||||
(derive :reitit.routes/api :reitit/routes)
|
||||
|
||||
Reference in New Issue
Block a user