├── .gitignore ├── README.org ├── env ├── dev │ ├── clj │ │ └── wacnet │ │ │ └── repl.clj │ └── cljs │ │ └── wacnet │ │ └── dev.cljs └── prod │ └── cljs │ └── wacnet │ ├── prod.cljs │ └── prod.cljs~ ├── externs.js ├── project.clj ├── resources └── public │ ├── bootstrap-3.3.6-dist │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── jquery-2.2.0.min.js │ │ └── npm.js │ ├── css │ ├── chosen-sprite.png │ ├── chosen-sprite@2x.png │ ├── fixed-data-table.min.css │ ├── material-design-iconic-font.min.css │ ├── re-com.css │ └── site.css │ ├── font-awesome │ ├── css │ │ └── font-awesome.min.css │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── fonts │ ├── Material-Design-Iconic-Font.eot │ ├── Material-Design-Iconic-Font.svg │ ├── Material-Design-Iconic-Font.ttf │ ├── Material-Design-Iconic-Font.woff │ └── Material-Design-Iconic-Font.woff2 │ ├── img │ ├── HVACIO-logo.svg │ ├── Vigilia-logo-name.svg │ ├── Vigilia-logo.svg │ ├── favicon.png │ ├── splash.png │ ├── splash.svg │ ├── wacnet-logo-name.svg │ ├── wacnet-logo.svg │ └── youtube.png │ └── web-nrepl │ ├── img │ └── clojure-logo.png │ ├── jquery-console │ └── jquery.console.js │ ├── tryclojure.css │ └── tryclojure.js ├── src ├── clj │ └── wacnet │ │ ├── api.clj │ │ ├── api │ │ ├── bacnet │ │ │ ├── common.clj │ │ │ ├── devices.clj │ │ │ └── local_device.clj │ │ ├── repl.clj │ │ ├── util.clj │ │ └── vigilia_logger.clj │ │ ├── handler.clj │ │ ├── local_device.clj │ │ ├── nrepl.clj │ │ ├── nrepl.clj~ │ │ ├── server.clj │ │ └── systray.clj ├── cljc │ └── wacnet │ │ └── bacnet_utils.cljc └── cljs │ └── wacnet │ ├── core.cljs │ ├── explorer │ ├── devices.cljs │ └── objects.cljs │ ├── handler.cljs │ ├── local_device │ └── configs.cljs │ ├── repl.cljs │ ├── routes.cljs │ ├── stateful.cljs │ ├── templates │ └── common.cljs │ ├── utils.cljs │ └── vigilia.cljs └── test └── wacnet └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | *.log -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Wacnet 2 | 3 | * Compiled Version 4 | The compiled version can be found [[https://hvac.io/docs/wacnet][here]]. 5 | 6 | * Rationale 7 | The purpose of BACnet is to provide interoperability between 8 | devices from multiple manufacturers. 9 | 10 | The idea is that if all devices speak the same language, no one 11 | will be 'trapped' with a proprietary protocol, forcing him to 12 | always buy at the same place. 13 | 14 | This common language also have other advantages, such as making it 15 | easy to gather data and make advanced analysis. Simply checking the 16 | historical data plotted on a graph is enough to find and solve most 17 | problem. 18 | 19 | However, despite all BACnet's promises, the landscape is still 20 | mostly occupied by a handful of manufacturers. 21 | 22 | In addition, BACnet is pitched as an 'open protocol', but the 23 | standards need to be ordered; even [[www.bacnet.org][bacnet.org]] doesn't provide a 24 | link to download the specs. This isn't making it easy for 25 | newcomers. Want to see and try BACnet? Well, send us money and 26 | we'll send you a boring pdf, and maybe, maybe you will stick with 27 | us. 28 | 29 | For the developers, there's some highly cryptic application that 30 | will provide some support, but for the newcomer that's simply 31 | counterproductive to even try them. 32 | 33 | This results in an horrible situations for the users. Building 34 | managers are often clueless as to what to do with a BACnet network. 35 | They don't know what's in it, nor do they know how to browse it. 36 | (Of course manufacturers will offer their own software... for a 37 | price.) There's nothing wrong with selling software, but for such a 38 | basic need, browsing the network, a free software should be 39 | available. It's like comparing Notepad and Word. Sure, sell Word, 40 | but the user should at least be able to write basic stuff in 41 | notepad. 42 | 43 | *Wacnet* is a humble try to ameliorate the situation. By enabling 44 | an /easy/ and almost instantaneous setup, anyone can at least see 45 | what's on the network. Newcomers can explore the different 46 | properties and learn them. 47 | 48 | 49 | * Usage 50 | ** Getting the application 51 | If you know Clojure, download the [[https://github.com/Frozenlock/wacnet][source]] and do =lein uberjar=. 52 | 53 | If you do not, download the pre-packaged version at 54 | [[https://hvac.io/docs/wacnet]]. 55 | 56 | ** Running the application 57 | 58 | No installation necessary and can run from a USB key! 59 | 60 | The standalone jar file can be started on any computer with Java 61 | installed. We recommend starting it from the command line as such: 62 | : java -jar 63 | 64 | For example: 65 | : java -jar wacnet-0.1.1-BETA-standalone.jar 66 | 67 | You can of course start it by simply double-clicking on the .jar 68 | file, but you might have a hard time finding the 'off' switch. 69 | (You will have to manually kill it.) 70 | 71 | Once the application is started, go to [[http://localhost:47800]] and 72 | browse your network! 73 | 74 | ** Advanced usage (REPL) 75 | The REPL is an interactive evaluation environment to enable power 76 | users to use tools tailored to their needs. If a feature is 77 | lacking, it's even possible to add it on-the-fly! 78 | 79 | * Warnings 80 | This software isn't polished. You might see errors (in which case 81 | please send them to us). 82 | 83 | * License 84 | 85 | Copyright (C) 2016 Frozenlock 86 | 87 | GNU General Public License version 3.0 (GPLv3) 88 | -------------------------------------------------------------------------------- /env/dev/clj/wacnet/repl.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.repl 2 | (:use wacnet.handler 3 | ring.server.standalone 4 | [ring.middleware file-info file])) 5 | 6 | (defonce server (atom nil)) 7 | 8 | (defn get-handler [] 9 | ;; #'app expands to (var app) so that when we reload our code, 10 | ;; the server is forced to re-resolve the symbol in the var 11 | ;; rather than having its own copy. When the root binding 12 | ;; changes, the server picks it up without having to restart. 13 | (-> #'app 14 | ; Makes static assets in $PROJECT_DIR/resources/public/ available. 15 | (wrap-file "resources") 16 | ; Content-Type, Content-Length, and Last Modified headers for files in body 17 | (wrap-file-info))) 18 | 19 | (defn start-server 20 | "used for starting the server in development mode from REPL" 21 | [& [port]] 22 | (let [port (if port (Integer/parseInt port) 3000)] 23 | (reset! server 24 | (serve (get-handler) 25 | {:port port 26 | :auto-reload? true 27 | :join? false})) 28 | (println (str "You can view the site at http://localhost:" port)))) 29 | 30 | (defn stop-server [] 31 | (.stop @server) 32 | (reset! server nil)) 33 | -------------------------------------------------------------------------------- /env/dev/cljs/wacnet/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load wacnet.dev 2 | (:require [wacnet.core :as core] 3 | [figwheel.client :as figwheel :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (figwheel/watch-and-reload 8 | :websocket-url "ws://localhost:3449/figwheel-ws" 9 | :jsload-callback core/mount-root) 10 | 11 | (core/init!) 12 | -------------------------------------------------------------------------------- /env/prod/cljs/wacnet/prod.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.prod 2 | (:require [wacnet.core :as core])) 3 | 4 | ;;ignore println statements in prod 5 | (set! *print-fn* (fn [& _])) 6 | 7 | (core/init!) 8 | -------------------------------------------------------------------------------- /env/prod/cljs/wacnet/prod.cljs~: -------------------------------------------------------------------------------- 1 | (ns wacnet.prod 2 | (:require [graphivac.core :as core])) 3 | 4 | ;;ignore println statements in prod 5 | (set! *print-fn* (fn [& _])) 6 | 7 | (core/init!) 8 | -------------------------------------------------------------------------------- /externs.js: -------------------------------------------------------------------------------- 1 | var TopLevel = { 2 | "abs" : function () {}, 3 | "add" : function () {}, 4 | "browser" : function () {}, 5 | "Cell" : function () {}, 6 | "classList" : function () {}, 7 | "close" : function () {}, 8 | "Column" : function () {}, 9 | "console" : function () {}, 10 | "contentDocument" : function () {}, 11 | "ctrlKey" : function () {}, 12 | "dataTransfer" : function () {}, 13 | "Date" : function () {}, 14 | "disconnect" : function () {}, 15 | "document" : function () {}, 16 | "FixedDataTable" : function () {}, 17 | "focus" : function () {}, 18 | "getBoundingClientRect" : function () {}, 19 | "getElementById" : function () {}, 20 | "getElementsByTagName" : function () {}, 21 | "goog" : function () {}, 22 | "hash" : function () {}, 23 | "height" : function () {}, 24 | "History" : function () {}, 25 | "history" : function () {}, 26 | "indexOf" : function () {}, 27 | "isChrome" : function () {}, 28 | "isOpera" : function () {}, 29 | "labs" : function () {}, 30 | "location" : function () {}, 31 | "log" : function () {}, 32 | "Math" : function () {}, 33 | "observe" : function () {}, 34 | "open" : function () {}, 35 | "parent" : function () {}, 36 | "parentNode" : function () {}, 37 | "parseInt" : function () {}, 38 | "pushState" : function () {}, 39 | "remove" : function () {}, 40 | "replaceState" : function () {}, 41 | "requestAnimationFrame" : function () {}, 42 | "ResizeObserver" : function () {}, 43 | "scrollIntoView" : function () {}, 44 | "setAttribute" : function () {}, 45 | "setData" : function () {}, 46 | "setDragImage" : function () {}, 47 | "setEnabled" : function () {}, 48 | "setSelectionRange" : function () {}, 49 | "setTimeout" : function () {}, 50 | "shiftKey" : function () {}, 51 | "style" : function () {}, 52 | "Table" : function () {}, 53 | "target" : function () {}, 54 | "token" : function () {}, 55 | "top" : function () {}, 56 | "toTimeString" : function () {}, 57 | "userAgent" : function () {}, 58 | "value" : function () {}, 59 | "WacnetVersion" : function () {}, 60 | "which" : function () {}, 61 | "window" : function () {}, 62 | "write" : function () {} 63 | } 64 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject wacnet "2.1.8" 2 | :description "Webserver to browse a BACnet network" 3 | :url "https://hvac.io" 4 | :license {:name "GNU General Public License V3" 5 | :url "http://www.gnu.org/licenses/gpl-3.0.html"} 6 | ;; WARNING 7 | ;; compiling with java 9 causes the webserver to hang when started on java 8 8 | :dependencies [[org.clojure/clojure "1.10.1"] 9 | 10 | ;; BACnet 11 | [bacure "1.1.10"] 12 | 13 | [io.hvac.vigilia/vigilia-logger "1.2.1"] 14 | 15 | ;; webserver stuff 16 | [bidi "2.1.6"] ; routing 17 | [aleph "0.4.6"] ; server 18 | [aleph-middleware "0.2.0"] 19 | [yada "1.2.16"] 20 | 21 | [trptcolin/versioneer "0.2.0"] 22 | 23 | ;; error logs 24 | [com.taoensso/timbre "5.1.0"] 25 | [com.fzakaria/slf4j-timbre "0.3.20"] 26 | 27 | ;; systemTray 28 | [com.dorkbox/SystemTray "3.17"] 29 | 30 | ;; nREPL 31 | [nrepl "1.0.0"] 32 | [cider/cider-nrepl "0.27.4"] 33 | 34 | ;; cljs 35 | [org.clojure/clojurescript "1.10.748"] 36 | [org.clojure/core.async "1.1.587"] 37 | [org.clojure/tools.reader "1.3.2"] 38 | [cljsjs/resize-observer-polyfill "1.4.2-0"] 39 | 40 | [reagent "0.8.1"] ;; [reagent "0.8.0-alpha2"] ;; doesn't work with clojure 1.9 on java 9 41 | 42 | [org.clojars.frozenlock/reagent-modals "0.2.8"] 43 | [org.clojars.frozenlock/reagent-keybindings "1.0.2"] 44 | [alandipert/storage-atom "2.0.1"] 45 | [cljs-ajax "0.8.0"] 46 | [re-com "2.6.0"] 47 | [com.andrewmcveigh/cljs-time "0.5.2"] 48 | [cljsjs/fixed-data-table-2 "0.8.23-0" 49 | :exclusions [cljsjs/react]]] 50 | 51 | :plugins [[lein-environ "1.1.0"] 52 | [lein-ancient "0.6.15"] 53 | [lein-cljsbuild "1.1.7"] 54 | [org.clojars.rasom/lein-externs "0.1.7"]] 55 | 56 | :manifest {"SplashScreen-Image" "public/img/splash.png"} 57 | 58 | :min-lein-version "2.5.0" 59 | 60 | :main wacnet.server 61 | 62 | :clean-targets ^{:protect false} [:target-path 63 | [:cljsbuild :builds :app :compiler :output-dir] 64 | [:cljsbuild :builds :app :compiler :output-to]] 65 | 66 | :cljsbuild 67 | {:builds {:min 68 | {:source-paths ["src/cljs" "src/cljc" "env/prod/cljs"] 69 | :compiler 70 | {:output-to "target/cljsbuild/public/js/app.js" 71 | :output-dir "target/cljsbuild/public/js" 72 | :source-map "target/cljsbuild/public/js/app.js.map" 73 | :optimizations :advanced 74 | :pretty-print false 75 | :externs ["externs.js"]}} 76 | :app 77 | {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] 78 | :figwheel {:on-jsload "wacnet.core/init!"} 79 | :compiler 80 | {:main "wacnet.dev" 81 | :asset-path "/js/out" 82 | :output-to "target/cljsbuild/public/js/app.js" 83 | :output-dir "target/cljsbuild/public/js/out" 84 | :source-map true 85 | :optimizations :none 86 | :pretty-print true}}}} 87 | 88 | :resource-paths ["resources" "target/cljsbuild"] 89 | 90 | :profiles {:dev {;:repl-options {:init-ns wacnet.repl} 91 | 92 | :dependencies [[ring/ring-mock "0.4.0"] 93 | [ring/ring-devel "1.8.0"] 94 | [figwheel-sidecar "0.5.20"] 95 | [nrepl "0.8.0"] 96 | [cider/piggieback "0.5.1"]] 97 | 98 | :source-paths ["env/dev/clj"] 99 | :plugins [[lein-figwheel "0.5.20"] 100 | [cider/cider-nrepl "0.27.4"]] 101 | 102 | :figwheel {:http-server-root "public" 103 | :readline false 104 | :server-port 3449 105 | :nrepl-port 7002 106 | :nrepl-middleware ["cider.piggieback/wrap-cljs-repl" 107 | "cider.nrepl/cider-middleware"] 108 | :css-dirs ["resources/public/css"] 109 | :ring-handler wacnet.handler/app} 110 | 111 | :env {:dev true} 112 | 113 | :cljsbuild {:builds {:app {:source-paths ["env/dev/cljs"] 114 | :compiler {:main "wacnet.dev" 115 | :source-map true}}}}} 116 | 117 | :uberjar {:source-paths ["env/prod/clj"] 118 | :prep-tasks ["compile" ["cljsbuild" "once" "min"]] 119 | :env {:production true} 120 | :aot :all 121 | :omit-source true}} 122 | 123 | :source-paths ["src/clj" "src/cljs" "src/cljc"]) 124 | -------------------------------------------------------------------------------- /resources/public/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA"} -------------------------------------------------------------------------------- /resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/bootstrap-3.3.6-dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /resources/public/bootstrap-3.3.6-dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /resources/public/css/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/css/chosen-sprite.png -------------------------------------------------------------------------------- /resources/public/css/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/css/chosen-sprite@2x.png -------------------------------------------------------------------------------- /resources/public/css/fixed-data-table.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * FixedDataTable v0.8.23 3 | * 4 | * Copyright Schrodinger, LLC 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | */ 11 | 12 | /** 13 | * Copyright Schrodinger, LLC 14 | * All rights reserved. 15 | * 16 | * This source code is licensed under the BSD-style license found in the 17 | * LICENSE file in the root directory of this source tree. An additional grant 18 | * of patent rights can be found in the PATENTS file in the same directory. 19 | * 20 | * @providesModule fixedDataTableCellGroupLayout 21 | */ 22 | 23 | .fixedDataTableCellGroupLayout_cellGroup { 24 | -webkit-backface-visibility: hidden; 25 | backface-visibility: hidden; 26 | left: 0; 27 | overflow: hidden; 28 | position: absolute; 29 | top: 0; 30 | white-space: nowrap; 31 | } 32 | 33 | .fixedDataTableCellGroupLayout_cellGroup > .public_fixedDataTableCell_main { 34 | display: inline-block; 35 | vertical-align: top; 36 | white-space: normal; 37 | } 38 | 39 | .fixedDataTableCellGroupLayout_cellGroupWrapper { 40 | position: absolute; 41 | top: 0; 42 | } 43 | /** 44 | * Copyright Schrodinger, LLC 45 | * All rights reserved. 46 | * 47 | * This source code is licensed under the BSD-style license found in the 48 | * LICENSE file in the root directory of this source tree. An additional grant 49 | * of patent rights can be found in the PATENTS file in the same directory. 50 | * 51 | * @providesModule fixedDataTableCellLayout 52 | */ 53 | 54 | .fixedDataTableCellLayout_main { 55 | border-right-style: solid; 56 | border-right-width: 1px; 57 | border-width: 0 1px 0 0; 58 | -webkit-box-sizing: border-box; 59 | box-sizing: border-box; 60 | display: block; 61 | overflow: hidden; 62 | position: absolute; 63 | white-space: normal; 64 | } 65 | 66 | .fixedDataTableCellLayout_lastChild { 67 | border-width: 0 1px 1px 0; 68 | } 69 | 70 | .fixedDataTableCellLayout_alignRight { 71 | text-align: right; 72 | } 73 | 74 | .fixedDataTableCellLayout_alignCenter { 75 | text-align: center; 76 | } 77 | 78 | .fixedDataTableCellLayout_wrap1 { 79 | display: table; 80 | } 81 | 82 | .fixedDataTableCellLayout_wrap2 { 83 | display: table-row; 84 | } 85 | 86 | .fixedDataTableCellLayout_wrap3 { 87 | display: table-cell; 88 | vertical-align: middle; 89 | } 90 | 91 | .fixedDataTableCellLayout_columnResizerContainer { 92 | position: absolute; 93 | right: 0px; 94 | width: 6px; 95 | z-index: 1; 96 | } 97 | 98 | .fixedDataTableCellLayout_columnResizerContainer:hover { 99 | cursor: ew-resize; 100 | } 101 | 102 | .fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob { 103 | visibility: visible; 104 | } 105 | 106 | .fixedDataTableCellLayout_columnResizerKnob { 107 | position: absolute; 108 | right: 0px; 109 | visibility: hidden; 110 | width: 4px; 111 | } 112 | /** 113 | * Copyright Schrodinger, LLC 114 | * All rights reserved. 115 | * 116 | * This source code is licensed under the BSD-style license found in the 117 | * LICENSE file in the root directory of this source tree. An additional grant 118 | * of patent rights can be found in the PATENTS file in the same directory. 119 | * 120 | * @providesModule fixedDataTableColumnResizerLineLayout 121 | */ 122 | 123 | .fixedDataTableColumnResizerLineLayout_mouseArea { 124 | cursor: ew-resize; 125 | position: absolute; 126 | right: -5px; 127 | width: 12px; 128 | } 129 | 130 | .fixedDataTableColumnResizerLineLayout_main { 131 | border-right-style: solid; 132 | border-right-width: 1px; 133 | -webkit-box-sizing: border-box; 134 | box-sizing: border-box; 135 | position: absolute; 136 | z-index: 10; 137 | pointer-events: none; 138 | } 139 | 140 | body[dir="rtl"] .fixedDataTableColumnResizerLineLayout_main { 141 | /* the resizer line is in the wrong position in RTL with no easy fix. 142 | * Disabling is more useful than displaying it. 143 | * #167 (github) should look into this and come up with a permanent fix. 144 | */ 145 | display: none !important; 146 | } 147 | 148 | .fixedDataTableColumnResizerLineLayout_hiddenElem { 149 | display: none !important; 150 | } 151 | /** 152 | * Copyright Schrodinger, LLC 153 | * All rights reserved. 154 | * 155 | * This source code is licensed under the BSD-style license found in the 156 | * LICENSE file in the root directory of this source tree. An additional grant 157 | * of patent rights can be found in the PATENTS file in the same directory. 158 | * 159 | * @providesModule fixedDataTableLayout 160 | */ 161 | 162 | .fixedDataTableLayout_main { 163 | border-style: solid; 164 | border-width: 1px; 165 | -webkit-box-sizing: border-box; 166 | box-sizing: border-box; 167 | overflow: hidden; 168 | position: relative; 169 | } 170 | 171 | .fixedDataTableLayout_header, 172 | .fixedDataTableLayout_hasBottomBorder { 173 | border-bottom-style: solid; 174 | border-bottom-width: 1px; 175 | } 176 | 177 | .fixedDataTableLayout_footer .public_fixedDataTableCell_main { 178 | border-top-style: solid; 179 | border-top-width: 1px; 180 | } 181 | 182 | .fixedDataTableLayout_topShadow, 183 | .fixedDataTableLayout_bottomShadow { 184 | height: 4px; 185 | left: 0; 186 | position: absolute; 187 | right: 0; 188 | z-index: 1; 189 | } 190 | 191 | .fixedDataTableLayout_bottomShadow { 192 | margin-top: -4px; 193 | } 194 | 195 | .fixedDataTableLayout_rowsContainer { 196 | overflow: hidden; 197 | position: relative; 198 | } 199 | 200 | .fixedDataTableLayout_horizontalScrollbar { 201 | bottom: 0; 202 | position: absolute; 203 | } 204 | /** 205 | * Copyright Schrodinger, LLC 206 | * All rights reserved. 207 | * 208 | * This source code is licensed under the BSD-style license found in the 209 | * LICENSE file in the root directory of this source tree. An additional grant 210 | * of patent rights can be found in the PATENTS file in the same directory. 211 | * 212 | * @providesModule fixedDataTableRowLayout 213 | */ 214 | 215 | .fixedDataTableRowLayout_main { 216 | -webkit-box-sizing: border-box; 217 | box-sizing: border-box; 218 | overflow: hidden; 219 | position: absolute; 220 | top: 0; 221 | } 222 | 223 | .fixedDataTableRowLayout_body { 224 | left: 0; 225 | position: absolute; 226 | top: 0; 227 | } 228 | 229 | .fixedDataTableRowLayout_rowExpanded { 230 | -webkit-box-sizing: border-box; 231 | box-sizing: border-box; 232 | left: 0; 233 | position: absolute; 234 | } 235 | 236 | .fixedDataTableRowLayout_fixedColumnsDivider { 237 | -webkit-backface-visibility: hidden; 238 | backface-visibility: hidden; 239 | border-left-style: solid; 240 | border-left-width: 1px; 241 | left: 0; 242 | position: absolute; 243 | top: 0; 244 | width: 0; 245 | } 246 | 247 | .fixedDataTableRowLayout_columnsShadow { 248 | position: absolute; 249 | width: 4px; 250 | } 251 | 252 | .fixedDataTableRowLayout_columnsRightShadow { 253 | right: 1px; 254 | } 255 | 256 | .fixedDataTableRowLayout_rowWrapper { 257 | position: absolute; 258 | top: 0; 259 | } 260 | /** 261 | * Copyright Schrodinger, LLC 262 | * All rights reserved. 263 | * 264 | * This source code is licensed under the BSD-style license found in the 265 | * LICENSE file in the root directory of this source tree. An additional grant 266 | * of patent rights can be found in the PATENTS file in the same directory. 267 | * 268 | * @providesModule ScrollbarLayout 269 | */ 270 | 271 | .ScrollbarLayout_main { 272 | -webkit-box-sizing: border-box; 273 | box-sizing: border-box; 274 | outline: none; 275 | overflow: hidden; 276 | position: absolute; 277 | -webkit-user-select: none; 278 | -moz-user-select: none; 279 | -ms-user-select: none; 280 | user-select: none; 281 | } 282 | 283 | .ScrollbarLayout_mainVertical { 284 | bottom: 0; 285 | right: 0; 286 | top: 0; 287 | width: 15px; 288 | } 289 | 290 | .ScrollbarLayout_mainHorizontal { 291 | bottom: 0; 292 | height: 15px; 293 | left: 0; 294 | -webkit-transition-property: background-color height; 295 | transition-property: background-color height; 296 | } 297 | 298 | /* Touching the scroll-track directly makes the scroll-track bolder */ 299 | .ScrollbarLayout_mainHorizontal.public_Scrollbar_mainActive, 300 | .ScrollbarLayout_mainHorizontal:hover { 301 | height: 17px; 302 | } 303 | 304 | .ScrollbarLayout_face { 305 | left: 0; 306 | overflow: hidden; 307 | position: absolute; 308 | z-index: 1; 309 | -webkit-transition-duration: 250ms; 310 | transition-duration: 250ms; 311 | -webkit-transition-timing-function: ease; 312 | transition-timing-function: ease; 313 | -webkit-transition-property: width; 314 | transition-property: width; 315 | } 316 | 317 | /** 318 | * This selector renders the "nub" of the scrollface. The nub must 319 | * be rendered as pseudo-element so that it won't receive any UI events then 320 | * we can get the correct `event.offsetX` and `event.offsetY` from the 321 | * scrollface element while dragging it. 322 | */ 323 | .ScrollbarLayout_face:after { 324 | border-radius: 6px; 325 | content: ''; 326 | display: block; 327 | position: absolute; 328 | -webkit-transition: background-color 250ms ease; 329 | transition: background-color 250ms ease; 330 | } 331 | 332 | .ScrollbarLayout_faceHorizontal { 333 | bottom: 0; 334 | left: 0; 335 | top: 0; 336 | } 337 | 338 | .ScrollbarLayout_faceHorizontal:after { 339 | bottom: 4px; 340 | left: 0; 341 | top: 4px; 342 | width: 100%; 343 | } 344 | 345 | .ScrollbarLayout_faceHorizontal.public_Scrollbar_faceActive:after, 346 | .ScrollbarLayout_main:hover .ScrollbarLayout_faceHorizontal:after { 347 | bottom: calc(4px/2); 348 | } 349 | 350 | .ScrollbarLayout_faceVertical { 351 | left: 0; 352 | right: 0; 353 | top: 0; 354 | } 355 | 356 | .ScrollbarLayout_faceVertical:after { 357 | height: 100%; 358 | left: 4px; 359 | right: 4px; 360 | top: 0; 361 | } 362 | 363 | .ScrollbarLayout_main:hover .ScrollbarLayout_faceVertical:after, 364 | .ScrollbarLayout_faceVertical.public_Scrollbar_faceActive:after { 365 | left: calc(4px/2); 366 | right: calc(4px/2); 367 | } 368 | /** 369 | * Copyright Schrodinger, LLC 370 | * All rights reserved. 371 | * 372 | * This source code is licensed under the BSD-style license found in the 373 | * LICENSE file in the root directory of this source tree. An additional grant 374 | * of patent rights can be found in the PATENTS file in the same directory. 375 | * 376 | * @providesModule fixedDataTable 377 | * 378 | */ 379 | 380 | /** 381 | * Table. 382 | */ 383 | .public_fixedDataTable_main { 384 | border-color: #d3d3d3; 385 | } 386 | 387 | .public_fixedDataTable_header, 388 | .public_fixedDataTable_hasBottomBorder { 389 | border-color: #d3d3d3; 390 | } 391 | 392 | .public_fixedDataTable_header .public_fixedDataTableCell_main { 393 | font-weight: bold; 394 | } 395 | 396 | .public_fixedDataTable_header, 397 | .public_fixedDataTable_scrollbarSpacer, 398 | .public_fixedDataTable_header .public_fixedDataTableCell_main { 399 | background-color: #f6f7f8; 400 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#efefef)); 401 | background-image: linear-gradient(#fff, #efefef); 402 | } 403 | 404 | .public_fixedDataTable_scrollbarSpacer { 405 | position: absolute; 406 | z-index: 99; 407 | top: 0; 408 | } 409 | 410 | .public_fixedDataTable_footer .public_fixedDataTableCell_main { 411 | background-color: #f6f7f8; 412 | border-color: #d3d3d3; 413 | } 414 | 415 | .public_fixedDataTable_topShadow { 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,0.1)), to(rgba(0,0,0,0))); 417 | background-image: linear-gradient(180deg, rgba(0,0,0,0.1), rgba(0,0,0,0)); 418 | } 419 | 420 | .public_fixedDataTable_bottomShadow { 421 | background-image: -webkit-gradient(linear, left bottom, left top, from(rgba(0,0,0,0.1)), to(rgba(0,0,0,0))); 422 | background-image: linear-gradient(0deg, rgba(0,0,0,0.1), rgba(0,0,0,0)); 423 | } 424 | 425 | .public_fixedDataTable_horizontalScrollbar .public_Scrollbar_mainHorizontal { 426 | background-color: #fff; 427 | } 428 | /** 429 | * Copyright Schrodinger, LLC 430 | * All rights reserved. 431 | * 432 | * This source code is licensed under the BSD-style license found in the 433 | * LICENSE file in the root directory of this source tree. An additional grant 434 | * of patent rights can be found in the PATENTS file in the same directory. 435 | * 436 | * @providesModule fixedDataTableCell 437 | */ 438 | 439 | /** 440 | * Table cell. 441 | */ 442 | .public_fixedDataTableCell_main { 443 | background-color: #fff; 444 | border-color: #d3d3d3; 445 | } 446 | 447 | .public_fixedDataTableCell_highlighted { 448 | background-color: #f4f4f4; 449 | } 450 | 451 | .public_fixedDataTableCell_cellContent { 452 | padding: 8px; 453 | } 454 | 455 | .public_fixedDataTableCell_columnResizerKnob { 456 | background-color: #0284ff; 457 | } 458 | .public_fixedDataTableCell_hasReorderHandle .public_fixedDataTableCell_cellContent { 459 | margin-left: 12px; 460 | } 461 | /** 462 | * Column reorder goodies. 463 | */ 464 | .fixedDataTableCellLayout_columnReorderContainer { 465 | border-color: #0284ff; 466 | background-color: rgba(0,0,0,0.1); 467 | width: 12px; 468 | margin-right: -12px; 469 | float: left; 470 | cursor: move; 471 | } 472 | .fixedDataTableCellLayout_columnReorderContainer:after { 473 | content: '::'; 474 | position: absolute; 475 | top: 50%; 476 | left: 1px; 477 | -webkit-transform: translateY(-50%); 478 | transform: translateY(-50%); 479 | } 480 | /** 481 | * Copyright Schrodinger, LLC 482 | * All rights reserved. 483 | * 484 | * This source code is licensed under the BSD-style license found in the 485 | * LICENSE file in the root directory of this source tree. An additional grant 486 | * of patent rights can be found in the PATENTS file in the same directory. 487 | * 488 | * @providesModule fixedDataTableColumnResizerLine 489 | * 490 | */ 491 | 492 | /** 493 | * Column resizer line. 494 | */ 495 | .public_fixedDataTableColumnResizerLine_main { 496 | border-color: #0284ff; 497 | } 498 | /** 499 | * Copyright Schrodinger, LLC 500 | * All rights reserved. 501 | * 502 | * This source code is licensed under the BSD-style license found in the 503 | * LICENSE file in the root directory of this source tree. An additional grant 504 | * of patent rights can be found in the PATENTS file in the same directory. 505 | * 506 | * @providesModule fixedDataTableRow 507 | */ 508 | 509 | /** 510 | * Table row. 511 | */ 512 | .public_fixedDataTableRow_main { 513 | background-color: #fff; 514 | } 515 | 516 | .public_fixedDataTableRow_highlighted, 517 | .public_fixedDataTableRow_highlighted .public_fixedDataTableCell_main { 518 | background-color: #f6f7f8; 519 | } 520 | 521 | .public_fixedDataTableRow_fixedColumnsDivider { 522 | border-color: #d3d3d3; 523 | } 524 | 525 | .public_fixedDataTableRow_columnsShadow { 526 | background-image: -webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.1)), to(rgba(0,0,0,0))); 527 | background-image: linear-gradient(90deg, rgba(0,0,0,0.1), rgba(0,0,0,0)); 528 | } 529 | 530 | .public_fixedDataTableRow_columnsRightShadow { 531 | -webkit-transform: rotate(180deg); 532 | transform: rotate(180deg); 533 | } 534 | /** 535 | * Copyright Schrodinger, LLC 536 | * All rights reserved. 537 | * 538 | * This source code is licensed under the BSD-style license found in the 539 | * LICENSE file in the root directory of this source tree. An additional grant 540 | * of patent rights can be found in the PATENTS file in the same directory. 541 | * 542 | * @providesModule Scrollbar 543 | * 544 | */ 545 | 546 | /** 547 | * Scrollbars. 548 | */ 549 | 550 | /* Touching the scroll-track directly makes the scroll-track bolder */ 551 | .public_Scrollbar_main.public_Scrollbar_mainActive, 552 | .public_Scrollbar_main { 553 | background-color: #fff; 554 | border-left: 1px solid #d3d3d3; 555 | } 556 | 557 | .public_Scrollbar_mainOpaque, 558 | .public_Scrollbar_mainOpaque.public_Scrollbar_mainActive, 559 | .public_Scrollbar_mainOpaque:hover { 560 | background-color: #fff; 561 | } 562 | 563 | .public_Scrollbar_face:after { 564 | background-color: #c2c2c2; 565 | } 566 | 567 | .public_Scrollbar_main:hover .public_Scrollbar_face:after, 568 | .public_Scrollbar_mainActive .public_Scrollbar_face:after, 569 | .public_Scrollbar_faceActive:after { 570 | background-color: #7d7d7d; 571 | } 572 | -------------------------------------------------------------------------------- /resources/public/css/site.css: -------------------------------------------------------------------------------- 1 | 2 | * { 3 | min-height: 0; 4 | min-width: 0; 5 | } 6 | 7 | html { 8 | height:100%; 9 | 10 | } 11 | 12 | body { 13 | height: 100%; 14 | font-size: 14px; 15 | } 16 | 17 | .navbar-header .navbar-brand { 18 | color: #3366CC; 19 | } 20 | 21 | .navbar-header .navbar-brand:hover { 22 | color: #3366CC; 23 | } 24 | 25 | 26 | 27 | .summary-detail { 28 | background-color: rgba(255,153,0,.1); 29 | padding: 20px; 30 | border: 1px lightgray solid; 31 | border-radius: 3px; 32 | overflow-x: auto; 33 | } 34 | 35 | .summary-detail .separator { 36 | background-color: rgba(255,153,0,1); 37 | } 38 | 39 | 40 | .left-side-navbar-container { 41 | border: 1px lightgray solid; 42 | border-radius: 3px; 43 | background-color: rgba(255,153,0,.05); 44 | } 45 | 46 | .left-side-navbar .separator { 47 | background-color: rgba(255,153,0,1); 48 | } 49 | 50 | 51 | .left-side-navbar > .nav-item { 52 | background-color: rgba(255,153,0,.05); 53 | border-bottom: 1px rgba(255,153,0,.15) solid; 54 | margin-top: -1px; 55 | cursor: pointer; 56 | color: inherit; 57 | text-decoration: none; 58 | } 59 | 60 | .left-side-navbar > .nav-item:hover { 61 | background-color: rgba(255,153,0,.15); 62 | 63 | } 64 | 65 | 66 | .left-side-navbar > .nav-item.selected { 67 | background-color: rgba(255,153,0,.15); 68 | border-left: 5px rgba(255,153,0,1) solid; 69 | font-weight: bold; 70 | } 71 | 72 | .device-id { 73 | display: inline-block; 74 | overflow: hidden; 75 | transition: max-width 0.5s ease-in-out; 76 | vertical-align: middle; 77 | margin-right: 5px; 78 | } 79 | 80 | .device-name { 81 | display: inline-block; 82 | vertical-align: middle; 83 | } 84 | 85 | 86 | .device-id.selected { 87 | border-right: 5px rgba(255,153,0,0.3) solid; 88 | } 89 | 90 | 91 | 92 | .left-side-navbar .nav-ids { 93 | background-color: rgba(255,153,0,0.3); 94 | padding: 8px; 95 | display: inline-block; 96 | } 97 | 98 | 99 | .unselectable { 100 | -moz-user-select: none; 101 | -webkit-user-select: none; 102 | -ms-user-select: none; 103 | user-select: none; 104 | } 105 | 106 | .selectable { 107 | -moz-user-select: text; 108 | -webkit-user-select: text; 109 | -ms-user-select: text; 110 | user-select: text; 111 | } 112 | 113 | 114 | 115 | .flash { 116 | animation-name: flash-animation; 117 | animation-duration: 1s; 118 | } 119 | 120 | @keyframes flash-animation { 121 | from { background: rgba(255,255,0,.30); } 122 | to { background: default; } 123 | } 124 | 125 | .message { 126 | position: fixed; 127 | top: 20px; 128 | right: 1em; 129 | min-width: 10em; 130 | z-index: 15;} 131 | 132 | 133 | .controllers .draggable .handle { 134 | color : rgba(255,153,0,1); 135 | } 136 | 137 | .fixed-data-table { 138 | border-radius: 3px 139 | } 140 | 141 | .public_fixedDataTable_bodyRow:hover .public_fixedDataTableCell_main{ 142 | background-color: rgba(255,153,0,0.1); 143 | } 144 | 145 | .public_fixedDataTable_main .selected-row .fixedDataTableRowLayout_body > :first-child .public_fixedDataTableCell_main{ 146 | font-weight : bold; 147 | } 148 | 149 | .public_fixedDataTable_main .highlight-row .public_fixedDataTableCell_main{ 150 | background-color: rgba(255,153,0,0.15); 151 | transition: border-left 0.2s ease-in-out; 152 | } 153 | 154 | .public_fixedDataTable_main .highlight-row .fixedDataTableRowLayout_body > :first-child .public_fixedDataTableCell_main{ 155 | border-left: 3px solid rgba(255,153,0,1); 156 | } 157 | 158 | .public_fixedDataTable_main .highlight-row:hover .public_fixedDataTableCell_main{ 159 | background-color: rgba(255,153,0,0.25); 160 | } 161 | -------------------------------------------------------------------------------- /resources/public/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /resources/public/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /resources/public/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /resources/public/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /resources/public/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /resources/public/fonts/Material-Design-Iconic-Font.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/fonts/Material-Design-Iconic-Font.eot -------------------------------------------------------------------------------- /resources/public/fonts/Material-Design-Iconic-Font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/fonts/Material-Design-Iconic-Font.ttf -------------------------------------------------------------------------------- /resources/public/fonts/Material-Design-Iconic-Font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/fonts/Material-Design-Iconic-Font.woff -------------------------------------------------------------------------------- /resources/public/fonts/Material-Design-Iconic-Font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/fonts/Material-Design-Iconic-Font.woff2 -------------------------------------------------------------------------------- /resources/public/img/HVACIO-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 22 | 26 | 30 | 31 | 40 | 41 | 63 | 65 | 66 | 68 | image/svg+xml 69 | 71 | 72 | 73 | 74 | 75 | 80 | 85 | 90 | 94 | 97 | 102 | 107 | 112 | 117 | 118 | HVAC.IO 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /resources/public/img/Vigilia-logo-name.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 30 | 36 | 40 | 45 | 51 | 52 | 53 | 75 | 77 | 78 | 80 | image/svg+xml 81 | 83 | 84 | 85 | 86 | 87 | 92 | 97 | 107 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /resources/public/img/Vigilia-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 68 | 69 | -------------------------------------------------------------------------------- /resources/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/img/favicon.png -------------------------------------------------------------------------------- /resources/public/img/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/img/splash.png -------------------------------------------------------------------------------- /resources/public/img/splash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 39 | 41 | 44 | 47 | 48 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 69 | 74 | 78 | 82 | 84 | 88 | 92 | 96 | 100 | 101 | 106 | 110 | 114 | 118 | 122 | 126 | 130 | 134 | 135 | 136 | 137 | 138 | 148 | 152 | 156 | 160 | 164 | 168 | 172 | 173 | 183 | 187 | 191 | 192 | 200 | 210 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /resources/public/img/wacnet-logo-name.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 62 | 72 | 76 | 80 | 84 | 88 | 92 | 96 | 97 | 105 | 115 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /resources/public/img/wacnet-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/public/img/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/img/youtube.png -------------------------------------------------------------------------------- /resources/public/web-nrepl/img/clojure-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/wacnet/eb39e27b906d8d7445c8057bca8603e348d9f6f6/resources/public/web-nrepl/img/clojure-logo.png -------------------------------------------------------------------------------- /resources/public/web-nrepl/tryclojure.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | } 4 | 5 | 6 | #header span.logo-try { 7 | color: #63b132; 8 | display:inline; 9 | } 10 | 11 | #header span.logo-clojure { 12 | color: #5881d8; 13 | display:inline 14 | } 15 | 16 | #console { 17 | background: #eee; 18 | margin: 10px; 19 | border-radius: 5px; 20 | -moz-border-radius: 5px; 21 | border: 1px solid #aaa; 22 | } 23 | 24 | #console div.jquery-console-inner { 25 | height: 20em; 26 | height: 40vh; 27 | margin: 10px 10px; 28 | overflow:auto; 29 | text-align:left; 30 | } 31 | #console div.jquery-console-message-value { 32 | color:#0066FF; 33 | font-family:monospace; 34 | padding:0.1em; 35 | } 36 | #console div.jquery-console-prompt-box { 37 | color:#444; font-family:monospace; 38 | } 39 | #console div.jquery-console-focus span.jquery-console-cursor { 40 | background:#333; color:#eee; font-weight:bold; 41 | } 42 | #console div.jquery-console-message-error { 43 | color:#ef0505; font-family:sans-serif; font-weight:bold; 44 | padding:0.1em; 45 | } 46 | #console div.jquery-console-message-success { 47 | color:#187718; font-family:monospace; 48 | padding:0.1em; 49 | } 50 | #console span.jquery-console-prompt-label { 51 | font-weight:bold; 52 | } 53 | 54 | /* Coderay alpha style */ 55 | .code { 56 | border-radius: 2px; 57 | -moz-border-radius: 1px; 58 | color: #000; 59 | } 60 | .code pre { margin: 0px; } 61 | 62 | span.code { white-space: pre; border: 0px; padding: 2px; } 63 | span.code:hover { cursor: pointer; } 64 | 65 | table.code { border-collapse: collapse; width: 100%; padding: 2px; } 66 | table.code td { padding: 2px 4px; vertical-align: top; } 67 | 68 | .code .line_numbers, .code .no { 69 | background-color: #def; 70 | color: gray; 71 | text-align: right; 72 | } 73 | 74 | /* keywords */ 75 | #changer .code span.r { 76 | color: #0000ff; 77 | font-weight: bold; 78 | } 79 | 80 | /* symbols */ 81 | #changer .code span.sy { 82 | color: #318495; 83 | } 84 | 85 | /* strings */ 86 | #changer .code span.s { 87 | color: #008800; 88 | } 89 | 90 | /* paren */ 91 | #changer .code span.of { 92 | color: #222; 93 | font-weight: bold; 94 | } 95 | 96 | /* comment */ 97 | #changer .code span.c { 98 | color: #ccc; 99 | } 100 | 101 | /* operator */ 102 | #changer .code span.cl { 103 | 104 | } 105 | 106 | /* number */ 107 | #changer .code span.i { 108 | color: #ff0000; 109 | } 110 | -------------------------------------------------------------------------------- /resources/public/web-nrepl/tryclojure.js: -------------------------------------------------------------------------------- 1 | var currentPage = -1; 2 | var pages = [ 3 | "page1", 4 | "page2", 5 | "page3", 6 | "page4", 7 | "page5", 8 | "page6", 9 | "page7", 10 | "page8", 11 | "page9", 12 | "page10", 13 | "page11", 14 | "end" 15 | ]; 16 | var pageExitConditions = [ 17 | { 18 | verify: function(data) { return false; } 19 | }, 20 | { 21 | verify: function(data) { return data.expr == "(+ 3 3)"; } 22 | }, 23 | { 24 | verify: function(data) { return data.expr == "(/ 10 3)"; } 25 | }, 26 | { 27 | verify: function(data) { return data.expr == "(/ 10 3.0)"; } 28 | }, 29 | { 30 | verify: function(data) { return data.expr == "(+ 1 2 3 4 5 6)"; } 31 | }, 32 | { 33 | verify: function (data) { return data.expr == "(defn square [x] (* x x))"; } 34 | }, 35 | { 36 | verify: function (data) { return data.expr == "(square 10)"; } 37 | }, 38 | { 39 | verify: function (data) { return data.expr == "((fn [x] (* x x)) 10)"; } 40 | }, 41 | { 42 | verify: function (data) { return data.expr == "(def square (fn [x] (* x x)))"; } 43 | }, 44 | { 45 | verify: function (data) { return data.expr == "(map inc [1 2 3 4])"; } 46 | }, 47 | { 48 | verify: function (data) { return false; } 49 | }, 50 | { 51 | verify: function (data) { return false; } 52 | } 53 | ]; 54 | 55 | function goToPage(pageNumber) { 56 | if (pageNumber == currentPage || pageNumber < 0 || pageNumber >= pages.length) { 57 | return; 58 | } 59 | 60 | currentPage = pageNumber; 61 | 62 | var block = $("#changer"); 63 | block.fadeOut(function(e) { 64 | block.load("/tutorial", { 'page' : pages[pageNumber] }, function() { 65 | block.fadeIn(); 66 | changerUpdated(); 67 | }); 68 | }); 69 | } 70 | 71 | function setupLink(url) { 72 | return function(e) { $("#changer").load(url, function(data) { $("#changer").html(data); }); } 73 | } 74 | 75 | function setupExamples(controller) { 76 | $(".code").click(function(e) { 77 | controller.promptText($(this).text()); 78 | }); 79 | } 80 | 81 | function getStep(n, controller) { 82 | $("#tuttext").load("tutorial", { step: n }, function() { setupExamples(controller); }); 83 | } 84 | 85 | function eval_clojure(code) { 86 | var data; 87 | $.ajax({ 88 | url: "/api/v1/repl", 89 | data: { expr : code }, 90 | async: false, 91 | success: function(res) { data = res; } 92 | }); 93 | return data; 94 | } 95 | 96 | function html_escape(val) { 97 | var result = val; 98 | result = result.replace(/\n/g, "
"); 99 | result = result.replace(/[<]/g, "<"); 100 | result = result.replace(/[>]/g, ">"); 101 | return result; 102 | } 103 | 104 | function doCommand(input) { 105 | if (input.match(/^gopage /)) { 106 | goToPage(parseInt(input.substring("gopage ".length))); 107 | return true; 108 | } 109 | 110 | switch (input) { 111 | case 'next': 112 | case 'forward': 113 | goToPage(currentPage + 1); 114 | return true; 115 | case 'previous': 116 | case 'prev': 117 | case 'back': 118 | goToPage(currentPage - 1); 119 | return true; 120 | case 'restart': 121 | case 'reset': 122 | case 'home': 123 | case 'quit': 124 | goToPage(0); 125 | return true; 126 | default: 127 | return false; 128 | } 129 | } 130 | 131 | function onValidate(input) { 132 | return (input != ""); 133 | } 134 | 135 | function onHandle(line, report) { 136 | var input = $.trim(line); 137 | 138 | // handle commands 139 | if (doCommand(input)) { 140 | report(); 141 | return; 142 | } 143 | 144 | // perform evaluation 145 | var data = eval_clojure(input); 146 | 147 | // handle error 148 | if (data.error) { 149 | return [{msg: data.message, className: "jquery-console-message-error"}]; 150 | } 151 | 152 | // handle page 153 | if (currentPage >= 0 && pageExitConditions[currentPage].verify(data)) { 154 | goToPage(currentPage + 1); 155 | } 156 | 157 | // display expr results 158 | return [{msg: data.result, className: "jquery-console-message-value"}]; 159 | } 160 | 161 | /** 162 | * This should be called anytime the changer div is updated so it can rebind event listeners. 163 | * Currently this is just to make the code elements clickable. 164 | */ 165 | function changerUpdated() { 166 | $("#changer code.expr").each(function() { 167 | $(this).css("cursor", "pointer"); 168 | $(this).attr("title", "Click to insert '" + $(this).text() + "' into the console."); 169 | $(this).click(function(e) { 170 | controller.promptText($(this).text()); 171 | controller.inner.click(); 172 | }); 173 | }); 174 | } 175 | 176 | var controller; 177 | 178 | $(document).ready(function() { 179 | controller = $("#console").console({ 180 | welcomeMessage:'Give me some Clojure:', 181 | promptLabel: '> ', 182 | commandValidate: onValidate, 183 | commandHandle: onHandle, 184 | autofocus:true, 185 | animateScroll:true, 186 | promptHistory:true 187 | }); 188 | 189 | $("#about").click(setupLink("about")); 190 | $("#links").click(setupLink("links")); 191 | $("#home").click(setupLink("home")); 192 | 193 | changerUpdated(); 194 | }); 195 | -------------------------------------------------------------------------------- /src/clj/wacnet/api.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.api 2 | (:require [aleph.http :refer [start-server]] 3 | [bidi.ring :refer [make-handler] :as bidi] 4 | [yada.yada :refer [yada] :as yada] 5 | [yada.body :as yb] 6 | [cognitect.transit :as transit] 7 | [wacnet.api.bacnet.devices :as bd] 8 | [wacnet.api.bacnet.local-device :as bld] 9 | [wacnet.api.vigilia-logger :as log] 10 | [wacnet.api.repl :as repl] 11 | [bacure.core :as b])) 12 | 13 | 14 | 15 | (defn make-api-routes 16 | [api-path] 17 | [api-path 18 | (yada/swaggered 19 | ["/" [["repl" repl/eval-resource] 20 | 21 | ["bacnet/" 22 | [["devices" [["" bd/devices-list] 23 | [["/" :device-id] [["" bd/device] 24 | ["/objects" [["" bd/objects] 25 | [["/" :object-id] bd/object]]]]]]] 26 | ["local-device" [["" bld/local-device] 27 | ["/" [["configs" bld/bacnet-configs]]] 28 | ["/" [["objects" bld/objects]]] 29 | 30 | ["/objects" [["" bld/objects] 31 | [["/" :object-id] bld/object]]]]] 32 | ["multi-objects" [["" bd/multi-objects]]]]] 33 | log/api-route]] 34 | {:info {:title "Wacnet API" 35 | :version "1.1" 36 | :description "API for Wacnet multiple features" 37 | :contact {:name "HVAC.IO" 38 | :email "contact@hvac.io" 39 | :url "https://hvac.io"}} 40 | :tags [{:name "Vigilia" 41 | :description ""} 42 | {:name "REPL" 43 | :description "Live command execution"} 44 | {:name "BACnet" 45 | :description "Access to the BACnet network"} 46 | {:name "Local Device" 47 | :description "BACnet local device"}] 48 | :basePath api-path})]) 49 | 50 | 51 | (comment (def server (start-server 52 | (bidi/make-handler (make-api-routes "/api/v1")) 53 | {:port 3000}))) 54 | 55 | 56 | (comment 57 | (yada/response-for (make-api-routes "") :get "/devices")) 58 | -------------------------------------------------------------------------------- /src/clj/wacnet/api/bacnet/common.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.api.bacnet.common 2 | (:require [bacure.coerce :as c] 3 | [bacure.coerce.type.enumerated :as ce] 4 | [bacure.local-device :as ld] 5 | [clojure.string :as string] 6 | [schema.core :as s] 7 | [yada.yada :as yada])) 8 | 9 | (def produced-types 10 | #{"application/transit+json" 11 | "application/json" 12 | "application/edn" 13 | "text/html"}) 14 | 15 | (def consumed-types 16 | #{"application/transit+json" 17 | "application/json" 18 | "application/edn"}) 19 | 20 | (s/defschema ObjectIdentifier 21 | [(s/one s/Keyword "Object type") 22 | (s/one s/Int "Object Instance")]) 23 | 24 | (s/defschema PropertyValue 25 | {s/Keyword ;; property identifier 26 | s/Any}) ;; prop value 27 | 28 | (s/defschema BACnetObject 29 | {(s/optional-key :object-identifier) ObjectIdentifier 30 | (s/optional-key :object-instance) s/Str 31 | (s/optional-key :object-id) s/Str 32 | (s/optional-key :object-type) s/Str 33 | (s/optional-key :object-name) s/Str 34 | (s/optional-key :description) s/Str 35 | s/Any s/Any}) 36 | 37 | (defn keyword-or-int [string] 38 | (when-not (empty? string) 39 | (or (try (Integer/parseInt string) 40 | (catch Exception e)) 41 | (keyword string)))) 42 | 43 | (defn clean-bacnet-object [bo] 44 | (let [obj (-> bo 45 | (update-in [:object-type] keyword-or-int) 46 | (update-in [:units] keyword-or-int))] 47 | (->> (for [[k v] obj] 48 | (when v [k v])) 49 | (remove nil?) 50 | (into {})))) 51 | 52 | (defn make-link 53 | "Given a request context, return the full URL (with scheme and host) 54 | for the current request, or for a given path." 55 | ([ctx] (make-link ctx (:uri (:request ctx)))) 56 | ([ctx path] 57 | (str (name (get-in ctx [:request :scheme])) 58 | "://" 59 | (or (get-in ctx [:request :headers "host"]) ;; for tests 60 | (get-in ctx [:request :server-name])) 61 | path))) 62 | 63 | (defn make-page-link [ctx page-num limit] 64 | (when page-num 65 | (let [params (:parameters ctx) 66 | query (:query params)] 67 | (make-link ctx (str (:uri (:request ctx)) 68 | "?" 69 | (string/join "&" 70 | (for [[k v] (assoc query :limit limit :page page-num)] 71 | (if (coll? v) 72 | (string/join "&" 73 | (for [i v] 74 | (str (name k) "=" 75 | (name i)))) 76 | (str (name k) "="v))))))))) 77 | 78 | (defn obj-id-to-object-identifier [obj-id] 79 | (->> (map #(Integer/parseInt %) 80 | (string/split obj-id #"\.")) 81 | (c/clojure->bacnet :object-identifier) 82 | (c/bacnet->clojure))) 83 | 84 | (defn object-identifier-to-obj-id 85 | "Convert a bacure object identifier to a shorter (and language 86 | agnostic) version. 87 | 88 | [:analog-input 1] --> \"0.1\"" 89 | [object-identifier] 90 | (let [[obj-type obj-inst] object-identifier] 91 | (str (c/key-or-num-to-int ce/object-type-map obj-type) 92 | "." 93 | obj-inst))) 94 | 95 | (defn binary-to-int 96 | "Walk the object properties and replace :inactive and :active values 97 | by 0 and 1. This makes binary objects similar to analog and 98 | multi-state objects." 99 | [obj-map] 100 | (if (some #{(str (:object-type obj-map))} 101 | ["3" "4" "5"]) 102 | (into {} (for [[k v] obj-map] 103 | [k (cond 104 | (= v :inactive) 0 105 | (= v :active) 1 106 | :else v)])) 107 | obj-map)) 108 | 109 | (defn prepare-obj-map [obj-map] 110 | (let [object-identifier (:object-identifier obj-map) 111 | [obj-type obj-inst] object-identifier 112 | obj-id (object-identifier-to-obj-id object-identifier)] 113 | (-> (dissoc obj-map :object-identifier) 114 | (update-in [:units] #(when (keyword? %) 115 | (-> (name %) 116 | (string/replace #"-" " ")))) 117 | ((fn [o] (->> (for [[k v] o] 118 | (when v [k v])) 119 | (remove nil?) 120 | (into {})))) 121 | (assoc :object-id obj-id 122 | :object-instance (str (last object-identifier)) 123 | :object-type (->> (first object-identifier) 124 | (c/key-or-num-to-int ce/object-type-map ) 125 | (str))) 126 | binary-to-int))) 127 | 128 | (defn paginated-object-list 129 | "Return a collection of object maps. Each one has, in addition to the 130 | given properties, the :object-id, :object-type 131 | and :object-instance. 132 | If no properties are given, only retrieve the name." 133 | [{:keys [device-id obj-qty-fn object-identifiers-fn get-prop-fn desired-properties limit page] 134 | :or {limit 20, page 0}}] 135 | (let [obj-qty (obj-qty-fn) 136 | all-array-indexes (range 1 (inc obj-qty)) 137 | current-pos (* limit (dec page)) 138 | remaining-positions (drop current-pos all-array-indexes) 139 | remaining? (> (count remaining-positions) limit) 140 | object-identifiers (object-identifiers-fn limit remaining-positions)] 141 | (when object-identifiers 142 | {:objects (for [raw-obj-map (get-prop-fn object-identifiers 143 | (or desired-properties [:object-name]))] 144 | (-> raw-obj-map 145 | (assoc :device-id (str device-id)) 146 | (prepare-obj-map))) 147 | :limit limit 148 | :next-page (when remaining? (inc page)) 149 | :current-page page 150 | :previous-page (when (> page 1) (dec page))}))) 151 | 152 | 153 | (defmacro with-bacnet-device 154 | "Tries to execute body only if the bacnet device is found. If not, 155 | return an HTTP error with a short description." 156 | [ctx local-device-id & body] 157 | `(let [ldo# (ld/local-device-object ~local-device-id)] 158 | (cond 159 | (and ldo# (.isInitialized ldo#)) 160 | (do ~@body) 161 | 162 | ldo# (let [response# (:response ~ctx) 163 | content-type# (yada/content-type ~ctx)] 164 | (merge response# {:status 500 165 | :body {:error "BACnet local device not initialized."}})) 166 | 167 | :else (let [response# (:response ~ctx)] 168 | (merge response# {:status 500 169 | :body {:error "BACnet local device not found."}}))))) 170 | 171 | (defmacro with-save-local 172 | "Unless an error is thrown, save the local device state." 173 | [& body] 174 | `(let [result# (do ~@body)] 175 | (ld/save-local-device-backup!) 176 | result#)) 177 | -------------------------------------------------------------------------------- /src/clj/wacnet/api/bacnet/devices.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.api.bacnet.devices 2 | (:require [bacure.coerce :as c] 3 | [bacure.coerce.type.enumerated :as ce] 4 | [bacure.core :as b] 5 | [bacure.remote-device :as rd] 6 | [clojure.set :as cs] 7 | [clojure.string :as string] 8 | [ring.swagger.schema :as rs] 9 | [schema.core :as s] 10 | [wacnet.api.bacnet.common :as co] 11 | [yada.resource :refer [resource]])) 12 | 13 | (def devices-list 14 | (resource 15 | {:produces [{:media-type co/produced-types 16 | :charset "UTF-8"}] 17 | :consumes [{:media-type co/consumed-types 18 | :charset "UTF-8"}] 19 | :access-control {:allow-origin "*"} 20 | :methods {:get {:summary "Devices list" 21 | :parameters {:query {(s/optional-key :refresh) 22 | (rs/field s/Bool 23 | {:description (str "Tries to find new devices on the network" 24 | " using a WhoIs broadcast.")})}} 25 | :description "The list of all known devices with an optional refresh." 26 | :swagger/tags ["BACnet"] 27 | :response (fn [ctx] 28 | (co/with-bacnet-device ctx nil 29 | (when (get-in ctx [:parameters :query :refresh]) 30 | (rd/discover-network)) 31 | {:href (co/make-link ctx) 32 | :devices (for [[k v] (rd/remote-devices-and-names nil)] 33 | {:device-id (str k) 34 | :device-name v 35 | :href (co/make-link ctx (str (:uri (:request ctx)) "/" k))})}))}}})) 36 | 37 | 38 | (defn device-summary [local-device-id device-id] 39 | (-> (b/remote-object-properties local-device-id device-id [:device device-id] 40 | [:description :vendor-identifier 41 | :vendor-name :object-name :model-name]) 42 | (first) 43 | (dissoc :object-identifier) 44 | (cs/rename-keys {:object-name :device-name}) 45 | (assoc :device-id (str device-id)))) 46 | 47 | (def device 48 | (resource 49 | {:produces [{:media-type co/produced-types 50 | :charset "UTF-8"}] 51 | :consumes [{:media-type co/consumed-types 52 | :charset "UTF-8"}] 53 | :access-control {:allow-origin "*"} 54 | :methods {:get {:summary "Device info" 55 | :parameters {:path {:device-id Long}} 56 | :description "A few properties and values for the given device-id." 57 | :swagger/tags ["BACnet"] 58 | :response (fn [ctx] 59 | (let [device-id (some-> ctx :parameters :path :device-id)] 60 | (co/with-bacnet-device ctx nil 61 | (assoc (device-summary nil device-id) 62 | :href (co/make-link ctx) 63 | :objects {:href (co/make-link ctx (str (:uri (:request ctx)) "/objects"))}))))}}})) 64 | 65 | 66 | (defn get-object-quantity [local-device-id device-id] 67 | (-> (b/remote-object-properties 68 | local-device-id device-id [:device device-id] [[:object-list 0]]) 69 | first 70 | :object-list)) 71 | 72 | 73 | ; TODO: use co/paginated-object-list instead 74 | (defn paginated-object-list 75 | "Return a collection of object maps. Each one has, in addition to the 76 | given properties, the :object-id, :object-type 77 | and :object-instance. 78 | If no properties are given, only retrieve the name." 79 | ([local-device-id device-id] (paginated-object-list local-device-id device-id nil 20 1)) 80 | ([local-device-id device-id desired-properties limit page] 81 | (let [obj-qty (get-object-quantity local-device-id device-id) 82 | all-array-indexes (range 1 (inc obj-qty)) 83 | cursor-pos (* limit (dec page)) 84 | after-cursor (drop cursor-pos all-array-indexes) 85 | remaining? (> (count after-cursor) limit) 86 | desired-array (for [i (take limit after-cursor)] 87 | [:object-list i]) 88 | get-prop-fn (fn [obj-ids props] 89 | (b/remote-object-properties 90 | local-device-id device-id obj-ids props)) 91 | object-identifiers (-> (if (> obj-qty limit) 92 | (get-prop-fn [:device device-id] desired-array) 93 | (get-prop-fn [:device device-id] :object-list)) 94 | first 95 | :object-list)] 96 | (when object-identifiers 97 | {:objects (for [raw-obj-map (get-prop-fn object-identifiers 98 | (or desired-properties :object-name))] 99 | (-> raw-obj-map 100 | (assoc :device-id (str device-id)) 101 | (co/prepare-obj-map))) 102 | :limit limit 103 | :next-page (when remaining? (inc page)) 104 | :current-page page 105 | :previous-page (when (> page 1) (dec page))})))) 106 | 107 | (defn get-object-properties 108 | "Get and prepare the object properties." 109 | [device-id obj-id properties] 110 | (-> (b/remote-object-properties nil device-id (co/obj-id-to-object-identifier obj-id) 111 | (or properties :all)) 112 | (first) 113 | (co/prepare-obj-map) 114 | (assoc :device-id (str device-id)))) 115 | 116 | 117 | (defn clean-errors 118 | "Remove the objects that can't be transmitted by the API." 119 | [b-error] 120 | (if (map? b-error) 121 | (->> (for [[k v] b-error] 122 | [k (dissoc v 123 | :apdu-error 124 | :timeout-error)]) 125 | (into {})) 126 | b-error)) 127 | 128 | 129 | 130 | (def objects 131 | (resource 132 | {:produces [{:media-type co/produced-types 133 | :charset "UTF-8"}] 134 | :consumes [{:media-type co/consumed-types 135 | :charset "UTF-8"}] 136 | :access-control {:allow-origin "*"} 137 | :methods {:post 138 | {:summary "New object" 139 | :description (str "Create a new BACnet object. It is possible to give an object ID, but it is " 140 | "suggested to rather give the object-type and let the remote device handle " 141 | "the object-instance itself.\n\n" 142 | "The object-type can be the integer value or the keyword. " 143 | "(Ex: \"0\" or \"analog-input\" for an analog input)\n\n" 144 | "It is possible to give additional properties (such as object-name).") 145 | :swagger/tags ["BACnet"] 146 | :parameters {:path {:device-id Long} 147 | :body co/BACnetObject} 148 | :response (fn [ctx] 149 | (let [body (get-in ctx [:parameters :body]) 150 | d-id (get-in ctx [:parameters :path :device-id])] 151 | (when body 152 | (co/with-bacnet-device ctx nil 153 | (let [clean-body (co/clean-bacnet-object body) 154 | o-id (:object-id clean-body) 155 | object-identifier (or (:object-identifier clean-body) 156 | (when o-id (co/obj-id-to-object-identifier o-id))) 157 | obj-map (-> clean-body 158 | (dissoc :object-id) 159 | (assoc :object-identifier object-identifier)) 160 | result (rd/create-remote-object! nil d-id obj-map)] 161 | (if-let [success (:success result)] 162 | (let [o-identifier (get success :object-identifier) 163 | new-o-id (co/object-identifier-to-obj-id o-identifier)] 164 | (-> (get-object-properties d-id new-o-id nil) 165 | (assoc :href (co/make-link ctx (str (:uri (:request ctx)) 166 | "/" new-o-id))))) 167 | (merge (:response ctx) 168 | {:status 500 169 | :body (clean-errors result)})))))))} 170 | :get {:parameters {:path {:device-id Long} 171 | :query {(s/optional-key :limit) 172 | (rs/field Long 173 | {:description "Maximum number of objects per page."}) 174 | (s/optional-key :page) Long 175 | (s/optional-key :properties) 176 | (rs/field [s/Keyword] 177 | {:description "List of wanted properties."})}} 178 | :summary "Objects list, optionally with all their properties." 179 | :description (str "List of all known objects for a given device." 180 | "\n\n" 181 | "Unless specified, the page will default to 1 and limit to 50 objects.") 182 | :swagger/tags ["BACnet"] 183 | :response (fn [ctx] 184 | (let [device-id (some-> ctx :parameters :path :device-id) 185 | limit (or (some-> ctx :parameters :query :limit) 50) 186 | page (or (some-> ctx :parameters :query :page) 1) 187 | properties (some-> ctx :parameters :query :properties)] 188 | (co/with-bacnet-device ctx nil 189 | (let [p-o-l (paginated-object-list nil device-id properties limit page) 190 | {:keys [next-page previous-page current-page objects]} p-o-l] 191 | (merge {:device 192 | {:href (co/make-link ctx 193 | (string/replace (:uri (:request ctx)) 194 | "/objects" ""))} 195 | :objects (for [o objects] 196 | (assoc o :href (co/make-link ctx (str (:uri (:request ctx)) 197 | "/" (:object-id o))))) 198 | :href (co/make-page-link ctx current-page limit)} 199 | (when-let [l (co/make-page-link ctx next-page limit)] 200 | {:next {:href l 201 | :page next-page}}) 202 | (when-let [l (co/make-page-link ctx previous-page limit)] 203 | {:previous {:href l 204 | :page previous-page}}))))))}}})) 205 | 206 | 207 | (def object 208 | (resource 209 | {:produces [{:media-type co/produced-types 210 | :charset "UTF-8"}] 211 | :consumes [{:media-type co/consumed-types 212 | :charset "UTF-8"}] 213 | :access-control {:allow-origin "*"} 214 | :methods {:put {:summary "Update object" 215 | :description (str "Update object properties.\n\n The properties expected are of the form :" 216 | "\n" 217 | "{property-identifier1 property-value1, " 218 | "property-identifier2 property-value2}" 219 | "\n\n" 220 | "WARNING : you can specify the priority, but you should ONLY do so" 221 | " if you understand the consequences.") 222 | :swagger/tags ["BACnet"] 223 | :parameters {:path {:device-id Long :object-id String} 224 | :body {:properties co/PropertyValue 225 | (s/optional-key :priority) Long}} 226 | :response (fn [ctx] 227 | (let [device-id (get-in ctx [:parameters :path :device-id]) 228 | o-id (get-in ctx [:parameters :path :object-id]) 229 | properties (get-in ctx [:parameters :body :properties]) 230 | priority (get-in ctx [:parameters :body :priority] nil)] 231 | (co/with-bacnet-device ctx nil 232 | (let [write-access-spec {(co/obj-id-to-object-identifier o-id) 233 | (for [[k v] properties] 234 | [k (rd/advanced-property v priority nil)])}] 235 | (let [result (try (rd/set-remote-properties! nil device-id write-access-spec) 236 | (catch Exception e 237 | {:error (.getMessage e)}))] 238 | (if (:success result) 239 | result 240 | (merge (:response ctx) 241 | {:status 500 242 | :body (clean-errors result)})))))))} 243 | :delete 244 | {:summary "Delete object" 245 | :description "Delete the given object." 246 | :swagger/tags ["BACnet"] 247 | :parameters {:path {:device-id Long :object-id String}} 248 | :response (fn [ctx] 249 | (let [device-id (get-in ctx [:parameters :path :device-id]) 250 | o-id (get-in ctx [:parameters :path :object-id])] 251 | (co/with-bacnet-device ctx nil 252 | (let [result (rd/delete-remote-object! nil device-id 253 | (co/obj-id-to-object-identifier o-id))] 254 | (if (:success result) 255 | result 256 | (merge (:response ctx) 257 | {:status 500 258 | :body (clean-errors result)}))))))} 259 | :get {:parameters {:path {:device-id Long :object-id String} 260 | :query {(s/optional-key :properties) 261 | (rs/field [s/Keyword] 262 | {:description "List of wanted properties."})}} 263 | :summary "Object properties." 264 | :description (str "Return the object properties specified in the query parameter. " 265 | "If none is given, try to return all of them.") 266 | :swagger/tags ["BACnet"] 267 | :response (fn [ctx] 268 | (let [device-id (some-> ctx :parameters :path :device-id) 269 | obj-id (some-> ctx :parameters :path :object-id) 270 | properties (some-> ctx :parameters :query :properties)] 271 | (co/with-bacnet-device ctx nil 272 | (-> (get-object-properties device-id obj-id properties) 273 | (assoc :href (co/make-link ctx))))))}}})) 274 | 275 | 276 | (defn decode-global-id 277 | "Return a map containing useful info from the global-ids." 278 | [id] 279 | (let [items (string/split id #"\.") 280 | [d-id o-type o-inst] (map #(Integer/parseInt %) (take-last 3 items))] 281 | {:device-id d-id 282 | :object-type o-type 283 | :object-instance o-inst 284 | :object-identifier (->> [o-type o-inst] 285 | (c/clojure->bacnet :object-identifier) 286 | (c/bacnet->clojure)) 287 | :global-id id})) 288 | 289 | (defn get-properties 290 | "Get the requested properties in parallel for every devices." 291 | ([objects-maps] (get-properties objects-maps :all)) 292 | ([objects-maps properties] 293 | ;; first get retrieve the data from the remote devices... 294 | (let [by-devices (group-by :device-id objects-maps) 295 | result-map (->> (pmap 296 | (fn [[device-id objects]] 297 | [device-id (->> (for [result (b/remote-object-properties 298 | nil device-id 299 | (map :object-identifier objects) 300 | properties)] 301 | [(:object-identifier result) result]) 302 | (into {}))]) by-devices) 303 | (into {}))] 304 | ;; then we format it back into a map using the global ids as keys. 305 | 306 | (->> (for [obj objects-maps] 307 | [(:global-id obj) (-> (get-in result-map [(:device-id obj) (:object-identifier obj)]) 308 | (co/prepare-obj-map))]) 309 | (into {}))))) 310 | 311 | (def multi-objects 312 | (resource 313 | {:produces [{:media-type co/produced-types 314 | :charset "UTF-8"}] 315 | :consumes [{:media-type co/consumed-types 316 | :charset "UTF-8"}] 317 | :access-control {:allow-origin "*"} 318 | :methods {:get 319 | {:summary "Multi object properties" 320 | :description (str "Retrieve the properties of multiple objects at the same time. " 321 | "A subset of properties can be returned by providing them in the optional " 322 | "'properties' field.\n\n The 'global-object-id' is the 'object-id' prepended " 323 | "with the 'device-id'." 324 | "\n\nExample: \"my-awesome.prefix.10122.3.3\"") 325 | :swagger/tags ["BACnet"] 326 | :parameters {:query {(s/optional-key :properties) 327 | (rs/field [s/Keyword] 328 | {:description "List of wanted properties."}) 329 | :global-object-ids [s/Str]}} 330 | :response (fn [ctx] 331 | (co/with-bacnet-device ctx nil 332 | (let [ids (get-in ctx [:parameters :query :global-object-ids]) 333 | properties (or (get-in ctx [:parameters :query :properties]) :all)] 334 | (get-properties (map decode-global-id ids) properties))))} 335 | }})) 336 | -------------------------------------------------------------------------------- /src/clj/wacnet/api/bacnet/local_device.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.api.bacnet.local-device 2 | (:require [bacure.core :as b] 3 | [bacure.local-save :as save] 4 | [bacure.remote-device :as rd] 5 | [bacure.local-device :as ld] 6 | [bacure.network :as net] 7 | [yada.resource :refer [resource]] 8 | [bidi.bidi :refer [path-for]] 9 | [clojure.walk :as w] 10 | [schema.core :as s] 11 | [ring.swagger.schema :as rs] 12 | [yada.yada :as yada] 13 | [wacnet.api.bacnet.common :as co] 14 | [clojure.set :as cs] 15 | [clojure.string :as string])) 16 | 17 | (def allowed-configs-keys 18 | #{:broadcast-address :device-id :port 19 | :apdu-timeout 20 | :number-of-apdu-retries :description 21 | :object-name 22 | :foreign-device-target}) 23 | 24 | (defn get-current-configs 25 | "Get the configuration from the local-device or (if not found) the 26 | configuration file." 27 | [local-device-id] 28 | (select-keys (merge ld/default-configs 29 | (or (ld/local-device-backup) 30 | (save/get-configs))) 31 | allowed-configs-keys)) 32 | 33 | (defn reset-and-save! 34 | "Reset the local device and save the configurations." 35 | [configs] 36 | (let [current-configs (ld/local-device-backup)] 37 | (ld/clear-all!) ;; remove all local devices 38 | ;; boot the local device 39 | (b/boot-up! (merge current-configs configs)) 40 | ;; create a config backup and save it locally 41 | (select-keys (ld/save-local-device-backup!) 42 | allowed-configs-keys))) 43 | 44 | 45 | (s/defschema ForeignDevice 46 | {(s/optional-key :host) s/Str (s/optional-key :port) s/Int}) 47 | 48 | (s/defschema LocalConfigs 49 | {(s/optional-key :broadcast-address) s/Str 50 | (s/optional-key :device-id) s/Int 51 | (s/optional-key :port) s/Int 52 | (s/optional-key :description) s/Str 53 | (s/optional-key :apdu-timeout) s/Int 54 | (s/optional-key :apdu-segment-timeout) s/Int 55 | (s/optional-key :number-of-apdu-retries) s/Int 56 | (s/optional-key :object-name) s/Str 57 | (s/optional-key :foreign-device-target) (s/maybe ForeignDevice)}) 58 | 59 | 60 | (def bacnet-configs 61 | (resource 62 | {:produces [{:media-type co/produced-types 63 | :charset "UTF-8"}] 64 | :consumes [{:media-type co/consumed-types 65 | :charset "UTF-8"}] 66 | :methods {:get {:summary "BACnet configs" 67 | :description "BACnet configs for the Wacnet local device." 68 | :swagger/tags ["Local Device"] 69 | :response (fn [ctx] 70 | (get-current-configs nil))} 71 | :post {:description (str "Update the provided fields in the local device configs. " 72 | "Will automatically reboot the local device with the new configs.") 73 | :swagger/tags ["Local Device"] 74 | :parameters {:body LocalConfigs} 75 | :response (fn [ctx] 76 | (let [new-configs (some-> ctx :parameters :body)] 77 | (when new-configs 78 | (reset-and-save! new-configs))))}}})) 79 | 80 | (defn local-device-summary [local-device-id] 81 | (let [initialized? (some-> (ld/local-device-object local-device-id) (.isInitialized))] 82 | (merge {:initialized initialized? 83 | :available-interfaces (net/interfaces-and-ips)} 84 | (when initialized? {:known-remote-device (count (rd/remote-devices local-device-id))})))) 85 | 86 | (def local-device 87 | (resource 88 | {:produces [{:media-type co/produced-types 89 | :charset "UTF-8"}] 90 | :consumes [{:media-type co/consumed-types 91 | :charset "UTF-8"}] 92 | :methods {:get {:summary "Local device" 93 | :description "BACnet local device info" 94 | :swagger/tags ["Local Device"] 95 | :response (fn [ctx] 96 | (local-device-summary nil))}}})) 97 | 98 | (defn get-object-list 99 | [] 100 | (map :object-identifier (ld/local-objects))) 101 | 102 | (defn get-obj-props 103 | "If provided `desired-properties` is nil, will return all properties." 104 | [oid desired-properties] 105 | (let [object (first (filter #(= oid (:object-identifier %)) (ld/local-objects))) 106 | props (if (seq desired-properties) 107 | (select-keys object (conj desired-properties :object-identifier)) 108 | object)] 109 | (when-not (empty? props) 110 | props))) 111 | 112 | (defn get-device-id [] 113 | (last (for [o (ld/local-objects) 114 | :let [[obj-type obj-inst] (:object-identifier o)] 115 | :when (= :device obj-type)] 116 | obj-inst))) 117 | 118 | (defn paginated-object-list 119 | [desired-properties limit page] 120 | (let [object-list (get-object-list)] 121 | (co/paginated-object-list 122 | {:obj-qty-fn #(count object-list) 123 | :device-id (get-device-id) 124 | :object-identifiers-fn (fn [limit remaining-positions] 125 | (->> object-list 126 | (drop (dec (first remaining-positions))) 127 | (take limit))) 128 | :get-prop-fn (fn [object-identifiers properties] 129 | (for [oid object-identifiers] 130 | (get-obj-props oid properties))) 131 | :desired-properties desired-properties 132 | :limit limit 133 | :page page}))) 134 | 135 | (def objects 136 | (resource 137 | {:produces [{:media-type co/produced-types 138 | :charset "UTF-8"}] 139 | :consumes [{:media-type co/consumed-types 140 | :charset "UTF-8"}] 141 | :access-control {:allow-origin "*"} 142 | :methods {:post 143 | {:summary "New object" 144 | :description (str "Create a new BACnet object. It is possible to give an object ID, but it is " 145 | "suggested to rather give the object-type and let the device handle " 146 | "the object-instance itself.\n\n" 147 | "The object-type can be the integer value or the keyword. " 148 | "(Ex: \"0\" or \"analog-input\" for an analog input)\n\n" 149 | "It is possible to give additional properties such as object-name.") 150 | :swagger/tags ["Local Device"] 151 | :parameters {:body co/BACnetObject} 152 | :response (fn [ctx] 153 | (let [body (get-in ctx [:parameters :body])] 154 | (when body 155 | (co/with-bacnet-device ctx nil 156 | (let [clean-body (co/clean-bacnet-object body) 157 | o-id (:object-id clean-body) 158 | object-identifier (or (:object-identifier clean-body) 159 | (when o-id (co/obj-id-to-object-identifier o-id))) 160 | obj-map (-> clean-body 161 | (dissoc :object-id) 162 | (assoc :object-identifier object-identifier)) 163 | result (try {:success 164 | (co/with-save-local 165 | (let [object (ld/add-object! nil obj-map)] 166 | (let [obj (co/prepare-obj-map object)] 167 | (assoc obj :href (co/make-link ctx (str (:uri (:request ctx)) 168 | "/" (:object-id obj)))))))} 169 | (catch Exception e 170 | {:error (.getMessage e)}))] 171 | (if-let [obj (:success result)] 172 | obj 173 | (merge (:response ctx) 174 | {:status 500 175 | :body result})))))))} 176 | 177 | :get {:parameters {:query {(s/optional-key :limit) 178 | (rs/field Long 179 | {:description "Maximum number of objects per page."}) 180 | (s/optional-key :page) Long 181 | (s/optional-key :properties) 182 | (rs/field [s/Keyword] 183 | {:description "List of wanted properties."})}} 184 | :summary "Objects list, optionally with all their properties." 185 | :description (str "List of all known objects." 186 | "\n\n" 187 | "Unless specified, the page will default to 1 and limit to 50 objects.") 188 | :swagger/tags ["Local Device"] 189 | :response (fn [ctx] 190 | (let [limit (or (some-> ctx :parameters :query :limit) 50) 191 | page (or (some-> ctx :parameters :query :page) 1) 192 | properties (some-> ctx :parameters :query :properties)] 193 | (co/with-bacnet-device ctx nil 194 | (let [p-o-l (paginated-object-list properties limit page) 195 | {:keys [next-page previous-page current-page objects]} p-o-l] 196 | (merge {:device 197 | {:href (co/make-link ctx 198 | (string/replace (:uri (:request ctx)) 199 | "/objects" ""))} 200 | :objects (for [o objects] 201 | (assoc o :href (co/make-link ctx (str (:uri (:request ctx)) 202 | "/" (:object-id o))))) 203 | :href (co/make-page-link ctx current-page limit)} 204 | (when-let [l (co/make-page-link ctx next-page limit)] 205 | {:next {:href l 206 | :page next-page}}) 207 | (when-let [l (co/make-page-link ctx previous-page limit)] 208 | {:previous {:href l 209 | :page previous-page}}))))))}}})) 210 | 211 | 212 | (def object 213 | (resource 214 | {:produces [{:media-type co/produced-types 215 | :charset "UTF-8"}] 216 | :consumes [{:media-type co/consumed-types 217 | :charset "UTF-8"}] 218 | :access-control {:allow-origin "*"} 219 | :methods {:put {:summary "Update object" 220 | :description (str "Update object properties.\n\n The properties expected are of the form :" 221 | "\n" 222 | "{property-identifier1 property-value1, " 223 | "property-identifier2 property-value2}") 224 | :swagger/tags ["Local Device"] 225 | :parameters {:path {:object-id String} 226 | :body {:properties co/PropertyValue}} 227 | :response (fn [ctx] 228 | (let [o-id (get-in ctx [:parameters :path :object-id]) 229 | properties (get-in ctx [:parameters :body :properties])] 230 | (co/with-bacnet-device ctx nil 231 | (let [result (try {:success 232 | (co/with-save-local 233 | (ld/update-object! nil (assoc properties :object-identifier (co/obj-id-to-object-identifier o-id))) 234 | "Object updated")} 235 | (catch Exception e 236 | {:error (.getMessage e)}))] 237 | (if (:success result) 238 | result 239 | (merge (:response ctx) 240 | {:status 500 241 | :body result}))))))} 242 | :delete 243 | {:summary "Delete object" 244 | :description "Delete the given object." 245 | :swagger/tags ["Local Device"] 246 | :parameters {:path {:object-id String}} 247 | :response (fn [ctx] 248 | (let [o-id (get-in ctx [:parameters :path :object-id])] 249 | (co/with-bacnet-device ctx nil 250 | (let [result (try (co/with-save-local 251 | (ld/remove-object! nil (co/obj-id-to-object-identifier o-id)) 252 | {:success "Object deleted"}) 253 | (catch Exception e 254 | {:error (.getMessage e)}))] 255 | (if (:success result) 256 | result 257 | (merge (:response ctx) 258 | {:status 500 259 | :body result}))))))} 260 | :get {:parameters {:path {:object-id String} 261 | :query {(s/optional-key :properties) 262 | (rs/field [s/Keyword] 263 | {:description "List of wanted properties."})}} 264 | :summary "Object properties." 265 | :description (str "Return the object properties specified in the query parameter. " 266 | "If none is given, try to return all of them.") 267 | :swagger/tags ["Local Device"] 268 | :response (fn [ctx] 269 | (let [obj-id (some-> ctx :parameters :path :object-id) 270 | properties (some-> ctx :parameters :query :properties)] 271 | (co/with-bacnet-device ctx nil 272 | (some-> (co/obj-id-to-object-identifier obj-id) 273 | (get-obj-props properties) 274 | (co/prepare-obj-map) 275 | (assoc :href (co/make-link ctx))))))}}})) 276 | -------------------------------------------------------------------------------- /src/clj/wacnet/api/repl.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.api.repl 2 | (:require [clojure.stacktrace :refer [root-cause]] 3 | [wacnet.api.bacnet.common :as co] 4 | [wacnet.nrepl :as wnrepl] 5 | [yada.resource :refer [resource]] 6 | [schema.core :as s]) 7 | (:import java.io.StringWriter 8 | [java.util.concurrent TimeoutException TimeUnit FutureTask])) 9 | 10 | (def users-namespaces (atom {})) 11 | 12 | (defn remove-ns-future 13 | "Unmap a namespace after `delay' in ms." 14 | [delay] 15 | (future (Thread/sleep delay) ;24 hours (* 60000 60 24) 16 | (-> *ns* .getName remove-ns))) 17 | 18 | (defmacro new-namespace 19 | "Create a new newspace and return it." [] 20 | (let [ns-name# (symbol (gensym "user-"))] 21 | `(binding [*ns* *ns*] 22 | (ns ~ns-name#) 23 | *ns*))) 24 | 25 | (defn throwable-ns 26 | "Creates a new ns that will expire after `delay' in ms. If init is 27 | provided, it will be evaluated when the namespace is created." 28 | [&{:keys [delay init]}] 29 | (let [new-ns (new-namespace)] 30 | (binding [*ns* new-ns] 31 | (when init (eval init)) 32 | (remove-ns-future delay)) 33 | new-ns)) 34 | 35 | (defn make-user-ns! [session-id] 36 | (let [delay-ms (* 60000 60 24) ;; 24 hours 37 | ns (throwable-ns :delay delay-ms 38 | :init wnrepl/repl-init)] 39 | (swap! users-namespaces assoc session-id ns) 40 | (future (Thread/sleep delay-ms) 41 | (swap! users-namespaces dissoc session-id)) 42 | ns)) 43 | 44 | 45 | ;; from clojail 46 | (def ^{:doc "Create a map of pretty keywords to ugly TimeUnits"} 47 | uglify-time-unit 48 | (into {} (for [[enum aliases] {TimeUnit/NANOSECONDS [:ns :nanoseconds] 49 | TimeUnit/MICROSECONDS [:us :microseconds] 50 | TimeUnit/MILLISECONDS [:ms :milliseconds] 51 | TimeUnit/SECONDS [:s :sec :seconds]} 52 | alias aliases] 53 | {alias enum}))) 54 | 55 | (defn thunk-timeout 56 | "Takes a function and an amount of time to wait for those function to finish 57 | executing. The sandbox can do this for you. unit is any of :ns, :us, :ms, 58 | or :s which correspond to TimeUnit/NANOSECONDS, MICROSECONDS, MILLISECONDS, 59 | and SECONDS respectively." 60 | ([thunk ms] 61 | (thunk-timeout thunk ms :ms nil)) ; Default to milliseconds, because that's pretty common. 62 | ([thunk time unit] 63 | (thunk-timeout thunk time unit nil)) 64 | ([thunk time unit tg] 65 | (let [task (FutureTask. thunk) 66 | thr (if tg (Thread. tg task) (Thread. task))] 67 | (try 68 | (.start thr) 69 | (.get task time (or (uglify-time-unit unit) unit)) 70 | (catch TimeoutException e 71 | (.cancel task true) 72 | (.stop thr) 73 | (throw (TimeoutException. "Execution timed out."))) 74 | (catch Exception e 75 | (.cancel task true) 76 | (.stop thr) 77 | (throw e)) 78 | (finally (when tg (.stop tg))))))) 79 | 80 | 81 | (defn eval-form [form namespace] 82 | (with-open [out (StringWriter.)] 83 | (binding [*out* out 84 | *ns* namespace] 85 | (let [result (eval form)] 86 | {:expr form 87 | :result [out result]})))) 88 | 89 | (defn eval-string [expr namespace] 90 | (let [form (binding [*read-eval* false] (read-string expr))] 91 | (thunk-timeout #(eval-form form namespace) (* 60000 5)))) 92 | 93 | 94 | (defn get-ns! 95 | "Return a namespace associated with the given session. If nothing is 96 | found, initiate the namespace." 97 | [session-id] 98 | (or (get @users-namespaces session-id) 99 | (make-user-ns! session-id))) 100 | 101 | (defn eval-request 102 | ([expr] (eval-request expr "web-repl")) 103 | ([expr session-id] 104 | (try 105 | (eval-string expr (get-ns! session-id)) 106 | (catch TimeoutException _ 107 | {:error true :message "Execution Timed Out!"}) 108 | (catch Exception e 109 | {:error true :message (str (root-cause e))})))) 110 | 111 | 112 | (def eval-resource 113 | (resource 114 | {:produces [{:media-type co/produced-types 115 | :charset "UTF-8"}] 116 | :consumes [{:media-type #{} 117 | :charset "UTF-8"}] 118 | :access-control {:allow-origin "*"} 119 | :swagger/tags ["REPL"] 120 | :methods {:get {:summary "Devices list" 121 | :parameters {:query {:expr s/Str}} 122 | :description "Evaluate the given expression in a Clojure repl." 123 | :response (fn [ctx] 124 | (let [session (some-> ctx :request :session) 125 | expr (some-> ctx :parameters :query :expr) 126 | {:keys [expr result error message] :as res} (eval-request expr session) 127 | data (if error 128 | res 129 | (let [[out res] result] 130 | {:expr (pr-str expr) 131 | :result (str out (pr-str res))}))] 132 | data))}}})) 133 | -------------------------------------------------------------------------------- /src/clj/wacnet/api/util.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.api.util) 2 | 3 | (def produced-types 4 | #{"application/transit+json" 5 | "application/transit+msgpack" 6 | "application/json" 7 | "application/edn" 8 | "text/html"}) 9 | 10 | 11 | (def consumed-types 12 | #{"application/transit+json" 13 | "application/transit+msgpack" 14 | "application/json" 15 | "application/edn"}) 16 | 17 | (defn make-link 18 | "Given a request context, return the full URL (with scheme and host) 19 | for the current request, or for a given path." 20 | ([ctx] (make-link ctx (:uri (:request ctx)))) 21 | ([ctx path] 22 | (str (name (get-in ctx [:request :scheme])) 23 | "://" 24 | (or (get-in ctx [:request :headers "host"]) ;; for tests 25 | (get-in ctx [:request :server-name])) 26 | path))) 27 | -------------------------------------------------------------------------------- /src/clj/wacnet/api/vigilia_logger.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.api.vigilia-logger 2 | (:require [bidi.bidi :refer [path-for]] 3 | [clojure.java.io :as io] 4 | [clojure.string :as str] 5 | [ring.swagger.schema :as rs] 6 | [schema.core :as s] 7 | [vigilia-logger.configs :as configs] 8 | [vigilia-logger.http :as http] 9 | [vigilia-logger.scan :as scan] 10 | [vigilia-logger.timed :as timed] 11 | [wacnet.api.util :as u] 12 | [wacnet.bacnet-utils :as bu] 13 | [yada.resource :refer [resource]]) 14 | (:import [java.io File])) 15 | 16 | (declare api-route 17 | configs) 18 | 19 | 20 | (defn scanning-state-resp [ctx] 21 | {:href (u/make-link ctx) 22 | :logging? (timed/is-logging?) 23 | :scanning-state (-> @scan/scanning-state 24 | (update-in [:start-time] #(-> % .getMillis .toString)) 25 | (update-in [:end-time] #(-> % .getMillis .toString)) 26 | (assoc :logging (timed/is-logging?) 27 | :local-logs (count (scan/find-unsent-logs)))) 28 | :configs {:href (u/make-link ctx (str "/" (path-for api-route configs)))}}) 29 | 30 | (def scanning-state 31 | (resource 32 | {:produces [{:media-type u/produced-types 33 | :charset "UTF-8"}] 34 | :consumes [{:media-type u/consumed-types 35 | :charset "UTF-8"}] 36 | :access-control {:allow-origin "*"} 37 | :methods {:post {:description "Start or stop the Vigilia logging." 38 | :swagger/tags ["Vigilia"] 39 | :parameters {:body {:logging (rs/field s/Bool {:description "True will start the logging, false will stop it."})}} 40 | :response (fn [ctx] 41 | (let [logging (some-> ctx :parameters :body :logging)] 42 | (if logging 43 | (timed/restart-logging) 44 | (timed/stop-logging)) 45 | (scanning-state-resp ctx)))} 46 | :get {:description (str "Various information about the state of the current scan (if any). Fields include: \n" 47 | "- scanning? \n" 48 | "- start-time \n" 49 | "- end-time \n" 50 | "- scanning-time-ms \n" 51 | "- ids-to-scan \n" 52 | "- ids-scanning \n" 53 | "- ids-scanned \n") 54 | :swagger/tags ["Vigilia"] 55 | :response scanning-state-resp}}})) 56 | 57 | (s/defschema Criterias 58 | {s/Keyword s/Any}) 59 | 60 | (s/defschema TargetObjects 61 | {s/Str [s/Str]}) 62 | 63 | 64 | 65 | (s/defschema Configs 66 | {(s/optional-key :api-root) s/Str 67 | (s/optional-key :logger-id) s/Str 68 | (s/optional-key :logger-version) s/Str 69 | (s/optional-key :logger-key) s/Str 70 | (s/optional-key :project-id) s/Str 71 | (s/optional-key :min-range) s/Int 72 | (s/optional-key :max-range) s/Int 73 | (s/optional-key :id-to-remove) [s/Int] 74 | (s/optional-key :id-to-keep) [s/Int] 75 | (s/optional-key :time-interval) s/Int 76 | (s/optional-key :criteria-coll) [Criterias] 77 | (s/optional-key :object-delay) s/Int 78 | (s/optional-key :target-objects) TargetObjects 79 | (s/optional-key :proxy-host) s/Str 80 | (s/optional-key :proxy-port) s/Int 81 | (s/optional-key :proxy-user) s/Str 82 | (s/optional-key :proxy-pass) s/Str 83 | (s/optional-key :logs-path) s/Str}) 84 | 85 | (defn expected-keys [] 86 | (for [[m _] Configs] 87 | (-> m first second))) 88 | 89 | (defn decode-target-objects [config-map] 90 | (if (:target-objects config-map) 91 | (update-in config-map [:target-objects] 92 | (fn [target-map] 93 | (->> (for [[device-id obj-id-coll] target-map] 94 | [(Integer/parseInt device-id) 95 | (vec (map bu/short-id-to-identifier obj-id-coll))]) 96 | (into {})))) 97 | config-map)) 98 | 99 | (defn encode-target-objects [config-map] 100 | (if (:target-objects config-map) 101 | (update-in config-map [:target-objects] 102 | (fn [target-map] 103 | (->> (for [[device-id obj-id-coll] target-map] 104 | [(str device-id) 105 | (vec (map bu/identifier-to-short-id obj-id-coll))]) 106 | (into {})))) 107 | config-map)) 108 | 109 | (defn validate-logs-path! 110 | "Check if the provided path exists. If it doesn't, try to create it. 111 | Return the path if it's valid." 112 | [path] 113 | (when (seq path) 114 | (let [normalized-path (str/replace path (re-pattern (str/re-quote-replacement java.io.File/separator)) "/") 115 | path (str (str/replace normalized-path (re-pattern "[/]$") "") "/") ;; insure the path ends with '/' 116 | dir (io/as-file path)] 117 | (when (or 118 | ;; if it exists, make sure it's a directory. 119 | (let [dir (io/as-file path)] 120 | (when (.exists dir) 121 | (.isDirectory dir))) 122 | 123 | ;; if it doesn't exist, try to create it and then check if it's a 124 | ;; directory. 125 | (do (io/make-parents (str path "dummy-filename")) 126 | (.isDirectory (io/as-file path)))) 127 | path)))) 128 | 129 | (def configs 130 | (let [response-map-fn (fn [ctx] 131 | (let [configs (-> (configs/fetch) 132 | (encode-target-objects))] 133 | {:href (u/make-link ctx) 134 | :vigilia {:href (when-let [p-id (:project-id configs)] 135 | (str (->> (configs/api-root) 136 | (re-find #"(.*)/api/") 137 | (last)) 138 | "/v/" p-id))} 139 | :configs (merge (into {} (for [k (expected-keys)] 140 | [k nil])) 141 | configs)}))] 142 | (resource 143 | {:produces [{:media-type u/produced-types 144 | :charset "UTF-8"}] 145 | :consumes [{:media-type u/consumed-types 146 | :charset "UTF-8"}] 147 | :access-control {:allow-origin "*"} 148 | :methods {:get {:description "Logger configurations" 149 | :swagger/tags ["Vigilia"] 150 | :response response-map-fn} 151 | :delete {:description (str "Delete the current logger configurations. " 152 | "This will prevent Wacnet from automatically scanning devices on boot.") 153 | :swagger/tags ["Vigilia"] 154 | :response (fn [ctx] 155 | (configs/delete!))} 156 | :post {:description "Update the provided fields in the Vigilia logger configs." 157 | :swagger/tags ["Vigilia"] 158 | :parameters {:body Configs} 159 | :response (fn [ctx] 160 | (when-let [new-configs (some-> ctx :parameters :body 161 | decode-target-objects 162 | (update :logs-path validate-logs-path!))] 163 | (configs/save! (->> (for [[k v] new-configs] 164 | (when-not (or (when (or (coll? v) 165 | (string? v) 166 | (= :api-root k)) 167 | (empty? v)) 168 | (nil? v)) 169 | [k v])) 170 | (remove nil?) 171 | (into {})))) 172 | (response-map-fn ctx))}}}))) 173 | 174 | (def test-api-root 175 | (resource 176 | {:produces [{:media-type u/produced-types 177 | :charset "UTF-8"}] 178 | :consumes [{:media-type u/consumed-types 179 | :charset "UTF-8"}] 180 | :access-control {:allow-origin "*"} 181 | :methods {:get {:description (str "Test communication with a remote Vigilia server. Will use the default API url or " 182 | "the one provided as a query argument.") 183 | :swagger/tags ["Vigilia"] 184 | :parameters {:query {(s/optional-key :api-root) 185 | (rs/field s/Str {:description "Alternative API url"})}} 186 | :response (fn [ctx] 187 | (let [api-root (let [url (some-> ctx :parameters :query :api-root)] 188 | (if (empty? url) 189 | configs/default-api-root 190 | url))] 191 | {:can-connect? (http/can-connect? api-root) 192 | :api-root api-root}))}}})) 193 | 194 | (def test-credentials 195 | (resource 196 | {:produces [{:media-type u/produced-types 197 | :charset "UTF-8"}] 198 | :consumes [{:media-type u/consumed-types 199 | :charset "UTF-8"}] 200 | :access-control {:allow-origin "*"} 201 | :methods {:get {:description (str "Test credentials with a remote Vigilia server. Will use the default " 202 | "currently saved credentials or " 203 | "the one provided as query arguments.") 204 | :swagger/tags ["Vigilia"] 205 | :parameters {:query {(s/optional-key :project-id) s/Str 206 | (s/optional-key :logger-key) s/Str}} 207 | :response (fn [ctx] 208 | (let [project-id (some-> ctx :parameters :query :project-id) 209 | logger-key (some-> ctx :parameters :query :logger-key) 210 | credentials (if (and project-id logger-key) 211 | {:project-id project-id :logger-key logger-key} 212 | (select-keys (configs/fetch) 213 | [:project-id :logger-key]))] 214 | {:credentials-valid? (http/credentials-valid? (:project-id credentials) 215 | (:logger-key credentials)) 216 | :credentials credentials}))}}})) 217 | 218 | 219 | 220 | (def api-route 221 | ["vigilia" [;["" :none] 222 | ["/logger" [["" scanning-state] 223 | ["/tests" [["/api-root" test-api-root] 224 | ["/credentials" test-credentials]]] 225 | ["/configs" configs] 226 | ]]]]) 227 | -------------------------------------------------------------------------------- /src/clj/wacnet/handler.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.handler 2 | (:require [bidi.ring :refer [make-handler] :as bidi] 3 | [wacnet.api :as api] 4 | [hiccup.page :as hp :refer [html5 include-css include-js]] 5 | [yada.yada :as yada :refer [yada resource]] 6 | [aleph.middleware.content-type :refer [wrap-content-type]] 7 | [aleph.middleware.session :refer [wrap-session]] 8 | [trptcolin.versioneer.core :as version])) 9 | 10 | (def mount-target 11 | [:div#app {:style "width:100%;height:100%"} 12 | [:div {:style "margin:10px;margin-bottom:none;"} 13 | [:h4 "Loading javascript application..."] 14 | [:p "If for any reason your browser can't load the application, you can still view your BACnet network using the API." 15 | [:ul 16 | [:li [:a {:href "/api/v1"} "Swagger UI for Wacnet API"]] 17 | [:li [:a {:href "/api/v1/bacnet/devices"} "Raw API"]]]]]]) 18 | 19 | (def loading-page 20 | (html5 21 | [:head 22 | [:meta {:name "viewport" 23 | :content "width=device-width, initial-scale=1"}] 24 | [:link {:rel "shortcut icon" :href "/img/favicon.png"}] 25 | (include-css "/css/site.css") 26 | 27 | ;;; font awesome 28 | (include-css "/font-awesome/css/font-awesome.min.css") 29 | 30 | ;;; bootstrap 31 | (include-css "/bootstrap-3.3.6-dist/css/bootstrap.min.css") 32 | (include-css "/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css") 33 | 34 | (include-css "/css/material-design-iconic-font.min.css") 35 | (include-css "/css/re-com.css") 36 | (include-css "/css/fixed-data-table.min.css") 37 | 38 | (include-js "/bootstrap-3.3.6-dist/js/jquery-2.2.0.min.js") 39 | (include-js "/bootstrap-3.3.6-dist/js/bootstrap.min.js") 40 | [:script {:type "text/javascript"} 41 | (str "var WacnetVersion = \"" (version/get-version "wacnet" "wacnet") "\"")]] 42 | 43 | [:body 44 | mount-target 45 | (include-js "/js/app.js") 46 | ])) 47 | 48 | (def loading-page-resource 49 | (resource {:produces "text/html" 50 | :methods {:get {:response (fn [ctx] 51 | loading-page)}}})) 52 | 53 | (def routes 54 | ["" 55 | [["/" loading-page-resource] 56 | (api/make-api-routes "/api/v1") 57 | ;; ["/js" (bidi/->ResourcesMaybe {:prefix "public/js/"})] 58 | ;; ["/css" (bidi/->ResourcesMaybe {:prefix "public/css/"})] 59 | ;; ["/img" (bidi/->ResourcesMaybe {:prefix "public/img/"})] 60 | ["/" (bidi/->ResourcesMaybe {:prefix "public/"})] 61 | ["/" (bidi/->ResourcesMaybe {:prefix "public/web-nrepl/"})] 62 | ;; 404 if nothing is found 63 | [true (fn [req] {:status 404 :body "404 not found"})]]]) 64 | 65 | 66 | (def handler 67 | (-> (make-handler routes) 68 | (wrap-content-type) 69 | ;; (wrap-session) 70 | )) 71 | 72 | (comment 73 | (def server 74 | (aleph.http/start-server handler {:port 3449}))) 75 | 76 | 77 | ;;; dev functions 78 | 79 | (defn wrap-deref [h] 80 | (fn [req] 81 | (let [result (h req)] 82 | (if (instance? clojure.lang.IDeref result) 83 | @result 84 | result)))) 85 | 86 | (def app 87 | (wrap-deref handler)) 88 | -------------------------------------------------------------------------------- /src/clj/wacnet/local_device.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.local-device 2 | (:require bacure.core 3 | [bacure.local-device :as ld] 4 | [bacure.local-save :as ls] 5 | [clojure.stacktrace :as st] 6 | [trptcolin.versioneer.core :as version] 7 | [vigilia-logger.timed :as timed])) 8 | 9 | (defn initialize! 10 | "Initialize the local BACnet device." [] 11 | (let [saved-configs (ls/get-configs) 12 | {:keys [object-name description]} saved-configs] 13 | (bacure.core/boot-up! 14 | {:vendor-name "HVAC.IO" 15 | :vendor-identifier 697 16 | :model-name "Wacnet" 17 | :object-name (or object-name "Wacnet webserver") 18 | :application-software-version (version/get-version "wacnet" "wacnet") 19 | :description (or description 20 | (str "Wacnet: BACnet webserver and toolkit. " 21 | "Access the web interface at " 22 | "http://"(bacure.network/get-any-ip)":47800, " 23 | "or use the Clojure nREPL on port 47999" 24 | "."))})) 25 | (timed/maybe-start-logging)) 26 | 27 | (defn java-version 28 | "Return the current java version as double."[] 29 | (-> (System/getProperty "java.specification.version") 30 | (Double/parseDouble))) 31 | 32 | (defn headless? 33 | "True if we are running without any graphical support." 34 | [] 35 | (java.awt.GraphicsEnvironment/isHeadless)) 36 | 37 | (defn exit-with-err-msg 38 | ([err-msg] (exit-with-err-msg err-msg false)) 39 | ([err-msg text-area] 40 | (println err-msg) 41 | (when-not (headless?) 42 | (javax.swing.JOptionPane/showMessageDialog 43 | nil 44 | (if text-area 45 | (javax.swing.JTextArea. err-msg) 46 | err-msg) 47 | "Error" 48 | javax.swing.JOptionPane/ERROR_MESSAGE)) 49 | (println "Wacnet will now exit") 50 | (System/exit 1))) 51 | 52 | (defn enforce-min-java! 53 | "Enforce the minimum required java version."[] 54 | (let [min-version 1.8 55 | current-version (java-version)] 56 | (when-not (>= current-version min-version) 57 | (let [err-msg (str "You need Java "min-version " or higher to run this application. \n" 58 | "(You are using Java "current-version ".)")] 59 | (exit-with-err-msg err-msg))))) 60 | 61 | 62 | (defn initialize-with-exit-on-fail! 63 | "Try to initialize the local device. If we can't bind to the BACnet 64 | port or encounter an error, show a message to the user and then exit." 65 | [] 66 | (try (initialize!) 67 | (enforce-min-java!) 68 | ;; port already taken exception 69 | (catch java.net.BindException e 70 | (let [err-msg (str "\n*Error*: The BACnet port ("(or (:port (:init-configs (ld/get-local-device nil))) 71 | 47808)")" 72 | " is already bound to another software.\n\t " 73 | "Please close the other software and try again.\n")] 74 | (exit-with-err-msg err-msg))) 75 | 76 | ;; configs and other errors 77 | (catch java.lang.Exception e 78 | (let [err-msg (str "\n*Error*: " 79 | "The application encountered an error while initiating. Try deleting the " 80 | "configs.clj file. If the problem persists, you can contact us and include " 81 | "the following stacktrace : \n\n" 82 | (with-out-str (st/print-stack-trace e)))] 83 | (exit-with-err-msg err-msg))))) 84 | -------------------------------------------------------------------------------- /src/clj/wacnet/nrepl.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.nrepl 2 | (:require [nrepl.core :as nrepl] 3 | [nrepl.server :as nrepl-server])) 4 | 5 | ;; this is required until this is fixed https://github.com/clojure-emacs/cider-nrepl/issues/447 6 | (defn nrepl-handler [] 7 | (require 'cider.nrepl) 8 | (ns-resolve 'cider.nrepl 'cider-nrepl-handler)) 9 | 10 | (def server (atom nil)) 11 | 12 | (defn server-eval "Send an expression to eval on the nrepl server." 13 | [to-eval] 14 | (with-open [conn (nrepl/connect :port (:port @server))] 15 | (-> (nrepl/client conn 1000) ; message receive timeout required 16 | (nrepl/message {:op "eval" :code (str to-eval)}) 17 | nrepl/response-values))) 18 | 19 | (def repl-init 20 | "Some stuff to do when initiating the repl. Mainly, we are loading 21 | the related bacure functions so the user doesn't have to change 22 | between namespace." 23 | '(do (require '[clojure.repl :refer [doc source apropos]]) 24 | (require '[clojure.pprint :refer [pprint print-table]]) 25 | (require '[bacure.core :refer :all]) 26 | (require '[bacure.network :refer :all]) 27 | (require '[bacure.remote-device :refer :all]) 28 | (require '[bacure.local-device :refer :all]))) 29 | 30 | (defn stop-nrepl! 31 | "Stop any current nrepl server." [] 32 | (when @server 33 | (nrepl-server/stop-server @server) 34 | (reset! server nil))) 35 | 36 | 37 | (defn start-nrepl! 38 | "Start (or restart) a REPL on a given port. Default to 47999 if none provided." 39 | [& port] 40 | (stop-nrepl!) 41 | (some->> (nrepl-server/start-server :port (or (first port) 47999) 42 | :bind "0.0.0.0" ;; any 43 | :handler (nrepl-handler)) 44 | (reset! server)) 45 | (server-eval repl-init)) 46 | 47 | -------------------------------------------------------------------------------- /src/clj/wacnet/nrepl.clj~: -------------------------------------------------------------------------------- 1 | (ns wacnet.nrepl 2 | (:require [clojure.tools.nrepl.server :refer [start-server stop-server]] 3 | [clojure.tools.nrepl :as nrepl] 4 | [cider.nrepl :refer [cider-nrepl-handler]])) 5 | 6 | (def server (atom nil)) 7 | 8 | (declare stop-nrepl) 9 | 10 | 11 | (defn server-eval "Send an expression to eval on the nrepl server." 12 | [to-eval] 13 | (with-open [conn (nrepl/connect :port (:port @server))] 14 | (-> (nrepl/client conn 1000) ; message receive timeout required 15 | (nrepl/message {:op "eval" :code (str to-eval)}) 16 | nrepl/response-values))) 17 | 18 | (def repl-init 19 | "Some stuff to do when initiating the repl. Mainly, we are loading 20 | the related bacure functions so the user doesn't have to change 21 | between namespace." 22 | '(do (require '[clojure.repl :refer [doc source apropos]]) 23 | (require '[clojure.pprint :refer [pprint print-table]]) 24 | (require '[bacure.core :refer :all]) 25 | (require '[bacure.network :refer :all]) 26 | (require '[bacure.remote-device :refer :all]) 27 | (require '[bacure.local-device :refer :all]))) 28 | 29 | 30 | (defn start-nrepl 31 | "Start (or restart) a REPL on a given port. Default to 47999 if none provided." 32 | [& port] 33 | (stop-nrepl) 34 | (reset! server (start-server :port (or (first port) 47999) :handler cider-nrepl-handler)) 35 | (server-eval repl-init)) 36 | 37 | (defn stop-nrepl 38 | "Stop any current nrepl server." [] 39 | (when @server 40 | (stop-server @server))) 41 | -------------------------------------------------------------------------------- /src/clj/wacnet/server.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.server 2 | (:gen-class 3 | :main true) 4 | (:require [taoensso.timbre :as timbre] 5 | [wacnet.handler :as h] 6 | [wacnet.local-device :as ld] 7 | [wacnet.nrepl :as nrepl] 8 | [wacnet.systray :as st] 9 | [yada.yada :as yada])) 10 | 11 | (defn headless? 12 | "True if we are running without any graphical support." 13 | [] 14 | (java.awt.GraphicsEnvironment/isHeadless)) 15 | 16 | (let [storage (atom nil)] 17 | (defn reset-webserver 18 | [] 19 | (when-let [close-fn (-> @storage :close)] 20 | (println "Server closed") 21 | (close-fn)) 22 | (reset! storage (yada/listener h/routes {:port 47800})) 23 | (println "Server started"))) 24 | 25 | (defn close-splash-screen! 26 | "Close the splash screen, if present."[] 27 | (when-not (headless?) 28 | (when-let [ss (java.awt.SplashScreen/getSplashScreen)] 29 | (.close ss)))) 30 | 31 | 32 | (defn -main [& m] 33 | (binding [timbre/*config* {:min-level :error}] 34 | (close-splash-screen!) ;; first thing to do once clojure is loaded. 35 | (ld/initialize-with-exit-on-fail!) ;; we possibly exit at this point 36 | (nrepl/start-nrepl!) 37 | (reset-webserver) 38 | (println (str "\n\n\n" 39 | "---> \n" 40 | " See the web interface at http://localhost:47800.\n" 41 | " You can also connect to the Clojure nrepl on port 47999.")) 42 | ;; when we have graphical support 43 | (when-not (headless?) 44 | ;; add the system tray icon 45 | (st/initialize-systray!) 46 | ;; and open the web interface 47 | (clojure.java.browse/browse-url "http://localhost:47800")))) 48 | -------------------------------------------------------------------------------- /src/clj/wacnet/systray.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.systray 2 | (:require [clojure.java.browse]) 3 | (:import (java.awt.event ActionListener) 4 | (dorkbox.systemTray Menu 5 | MenuItem 6 | Separator 7 | SystemTray))) 8 | 9 | (def menu-items [["Open in browser" #(clojure.java.browse/browse-url "http://localhost:47800")] 10 | [:separator] 11 | ["Quit Wacnet" #(System/exit 0)]]) 12 | 13 | (defn config-menu! [systray items] 14 | (when-let [menu (.getMenu systray)] 15 | (doseq [item items] 16 | (let [string-or-key (first item) 17 | func (last item) 18 | item (if (= string-or-key :separator) 19 | ;; separator 20 | (Separator.) 21 | ;; menu item 22 | (let [listener (proxy [ActionListener] [] 23 | (actionPerformed [evt] (func)))] 24 | (MenuItem. string-or-key listener)))] 25 | (.add menu item))))) 26 | 27 | (defn initialize-systray! [] 28 | (when-let [systray (SystemTray/get)] 29 | (let [title "Wacnet" 30 | image (clojure.java.io/resource "public/img/favicon.png")] 31 | ;; basic system tray configs 32 | (doto systray 33 | (.setImage image) 34 | (.setTooltip "Wacnet") 35 | (.setStatus "Wacnet")) 36 | 37 | ;; the menu (what is shown when we click the icon) 38 | (config-menu! systray menu-items)))) 39 | -------------------------------------------------------------------------------- /src/cljc/wacnet/bacnet_utils.cljc: -------------------------------------------------------------------------------- 1 | (ns wacnet.bacnet-utils 2 | (:require [clojure.set :as cset] 3 | [clojure.string :as s] 4 | #?(:clj [bacure.coerce.type.enumerated :as enum]) 5 | #?(:clj [bacure.coerce :as c])) 6 | #?(:cljs (:require-macros [wacnet.bacnet-utils :refer [object-types-map engineering-units-map]]))) 7 | 8 | #?(:clj (defmacro object-types-map [] 9 | enum/object-type-map)) 10 | 11 | (def object-types (object-types-map)) 12 | 13 | (defn short-id-to-identifier 14 | "Convert a short id to the extended identifier. 15 | Example: \"1.1\" --> [:analog-input 1]" 16 | [id] 17 | (let [[type instance] (s/split id #"\.") 18 | types (into {} (for [[k v] object-types] 19 | [v k]))] 20 | [(let [parsed (#?(:cljs js/parseInt 21 | :clj Integer/parseInt) type)] 22 | (get types parsed parsed)) 23 | (#?(:cljs js/parseInt 24 | :clj Integer/parseInt) instance)])) 25 | 26 | (defn identifier-to-short-id 27 | "Convert an object identifier to its short string equivalent. 28 | Example: \"1.1\" --> [:analog-input 1] " 29 | [identifier] 30 | (let [[type instance] identifier] 31 | (str (get object-types type type) "." instance))) 32 | 33 | ;; #?(:clj (defmacro object-type-map-name [] 34 | ;; (into {} (for [[k v] enum/object-type-map] 35 | ;; [(str v) (-> (name k) 36 | ;; (s/replace #"-" " ") 37 | ;; (s/capitalize))])))) 38 | 39 | #?(:clj (defmacro engineering-units-map [] 40 | (into {} (for [[k v] enum/engineering-units-map] 41 | [(str v) (-> (name k) 42 | (s/replace #"-" " ") 43 | (s/capitalize))])))) 44 | 45 | (def engineering-units (engineering-units-map)) 46 | -------------------------------------------------------------------------------- /src/cljs/wacnet/core.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.core 2 | (:require [reagent.core :as r] 3 | [wacnet.explorer.devices :as d] 4 | [wacnet.local-device.configs :as cf] 5 | [wacnet.handler :as h] 6 | )) 7 | 8 | 9 | (defn main-page [] 10 | [h/main-page]) 11 | 12 | ;; ------------------------- 13 | ;; Initialize app 14 | 15 | (defn mount-root [] 16 | (r/render [main-page] (.getElementById js/document "app"))) 17 | 18 | 19 | (defn ^:export init! 20 | "" 21 | [] 22 | (h/hook-browser-navigation!) ;; will redirect to default tab 23 | (mount-root)) 24 | 25 | -------------------------------------------------------------------------------- /src/cljs/wacnet/explorer/objects.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.explorer.objects 2 | (:require [reagent.core :as r] 3 | [re-com.core :as re] 4 | [goog.string :as gstring] 5 | [ajax.core :refer [GET POST DELETE]] 6 | [clojure.string :as s] 7 | [wacnet.templates.common :as tp] 8 | [wacnet.routes :as routes] 9 | [wacnet.bacnet-utils :as bu :refer-macros [object-types-map engineering-units-map]])) 10 | 11 | 12 | (def object-int-name-map 13 | (into {} (for [[k v] bu/object-types] 14 | [(str v) (-> (name k) 15 | (s/replace #"-" " ") 16 | (s/capitalize))]))) 17 | 18 | 19 | 20 | (defn object-type-name 21 | "Given an object type (number), return its name." 22 | [object-type] 23 | (get object-int-name-map (str object-type))) 24 | 25 | ;; (defn short-id-to-identifier 26 | ;; "Convert a short id to the extended identifier. 27 | ;; Example: \"1.1\" --> [:analog-input 1]" 28 | ;; [id] 29 | ;; (let [[type instance] (s/split id #"\.") 30 | ;; types (into {} (for [[k v] object-types] 31 | ;; [v k]))] 32 | ;; [(get types (js/parseInt type)) 33 | ;; (js/parseInt instance)])) 34 | 35 | (defn identifier-to-short-id [identifier] 36 | (let [[type instance] identifier] 37 | (str (get bu/object-types type) "." instance))) 38 | 39 | (defn object-icon 40 | "Return an icon component depending on the object type." 41 | [object-type] 42 | (let [objects {"0" [:span [:i.fa.fa-fw.fa-sign-in]" "[:i.fa.fa-fw.fa-signal]] ;; analog input 43 | "1" [:span [:i.fa.fa-fw.fa-signal]" "[:i.fa.fa-fw.fa-sign-out]] ;; analog output 44 | "2" [:span [:i.fa.fa-fw.fa-signal]] ;; analog value 45 | "3" [:span [:i.fa.fa-fw.fa-sign-in]" "[:i.fa.fa-fw.fa-adjust]] ;; binary input 46 | "4" [:span [:i.fa.fa-fw.fa-adjust]" "[:i.fa.fa-fw.fa-sign-out]] ;; binary output 47 | "5" [:span [:i.fa.fa-fw.fa-adjust]] ;; binary value 48 | "8" [:span [:i.fa.fa-fw.fa-server]] ;; device 49 | "10" [:span [:i.fa.fa-fw.fa-file-o]] ;;file 50 | "12" [:span [:i.fa.fa-fw.fa-circle-o-notch]] ;; loop 51 | }] 52 | (get objects object-type))) 53 | 54 | (defn object-value 55 | "If the object is binary, return ON/OFF instead of 0 and 1." 56 | [object-type present-value] 57 | (if (some #{object-type} ["3" "4" "5"]) 58 | ;; binary objects 59 | (cond 60 | (= 1 present-value) [:div.label.label-success "ON"] 61 | (= 0 present-value) [:div.label.label-danger "OFF"] 62 | :else [:div]) 63 | ;; other objects 64 | [:span {:style {:text-align "right"}} 65 | (let [v present-value] 66 | (cond 67 | (number? v) (gstring/format "%.2f" v) 68 | (or (nil? v) (coll? v)) "---" 69 | (boolean? v) (str v) 70 | :else (name v)))])) 71 | 72 | 73 | 74 | (defn api-path [api-root & args] 75 | (str api-root (s/join "/" args))) 76 | 77 | 78 | (defn create-object! [loading? error? device-id props-map configs callback] 79 | (reset! loading? true) 80 | (reset! error? nil) 81 | (POST (api-path (:api-root configs) "bacnet" "devices" device-id "objects") 82 | {:handler (fn [resp] 83 | (reset! loading? nil) 84 | (when callback 85 | (callback resp)) 86 | (prn resp)) 87 | :params props-map 88 | :response-format :transit 89 | :error-handler (fn [error-resp] 90 | (reset! loading? nil) 91 | (reset! error? (:response error-resp)))})) 92 | 93 | 94 | (comment 95 | {:object-instance "1", :object-id "0.1", :status-flags {:in-alarm false, :fault false, :out-of-service true, :overridden false}, :present-value 0, :object-type "0", :out-of-service true, :event-state :normal, :object-name "Analog Input 1", :units "no units", :href "http://localhost:3449/api/v1/bacnet/devices/1338/objects/0.1"}) 96 | 97 | 98 | 99 | (defn create-new-object-modal [device-id configs callback close-modal!] 100 | (let [object-props (r/atom {:object-type "0" 101 | :object-name "Input" 102 | :description "" 103 | :units "" 104 | }) 105 | obj-type-a (r/cursor object-props [:object-type]) 106 | units-a (r/cursor object-props [:units]) 107 | error? (r/atom nil) 108 | loading? (r/atom nil) 109 | obj-choices (->> (for [[k v] object-int-name-map] 110 | {:id k :label v}) 111 | (sort-by :label)) 112 | units-choices (->> (for [[k v] bu/engineering-units] 113 | {:id k :label v}) 114 | (sort-by :label))] 115 | (fn [device-id configs callback] 116 | [:div 117 | [:div.modal-header [:h3 [:i.fa.fa-plus] " New BACnet Object"]] 118 | [:div.modal-body 119 | [re/v-box 120 | :gap "10px" 121 | :children [[re/single-dropdown :choices obj-choices 122 | :model obj-type-a :on-change #(reset! obj-type-a %) :width "200px"] 123 | [:div [:b "Object name"] [tp/live-edit :input object-props :object-name]] 124 | [:div [:b "Description (optional)"] [tp/live-edit :textarea object-props :description]] 125 | [:div [:b "Units "] 126 | [re/single-dropdown 127 | :choices units-choices 128 | :model units-a :on-change #(reset! units-a %) :width "300px"]] 129 | (when-let [err @error?] 130 | [:span.alert.alert-warning {:style {:width "100%"}} (str err)])]]] 131 | [:div.modal-footer 132 | [re/h-box :children 133 | ;:gap "10px" 134 | [[re/gap :size "1"] 135 | [:button.btn.btn-default {:on-click close-modal!} "Cancel"] 136 | [re/gap :size "10px"] 137 | [:button.btn.btn-primary 138 | {:disabled (empty? (:object-name @object-props)) 139 | :on-click #(create-object! loading? error? device-id @object-props 140 | configs callback)} 141 | "Create!" (when @loading? 142 | [:span " " [:i.fa.fa-spinner.fa-pulse]])]]]]]))) 143 | 144 | 145 | 146 | (defn delete-object! [loading? error? device-id props-map configs callback] 147 | (reset! loading? true) 148 | (reset! error? nil) 149 | (DELETE (api-path (:api-root configs) "bacnet" "devices" device-id "objects" 150 | (str (:object-type props-map) "." (:object-instance props-map))) 151 | {:handler (fn [resp] 152 | (reset! loading? nil) 153 | (when callback 154 | (callback resp)) 155 | (prn resp)) 156 | :params props-map 157 | :response-format :transit 158 | :error-handler (fn [error-resp] 159 | (reset! loading? nil) 160 | (reset! error? (:response error-resp)))})) 161 | 162 | 163 | (defn delete-object-modal [bacnet-object configs callback close-modal!] 164 | (let [error? (r/atom nil) 165 | loading? (r/atom nil)] 166 | (fn [bacnet-object configs callback] 167 | [:div 168 | [:div.modal-header [:h3 [:i.fa.fa-trash] " Delete BACnet object"]] 169 | [:div.modal-body 170 | [:div.alert.alert-warning "Are you sure you want to delete this object?" 171 | [:div [:strong (:object-name bacnet-object)]]] 172 | (when-let [err @error?] 173 | [:span.alert.alert-warning {:style {:width "100%"}} (str err)])] 174 | [:div.modal-footer 175 | [re/h-box :children 176 | [[re/gap :size "1"] 177 | [:button.btn.btn-default {:on-click close-modal!} "Cancel"] 178 | [re/gap :size "10px"] 179 | [:button.btn.btn-danger 180 | {:on-click #(delete-object! loading? error? (:device-id bacnet-object) 181 | bacnet-object configs callback)} "Delete!" 182 | (when @loading? 183 | [:span " " [:i.fa.fa-spinner.fa-pulse]])]]] 184 | ]]))) 185 | 186 | 187 | 188 | ;; (defn control-loop-view [obj] 189 | ;; (let [p-id (:project-id obj) 190 | ;; d-id (:device-id obj) 191 | ;; o-fn (fn [k] 192 | ;; (let [c-o (make-loop-obj obj k)] 193 | ;; [:span 194 | ;; [inline-object c-o] 195 | ;; ]))] 196 | ;; [re/v-box 197 | ;; :children [[re/line :size "1px"] 198 | ;; [re/box :child [:span (bfo/object-icon (:object-type obj)) " " 199 | ;; (t/t @t/locale :objects/control-loop)]] 200 | ;; [re/h-box 201 | ;; :gap "20px" 202 | ;; :children 203 | ;; [[re/v-box 204 | ;; :size "1" 205 | ;; :children [[re/title :level :level3 :underline? true 206 | ;; :label (t/t @t/locale :vigilia/controlled)] 207 | ;; (o-fn :controlled-variable-reference)]] 208 | ;; [re/line :size "1px"] 209 | ;; ;[re/label :label [:i.fa.fa-arrow-right]] 210 | ;; [re/v-box 211 | ;; :size "1" 212 | ;; :children [[re/title :level :level3 :underline? true 213 | ;; :label (t/t @t/locale :vigilia/manipulated)] 214 | ;; (o-fn :manipulated-variable-reference)]] 215 | ;; [re/line :size "1px"] 216 | ;; [re/v-box 217 | ;; :size "1" 218 | ;; :children [[re/title :level :level3 :underline? true 219 | ;; :label (t/t @t/locale :vigilia/setpoint)] 220 | ;; (o-fn :setpoint-reference)]]]]]])) 221 | 222 | 223 | (defn device-link [obj] 224 | (let [d-id (:device-id obj)] 225 | (when d-id 226 | [:a {:href (routes/path-for :devices-with-id :device-id d-id)} 227 | "("d-id")" " " [:b (:device-name obj)]]))) 228 | 229 | (defn make-parent-device-link 230 | "Given the device-id, will retrieve the device name and make a link." 231 | [object configs] 232 | (let [parent-device (r/atom nil)] 233 | (fn [object configs] 234 | (let [{:keys [device-id]} object] 235 | (when-not (= (:device-id @parent-device) device-id) 236 | (GET (api-path (:api-root configs) 237 | "bacnet" "devices" device-id) 238 | {:response-format :transit 239 | :handler #(reset! parent-device %) 240 | :error-handler prn})) 241 | [:div (device-link @parent-device)])))) 242 | 243 | 244 | 245 | (defn map-to-bootstrap [map] 246 | [:div 247 | (for [m map] 248 | ^{:key (key m)} 249 | [:div.row {:style {:background "rgba(0,0,0,0.05)" 250 | :border-radius "3px" 251 | :margin "2px"}} 252 | [:div.col-sm-4 [:strong (name (key m))]] 253 | [:div.col-sm-8 {:style {:overflow :hidden}} 254 | (let [v (val m)] 255 | (if (map? v) 256 | (map-to-bootstrap v) 257 | [:div {:style {:margin-left "1em"}} 258 | (cond (keyword? v) (name v) 259 | (and (coll? v) (> (count v) 2)) [:ul 260 | (for [[id item] (map-indexed vector v)] 261 | ^{:key id} 262 | [:li (str item)])] 263 | :else (str v))]))]])]) 264 | 265 | 266 | (defn prop-table [object configs] 267 | (let [last-update-object (r/atom nil) 268 | error? (r/atom nil) 269 | loading? (r/atom nil)] 270 | (fn [object configs] 271 | (let [current-object (or @last-update-object object) 272 | ;; use the latest object data, or fallback to the provided object 273 | {:keys [project-id device-id object-type object-instance]} current-object 274 | object-type-name (-> object-type object-type-name) 275 | load-props-fn (fn [] 276 | (reset! loading? true) 277 | (reset! error? nil) 278 | (GET (:href object) 279 | {:response-format :transit 280 | :handler #(do (reset! last-update-object %) 281 | (reset! loading? nil)) 282 | :error-handler (fn [error-resp] 283 | (reset! loading? nil) 284 | (reset! error? (:response error-resp)))}))] 285 | [:div 286 | [re/v-box 287 | :gap "10px" 288 | ;:min-width "500px" 289 | :children [[:a {:href (:href object) 290 | :target "_blank"} "See in API " [:i.fa.fa-external-link]] 291 | [re/gap :size "10px"] 292 | (when-let [err @error?] 293 | [:span.alert.alert-warning {:style {:width "100%"}} (str err)]) 294 | [map-to-bootstrap (dissoc current-object :href :object-id)] 295 | [:button.btn.btn-sm.btn-default {:on-click load-props-fn} 296 | "Load all properties" 297 | (when @loading? 298 | [:span " " [:i.fa.fa-spinner.fa-pulse]])]]]])))) 299 | 300 | (defn prop-modal [vigilia-object configs ok-btn] 301 | [:div 302 | [:div.modal-header 303 | [:h3 [object-icon (:object-type vigilia-object)] 304 | " "(or (seq (:object-name vigilia-object)) 305 | "< no name >")]] 306 | [:div.modal-body 307 | [prop-table vigilia-object configs]] 308 | [:div.modal-footer ok-btn]]) 309 | -------------------------------------------------------------------------------- /src/cljs/wacnet/handler.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.handler 2 | (:require [reagent.core :as r] 3 | ;[ajax.core :refer [GET POST]] 4 | [bidi.bidi :as bidi] 5 | [bidi.router :as router] 6 | [wacnet.routes :as routes] 7 | [goog.events :as events] 8 | [goog.history.EventType :as EventType] 9 | [re-com.core :as re] 10 | [wacnet.explorer.devices :as d] 11 | [wacnet.local-device.configs :as cf] 12 | [wacnet.repl :as repl] 13 | [wacnet.vigilia :as vi] 14 | [clojure.string :as s] 15 | [goog.net.jsloader] 16 | [reagent-modals.modals :as mod] 17 | [wacnet.stateful :as state] 18 | [reagent-keybindings.keyboard :as kb]) 19 | (:import goog.History)) 20 | 21 | 22 | 23 | ;; (defn make-device-tab [params] 24 | ;; (let [selected-device-id (r/atom (:device-id params))] 25 | ;; (fn [params] 26 | ;; (reset! selected-device-id (:device-id params)) 27 | ;; [d/controllers-view selected-device-id]))) 28 | 29 | 30 | (defn logo [] 31 | (let [height "2.3em"] 32 | [:a {:href "/" 33 | :style {:line-height height}} 34 | [:img {:src "/img/wacnet-logo-name.svg" 35 | :style {:height height}}]])) 36 | 37 | 38 | 39 | (defn default-tab-content [] 40 | [:div "empty tab"]) 41 | 42 | (defonce current-tab-content (r/atom [default-tab-content])) 43 | 44 | (defn make-configs-tab [params] 45 | [cf/configs-form params]) 46 | ;; In these tabs we put what is available to the user. 47 | 48 | (def tabs {:devices {:name "Explorer" 49 | :content #'d/controllers-view} 50 | :local-device-configs {:name "Configs" 51 | :content #'make-configs-tab} 52 | :repl {:name "REPL" 53 | :content #'repl/repl-page} 54 | :vigilia {:name "Vigilia" 55 | :content #'vi/vigilia-page} 56 | }) 57 | 58 | 59 | 60 | (def current-tab-id (r/atom :devices)) 61 | 62 | (defn goto-tab! [tab-id] 63 | (reset! current-tab-id tab-id) 64 | (let [url (routes/path-for tab-id)] 65 | (routes/goto-hash! url))) 66 | 67 | (defn navigate! [url] 68 | (let [url-sans-hash (s/replace url "#" "") 69 | matched-route (bidi/match-route routes/app-routes url-sans-hash)] 70 | (let [{:keys [handler route-params]} matched-route] 71 | (if-not handler 72 | (goto-tab! :local-device-configs) ;; default tab 73 | (do 74 | (reset! current-tab-id handler) 75 | (state/set-url-params! route-params) 76 | (reset! current-tab-content 77 | [(:content (get tabs handler))])))))) 78 | 79 | 80 | 81 | 82 | (defn tab 83 | "Make the `tab` button. Use a different class if the tab is 84 | active." [k] 85 | (let [active? (= k @current-tab-id)] 86 | [:li {:class (when active? "active") 87 | :style {:cursor (when-not active? "pointer")} 88 | :on-click (when-not active? #(goto-tab! k))} 89 | [:a (:name (get tabs k))]])) 90 | 91 | 92 | (defn horizontal-tabs [tabs-map] 93 | [:ul.nav.nav-tabs {:style {:margin "10px" 94 | :display "inline-block" 95 | :vertical-align "middle"}} 96 | (for [k (keys tabs)] 97 | ^{:key k} 98 | [tab k])]) 99 | 100 | ;; :error-handler prn})) 101 | 102 | (defn main-tab [] 103 | @current-tab-content) 104 | 105 | 106 | (defn dark-mode-css [] 107 | [:div 108 | [:link {:href "/bootstrap-3.3.6-dist/css/bootstrap-dark.min.css" :rel :stylesheet :type "text/css"}] 109 | [:style "svg text {fill:white;}"] 110 | [:style ".graphivac.editor {fill:white;}"]]) 111 | 112 | 113 | (defn about-modal [] 114 | [:div 115 | [:div.modal-header [:h2 [:img {:src "/img/wacnet-logo-name.svg" 116 | :style {:height "1.5em"}}] 117 | [mod/close-button]] 118 | [:div "Version : " js/WacnetVersion]] 119 | [:div.modal-body 120 | [:p "Wacnet is " 121 | [:a {:href "https://github.com/Frozenlock/wacnet" :target "_blank"} "free and open source"] 122 | ", provided to you by " [:a {:href "https://hvac.io" :target "_blank"} "HVAC.IO"]] 123 | [:p "Additional info is available on the " 124 | [:a {:href "https://wiki.hvac.io/doku.php?id=suppliers:hvac.io:wacnet" 125 | :target "_blank"} "Wiki"] "."] 126 | [:p "If you'd like to have a feature implemented, or need your own BACnet application, " 127 | "contact us at " [:a {:href "mailto:contact@hvac.io"} "contact@hvac.io"] "."] 128 | [:hr] 129 | [:div "Announcements :" 130 | [repl/gist "8406e487ecc70ee204d0"]]] 131 | [:div.modal-footer 132 | [:div.text-right 133 | [:button.btn.btn-primary {:on-click #(mod/close-modal!)} "Ok"]]]]) 134 | 135 | (defn main-page [] 136 | (let [title-atom (r/atom "")] 137 | (fn [] 138 | [:div {:style {:height "100%" :width "100%"}} 139 | [re/v-box 140 | :height "100%" 141 | :children 142 | [;[dark-mode-css] 143 | [kb/keyboard-listener] 144 | [mod/modal-window] 145 | [re/h-box 146 | :align :center 147 | :gap "10px" 148 | :children 149 | [[re/gap :size "10px"] 150 | [:span [logo]] 151 | [horizontal-tabs tabs] 152 | [re/gap :size "1"] 153 | [:div {:style {:margin "10px" 154 | :margin-left "50px" 155 | :display "inline-block" 156 | :vertical-align "middle"}} 157 | [:a.text-sm {:href "/api/v1" :target "_blank"} "API "[:i.fa.fa-external-link]]] 158 | [:button.btn.btn-sm.btn-default 159 | {:on-click #(mod/modal! [about-modal])} "About "[:i.fa.fa-question-circle]] 160 | [re/gap :size "10px"]]] 161 | [re/box 162 | :size "1" 163 | :child [main-tab]]]]]))) 164 | 165 | 166 | 167 | ;; history 168 | ;; must be called after routes have been defined 169 | (defn hook-browser-navigation! [] 170 | (let [h (History.)] 171 | (events/listen h EventType/NAVIGATE 172 | (fn [event] 173 | (navigate! (str "#" (.-token event))))) 174 | (.setEnabled h true))) 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/cljs/wacnet/local_device/configs.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.local-device.configs 2 | (:require [reagent.core :as r] 3 | [re-com.core :as re] 4 | [ajax.core :refer [GET POST]] 5 | [wacnet.templates.common :as common])) 6 | 7 | (defn get-configs [configs-a loading-a error-a] 8 | (reset! loading-a true) 9 | (reset! error-a nil) 10 | (GET "/api/v1/bacnet/local-device/configs" 11 | {:handler (fn [response] 12 | (reset! loading-a nil) 13 | (reset! configs-a response)) 14 | :response-format :transit 15 | :error-handler (fn [_] 16 | (reset! loading-a nil) 17 | (reset! error-a true))})) 18 | 19 | (defn summary [config-a] 20 | (let [ld-summary (r/atom nil) 21 | get-summary! (fn [] 22 | (GET "/api/v1/bacnet/local-device" 23 | {:response-format :transit 24 | :handler #(reset! ld-summary %) 25 | :error-handler (fn [%] 26 | (reset! ld-summary nil) 27 | (prn %))}))] 28 | (r/create-class 29 | {:component-did-mount get-summary! 30 | :reagent-render 31 | (fn [] 32 | [:div.text-center [:h4 "Avaiblable Interfaces"] 33 | (for [interface (:available-interfaces @ld-summary) 34 | :let [i-name (:interface interface) 35 | ips (:ips interface) 36 | b-ad (:broadcast-address interface)]] 37 | ^{:key i-name} 38 | [:div.btn.btn-default {:on-click #(swap! config-a assoc :broadcast-address b-ad) 39 | :style {:cursor :pointer}} 40 | [:b (:interface interface)] 41 | [:div 42 | ;[:div "Ips : " (:ips interface) " "] 43 | [:div "Broadcast address : " (:broadcast-address interface)]]])])}))) 44 | 45 | 46 | (defn parse-configs 47 | "Parse some config fields into integers." 48 | [configs-map] 49 | (let [parse-maybe (fn [str-or-num] 50 | (if (number? str-or-num) str-or-num 51 | (js/parseInt str-or-num)))] 52 | (-> configs-map 53 | (update-in [:device-id] parse-maybe) 54 | (update-in [:port] parse-maybe) 55 | ((fn [cm] 56 | (cond-> cm 57 | (get-in cm [:foreign-device-target :port]) 58 | (update-in [:foreign-device-target :port] parse-maybe))))))) 59 | 60 | (defn update-configs-btn [configs-a] 61 | (let [error? (r/atom nil) 62 | loading? (r/atom nil) 63 | success? (r/atom nil) 64 | update-configs! (fn [config-map] 65 | (reset! loading? true) 66 | (POST "/api/v1/bacnet/local-device/configs" 67 | {:params (parse-configs config-map) 68 | :response-format :transit 69 | :keywords? true 70 | :handler (fn [resp] 71 | (reset! success? true) 72 | (reset! loading? nil) 73 | (reset! error? nil)) 74 | :error-handler (fn [resp] 75 | (reset! loading? nil) 76 | (reset! success? nil) 77 | (reset! error? true))}))] 78 | (fn [] 79 | [:span 80 | (when @success? 81 | (js/setTimeout #(reset! success? nil) 3000) 82 | [:div.message [:div.alert.alert-success "Configs updated!"]]) 83 | (when @error? 84 | (js/setTimeout #(reset! error? nil) 10000) 85 | [:div.message [:div.alert.alert-danger.text-left 86 | [:div "Error : " (:status-text @error?)] 87 | [:div "The server encountered an error. Double check the configuration."] 88 | [:div "Also make sure the BACnet port is not already taken by another software."]]]) 89 | [:button.btn.btn-primary 90 | {:on-click #(update-configs! (-> @configs-a 91 | (select-keys [:description :broadcast-address :device-id :port 92 | :apdu-timeout 93 | :number-of-apdu-retries 94 | :foreign-device-target])))} 95 | "Update / Reboot device"]]))) 96 | 97 | 98 | (defn configs-form 99 | [] 100 | (let [configs-a (r/atom nil) 101 | success? (r/atom nil) 102 | error? (r/atom nil) 103 | loading? (r/atom nil)] 104 | (r/create-class 105 | {:component-did-mount (fn [_] 106 | (get-configs configs-a loading? error?)) 107 | ;:should-component-update (fn [_ _ _] true) 108 | :reagent-render 109 | (fn [] 110 | [:div.container 111 | [:h3.text-center "Local Device Configurations"] 112 | [:hr] 113 | [:div 114 | (if @loading? 115 | [re/throbber] 116 | [:div 117 | [:div.form-horizontal 118 | (for [[k v] (->> (dissoc @configs-a :foreign-device-target) 119 | (merge {:description nil 120 | :broadcast-address nil 121 | :device-id nil 122 | :object-name nil 123 | :apdu-timeout nil 124 | :number-of-apdu-retries nil 125 | :port nil}))] 126 | ^{:key k} 127 | [common/form-group (name k) k 128 | [common/live-edit :input configs-a k]])] 129 | [:hr] 130 | ;; [:div.panel.panel-default 131 | ;; [:div.panel-heading.panel-toggle {:data-toggle "collapse" :data-target "#foreign-device" 132 | ;; :style {:cursor :pointer}} 133 | ;; [:h4.panel-title.text-center "Foreign Device (optional) " [:i.fa.fa-chevron-down]]]] 134 | ;; (let [fdt-a (r/cursor configs-a [:foreign-device-target])] 135 | ;; [:div.panel-collapse.collapse {:id "foreign-device"} 136 | ;; [:div.panel-body 137 | ;; [:h5.text-center "Foreign Device Registration"] 138 | ;; [:div.form-horizontal 139 | ;; (for [k [:host :port]] 140 | ;; ^{:key k} 141 | ;; [common/form-group (name k) k 142 | ;; [common/live-edit :input fdt-a k]])]]]) 143 | ]) 144 | [:div.text-right 145 | [update-configs-btn configs-a]] 146 | [summary configs-a]]])}))) 147 | -------------------------------------------------------------------------------- /src/cljs/wacnet/repl.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.repl 2 | (:require [reagent.core :as r] 3 | [ajax.core :refer [GET POST]] 4 | [re-com.core :as re] 5 | [wacnet.templates.common :as common] 6 | [goog.net.jsloader])) 7 | 8 | ;; (defn gist [gist-id] 9 | ;; (let [data (r/atom nil) 10 | ;; fetch-fn (fn [] 11 | ;; (GET (str "https://gist.github.com/" gist-id ".json&callback=callback") 12 | ;; :handler (fn [resp] (reset! data resp))))] 13 | ;; (r/create-class 14 | ;; {:display-name "Gist" 15 | ;; :component-did-mount fetch-fn 16 | ;; :reagent-render 17 | ;; (fn [gist-id] 18 | ;; [:div (str @data)])}))) 19 | 20 | 21 | (defn gist [gist-id] 22 | (let [is-loaded? (fn [iframe] 23 | (-> (aget iframe "contentWindow" "document" "body") 24 | (.getElementsByTagName "div") 25 | (array-seq) 26 | (first) 27 | (nil?) 28 | (not))) 29 | resize-iframe (fn [iframe] 30 | (let [height (aget iframe "contentWindow" "document" "body" "scrollHeight")] 31 | (aset iframe "height" (str height "px")))) 32 | forget-me? (atom nil) 33 | when-loaded (fn when-loaded [iframe this-fn] 34 | (when-not @forget-me? 35 | (if (is-loaded? iframe) 36 | (this-fn) 37 | (js/setTimeout #(when-loaded iframe this-fn) 1000))))] 38 | (r/create-class 39 | {:component-did-mount (fn [this] 40 | (let [iframe (r/dom-node this) 41 | doc (.-contentDocument iframe) 42 | script (str "") 44 | iframe-html (str ""script "")] 46 | (doto doc 47 | (.open) 48 | (.write iframe-html) 49 | (.close)) 50 | (when-loaded iframe #(resize-iframe iframe)))) 51 | :component-will-unmount #(reset! forget-me? true) 52 | :reagent-render 53 | (fn [gist-id] 54 | [:iframe {:width "100%" 55 | :frame-border 0}])}))) 56 | 57 | (defn repl-page [] 58 | (r/create-class 59 | {:component-did-mount (fn [this] 60 | (goog.net.jsloader.safeLoadMany 61 | (clj->js 62 | [(-> (goog.string.Const.from "/jquery-console/jquery.console.js") 63 | (goog.html.TrustedResourceUrl.fromConstant)) 64 | (-> (goog.string.Const.from "/tryclojure.js") 65 | (goog.html.TrustedResourceUrl.fromConstant))]))) 66 | :reagent-render 67 | (fn [] 68 | [common/scrollable 69 | [:div.container 70 | [:link {:href "/tryclojure.css" :rel "stylesheet" :type "text/css"}] 71 | [:div [:div#header 72 | [:h1 73 | [:img {:src "/img/clojure-logo.png" :style {:height "1em" :margin-bottom "5px"}}] 74 | [:a {:href "http://www.clojure.org"} [:span.logo-clojure "Clo" [:em "j"] "ure"]] 75 | [:span.logo-try " REPL "]]] 76 | [:pre [:div#console.console]]] 77 | [:div.row 78 | [:div.col-sm-6.col-sm-offset-3 79 | [:h2 "REPL?"] 80 | [:p 81 | "REPL means "[:i "Read Eval Print Loop."]" " 82 | "It's an interactive development environment. " 83 | "With this, you can define new functions and use them on the spot, " 84 | "without the need to recompile!"] 85 | [:p 86 | "Try it! Define a new function by typing "[:code "(defn square [x] (* x x))"] 87 | " and then pressing ENTER. Now use your new function: " 88 | [:code "(square 10)"]] 89 | 90 | [:i 91 | "Want a better experience? Use Emacs with " 92 | [:a {:href "https://github.com/clojure-emacs/cider/blob/master/README.md"} 93 | "cider"] 94 | " (or any other nrepl interface) " 95 | "and connect to port 47999!"]]] 96 | [:div.row 97 | [:div.col-sm-12 98 | [:h3 "Examples"] 99 | [:p 100 | "Check the "[:a {:href "https://wiki.hvac.io/doku.php?id=suppliers:hvac.io:wacnet#scripts"} 101 | "Wiki"] 102 | " to see scripts made by users."] 103 | [:h4 "List of remote devices"] 104 | [:p 105 | "This one is simple enough: " 106 | [gist "Frozenlock/3ce34c61a5b995d5213d"]] 107 | [:h4 "Properties values"] 108 | [:p 109 | "This will retrieve the value of the properties "[:code ":present-value"]" and " 110 | [:code ":description"]" :" 111 | [gist "Frozenlock/69e45162d0008fc7982c"] 112 | ] 113 | [:h4 "Filter objects"] 114 | [:p 115 | "You might want to filter objects base on their properties. " 116 | "Quite hard to do with with a simple webpage interface, but effortless by using " 117 | "this command: " 118 | [gist "Frozenlock/41ee57a1a7a4b0cd5f1a"] 119 | ] 120 | [:h4 "Export results"] 121 | [:p 122 | "Found something interesting? Easily export it for later analysis: " 123 | [gist "Frozenlock/345578e4aecc770482b2"] 124 | ]]]]])})) 125 | -------------------------------------------------------------------------------- /src/cljs/wacnet/routes.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.routes 2 | (:require [bidi.bidi :as bidi])) 3 | 4 | 5 | (def app-routes 6 | ["/" [["" :home] 7 | ["repl" :repl] 8 | ["explorer" 9 | [["" :devices] 10 | [["/" :device-id] (bidi/tag :devices :devices-with-id)] 11 | [["/" :device-id "/" :object-type "/" :object-instance] 12 | (bidi/tag :devices :devices-with-object)]]] 13 | ["vigilia" 14 | [["" :vigilia]]] 15 | ["configs" :local-device-configs] 16 | ]]) 17 | 18 | (def path-for 19 | (comp #(str "#" %) (partial bidi/path-for app-routes))) 20 | 21 | (defn replace-hash! 22 | "Replace the current hash" 23 | [string] 24 | (js/window.history.replaceState {} nil string)) 25 | 26 | 27 | (defn push-hash! 28 | "Replace the current hash and insert a new history" 29 | [string] 30 | (js/window.history.pushState {} nil string)) 31 | 32 | (defn goto-hash! 33 | "Go to specified hash and insert history" 34 | [string] 35 | (set! js/window.location.hash string)) 36 | -------------------------------------------------------------------------------- /src/cljs/wacnet/stateful.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.stateful 2 | (:require [reagent.core :as r] 3 | [alandipert.storage-atom :as sa])) 4 | 5 | (def show-ids-a (-> (r/atom nil) 6 | (sa/local-storage :show-ids))) 7 | 8 | (defonce state (r/atom {})) 9 | 10 | 11 | (defn get-in-state [ks] 12 | (let [temp-a (r/cursor state ks)] 13 | @temp-a)) 14 | 15 | (defn set-in-state! [ks v] 16 | (swap! state assoc-in ks v) 17 | nil) 18 | 19 | (defn url-params-atom [] 20 | (r/cursor state [:url-params])) 21 | 22 | (defn set-url-params! [m] 23 | (set-in-state! [:url-params] m)) 24 | 25 | (defn get-url-params [] 26 | (get-in-state [:url-params])) 27 | -------------------------------------------------------------------------------- /src/cljs/wacnet/templates/common.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.templates.common 2 | (:require [re-com.core :as re] 3 | [reagent.core :as r])) 4 | 5 | (defn is-chrome-or-opera? 6 | "True if the browser is Chrome(ium) or Opera" 7 | [] 8 | (let [browser (-> js/goog .-labs .-userAgent .-browser)] 9 | (or (.isChrome browser) 10 | (.isOpera browser)))) 11 | 12 | 13 | (defn cond-h-split 14 | "If the browser is not Chrome or Opera, replace the split by a 15 | simple h-box." [& {:keys [panel-1 panel-2 size initial-split]}] 16 | (if (is-chrome-or-opera?) 17 | [re/h-split 18 | :initial-split initial-split 19 | :panel-1 panel-1 20 | :panel-2 panel-2] 21 | [re/h-box 22 | :size "1" 23 | :height "100%" 24 | :children [[re/box 25 | :size (str initial-split) 26 | :child panel-1] 27 | [re/box 28 | :size (str (- initial-split 100)) 29 | :child panel-2]]] 30 | )) 31 | 32 | 33 | 34 | (defn scrollable* 35 | ([content] (scrollable* {} content)) 36 | ([attr content] 37 | [:div (merge {:style {:overflow-y "auto" 38 | :height "100vh"}} attr) 39 | content])) 40 | 41 | (def scrollable 42 | (with-meta scrollable* 43 | {:component-did-mount 44 | #(let [node (r/dom-node %) 45 | top (.-top (.getBoundingClientRect node))] 46 | (set! (.-style.height node) (str "calc(100vh - "top"px)")))})) 47 | 48 | ;; (defn scrollable [attr content] 49 | ;; [:div (merge {:style {;:overflow-y "auto" 50 | ;; ;; :height "100%" 51 | ;; }} attr) 52 | ;; content]) 53 | 54 | 55 | ;;;;;;;;;;;;;;;;;;;;;; 56 | 57 | 58 | (defn input 59 | ([{:keys [text on-save on-stop input-type] :as args}] 60 | (input args "form-control")) 61 | ([{:keys [text on-save on-stop input-type]} class] 62 | (let [val (r/atom text) 63 | stop #(do ;(reset! val "") 64 | (if on-stop (on-stop))) 65 | save #(let [v (-> @val str clojure.string/trim)] 66 | (when on-save 67 | (on-save v)))] 68 | (fn [props] 69 | [input-type (merge 70 | {:value @val :on-blur save 71 | :class class 72 | :style {:width "100%"} 73 | :on-change #(reset! val (-> % .-target .-value)) 74 | :on-key-up #(case (.-which %) 75 | 13 (save) ; enter 76 | 27 (stop) ; esc 77 | nil)} 78 | props)])))) 79 | 80 | (def edit 81 | (-> input 82 | (with-meta {:component-did-mount 83 | #(let [node (r/dom-node %) 84 | n-value (count (.-value node))] 85 | (.focus node) 86 | (.setSelectionRange node n-value n-value) 87 | )}))) 88 | 89 | (def edit-with-select 90 | (-> input 91 | (with-meta {:component-did-mount 92 | #(let [node (r/dom-node %) 93 | n-value (count (.-value node))] 94 | (.focus node) 95 | (.setSelectionRange node 0 n-value))}))) 96 | 97 | 98 | (defn save-edit-field [atom key value] 99 | (if-not (empty? value) 100 | (swap! atom assoc key value) 101 | (swap! atom dissoc key))) 102 | 103 | 104 | (defn editable [input-type atom key] 105 | [edit 106 | {:text (get @atom key) 107 | :input-type input-type 108 | :on-save (partial save-edit-field atom key)}]) 109 | 110 | (defn editable-with-select [input-type atom key] 111 | [edit-with-select 112 | {:text (get @atom key) 113 | :input-type input-type 114 | :on-save (partial save-edit-field atom key)}]) 115 | 116 | (defn live-edit 117 | "Same as editable, but immediately updates the atom." 118 | [input-type atom key] 119 | (let [value (get @atom key)] 120 | [input-type {:value value 121 | :class "form-control" 122 | :style {:width "100%"} 123 | :on-change (fn [evt] 124 | (let [temp-val (-> evt .-target .-value) 125 | new-val (if (number? value) 126 | (js/parseInt temp-val) temp-val)] 127 | (if (empty? temp-val) 128 | (swap! atom dissoc key) 129 | (swap! atom assoc key new-val))))}])) 130 | 131 | ;;; bootstrap 132 | 133 | (defn form-group [label id body] 134 | [:div.form-group.form-group-sm 135 | [:label.col-sm-6.control-label {:for id} label] 136 | [:div.col-sm-6 body]]) 137 | -------------------------------------------------------------------------------- /src/cljs/wacnet/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns wacnet.utils 2 | (:require [goog.Timer :as timer] 3 | [reagent.core :as r] 4 | [goog.object :as gobj] 5 | [clojure.string :as s] 6 | cljsjs.resize-observer-polyfill)) 7 | 8 | 9 | 10 | (defn evt-add-class [event class] 11 | (.add (.-classList (.-target event)) class)) 12 | 13 | (defn evt-remove-class [event class] 14 | (.remove (.-classList (.-target event)) class)) 15 | 16 | (defn evt-set-attr [event attr attr-value] 17 | (.setAttribute (.-target event) attr attr-value)) 18 | 19 | 20 | (defn debounce-factory 21 | "Return a function that will always store a future call into the 22 | same atom. If recalled before the time is elapsed, the call is 23 | replaced without being executed." [] 24 | (let [f (atom nil)] 25 | (fn [func ttime] 26 | (when @f 27 | (timer/clear @f)) 28 | (reset! f (timer/callOnce func ttime))))) 29 | 30 | 31 | (defn- where* 32 | [criteria not-found-result] 33 | (fn [m] 34 | (every? (fn [[k v]] 35 | (let [tested-value (get m k :not-found)] 36 | (cond 37 | (= tested-value :not-found) not-found-result 38 | ;; (and (fn? v) tested-value) (try (v tested-value) 39 | ;; (catch Exception e)) 40 | ;; catch exception if there's an error on the provided testing function 41 | ;; (for example, if we use '>' on a string) 42 | (number? tested-value) (== tested-value v) 43 | (and (regexp? v) 44 | (string? tested-value)) (re-find v tested-value) 45 | (= tested-value v) :pass))) criteria))) 46 | 47 | (defn where 48 | "Will test with criteria map as a predicate. If the value of a 49 | key-val pair is a function, use it as a predicate. If the tested map 50 | value is not found, fail. 51 | 52 | For example: 53 | Criteria map: {:a #(> % 10) :b \"foo\"} 54 | Tested-maps {:a 20 :b \"foo\"} success 55 | {:b \"foo\"} fail 56 | {:a nil :b \"foo\"} fail" 57 | [criteria] 58 | (where* criteria false)) 59 | 60 | (defn make-fuzzy-regex [s] 61 | (->> (for [splitted (s/split s #"\.")] 62 | (->> (map #(str ".*" %) splitted) 63 | (apply str))) 64 | (s/join "\\.") 65 | (#(str "(?i)" % ".*")))) 66 | 67 | 68 | (defn is-embed? 69 | "Check if we are currently in an iframe." [] 70 | (not= js/window js/window.parent)) 71 | 72 | 73 | 74 | (defn js-get-keys [o] 75 | (goog.object/getKeys o)) 76 | 77 | (defn js-get [o k] 78 | (goog.object/get o k)) 79 | 80 | (defn js-set [o k v] 81 | (goog.object/set o k v)) 82 | 83 | (defn js-get-in [o js-ks] 84 | (goog.object/getValueByKeys o js-ks)) 85 | 86 | 87 | (defn auto-sizer [content-fn] 88 | (let [size-a (r/atom {}) 89 | dom-node (atom nil) 90 | observer (new js/ResizeObserver 91 | (fn [observer-entries] 92 | (let [content-rect (-> (first observer-entries) 93 | (js-get "contentRect")) 94 | width (js-get content-rect "width") 95 | height (js-get content-rect "height")] 96 | (reset! size-a {:width width :height height}))))] 97 | (r/create-class 98 | {:component-did-mount (fn [this] 99 | (.observe observer (r/dom-node @dom-node))) 100 | :component-will-unmount (fn [_] (.disconnect observer)) 101 | :reagent-render 102 | (fn [content-fn] 103 | [:div {:ref #(reset! dom-node %) 104 | :style {:height "100%" 105 | :width "100%"}} 106 | [content-fn @size-a]])}))) 107 | -------------------------------------------------------------------------------- /test/wacnet/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns wacnet.core-test 2 | (:require [clojure.test :refer :all] 3 | [wacnet.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | --------------------------------------------------------------------------------