From 5b62a20bba4b215ed482e7df481027b098dcb6f5 Mon Sep 17 00:00:00 2001 From: niten Date: Tue, 11 Apr 2023 08:01:31 -0700 Subject: [PATCH] Initial code checkin, untested --- src/suanni/syno_eyes.clj | 210 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 1 deletion(-) diff --git a/src/suanni/syno_eyes.clj b/src/suanni/syno_eyes.clj index 6a5f871..27d42aa 100644 --- a/src/suanni/syno_eyes.clj +++ b/src/suanni/syno_eyes.clj @@ -1,2 +1,210 @@ (ns suanni.syno-eyes - (:require [suanni.eyes :refer [SuanniEyes]])) + (:require [suanni.eyes :refer [SuanniEyes]] + [fudo-clojure.http.client :as client] + [fudo-clojure.http.request :as req] + [fudo-clojure.logging :as log] + [fudo-clojure.result :as result] + [clojure.string :as str]) + (:import java.net.InetAddress)) + +;; ### BaseSynoClient +;; +;; In order to fully initialize the SynoClient, we need to be able to query the +;; Synology host to get path/version information and to authenticate. This base +;; client will have enough functionality to do that. Calling `authenticate!` on +;; the base client will actually perform the queries necessary to initialize +;; full functionality. + +(defprotocol IBaseSynoClient + (get! [_ req]) + (initialize! [_ username passwd])) + +;; ### SynoClient +;; +;; The SynoClient is the client that is actually able to do things like list +;; cameras and take snapshots. + +(defprotocol ISynoClient + (disconnect! [_]) + (camera-snapshot! [_ camera-id]) + (list-cameras! [_]) + (get-camera-by-location! [_ loc])) + +(defprotocol ICamera + (id [_]) + (location [_]) + (vendor [_]) + (model [_]) + (host [_]) + (port [_]) + (take-snapshot! [_])) + +(defrecord Camera [conn data] + ICamera + (id [_] (:id data)) + (location [_] (-> data :newName keyword)) + (vendor [_] (-> data :vendor)) + (model [_] (-> data :model)) + (host [_] (-> data :ip)) + (port [_] (-> data :port)) + (take-snapshot! [self] (camera-snapshot! conn (id self)))) + +(defn- perform-request! [http-client req] + (result/bind (client/execute-request! http-client req) + (fn [resp] + (if (:error resp) + (throw (ex-info "error performing request" + {:request req + :error (:error resp) + :response resp})) + (cond (:data resp) (:data resp) + (:body resp) (:body resp)))))) + +(defn- get-hostname [] + (-> (InetAddress/getLocalHost) + (.getHostName))) + +(defn- make-api-info-request + [{api :api}] + (-> (req/base-request) + (req/with-path "/webapi/query.cgi") + (req/withQueryParams + { + :api :SYNO.API.Info + :method :Query + :version 1 + :query api + }))) + +(defn- make-auth-request + [{max-version :maxVersion + path :path + account :account + passwd :passwd}] + (-> (req/base-request) + (req/with-path (format "/webapi/%s" path)) + (req/with-query-params + { + :version max-version + :session :SurveillanceStation + :api :SYNO.API.Auth + :method :login + :account account + :passwd passwd + :format :sid + :enable_device_token true + :device_name (get-hostname) + }))) + +(defn- make-logout-request + [{max-version :maxVersion path :path}] + (-> (req/base-request) + (req/with-path (format "/webapi/%s" path)) + (req/withQueryParams + { + :version max-version + :session :SurveillanceStation + :api :SYNO.API.Auth + :method :Logout + }))) + +(defn- make-list-cameras-request [{max-version :maxVersion path :path}] + (-> (req/base-request) + (req/with-path (format "webapi/%s" path)) + (req/with-query-params + { + :version max-version + :session :SurveillanceStation + :api :SYNO.SurveillanceStation.Camera + :method :List + }))) + +(defn- make-snapshot-request + [{max-version :maxVersion path :path} camera-id] + (-> (req/base-request) + (req/with-path (format "/webapi/%s" path)) + (req/with-response-format :binary) + (req/with-option :as :byte-array) + (req/withQueryParams + { + :version max-version + :session :SurveillanceStation + :api :SYNO.SurveillanceStation.Camera + :method :GetSnapshot + :id camera-id + }))) + +(defn- get-api-info! [conn api] + (some-> conn + (get! (make-api-info-request {:api api})) + api)) + +(defn- authenticate! [conn username passwd] + (->> (get-api-info! conn :SYNO.API.Auth) + (merge {:username username :passwd passwd}) + (make-auth-request) + (get! conn))) + +(defn- find-first [pred lst] + (loop [els lst] + (if (pred (first els)) + (first els) + (recur (rest els))))) + +(defrecord SynoClient [conn auth-info api-info logger] + IBaseSynoClient + (get! [_ req] + (get! conn + (-> req + (req/with-query-params + {:device_id (:device_id auth-info) + :_sid (:sid auth-info)})))) + (initialize! [_ _ _] + (throw (ex-info "client already initialized!" {}))) + + ISynoClient + (disconnect! [self] + (->> (:SYNO.API.Auth api-info) + (make-logout-request) + (get! self))) + (camera-snapshot! [self camera-id] + (log/info! logger (format "fetching snapshot from camera %s" camera-id)) + (get! self (make-snapshot-request (:SYNO.SurveillanceStation.Camera api-info) camera-id))) + (list-cameras! [self] + (log/info! logger "fetching camera list") + (let [cams (into [] + (map (partial ->Camera self)) + (-> self + (get! (make-list-cameras-request + (:SYNO.SurveillanceStation.Camera api-info))) + :cameras))] + (log/info! logger (format "fetched %s cameras" (count cams))) + cams)) + (get-camera-by-location! [self loc] + (->> (list-cameras! self) + (find-first (fn [cam] (= loc (location cam))))))) + +(defn- initialize-connection! + [conn & {username :username passwd :passwd logger :logger}] + (let [api-info (into {} (map (fn [api] [api (get-api-info! conn api)])) + [:SYNO.SurveillanceStation.Camera + :SYNO.API.Auth]) + auth-info (authenticate! conn username passwd)] + (->SynoClient conn auth-info api-info logger))) + +(defn create [& {host :host port :port logger :logger}] + (let [http-client (client/json-client)] + (reify + IBaseSynoClient + (get! [_ req] + (perform-request! http-client + (-> req + (req/with-host host) + (req/with-port port) + (req/with-option :insecure? true) + (req/as-get)))) + (initialize! [self username passwd] + (initialize-connection! self + :username username + :passwd passwd + :logger logger)))))