├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config └── nimha_default.cfg ├── devops ├── debian │ ├── changelog │ ├── control │ ├── copyright │ ├── nimha.install │ ├── nimha.postinst │ ├── nimha.service │ ├── rules │ └── source │ │ └── format └── helpers │ └── camrecorder.nim ├── nimha.nim ├── nimha.nim.cfg ├── nimha.nimble ├── nimhapkg ├── mainmodules │ ├── nimha_cron.nim │ ├── nimha_gateway.nim │ ├── nimha_gateway_ws.nim │ ├── nimha_webinterface.nim │ ├── nimha_websocket.nim │ └── nimha_xiaomilistener.nim ├── modules │ ├── alarm │ │ └── alarm.nim │ ├── mail │ │ └── mail.nim │ ├── os │ │ └── os_utils.nim │ ├── owntracks │ │ └── owntracks.nim │ ├── pushbullet │ │ └── pushbullet.nim │ ├── rpi │ │ └── rpi_utils.nim │ ├── rss │ │ └── rss_reader.nim │ ├── web │ │ ├── web_certs.nim │ │ └── web_utils.nim │ └── xiaomi │ │ └── xiaomi_utils.nim ├── resources │ ├── database │ │ ├── database.nim │ │ ├── modules │ │ │ ├── alarm_database.nim │ │ │ ├── cron_database.nim │ │ │ ├── filestream_database.nim │ │ │ ├── mail_database.nim │ │ │ ├── mqtt_database.nim │ │ │ ├── os_database.nim │ │ │ ├── owntracks_database.nim │ │ │ ├── pushbullet_database.nim │ │ │ ├── rpi_database.nim │ │ │ ├── rss_database.nim │ │ │ └── xiaomi_database.nim │ │ └── sql_safe.nim │ ├── mqtt │ │ ├── mqtt_func.nim │ │ └── mqtt_templates.nim │ ├── users │ │ ├── password.nim │ │ ├── user_add.nim │ │ └── user_check.nim │ ├── utils │ │ ├── common.nim │ │ ├── dates.nim │ │ ├── log_utils.nim │ │ └── parsers.nim │ └── www │ │ └── google_recaptcha.nim └── tmpl │ ├── alarm.tmpl │ ├── alarm_numpad.tmpl │ ├── certificates.tmpl │ ├── cron.tmpl │ ├── dashboard.tmpl │ ├── filestream.tmpl │ ├── mail.tmpl │ ├── main.tmpl │ ├── mqtt.tmpl │ ├── os.tmpl │ ├── owntracks.tmpl │ ├── pushbullet.tmpl │ ├── rpi.tmpl │ ├── rss.tmpl │ ├── settings.tmpl │ ├── users.tmpl │ └── xiaomi.tmpl ├── private └── screenshots │ └── dashboard.png └── public ├── css └── style.css ├── images ├── favicon │ ├── android-chrome-192x192.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── icon_handle.png ├── icon_pause.png └── icon_trash.png └── js └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Do not include database 4 | data/* 5 | 6 | # Log 7 | log 8 | log/* 9 | 10 | # Include screenshots 11 | private/* 12 | !private/screenshots/ 13 | 14 | # Temp folder 15 | tmp 16 | tmp/* 17 | 18 | # Nimblecache (systemInstall) 19 | nimblecache 20 | nimblecache/* 21 | 22 | # Nimcache folders 23 | nimcache 24 | nimcache/* 25 | */nimcache 26 | */*/nimcache 27 | */*/*/nimcache 28 | 29 | # Secret details 30 | config/nimha_dev.cfg 31 | 32 | # Main modules 33 | nimhapkg/mainmodules/nimha_cron 34 | nimhapkg/mainmodules/nimha_gateway 35 | nimhapkg/mainmodules/nimha_gateway_ws 36 | nimhapkg/mainmodules/nimha_webinterface 37 | nimhapkg/mainmodules/nimha_websocket 38 | nimhapkg/mainmodules/nimha_xiaomilistener 39 | 40 | # Launcher 41 | nimha 42 | 43 | # VScode 44 | .vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.5 2 | - Updated to Nim 1.0.4 3 | - Alarm: Alarm log available 4 | - Alarm: Admin user can change status without password, but still needs to be logged in. 5 | - Alarm: New column in dbAlarm database: 6 | ``` 7 | sqlite3 data/dbAlarm.db 8 | ALTER TABLE alarm_password ADD COLUMN name VARCHAR(300); 9 | ``` 10 | 11 | # v0.4.2 12 | - Server info page 13 | - Check NimHA log in browser 14 | - Restart system or NimHA 15 | 16 | # v0.4.1 17 | - OS templates, run a local command when e.g. the alarm is ringing 18 | - Fix websocket #22 19 | 20 | # v0.4.0 21 | - Databases splitted into multiple instances instead of 1. This is due to problems with concurrency in SQLite databases. If you would like to preserve your current DB values, make a copy of your DB and name them as below inside the `data` folder. 22 | 1) dbAlarm.db 23 | 2) dbCron.db 24 | 3) dbFile.db 25 | 4) dbMail.db 26 | 5) dbMqtt.db 27 | 6) dbOwntracks.db 28 | 7) dbPushbullet.db 29 | 8) dbRpi.db 30 | 9) dbRss.db 31 | 10) dbXiaomi.db 32 | 11) dbWeb.db -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Nim Home Assistant 2 | 3 | ## Running development versions 4 | 5 | Build with -d:dev as: 6 | 7 | See [Install](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Install-NimHA) 8 | 9 | Create `./config/nimha_dev.cfg` from `./config/nimha_default.cfg` 10 | 11 | ## Modules 12 | 13 | See [Develop new module](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Develop-new-module) 14 | 15 | ## Packaging 16 | 17 | See the `#installpath` comments to update system paths before build and compile with `-d:systemInstall`. 18 | 19 | NimHA installs a dedicated Nimble package cache and runs Nim to build the modules. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nim Homeassistant 2 | 3 | Nim Home Assistant (NimHA) is a hub for combining multiple home automation devices and automating jobs. Nim Home Assistant is developed to run on a Raspberry Pi with a 7" touchscreen, mobile devices and on large screens. 4 | 5 | 6 | # Wiki 7 | 8 | Please visit the [Wiki page](https://github.com/ThomasTJdev/nim_homeassistant/wiki) for more information on installation, requirements and the modules. 9 | 10 | 11 | # Current status 12 | 13 | > NimHA is currently in **BETA**. 14 | 15 | Work before reaching stable: 16 | ~~- Avoid database lock error (multiple connections at the same time - which SQLite does not like)~~ 17 | - The alarm module's countdown proc() is currently not working (this means that when the alarm has been triggered, it will immediately go into ringing mode) 18 | 19 | ____ 20 | ![Blog](private/screenshots/dashboard.png) 21 | ____ 22 | 23 | # Features 24 | 25 | ### Dashboard 26 | * Interactive dashboard showing all the data in separate cards. 27 | * Drag and drop the cards in a custom order 28 | * The dashboard uses websocket, so there is no need for refreshing the page - the data renders continuously 29 | * Responsive design for PC's, mobile phones and Raspberry Pi 7" touchscreen 30 | 31 | ### [Alarm system](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Alarm-system) 32 | * Alarm system integrated with Xiaomi IOT devices 33 | * Custom actions when the alarm status changes, e.g. from armed to ringing send mail 34 | * Custom alarm codes for each user 35 | * User defined arm time 36 | 37 | ### [Xiaomi IOT devices](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Xiaomi) 38 | * Integrated with Xiaomi Smart Home devices 39 | * Constantly monitor your devices 40 | * Send commands to your device, e.g. play sound on the gateway 41 | * Auto discovery of devices 42 | 43 | ### [Cronjobs](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Cron-jobs) 44 | * Schedule automatic actions on the minute 45 | * Utilize actions from the different modules, e.g. send mails, notifications, etc. 46 | 47 | ### [SSL certificate watch](https://github.com/ThomasTJdev/nim_homeassistant/wiki/SSL-certificates-watch) 48 | * Monitor the expiration date on your SSL certificates 49 | 50 | ### [Owntracks](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Owntracks) 51 | * View where each of your Owntrack devices are located 52 | * Add custom waypoints to the map 53 | * Use [Google maps](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Google-Maps) 54 | 55 | ### [Mail](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Mail) 56 | * Connect to your mail server and create mail templates, which can be used in the different modules 57 | 58 | ### [MQTT](https://github.com/ThomasTJdev/nim_homeassistant/wiki/MQTT) 59 | * Define custom MQTT templates (topic and message) 60 | * Send MQTT message when the alarm changes status or with cronjobs 61 | * Send test messages via MQTT 62 | 63 | ### [OS commands](https://github.com/ThomasTJdev/nim_homeassistant/wiki/OS-commands) 64 | * Create templates which can be used in different modules 65 | * Test commands from the browser 66 | 67 | ### OS stats 68 | * Monitor the health of your system 69 | 70 | ### [Raspberry Pi](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Raspberry-Pi) 71 | ** Compile with `-d:rpi` to enable ** 72 | * Automate actions using the Raspberry Pi's GPIO 73 | * Write to the pins 74 | * Read the values from the pins 75 | 76 | ### [RSS feed](https://github.com/ThomasTJdev/nim_homeassistant/wiki/RSS-feed) 77 | * Keep an eye on your favorite RSS feeds 78 | 79 | ### [Filestream](https://github.com/ThomasTJdev/nim_homeassistant/wiki/MJPEG-stream) 80 | * Watch your MJPEG stream from the dashboard 81 | * Show a static image from URL 82 | * Get a static image from a LAN url, save the image to NimHA and show it, to avoid exposing LAN url to the internet 83 | 84 | ### [Pushbullet](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Pushbullet) 85 | * Stay notified with the Pushbullet integration on all your devices 86 | 87 | 88 | ### .. and more to come 89 | 90 | 91 | # Run Nim Home Assistant 92 | 93 | ### 1) Ensure that all the requirements are fulfilled: [Wiki - Requirements](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Requirements) 94 | 95 | ### 2) Install and run NimHA: [Wiki - Install](https://github.com/ThomasTJdev/nim_homeassistant/wiki/Install-NimHA) 96 | 97 | -------------------------------------------------------------------------------- /config/nimha_default.cfg: -------------------------------------------------------------------------------- 1 | # Development: config/nimha_dev.cfg 2 | # Production: /etc/nimha/nimha.cfg 3 | 4 | [Home] 5 | # Insert your home latitude and longitude 6 | lat = "" 7 | lon = "" 8 | 9 | # Sandboxing command used to start modules. When empty, no sandboxing is done. 10 | #default_sandbox = "/usr/bin/firejail --noprofile --seccomp --caps.drop=all --private-dev --private-tmp --nice=10 --no3d --nodbus --novideo --noroot" 11 | default_sandbox = "" 12 | 13 | [MQTT] 14 | # Connection details to your MQTT broker 15 | mqttPathSub = "/usr/bin/mosquitto_sub" 16 | mqttPathPub = "/usr/bin/mosquitto_pub" 17 | mqttUsername = "" 18 | mqttPassword = "" 19 | mqttIp = "" # E.g. "tcp://192.168.1.100:1883" 20 | mqttPort = "1883" 21 | 22 | [Google] 23 | # Aquire you API key here https://cloud.google.com/maps-platform/#get-started 24 | mapsAPI = "" 25 | 26 | [reCAPTCHA] 27 | # Register your reCAPTCHA key here https://www.google.com/recaptcha/admin 28 | Sitekey = "" 29 | Secretkey = "" 30 | 31 | [Websocket] 32 | # The connection details for your websocket 33 | wsAddress = "127.0.0.1" 34 | wsProtocol = "ws" 35 | wsPort = "25437" 36 | wsLocalKey = "" 37 | 38 | [Database] 39 | folder = "data" 40 | host = "data/nimha.db" 41 | name = "nimha" 42 | user = "user" 43 | pass = "" 44 | -------------------------------------------------------------------------------- /devops/debian/changelog: -------------------------------------------------------------------------------- 1 | nimha (0.1.2) UNRELEASED; urgency=medium 2 | 3 | * Internal build 4 | 5 | -- Federico Ceratto Fri, 29 Mar 2019 23:22:27 +0000 6 | -------------------------------------------------------------------------------- /devops/debian/control: -------------------------------------------------------------------------------- 1 | Source: nimha 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Federico Ceratto 5 | Build-Depends: debhelper-compat (= 12), 6 | dh-systemd 7 | Standards-Version: 4.3.1 8 | 9 | Package: nimha 10 | Architecture: any 11 | Depends: ${shlibs:Depends}, ${misc:Depends}, mosquitto-clients 12 | Description: Nim Home assitant 13 | -------------------------------------------------------------------------------- /devops/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: nim-homeassistant 3 | Source: https://github.com/ThomasTJdev/nim_homeassistant 4 | 5 | Files: * 6 | Copyright: 2018-2019 Thomas T. Jarløv 7 | License: GPL-3.0+ 8 | 9 | Files: debian/* 10 | Copyright: 2019 Federico Ceratto 11 | License: GPL-3.0+ 12 | 13 | License: GPL-3.0+ 14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 | . 19 | This package is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | . 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | . 27 | On Debian systems, the complete text of the GNU General 28 | Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". 29 | -------------------------------------------------------------------------------- /devops/debian/nimha.install: -------------------------------------------------------------------------------- 1 | nimha usr/sbin 2 | 3 | # Install UI-related files that can be modified 4 | public var/lib/nimha 5 | 6 | # Install modules that can be modified and required shared code 7 | nimhapkg/* var/lib/nimha 8 | -------------------------------------------------------------------------------- /devops/debian/nimha.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | user=nimha 6 | group=nimha 7 | 8 | if [ "$1" = "configure" ]; then 9 | if ! getent passwd $user >/dev/null; then 10 | addgroup --system --quiet $user 11 | adduser --system --quiet --ingroup $group --no-create-home --home /var/lib/$user $user 12 | fi 13 | chown -R nimha:nimha /var/lib/nimha 14 | chown -R nimha:nimha /var/run/nimha 15 | # delete binaries built from previous versions 16 | find /var/lib/nimha -type f -executable -delete 17 | # empty tmp dir 18 | rm /var/run/nimha/* -rf 19 | 20 | fi 21 | 22 | #DEBHELPER# 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /devops/debian/nimha.service: -------------------------------------------------------------------------------- 1 | # nimha systemd target 2 | 3 | [Unit] 4 | Description=nimha 5 | Documentation=man:nimha 6 | Before=network-online.target 7 | 8 | [Service] 9 | Type=simple 10 | ExecStart=/usr/sbin/nimha --cfg /etc/nimha.cfg 11 | TimeoutStopSec=10 12 | KillMode=mixed 13 | KillSignal=SIGTERM 14 | 15 | User=nimha 16 | Group=nimha 17 | 18 | Restart=on-abnormal 19 | RestartSec=2s 20 | #LimitNOFILE=65536 21 | 22 | WorkingDirectory=/ 23 | #WatchdogSec=30s 24 | 25 | # Hardening 26 | NoNewPrivileges=yes 27 | 28 | CapabilityBoundingSet= 29 | 30 | # Configure system call filtering whitelist 31 | SystemCallFilter=@memlock @system-service 32 | 33 | # Connect is used by Nimble :-/ 34 | #SystemCallFilter=~connect 35 | 36 | ProtectSystem=strict 37 | PrivateDevices=yes 38 | PrivateTmp=yes 39 | ProtectHome=yes 40 | ProtectKernelModules=true 41 | ProtectKernelTunables=yes 42 | 43 | PrivateUsers=no 44 | 45 | StandardOutput=syslog 46 | StandardError=syslog 47 | 48 | ReadWriteDirectories=-/proc/self 49 | ReadWriteDirectories=-/var/lib/nimha 50 | ReadWriteDirectories=-/var/run/nimha 51 | 52 | [Install] 53 | WantedBy=multi-user.target 54 | -------------------------------------------------------------------------------- /devops/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | export DH_VERBOSE=1 3 | # hardened using nim.cfg 4 | 5 | %: 6 | dh $@ 7 | 8 | override_dh_auto_build: 9 | nimble build --verbose 10 | 11 | override_dh_installsystemd: 12 | dh_installsystemd --name=nimha 13 | 14 | override_dh_dwz: 15 | true 16 | -------------------------------------------------------------------------------- /devops/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /devops/helpers/camrecorder.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch 2 | import json 3 | import os 4 | import osproc 5 | import strutils 6 | import streams 7 | import times 8 | 9 | 10 | var ffmpeg: Process 11 | var usbOn = true 12 | var isRunning = false 13 | 14 | discard execCmd("/home/pi/nimha/usbcontrol/usboff.sh") 15 | 16 | template jn*(json: JsonNode, data: string): string = 17 | ## Avoid error in parsing JSON 18 | try: 19 | json[data].getStr() 20 | except: 21 | "" 22 | 23 | 24 | proc mqttParser(payload: string) {.async.} = 25 | ## Parse the raw output from Mosquitto sub 26 | ## and start action 27 | 28 | when defined(dev): 29 | echo payload 30 | 31 | let topicName = payload.split(" ")[0] 32 | let message = parseJson(payload.replace(topicName & " ", "")) 33 | 34 | let status = jn(message, "status") 35 | 36 | if status == "ringing" and not isRunning: 37 | isRunning = true 38 | if not usbOn: 39 | discard execCmd("/home/pi/nimha/usbcontrol/usbon.sh") 40 | usbOn = true 41 | sleep(6000) # Waiting time for camera to initialize 42 | let filename = multiReplace($getTime(), [(":", "-"), ("+", "_"), ("T", "_")]) 43 | ffmpeg = startProcess("ffmpeg -timelimit 900 -f video4linux2 -framerate 25 -video_size 640x480 -i /dev/video1 -f alsa -i plughw:CameraB409241,0 -ar 22050 -ab 64k -strict experimental -acodec aac -vcodec mpeg4 -vb 20M -y /mnt/nimha/media/" & filename & ".mp4", options = {poEvalCommand}) 44 | 45 | elif status in ["disarmed", "armAway", "armHome"]: 46 | isRunning = false 47 | if running(ffmpeg): 48 | terminate(ffmpeg) 49 | if usbOn: 50 | discard execCmd("/home/pi/nimha/usbcontrol/usboff.sh") 51 | usbOn = off 52 | 53 | 54 | 55 | let s_mqttPathSub = "/usr/bin/mosquitto_sub" 56 | let s_mqttPassword = "secretPassword" 57 | let s_clientName = "secretUsername" 58 | let s_mqttIp = "ip" 59 | let s_mqttPort = "8883" 60 | let s_topic = "alarminfo" 61 | 62 | proc mosquittoSub() = 63 | var mqttProcess = startProcess(s_mqttPathSub & " -v -t " & s_topic & " -u " & s_clientName & " -P " & s_mqttPassword & " -h " & s_mqttIp & " -p " & s_mqttPort, options = {poEvalCommand}) 64 | 65 | while running(mqttProcess): 66 | asyncCheck mqttParser(readLine(outputStream(mqttProcess))) 67 | 68 | 69 | mosquittoSub() 70 | quit() -------------------------------------------------------------------------------- /nimha.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import os 4 | import osproc 5 | import parsecfg 6 | import re 7 | import sequtils 8 | import strutils 9 | import tables 10 | 11 | import nimhapkg/resources/utils/common 12 | import nimhapkg/resources/users/user_add 13 | import nimhapkg/resources/database/database 14 | 15 | # Import procs to generate database tables 16 | import nimhapkg/resources/database/modules/alarm_database 17 | import nimhapkg/resources/database/modules/cron_database 18 | import nimhapkg/resources/database/modules/mail_database 19 | import nimhapkg/resources/database/modules/filestream_database 20 | import nimhapkg/resources/database/modules/mqtt_database 21 | import nimhapkg/resources/database/modules/os_database 22 | import nimhapkg/resources/database/modules/owntracks_database 23 | import nimhapkg/resources/database/modules/pushbullet_database 24 | import nimhapkg/resources/database/modules/rpi_database 25 | import nimhapkg/resources/database/modules/rss_database 26 | import nimhapkg/resources/database/modules/xiaomi_database 27 | import nimhapkg/modules/web/web_certs 28 | 29 | const moduleNames = ["websocket", "gateway_ws", "gateway", "webinterface", "cron", 30 | "xiaomilistener"] 31 | 32 | var runInLoop = true 33 | 34 | var modules = initTable[string, Process]() 35 | 36 | let modulesDir = 37 | when defined(dev) or not defined(systemInstall): 38 | getAppDir() / "nimhapkg/mainmodules/" 39 | else: 40 | #installpath 41 | "/var/lib/nimha/mainmodules" 42 | 43 | let dict = loadConf("") 44 | 45 | 46 | proc stop_and_quit() {.noconv.} = 47 | runInLoop = false 48 | for name, p in modules.pairs: 49 | echo "Stopping " & name 50 | kill(p) 51 | echo "Program quitted." 52 | quit() 53 | 54 | setControlCHook(stop_and_quit) 55 | 56 | 57 | proc secretCfg() = 58 | ## Check if config file exists 59 | 60 | when defined(dev) or not defined(systemInstall): 61 | let secretFn = getAppDir() / "config/nimha_dev.cfg" 62 | if not fileExists(secretFn): 63 | copyFile(getAppDir() / "config/nimha_default.cfg", secretFn) 64 | echo "\nThe config file has been generated at " & secretFn & ". Please fill in your data\n" 65 | else: 66 | if not fileExists("/etc/nimha/nimha.cfg"): 67 | echo "\nConfig file /etc/nimha/nimha.cfg does not exists\n" 68 | quit(0) 69 | 70 | proc updateJsFile() = 71 | ## Updates the JS file with Websocket details from the config file 72 | 73 | let wsAddressTo = "var wsAddress = \"" & dict.getSectionValue("Websocket","wsAddress") & "\"" 74 | let wsProtocolTo = "var wsProtocol = \"" & dict.getSectionValue("Websocket","wsProtocol") & "\"" 75 | let wsPortTo = "var wsPort = \"" & dict.getSectionValue("Websocket","wsPort") & "\"" 76 | 77 | let wsAddressFrom = "var wsAddress = \"127.0.0.1\"" 78 | let wsProtocolFrom = "var wsProtocol = \"ws\"" 79 | let wsPortFrom = "var wsPort = \"25437\"" 80 | 81 | #installpath 82 | const persistent_dir = "/var/lib/nimha" 83 | let fn = 84 | when defined(dev) or not defined(systemInstall): 85 | getAppDir() / "public/js/script.js" 86 | else: 87 | persistent_dir / "public/js/script.js" 88 | 89 | fn.writeFile fn.readFile.replace(re("var wsAddress = \".*\""), wsAddressTo) 90 | fn.writeFile fn.readFile.replace(re("var wsProtocol = \".*\""), wsProtocolTo) 91 | fn.writeFile fn.readFile.replace(re("var wsPort = \".*\""), wsPortTo) 92 | 93 | echo "Javascript: File updated with websocket connection details\n" 94 | 95 | 96 | proc checkMosquittoBroker() = 97 | ## Check is the path to Mosquitto broker exists else quit 98 | 99 | var mosquitto: File 100 | if not mosquitto.open(dict.getSectionValue("MQTT", "mqttPathSub")): 101 | echo "\n\nMosquitto broker: Error in path. No file found at " & dict.getSectionValue("MQTT","mqttPathSub") & "\n" 102 | quit() 103 | 104 | 105 | proc createDbTables() = 106 | ## Create all tables 107 | ## 108 | ## To be macro based 109 | 110 | var db = conn() 111 | 112 | var dbAlarm = conn("dbAlarm.db") 113 | var dbCron = conn("dbCron.db") 114 | var dbFile = conn("dbFile.db") 115 | var dbMail = conn("dbMail.db") 116 | var dbMqtt = conn("dbMqtt.db") 117 | var dbOs = conn("dbOs.db") 118 | var dbOwntracks = conn("dbOwntracks.db") 119 | var dbPushbullet = conn("dbPushbullet.db") 120 | var dbRpi = conn("dbRpi.db") 121 | var dbRss = conn("dbRss.db") 122 | var dbXiaomi = conn("dbXiaomi.db") 123 | var dbWeb = conn("dbWeb.db") 124 | 125 | alarmDatabase(dbAlarm) 126 | mailDatabase(dbMail) 127 | osDatabase(dbOs) 128 | owntracksDatabase(dbOwntracks) 129 | pushbulletDatabase(dbPushbullet) 130 | rssDatabase(dbRss) 131 | xiaomiDatabase(dbXiaomi) 132 | cronDatabase(dbCron) 133 | filestreamDatabase(dbFile) 134 | mqttDatabase(dbMqtt) 135 | rpiDatabase(dbRpi) 136 | certDatabase(dbWeb) 137 | 138 | 139 | proc spawnModule(name: string) = 140 | let fn = modulesDir / "nimha_" & name 141 | let default_sandbox_cmd = dict.getSectionValue("Home", "default_sandbox") 142 | if default_sandbox_cmd == "": 143 | echo "Warning: running modules without sandbox" 144 | let cmdline = 145 | if default_sandbox_cmd == "": 146 | fn 147 | else: 148 | default_sandbox_cmd & " " & fn 149 | let exe = cmdline.splitWhitespace()[0] 150 | let args = cmdline.splitWhitespace()[1..^1] 151 | echo "Spawning " & name & " as " & cmdline 152 | let p = startProcess(exe, args=args, options = {poParentStreams}) 153 | modules[name] = p 154 | 155 | proc launcherActivated() = 156 | ## Executing the main-program in a loop. 157 | 158 | # Add an admin user 159 | if "newuser" in commandLineParams(): 160 | createAdminUser(commandLineParams()) 161 | 162 | echo "\nNim Home Assistant: Starting launcher" 163 | echo " .. please wait\n" 164 | 165 | for name in moduleNames: 166 | spawnModule(name) 167 | sleep(2000) 168 | 169 | echo "\n .. waiting time over" 170 | echo "Nim Home Assistant: Launcher initialized\n" 171 | 172 | while runInLoop: 173 | 174 | sleep(3000) 175 | 176 | for name, p in modules.pairs: 177 | if not running(p): 178 | echo name & " exited." 179 | if name == "websocket": 180 | kill(modules["gateway_ws"]) 181 | spawnModule(name) 182 | sleep(2000) 183 | 184 | echo "Nim Home Assistant: Quitted" 185 | 186 | proc compileModule(devC, modulename: string) = 187 | ## Compile a module using Nim 188 | echo "compiling " & modulename 189 | let nimblepath = getNimbleCache() / "pkgs" 190 | let fn = modulesDir / modulename & ".nim" 191 | if not fileExists(fn): 192 | echo "ERROR: " & fn & " not found" 193 | quit(1) 194 | let cmd = "nim c --NimblePath:" & nimblepath & " -d:ssl " & devC & " " & fn 195 | echo "running " & cmd 196 | if execCmd(cmd) == 0: 197 | echo modulename & " compiling done" 198 | else: 199 | echo "An error has occurred compiling " & modulename 200 | # TODO: handle broken modules 201 | quit(1) 202 | 203 | 204 | proc compileIt() = 205 | echo "Checking if runners in $# need compiling" % modulesDir 206 | 207 | var devC = "" 208 | when defined(dev): 209 | devC.add(" -d:dev ") 210 | when defined(devmailon): 211 | devC.add(" -d:devmailon ") 212 | when defined(logoutput): 213 | devC.add(" -d:logoutput " ) 214 | 215 | when not defined(dev) and defined(systemInstall): 216 | block: 217 | #installpath 218 | echo "Setting up Nimble and required dependencies" 219 | let cmd = "nimble install -y --verbose websocket bcrypt multicast nimcrypto xiaomi jester recaptcha --nimbleDir:" & getNimbleCache() 220 | echo cmd 221 | if execCmd(cmd) != 0: 222 | echo "Error running nimble" 223 | quit(1) 224 | 225 | # Websocket 226 | if not fileExists(modulesDir / "nimha_websocket") or defined(rc) or defined(rcwss): 227 | compileModule(devC, "nimha_websocket") 228 | 229 | # Cron jobs 230 | if not fileExists(modulesDir / "nimha_cron") or defined(rc) or defined(rccron): 231 | compileModule(devC, "nimha_cron") 232 | 233 | # Webinterface 234 | if not fileExists(modulesDir / "nimha_webinterface") or defined(rc) or defined(rcwebinterface): 235 | compileModule(devC, "nimha_webinterface") 236 | 237 | # Gateway websocket 238 | if not fileExists(modulesDir / "nimha_gateway_ws") or defined(rc) or defined(rcgatewayws): 239 | compileModule(devC, "nimha_gateway_ws") 240 | 241 | # Gateway 242 | if not fileExists(modulesDir / "nimha_gateway") or defined(rc) or defined(rcgateway): 243 | compileModule(devC, "nimha_gateway") 244 | 245 | # Xiaomi listener 246 | if not fileExists(modulesDir / "nimha_xiaomilistener") or defined(rc) or defined(rcxlistener): 247 | compileModule(devC, "nimha_xiaomilistener") 248 | 249 | 250 | proc requirements() = 251 | when defined(dev) or not defined(systemInstall): 252 | discard existsOrCreateDir(getTmpDir()) 253 | secretCfg() 254 | updateJsFile() 255 | checkMosquittoBroker() 256 | createDbTables() 257 | compileIt() 258 | launcherActivated() 259 | 260 | proc main() = 261 | requirements() 262 | 263 | when isMainModule: 264 | main() 265 | -------------------------------------------------------------------------------- /nimha.nim.cfg: -------------------------------------------------------------------------------- 1 | -d:ssl -------------------------------------------------------------------------------- /nimha.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.4.5" 3 | author = "Thomas T. Jarløv (https://github.com/ThomasTJdev)" 4 | description = "Nim Home Assistant" 5 | license = "GPLv3" 6 | bin = @["nimha"] 7 | skipDirs = @["private"] 8 | installDirs = @["config", "public", "nimhapkg"] 9 | 10 | 11 | 12 | # Dependencies 13 | requires "nim >= 1.0.4" 14 | requires "jester 0.4.3" 15 | requires "httpbeast 0.2.2" 16 | requires "recaptcha >= 1.0.2" 17 | requires "bcrypt >= 0.2.1" 18 | requires "multicast 0.1.4" 19 | requires "websocket 0.4.1" 20 | requires "wiringPiNim >= 0.1.0" 21 | requires "xiaomi >= 0.1.4" 22 | 23 | 24 | import distros 25 | 26 | task setup, "Setup started": 27 | if detectOs(Windows): 28 | echo "Cannot run on Windows" 29 | quit() 30 | 31 | before install: 32 | setupTask() 33 | 34 | after install: 35 | echo "Development: Copy config/nimha_default.cfg to config/nimha_dev.cfg\n" 36 | echo "Production: Copy config/nimha_default.cfg to /etc/nimha/nimha.cfg\n" -------------------------------------------------------------------------------- /nimhapkg/mainmodules/nimha_cron.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | 4 | import parsecfg, db_sqlite, strutils, asyncdispatch, json, times 5 | from os import sleep 6 | 7 | import ../resources/database/database 8 | import ../modules/mail/mail 9 | import ../modules/os/os_utils 10 | import ../resources/mqtt/mqtt_templates 11 | import ../modules/pushbullet/pushbullet 12 | when defined(rpi): 13 | import ../modules/rpi/rpi_utils 14 | import ../modules/xiaomi/xiaomi_utils 15 | import ../resources/utils/log_utils 16 | 17 | 18 | var db = conn("dbCron.db") 19 | 20 | 21 | 22 | #[ 23 | type 24 | Cronjob = ref object 25 | element: string 26 | jobid: string 27 | time: string 28 | active: bool 29 | 30 | Cron = ref object 31 | cronjobs: seq[Cronjob] 32 | 33 | 34 | var cron = Cron(cronjobs: @[]) 35 | 36 | 37 | proc newCronjob(element, jobid, time, active: string): Cronjob = 38 | ## Generate new cronjob 39 | 40 | return Cronjob( 41 | element: element, 42 | jobid: jobid, 43 | time: time, 44 | active: parseBool(active) 45 | ) 46 | 47 | 48 | proc cronUpdateJobs() = 49 | ## Update the cronjob container 50 | 51 | let cronActions = getAllRows(db, sql"SELECT element, action_ref, time, active FROM cron_actions") 52 | 53 | var newCronjobs: seq[Cronjob] = @[] 54 | for row in cronActions: 55 | if row[3] == "false": 56 | continue 57 | 58 | newCronjobs.add(newCronjob(row[0], row[1], row[2], row[3])) 59 | 60 | cron.cronjobs = newCronjobs 61 | 62 | 63 | proc cronJobRun() {.async.} = 64 | ## Run the cron jobs 65 | 66 | let cronTime = format(local(now()), "HH:mm") 67 | echo format(local(now()), "HH:mm:ss") 68 | if cron.cronjobs.len() > 0: 69 | sleep(5000) 70 | var newCronjobs: seq[Cronjob] = @[] 71 | for cronitem in cron.cronjobs: 72 | if not cronitem.active: 73 | continue 74 | 75 | # Add job to seq 76 | newCronjobs.add(cronitem) 77 | 78 | # Check time. If hour and minut fits (24H) then go 79 | if cronTime != cronitem.time: 80 | continue 81 | 82 | cronitem.active = false 83 | 84 | case cronitem.element 85 | of "pushbullet": 86 | echo "push" 87 | pushbulletSendDb(cronitem.jobid) 88 | 89 | of "mail": 90 | sendMailDb(cronitem.jobid) 91 | 92 | of "xiaomi": 93 | asyncCheck xiaomiWriteTemplate(db, cronitem.jobid) 94 | 95 | else: 96 | discard 97 | 98 | # Update seq - exclude non active 99 | cron.cronjobs = newCronjobs 100 | ]# 101 | 102 | 103 | proc cronJobRun(time: string) = 104 | ## Run the cron jobs 105 | 106 | let cronActions = getAllRows(db, sql"SELECT element, action_ref FROM cron_actions WHERE active = ? AND time = ?", "true", time) 107 | 108 | if cronActions.len() == 0: 109 | return 110 | 111 | logit("cron", "DEBUG", "Executing cron activities. Total number: " & $cronActions.len()) 112 | 113 | for row in cronActions: 114 | 115 | case row[0] 116 | of "pushbullet": 117 | pushbulletSendDb(row[1]) 118 | 119 | of "mail": 120 | sendMailDb(row[1]) 121 | 122 | of "os": 123 | asyncCheck osRunTemplate(row[1]) 124 | 125 | of "mqtt": 126 | mqttActionSendDb(row[1]) 127 | 128 | of "rpi": 129 | when defined(rpi): 130 | discard rpiAction(row[1]) 131 | 132 | of "xiaomi": 133 | ## TODO: This leads to error sometimes 134 | xiaomiWriteTemplate(row[1]) 135 | 136 | else: 137 | discard 138 | 139 | 140 | 141 | proc cronJob() = 142 | ## Run the main cron job 143 | ## 144 | ## Check every minute if an action is required 145 | ## 146 | ## Currently using sleep - should it be sleepAsync 147 | ## and moved inside another main module? SleepAsync 148 | ## messes up the RPi CPU 149 | 150 | logit("cron", "INFO", "Cron main started") 151 | 152 | 153 | while true: 154 | 155 | cronJobRun(format(local(now()), "HH:mm")) 156 | 157 | # Wait before next cron check 158 | # Get current seconds, subtract from 60 to 159 | # get next starting minute and sleep. This 160 | # will accept that we are blocking using sleep 161 | 162 | let sleepTime = 60 - parseInt(format(local(now()), "ss")) 163 | 164 | sleep(sleepTime * 1000) 165 | 166 | 167 | 168 | when isMainModule: 169 | cronJob() 170 | runForever() 171 | -------------------------------------------------------------------------------- /nimhapkg/mainmodules/nimha_gateway.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import asyncdispatch 4 | import osproc 5 | import strutils 6 | import streams 7 | 8 | import ../modules/alarm/alarm 9 | import ../resources/mqtt/mqtt_func 10 | import ../modules/os/os_utils 11 | import ../modules/owntracks/owntracks 12 | import ../modules/pushbullet/pushbullet 13 | when defined(rpi): 14 | import ../modules/rpi/rpi_utils 15 | import ../modules/rss/rss_reader 16 | import ../modules/web/web_utils 17 | import ../modules/xiaomi/xiaomi_utils 18 | import ../resources/utils/log_utils 19 | 20 | 21 | proc mosquittoParse(payload: string) {.async.} = 22 | ## Parse the raw output from Mosquitto sub 23 | 24 | let topicName = payload.split(" ")[0] 25 | let message = payload.replace(topicName & " ", "") 26 | 27 | if topicName notin ["xiaomi"]: 28 | logit("gateway", "DEBUG", "Topic: " & topicName & " - Payload: " & message) 29 | 30 | if topicName == "alarm": 31 | asyncCheck alarmParseMqtt(message) 32 | 33 | elif topicName == "osstats": 34 | asyncCheck osParseMqtt(message) 35 | 36 | elif topicName == "rss": 37 | asyncCheck rssParseMqtt(message) 38 | 39 | elif topicName == "rpi": 40 | when defined(rpi): 41 | asyncCheck rpiParseMqtt(message) 42 | 43 | elif topicName == "pushbullet": 44 | pushbulletParseMqtt(message) 45 | 46 | elif topicName == "webutils": 47 | asyncCheck webParseMqtt(message) 48 | 49 | elif topicName == "xiaomi": 50 | asyncCheck xiaomiParseMqtt(message, alarm[0]) 51 | 52 | elif topicName.substr(0, 8) == "owntracks": 53 | asyncCheck owntracksParseMqtt(message, topicName) 54 | 55 | elif topicName == "history": 56 | # Add history to var and every nth update database with it. 57 | # SQLite can not cope with all the data, which results in 58 | # database is locked, and history elements are discarded 59 | discard 60 | 61 | else: 62 | discard 63 | 64 | 65 | 66 | var mqttProcess: Process 67 | 68 | proc mosquittoSub() = 69 | ## Start Mosquitto sub listening on # 70 | 71 | logit("gateway", "INFO", "Mosquitto GATEWAY started") 72 | 73 | mqttProcess = startProcess(s_mqttPathSub & " -v -t \"#\" -u nimhawss -P " & s_mqttPassword & " -h " & s_mqttIp & " -p " & s_mqttPort, options = {poEvalCommand}) 74 | 75 | while running(mqttProcess): 76 | asyncCheck mosquittoParse(readLine(outputStream(mqttProcess))) 77 | 78 | 79 | mosquittoSub() 80 | quit() -------------------------------------------------------------------------------- /nimhapkg/mainmodules/nimha_gateway_ws.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | 4 | import asyncdispatch 5 | import osproc 6 | import parsecfg 7 | import strutils 8 | import streams 9 | import websocket 10 | 11 | from os import sleep, getAppDir 12 | 13 | import ../resources/mqtt/mqtt_func 14 | import ../resources/utils/log_utils 15 | import ../resources/utils/common 16 | 17 | 18 | var ws: AsyncWebSocket 19 | 20 | var localhostKey = "" 21 | 22 | 23 | proc setupWs() = 24 | ## Setup connection to WS 25 | 26 | logit("WSgateway", "INFO", "Mosquitto Client Websocket connection started") 27 | 28 | ws = waitFor newAsyncWebsocketClient("127.0.0.1", Port(25437), path = "/", protocols = @["nimha"]) 29 | 30 | # Set WSS key for communication without verification on 127.0.0.1 31 | let dict = loadConf("gateway_ws") 32 | localhostKey = dict.getSectionValue("Websocket", "wsLocalKey") 33 | 34 | 35 | proc mosquittoParse(payload: string) {.async.} = 36 | ## Parse the raw output from Mosquitto sub 37 | 38 | let topicName = payload.split(" ")[0] 39 | let message = payload.replace(topicName & " ", "") 40 | 41 | logit("WSgateway", "DEBUG", "Payload: " & message) 42 | 43 | if isNil(ws): 44 | setupWs() 45 | 46 | if not isNil(ws): 47 | waitFor ws.sendText(localhostKey & message) 48 | else: 49 | logit("WSgateway", "ERROR", "127.0.0.1 client websocket not connected") 50 | 51 | 52 | proc mosquittoSub() = 53 | ## Start Mosquitto sub listening on # 54 | 55 | logit("WSgateway", "INFO", "Mosquitto WS started") 56 | 57 | # TODO: this leaks the password in the process name 58 | # https://github.com/eclipse/mosquitto/issues/1141 59 | var cmd = s_mqttPathSub & " -v -t \"wss/to\" -u nimhagate -p " & s_mqttPort 60 | if s_mqttPassword != "": 61 | cmd.add " -P " & s_mqttPassword 62 | if s_mqttIp != "": 63 | cmd.add " -h " & s_mqttIp 64 | 65 | let mqttProcess = startProcess(cmd, options = {poEvalCommand}) 66 | 67 | while running(mqttProcess): 68 | asyncCheck mosquittoParse(readLine(outputStream(mqttProcess))) 69 | 70 | 71 | setupWs() 72 | mosquittoSub() 73 | quit() 74 | -------------------------------------------------------------------------------- /nimhapkg/mainmodules/nimha_websocket.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | ## Websocket for communicating with browser. 4 | ## Data is delivered from a websocket client 5 | ## on 127.0.0.1. Data is sent within a static 6 | ## loop every 1,5 second to the browser. 7 | 8 | import asyncdispatch 9 | import asynchttpserver 10 | import asyncnet 11 | import db_sqlite 12 | import json 13 | import os 14 | import osproc 15 | import parsecfg 16 | import random 17 | import re 18 | import sequtils 19 | import strutils 20 | import streams 21 | import times 22 | import websocket 23 | 24 | import ../resources/database/database 25 | import ../resources/mqtt/mqtt_func 26 | import ../resources/users/password 27 | import ../resources/utils/dates 28 | import ../resources/utils/log_utils 29 | import ../resources/utils/common 30 | 31 | 32 | type 33 | Client = ref object 34 | ws: AsyncWebSocket 35 | socket: AsyncSocket 36 | connected: bool 37 | hostname: string 38 | lastMessage: float 39 | history: string 40 | wsSessionStart: int 41 | key: string 42 | userStatus: string 43 | 44 | Server = ref object 45 | clients: seq[Client] 46 | needsUpdate: bool 47 | 48 | #Users = tuple[userid: string, ip: string, key: string, status: string] 49 | 50 | # Contains the clients 51 | var server = Server(clients: @[]) 52 | 53 | # Contains the user data 54 | #var users: seq[Users] = @[] 55 | 56 | 57 | # Set key for communication without verification on 127.0.0.1 58 | const rlAscii = toSeq('a'..'z') 59 | const rhAscii = toSeq('A'..'Z') 60 | const rDigit = toSeq('0'..'9') 61 | randomize() 62 | var localhostKey = "" 63 | for i in countUp(1, 62): 64 | case rand(2) 65 | of 0: localhostKey.add(rand(rlAscii)) 66 | of 1: localhostKey.add(rand(rhAscii)) 67 | of 2: localhostKey.add(rand(rDigit)) 68 | else: discard 69 | let localhostKeyLen = localhostKey.len() 70 | let configFile = when defined(dev) or not defined(systemInstall): replace(getAppDir(), "/nimhapkg/mainmodules", "") & "/config/nimha_dev.cfg" else: "/etc/nimha/nimha.cfg" 71 | for fn in [configFile]: 72 | # When using setSectionKey formatting and comments are deleted.. 73 | fn.writeFile fn.readFile.replace(re("wsLocalKey = \".*\""), "wsLocalKey = \"" & localhostKey & "\"") 74 | 75 | 76 | 77 | var db = conn() 78 | 79 | 80 | 81 | var msgHi: seq[string] = @[] 82 | 83 | proc wsmsgMessages*(): string = 84 | 85 | if msgHi.len() == 0: 86 | return "" 87 | 88 | var json = "" 89 | for element in msgHi: 90 | if json != "": 91 | json.add("," & element) 92 | else: 93 | json.add(element) 94 | msgHi = @[] 95 | 96 | return "{\"handler\": \"history\", \"data\" :[" & json & "]}" 97 | 98 | #[ 99 | proc loadUsers() = 100 | ## Load the user data 101 | 102 | users = @[] 103 | 104 | let allUsers = getAllRows(db, sql"SELECT session.userid, session.password, session.salt, person.status FROM session LEFT JOIN person ON person.id = session.userid") 105 | for row in allUsers: 106 | users.add((userid: row[0], ip: row[1], key: row[2], status: row[3])) 107 | 108 | 109 | template checkUserAccess(hostname, key, userID: string) = 110 | ## Check if the user (requester) has access 111 | 112 | var access = false 113 | 114 | for user in users: 115 | if user[0] == userID and hostname == user[1] and key == user[2] and status in ["Admin", "Moderator", "Normal"]: 116 | access = true 117 | break 118 | 119 | if not access: 120 | break 121 | ]# 122 | 123 | 124 | proc newClient(ws: AsyncWebSocket, socket: AsyncSocket, hostname: string): Client = 125 | ## Generate the client 126 | 127 | return Client( 128 | ws: ws, 129 | socket: socket, 130 | connected: true, 131 | hostname: hostname, 132 | lastMessage: epochTime(), 133 | history: "" 134 | ) 135 | 136 | 137 | 138 | proc updateClientsNow() {.async.} = 139 | ## Updates clients list 140 | 141 | var newClients: seq[Client] = @[] 142 | for client in server.clients: 143 | if not client.connected: 144 | continue 145 | newClients.add(client) 146 | 147 | # Overwrite with new list containing only connected clients. 148 | server.clients = newClients 149 | server.needsUpdate = false 150 | 151 | 152 | 153 | proc wsConnectedUsers(): string = 154 | ## Generate JSON for active users 155 | 156 | var users = "" 157 | 158 | for client in server.clients: 159 | if not client.connected: 160 | continue 161 | 162 | if users != "": 163 | users.add(",") 164 | users.add("{\"hostname\": \"" & client.hostname & "\", \"lastMessage\": \"" & epochDate($toInt(client.lastMessage), "DD MMM HH:mm") & "\"}") 165 | 166 | var json = "{\"handler\": \"action\", \"element\": \"websocket\", \"value\": \"connectedusers\", \"users\": [" & users & "]}" 167 | 168 | return json 169 | 170 | 171 | 172 | proc wsSendConnectedUsers() {.async.} = 173 | ## Send JSON with connected users 174 | 175 | msgHi.add(wsConnectedUsers()) 176 | 177 | 178 | 179 | proc pong(server: Server) {.async.} = 180 | ## Send JSON to connected users every nth second. 181 | ## This is used for to ensure, that the socket 182 | ## can follow the pace with messages. Without 183 | ## this it crashes. 184 | 185 | var updateClients = false 186 | while true: 187 | let json = wsmsgMessages() 188 | 189 | if server.clients.len() != 0 and json != "": 190 | for client in server.clients: 191 | 192 | try: 193 | if not client.connected: 194 | continue 195 | await client.ws.sendText(json) 196 | 197 | except: 198 | echo("WSS: Pong msg failed") 199 | client.connected = false 200 | updateClients = true 201 | continue 202 | 203 | if updateClients: 204 | await updateClientsNow() 205 | updateClients = false 206 | 207 | await sleepAsync(1500) 208 | 209 | 210 | template js(json: string): JsonNode = 211 | ## Avoid error in parsing JSON 212 | 213 | try: 214 | parseJson(data) 215 | except: 216 | parseJson("{}") 217 | 218 | 219 | template jn(json: JsonNode, data: string): string = 220 | ## Avoid error in parsing JSON 221 | 222 | try: 223 | json[data].getStr() 224 | except: 225 | "" 226 | 227 | proc onRequest*(req: Request) {.async,gcsafe.} = 228 | ## Per request start listening on socket and 229 | ## update client list 230 | ## 231 | ## This proc is not gcsafe, but removing pragma 232 | ## gcsafe corrupts waitFor serve() 233 | 234 | let (ws, error) = await verifyWebsocketRequest(req, "nimha") 235 | 236 | if ws.isNil: 237 | logit("websocket", "ERROR", "WS negotiation failed") 238 | await req.respond(Http400, "WebSocket negotiation failed: " & error) 239 | 240 | req.client.close() 241 | 242 | else: 243 | var hostname = req.hostname 244 | if req.headers.hasKey("x-forwarded-for"): 245 | hostname = req.headers["x-forwarded-for"] 246 | logit("websocket", "INFO", "Connection from: " & hostname) 247 | 248 | server.clients.add(newClient(ws, req.client, hostname)) 249 | var myClient = server.clients[^1] 250 | asyncCheck wsSendConnectedUsers() 251 | 252 | when defined(dev): 253 | logit("websocket", "INFO", "Client connected from: " & hostname) 254 | logit("websocket", "INFO", "Active users: " & $server.clients.len()) 255 | 256 | while true: 257 | 258 | try: 259 | let (opcode, data) = await myClient.ws.readData() 260 | #let (opcode, data) = await readData(myClient.socket, true) 261 | 262 | if myClient.hostname == "127.0.0.1" and data.substr(0, localhostKeyLen-1) == localhostKey: 263 | logit("websocket", "DEBUG", "127.0.0.1 message: " & data.substr(localhostKeyLen, data.len())) 264 | msgHi.add(data.substr(localhostKeyLen, data.len())) 265 | 266 | else: 267 | 268 | myClient.lastMessage = epochTime() 269 | 270 | case opcode 271 | of Opcode.Text: 272 | let js = js(data) 273 | 274 | # To be removed 275 | if js == parseJson("{}"): 276 | logit("websocket", "ERROR", "Parsing JSON failed") 277 | return 278 | 279 | # Check user access. Current check is set to every 5 minutes (300s) - if user account is deleted, connection will be terminated 280 | if myClient.wsSessionStart + 300 < toInt(epochTime()) or myClient.key == "" or myClient.userStatus == "": 281 | let key = jn(js, "key") 282 | let userid = getValue(db, sql"SELECT userid FROM session WHERE ip = ? AND key = ? AND userid = ?", hostname, key, jn(js, "userid")) 283 | myClient.userStatus = getValue(db, sql"SELECT status FROM person WHERE id = ?", userid) 284 | myClient.wsSessionStart = toInt(epochTime()) 285 | myClient.key = key 286 | 287 | if myClient.userStatus notin ["Admin", "Moderator", "Normal"]: 288 | logit("websocket", "ERROR", "Client messed up in sid and userid") 289 | myClient.connected = false 290 | asyncCheck updateClientsNow() 291 | break 292 | 293 | # Respond 294 | await myClient.ws.sendText("{\"event\": \"received\"}") 295 | 296 | if data == "ping": 297 | discard 298 | else: 299 | logit("websocket", "DEBUG", "Client message: " & data) 300 | var dataReady = js 301 | dataReady["hostname"] = %* hostname 302 | asyncCheck mqttSendAsync("wss", jn(parseJson(data), "element"), $dataReady) 303 | 304 | of Opcode.Close: 305 | let (closeCode, reason) = extractCloseData(data) 306 | logit("websocket", "INFO", "Socket went away, close code: " & $closeCode & ", reason: " & $reason) 307 | myClient.connected = false 308 | asyncCheck updateClientsNow() 309 | break 310 | else: 311 | let (closeCode, reason) = extractCloseData(data) 312 | logit("websocket", "INFO", "Case else, close code: " & $closeCode & ", reason: " & $reason) 313 | myClient.connected = false 314 | asyncCheck updateClientsNow() 315 | 316 | 317 | except: 318 | logit("websocket", "ERROR", "Encountered exception: " & getCurrentExceptionMsg()) 319 | myClient.connected = false 320 | asyncCheck updateClientsNow() 321 | break 322 | 323 | 324 | myClient.connected = false 325 | myClient.key = "" 326 | myClient.userStatus = "" 327 | try: 328 | await myClient.ws.close() 329 | logit("websocket", "INFO", ".. socket went away.") 330 | except: 331 | logit("websocket", "INFO", ".. socket went away but couldn't close it") 332 | 333 | 334 | 335 | 336 | when isMainModule: 337 | logit("websocket", "INFO", "Websocket main started") 338 | 339 | asyncCheck pong(server) 340 | 341 | 342 | # Bad solution.. !!! 343 | var httpServer: AsyncHttpServer 344 | try: 345 | httpServer = newAsyncHttpServer() 346 | asyncCheck serve(httpServer, Port(25437), onRequest) 347 | 348 | runForever() 349 | 350 | except IOError: 351 | logit("websocket", "ERROR", "IOError.. damn") 352 | 353 | 354 | close(httpServer) 355 | -------------------------------------------------------------------------------- /nimhapkg/mainmodules/nimha_xiaomilistener.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | import xiaomi 3 | 4 | import ../resources/mqtt/mqtt_func 5 | import ../resources/utils/log_utils 6 | 7 | 8 | proc xiaomiListen() = 9 | ## Listen for Xiaomi 10 | 11 | xiaomiConnect() 12 | 13 | while true: 14 | mqttSend("xiaomilisten", "xiaomi", xiaomiReadMessage()) 15 | 16 | xiaomiDisconnect() 17 | 18 | 19 | when isMainModule: 20 | logit("xiaomi", "INFO", "Xiaomi multicast listener is started") 21 | xiaomiListen() -------------------------------------------------------------------------------- /nimhapkg/modules/alarm/alarm.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | # 3 | ## Alarm module 4 | ## 5 | ## Available alarm status: 6 | ## - disarmed 7 | ## - armAway 8 | ## - armHome 9 | ## - triggered 10 | ## - ringing 11 | 12 | import parsecfg, db_sqlite, strutils, asyncdispatch, json, times 13 | 14 | import ../../resources/database/database 15 | import ../../resources/mqtt/mqtt_func 16 | import ../../resources/mqtt/mqtt_templates 17 | import ../../resources/users/password 18 | import ../../resources/utils/log_utils 19 | import ../mail/mail 20 | import ../os/os_utils 21 | import ../pushbullet/pushbullet 22 | when defined(rpi): 23 | import ../rpi/rpi_utils 24 | import ../xiaomi/xiaomi_utils 25 | 26 | 27 | 28 | type 29 | Alarm = tuple[status: string, armtime: string, countdownTime: string, armedtime: string] 30 | AlarmPasswords = tuple[userid: string, password: string, salt: string] 31 | AlarmActions = tuple[id: string, action: string, action_ref: string, alarmstate: string] 32 | 33 | var alarm*: Alarm 34 | var alarmPasswords: seq[AlarmPasswords] = @[] 35 | var alarmActions: seq[AlarmActions] = @[] 36 | 37 | 38 | var db = conn() 39 | var dbAlarm = conn("dbAlarm.db") 40 | 41 | 42 | proc alarmLoadStatus() = 43 | ## Load alarm status 44 | 45 | let aStatus = getValue(dbAlarm, sql"SELECT status FROM alarm WHERE id = ?", "1") 46 | let aArmtime = getValue(dbAlarm, sql"SELECT value FROM alarm_settings WHERE element = ?", "armtime") 47 | let aCountdown = getValue(dbAlarm, sql"SELECT value FROM alarm_settings WHERE element = ?", "countdown") 48 | 49 | alarm = (status: aStatus, armtime: aArmtime, countdownTime: aCountdown, armedtime: $toInt(epochTime())) 50 | 51 | 52 | proc alarmLoadPasswords() = 53 | ## Load the alarm passwords 54 | 55 | alarmPasswords = @[] 56 | 57 | let allPasswords = getAllRows(dbAlarm, sql"SELECT userid, password, salt FROM alarm_password ") 58 | for row in allPasswords: 59 | alarmPasswords.add((userid: row[0], password: row[1], salt: row[2])) 60 | 61 | 62 | proc alarmLoadActions() = 63 | ## Load the alarm passwords 64 | 65 | alarmActions = @[] 66 | 67 | let allActions = getAllRows(dbAlarm, sql"SELECT id, action, action_ref, alarmstate FROM alarm_actions") 68 | for row in allActions: 69 | alarmActions.add((id: row[0], action: row[1], action_ref: row[2], alarmstate: row[3])) 70 | 71 | 72 | template jn(json: JsonNode, data: string): string = 73 | ## Avoid error in parsing JSON 74 | try: json[data].getStr() except: "" 75 | 76 | 77 | proc alarmAction() = 78 | ## Run the action based on the alarm state 79 | 80 | for action in alarmActions: 81 | if action[3] == alarm[0]: 82 | logit("alarm", "DEBUG", "alarmAction(): " & action[1] & " - id: " & action[2]) 83 | 84 | case action[1] 85 | of "pushbullet": 86 | pushbulletSendDb(action[2]) 87 | of "mail": 88 | sendMailDb(action[2]) 89 | of "os": 90 | asyncCheck osRunTemplate(action[2]) 91 | of "mqtt": 92 | mqttActionSendDb(action[2]) 93 | of "rpi": 94 | when defined(rpi): 95 | discard rpiAction(action[2]) 96 | of "xiaomi": 97 | xiaomiWriteTemplate(action[2]) 98 | 99 | 100 | proc alarmSetStatus(newStatus, trigger, device: string, userID = "") = 101 | # Check that doors, windows, etc are ready 102 | 103 | asyncCheck mqttSendAsync("alarm", "alarminfo", "{\"action\": \"iotinfo\", \"element\": \"alarm\", \"status\": \"" & newStatus & "\", \"value\": \"\"}") 104 | 105 | alarm[0] = newStatus 106 | exec(dbAlarm, sql"UPDATE alarm SET status = ?", newStatus) 107 | 108 | if userID != "": 109 | discard tryExec(dbAlarm, sql"INSERT INTO alarm_history (status, trigger, device, userid) VALUES (?, ?, ?, ?)", newStatus, trigger, device, userID) 110 | else: 111 | discard tryExec(dbAlarm, sql"INSERT INTO alarm_history (status, trigger, device) VALUES (?, ?, ?)", newStatus, trigger, device) 112 | 113 | alarmAction() 114 | 115 | 116 | proc alarmRinging*(db: DbConn, trigger, device: string) = 117 | ## The alarm is ringing 118 | 119 | logit("alarm", "INFO", "alarmRinging(): Status = ringing") 120 | 121 | alarmSetStatus("ringing", trigger, device) 122 | 123 | mqttSend("alarm", "wss/to", "{\"handler\": \"action\", \"element\": \"alarm\", \"action\": \"setstatus\", \"value\": \"ringing\"}") 124 | 125 | 126 | proc alarmTriggered*(db: DbConn, trigger, device: string) = 127 | ## The alarm has been triggereds 128 | 129 | logit("alarm", "INFO", "alarmTriggered(): Status = triggered") 130 | 131 | # Check if the armtime is over 132 | let armTimeOver = parseInt(alarm[1]) + parseInt(alarm[3]) 133 | if armTimeOver > toInt(epochTime()): 134 | logit("alarm", "INFO", "alarmTriggered(): Triggered alarm cancelled to due to armtime") 135 | return 136 | 137 | else: 138 | logit("alarm", "INFO", "alarmTriggered(): Triggered alarm true - armtime done") 139 | 140 | # Change the alarm status 141 | #alarmSetStatus("triggered", trigger, device) 142 | 143 | # Send info about the alarm is triggered 144 | #mqttSend("alarm", "wss/to", "{\"handler\": \"action\", \"element\": \"alarm\", \"action\": \"setstatus\", \"value\": \"triggered\"}") 145 | 146 | ############ 147 | # Due to non-working async trigger countdown (sleepAsync), it's skipped at the moment 148 | ############ 149 | 150 | alarmRinging(db, trigger, device) 151 | 152 | 153 | proc alarmParseMqtt*(payload: string) {.async.} = 154 | ## Parse MQTT message 155 | 156 | let js = parseJson(payload) 157 | let action = jn(js, "action") 158 | 159 | if action == "adddevice": 160 | alarmLoadActions() 161 | 162 | elif action == "deletedevice": 163 | alarmLoadActions() 164 | 165 | elif action == "updatealarm": 166 | alarmLoadStatus() 167 | 168 | elif action == "updateuser": 169 | alarmLoadPasswords() 170 | 171 | elif action == "triggered" and alarm[0] in ["armAway", "armHome"]: 172 | alarmTriggered(db, jn(js, "value"), jn(js, "sid")) 173 | 174 | # Change alarm status as Admin, which require no code 175 | elif action == "activatenocode": 176 | let userID = jn(js, "userid") 177 | let key = jn(js, "key") 178 | 179 | let userIDcheck = getValue(db, sql"SELECT userid FROM session WHERE ip = ? AND userid = ? AND key = ?", jn(js, "hostname"), userID, key) 180 | let userStatus = getValue(db, sql"SELECT status FROM person WHERE id = ?", userIDcheck) 181 | 182 | if userStatus != "Admin": 183 | mqttSend("alarm", "wss/to", "{\"handler\": \"response\", \"value\": \"You cannot change the alarm status\", \"error\": \"true\"}") 184 | return 185 | 186 | let status = jn(js, "status") 187 | 188 | if status in ["armAway", "armHome"]: 189 | alarm[3] = $toInt(epochTime()) 190 | alarmSetStatus(status, "user", "", userID) 191 | 192 | elif status == "disarmed": 193 | alarm[3] = $toInt(epochTime()) 194 | alarmSetStatus(status, "user", "", userID) 195 | 196 | mqttSend("alarm", "wss/to", "{\"handler\": \"action\", \"element\": \"alarm\", \"action\": \"setstatus\", \"value\": \"" & status & "\"}") 197 | 198 | elif action == "activate": 199 | let userID = jn(js, "userid") 200 | var passOk = false 201 | 202 | # Check passwords 203 | if alarmPasswords.len() > 0: 204 | let passwordUser = jn(js, "password") 205 | 206 | for password in alarmPasswords: 207 | if userID == password[0]: 208 | if password[1] == makePassword(passwordUser, password[2], password[1]): 209 | passOk = true 210 | 211 | else: 212 | # If there's no password protection - accept 213 | passOk = true 214 | 215 | if not passOk: 216 | mqttSend("alarm", "wss/to", "{\"handler\": \"response\", \"value\": \"Wrong alarm password\", \"error\": \"true\"}") 217 | return 218 | 219 | let status = jn(js, "status") 220 | 221 | if status in ["armAway", "armHome"]: 222 | alarm[3] = $toInt(epochTime()) 223 | alarmSetStatus(status, "user", "", userID) 224 | 225 | elif status == "disarmed": 226 | alarm[3] = $toInt(epochTime()) 227 | alarmSetStatus(status, "user", "", userID) 228 | 229 | mqttSend("alarm", "wss/to", "{\"handler\": \"action\", \"element\": \"alarm\", \"action\": \"setstatus\", \"value\": \"" & status & "\"}") 230 | 231 | 232 | ## Load alarm data 233 | alarmLoadStatus() 234 | alarmLoadPasswords() 235 | alarmLoadActions() 236 | -------------------------------------------------------------------------------- /nimhapkg/modules/mail/mail.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import asyncdispatch 4 | import db_sqlite 5 | import smtp 6 | import strutils 7 | 8 | import ../../resources/database/database 9 | 10 | var db = conn() 11 | var dbMail = conn("dbMail.db") 12 | 13 | var smtpAddress = "" 14 | var smtpPort = "" 15 | var smtpFrom = "" 16 | var smtpUser = "" 17 | var smtpPassword = "" 18 | 19 | 20 | proc sendMailNow*(subject, message, recipient: string) {.async.} = 21 | ## Send the email through smtp 22 | 23 | when defined(dev): 24 | echo "Dev: Mail start" 25 | 26 | #when defined(dev) and not defined(devemailon): 27 | # echo "Dev is true, email is not send" 28 | # return 29 | const otherHeaders = @[("Content-Type", "text/html; charset=\"UTF-8\"")] 30 | 31 | var client = newAsyncSmtp(useSsl = true, debug = false) 32 | await client.connect(smtpAddress, Port(parseInt(smtpPort))) 33 | 34 | await client.auth(smtpUser, smtpPassword) 35 | 36 | let from_addr = smtpFrom 37 | let toList = @[recipient] 38 | 39 | var headers = otherHeaders 40 | headers.add(("From", from_addr)) 41 | 42 | let encoded = createMessage(subject, message, toList, @[], headers) 43 | 44 | try: 45 | echo "sin" 46 | await client.sendMail(from_addr, toList, $encoded) 47 | 48 | except: 49 | echo "Error in sending mail: " & recipient 50 | 51 | when defined(dev): 52 | echo "Email send" 53 | 54 | 55 | proc sendMailDb*(mailID: string) = 56 | ## Get data from mail template and send 57 | ## Uses Sync Socket 58 | 59 | when defined(dev): 60 | echo "Dev: Mail start" 61 | 62 | let mail = getRow(dbMail, sql"SELECT recipient, subject, body FROM mail_templates WHERE id = ?", mailID) 63 | 64 | let recipient = mail[0] 65 | let subject = mail[1] 66 | let message = mail[2] 67 | 68 | const otherHeaders = @[("Content-Type", "text/html; charset=\"UTF-8\"")] 69 | 70 | var client = newSmtp(useSsl = true, debug = false) 71 | client.connect(smtpAddress, Port(parseInt(smtpPort))) 72 | client.auth(smtpUser, smtpPassword) 73 | 74 | let from_addr = smtpFrom 75 | let toList = @[recipient] 76 | 77 | var headers = otherHeaders 78 | headers.add(("From", from_addr)) 79 | 80 | let encoded = createMessage(subject, message, toList, @[], headers) 81 | 82 | try: 83 | client.sendMail(from_addr, toList, $encoded) 84 | 85 | except: 86 | echo "Error in sending mail: " & recipient 87 | 88 | when defined(dev): 89 | echo "Email send" 90 | 91 | 92 | proc mailUpdateParameters*() = 93 | ## Update mail settings in variables 94 | 95 | let mailSettings = getRow(dbMail, sql"SELECT address, port, fromaddress, user, password FROM mail_settings WHERE id = ?", "1") 96 | 97 | smtpAddress = mailSettings[0] 98 | smtpPort = mailSettings[1] 99 | smtpFrom = mailSettings[2] 100 | smtpUser = mailSettings[3] 101 | smtpPassword = mailSettings[4] 102 | 103 | 104 | mailUpdateParameters() -------------------------------------------------------------------------------- /nimhapkg/modules/os/os_utils.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import osproc, strutils, asyncdispatch, json, db_sqlite 4 | 5 | import ../../resources/database/database 6 | import ../../resources/mqtt/mqtt_func 7 | import ../../resources/utils/log_utils 8 | 9 | var db = conn() 10 | var dbOs = conn("dbOs.db") 11 | 12 | 13 | proc osFreeMem*(): string = 14 | return execProcess("free -m | awk 'NR==2{print $4}'") 15 | 16 | proc osFreeSwap*(): string = 17 | return execProcess("free -m | awk 'NR==3{print $4}'") 18 | 19 | proc osUsedMem*(): string = 20 | return execProcess("free -m | awk 'NR==2{print $3}'") 21 | 22 | proc osUsedSwap*(): string = 23 | return execProcess("free -m | awk 'NR==3{print $3}'") 24 | 25 | proc osConnNumber*(): string = 26 | return execProcess("netstat -ant | grep ESTABLISHED | wc -l") 27 | 28 | proc osHostIp*(): string = 29 | return execProcess("hostname --ip-address") 30 | 31 | proc osData*(): string = 32 | result = "{\"handler\": \"action\", \"element\": \"osstats\", \"action\": \"read\", \"freemem\": \"" & osFreeMem() & "\", \"freeswap\": \"" & osFreeSwap() & "\", \"usedmem\": \"" & osUsedMem() & "\", \"usedswap\": \"" & osUsedSwap() & "\", \"connections\": \"" & osConnNumber() & "\", \"hostip\": \"" & osHostIp() & "\"}" 33 | 34 | return replace(result, "\n", "") 35 | 36 | 37 | proc osParseMqtt*(payload: string) {.async.} = 38 | ## Parse OS utils MQTT 39 | 40 | let js = parseJson(payload) 41 | 42 | if js["value"].getStr() == "refresh": 43 | mqttSend("os", "wss/to", osData()) 44 | 45 | 46 | proc osRunCommand*(command: string) = 47 | ## Run os template command 48 | 49 | if execCmd(command) == 1: 50 | logit("osutils", "ERROR", "Command failed: " & command) 51 | 52 | 53 | proc osRunTemplate*(osID: string) {.async.} = 54 | ## Run os template command 55 | 56 | let command = getValue(dbOs, sql("SELECT command FROM os_templates WHERE id = ?"), osID) 57 | let pros = execProcesses([command], options = {poEvalCommand}) -------------------------------------------------------------------------------- /nimhapkg/modules/owntracks/owntracks.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite, strutils, json, asyncdispatch, parsecfg 4 | import ../../resources/database/database 5 | import ../../resources/mqtt/mqtt_func 6 | import ../../resources/utils/dates 7 | import ../../resources/utils/parsers 8 | import ../../resources/utils/common 9 | 10 | from os import getAppDir 11 | 12 | var db = conn() 13 | var dbOwntracks = conn("dbOwntracks.db") 14 | 15 | 16 | let dict = loadConf("owntrack") 17 | let homeLat = dict.getSectionValue("Home","lat") 18 | let homeLon = dict.getSectionValue("Home","lon") 19 | 20 | 21 | proc owntracksAddWaypoints(topic, data: string) {.async.} = 22 | ## Add owntrack waypoints to DB 23 | 24 | let js = parseJson(data) 25 | 26 | let waypoints = js["waypoints"] 27 | let deviceID = split(topic, "/")[2] 28 | let username = split(topic, "/")[1] 29 | 30 | for waypoint in items(waypoints): 31 | if getValue(dbOwntracks, sql"SELECT id FROM owntracks_waypoints WHERE username = ? AND desc = ?", username, waypoint["desc"].getStr()) == "": 32 | exec(dbOwntracks, sql"INSERT INTO owntracks_waypoints (username, device_id, desc, lat, lon, rad) VALUES (?, ?, ?, ?, ?, ?)", username, deviceID, waypoint["desc"].getStr(), waypoint["lat"].getFloat(), waypoint["lon"].getFloat(), waypoint["rad"].getInt()) 33 | else: 34 | exec(dbOwntracks, sql"UPDATE owntracks_waypoints SET username = ?, device_id = ?, desc = ?, lat = ?, lon = ?, rad = ? WHERE username = ? AND device_id = ?", username, deviceID, waypoint["desc"].getStr(), waypoint["lat"].getFloat(), waypoint["lon"].getFloat(), waypoint["rad"].getInt(), username, deviceID) 35 | 36 | 37 | 38 | proc owntracksLastLocations(init = false) {.async.} = 39 | ## Returns the latest owntracks locations for all devices including waypoints 40 | 41 | var json = "{\"handler\": \"action\", \"element\": \"owntracks\"" 42 | 43 | # Check type 44 | if init: 45 | json.add(", \"value\": \"init\"") 46 | else: 47 | json.add(", \"value\": \"refresh\"") 48 | 49 | # Check if home location is defined 50 | if homeLat != "" and homeLon != "": 51 | json.add(",\"home\": {\"lat\": \"" & homeLat & "\", \"lon\": \"" & homeLon & "\"}") 52 | 53 | # Get latest history data 54 | let allLocations = getAllRows(dbOwntracks, sql"SELECT DISTINCT device_id, lat, lon, creation FROM owntracks_history GROUP BY device_id ORDER BY creation DESC") 55 | if allLocations.len() != 0: 56 | var moreThanOne = false 57 | json.add(", \"devices\": [") 58 | for device in allLocations: 59 | if moreThanOne: 60 | json.add(",") 61 | 62 | json.add("{") 63 | json.add("\"device\": \"" & device[0] & "\",") 64 | json.add("\"lat\": \"" & device[1] & "\",") 65 | json.add("\"lon\": \"" & device[2] & "\",") 66 | json.add("\"date\": \"" & epochDate(device[3], "DD MMM HH:mm") & "\"") 67 | json.add("}") 68 | 69 | moreThanOne = true 70 | 71 | json.add("]") 72 | 73 | # Get latest history data 74 | let allWaypoints = getAllRows(dbOwntracks, sql"SELECT DISTINCT desc, lat, lon, rad, creation FROM owntracks_waypoints") 75 | if allWaypoints.len() != 0: 76 | var moreThanOne = false 77 | json.add(", \"waypoints\": [") 78 | for device in allWaypoints: 79 | if moreThanOne: 80 | json.add(",") 81 | 82 | json.add("{") 83 | json.add("\"desc\": \"" & device[0] & "\",") 84 | json.add("\"lat\": \"" & device[1] & "\",") 85 | json.add("\"lon\": \"" & device[2] & "\",") 86 | json.add("\"rad\": \"" & device[3] & "\",") 87 | json.add("\"date\": \"" & epochDate(device[4], "DD MMM HH:mm") & "\"") 88 | json.add("}") 89 | 90 | moreThanOne = true 91 | 92 | json.add("]") 93 | 94 | 95 | json.add("}") 96 | 97 | mqttSend("owntracks", "wss/to", json) 98 | 99 | 100 | 101 | proc owntracksHistoryAdd(topic, data: string) {.async.} = 102 | ## Add owntrack element to database 103 | 104 | let js = parseJson(data) 105 | 106 | # Assign owntrack data 107 | let deviceID = split(topic, "/")[2] 108 | let username = split(topic, "/")[1] 109 | let trackerID = jn(js, "tid") 110 | let lat = jnFloat(js, "lat") 111 | let lon = jnFloat(js, "lon") 112 | let conn = jn(js, "conn") 113 | 114 | # Check if device exists or add it 115 | if getValue(dbOwntracks, sql"SELECT device_id FROM owntracks_devices WHERE device_id = ? AND tracker_id = ?", deviceID, trackerID) == "": 116 | exec(dbOwntracks, sql"INSERT INTO owntracks_devices (device_id, username, tracker_id) VALUES (?, ?, ?)", deviceID, username, trackerID) 117 | 118 | # Add history 119 | if jn(js, "_type") == "location" and lat != 0 and lon != 0: 120 | exec(dbOwntracks, sql"INSERT INTO owntracks_history (device_id, username, tracker_id, lat, lon, conn) VALUES (?, ?, ?, ?, ?, ?)", deviceID, username, trackerID, lat, lon, conn) 121 | 122 | 123 | proc owntracksParseMqtt*(payload, topic: string) {.async.} = 124 | ## Parse owntracks MQTT 125 | 126 | let js = parseJson(payload) 127 | 128 | # Update with last location or waypoints 129 | if hasKey(js, "_type"): 130 | if jn(js, "_type") == "location": 131 | asyncCheck owntracksHistoryAdd(topic, payload) 132 | 133 | elif jn(js, "_type") == "waypoints": 134 | asyncCheck owntracksAddWaypoints(topic, payload) 135 | 136 | # Send data to websocket 137 | elif js["value"].getStr() == "init": 138 | asyncCheck owntracksLastLocations(true) 139 | 140 | else: 141 | asyncCheck owntracksLastLocations() 142 | 143 | -------------------------------------------------------------------------------- /nimhapkg/modules/pushbullet/pushbullet.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | # 3 | # Todo: Implement nim solution instead of curl 4 | 5 | import asyncdispatch 6 | 7 | import db_sqlite, osproc, json, strutils, parsecfg 8 | import ../../resources/database/database 9 | import ../../resources/mqtt/mqtt_func 10 | 11 | 12 | var pushbulletAPI = "" 13 | 14 | var db = conn() 15 | var dbPushbullet = conn("dbPushbullet.db") 16 | 17 | 18 | proc pushbulletSendCurl(pushType = "note", title = "title", body = "body"): string = 19 | ## Excecute curl with info to pushbullet api 20 | 21 | let output = execProcess("curl -u " & pushbulletAPI & ": -X POST https://api.pushbullet.com/v2/pushes --header 'Content-Type: application/json' --data-binary '{\"type\": \"" & pushType & "\", \"title\": \"" & title & "\", \"body\": \"" & body & "\"}'") 22 | 23 | return output 24 | 25 | 26 | template jsonHasKey(data: string): bool = 27 | ## Check JSON for "error" key 28 | try: 29 | if hasKey(parseJson(data), "error"): 30 | true 31 | else: 32 | false 33 | except: 34 | false 35 | 36 | 37 | #[ 38 | proc pushbulletHistory(db: DbConn, resp, title, body: string): string = 39 | ## Adds pushbullet to the history 40 | 41 | if jsonHasKey(resp): 42 | exec(db, sql"INSERT INTO history (element, identificer, value, error) VALUES (?, ?, ?, ?)", "pushbullet", "send", resp, "1") 43 | 44 | mqttSend("wss/to", "pushbullet", "{\"handler\": \"response\", \"value\": \"Pushbullet error\", \"error\": \"true\"}") 45 | 46 | else: 47 | exec(db, sql"INSERT INTO history (element, identifier, value) VALUES (?, ?, ?)", "pushbullet", "send", "Notification delivered. Title: " & title & " - Body: " & body) 48 | ]# 49 | 50 | 51 | proc pushbulletSendDb*(pushID: string) = 52 | ## Sends a push from database 53 | 54 | let push = getRow(dbPushbullet, sql"SELECT title, body FROM pushbullet_templates WHERE id = ?", pushID) 55 | 56 | let resp = pushbulletSendCurl("note", push[0], push[1]) 57 | 58 | # Why save the pushbullet history? 59 | #discard pushbulletHistory(dbPushbullet, resp, push[0], push[1]) 60 | 61 | 62 | 63 | proc pushbulletSendTest*() = 64 | ## Sends a test pushmessage 65 | 66 | let resp = pushbulletSendCurl("note", "Test title", "Test body") 67 | 68 | if jsonHasKey(resp): 69 | mqttSend("pushbullet", "wss/to", "{\"handler\": \"response\", \"value\": \"Pushbullet error\", \"error\": \"true\"}") 70 | 71 | else: 72 | mqttSend("pushbullet", "wss/to", "{\"handler\": \"response\", \"value\": \"Pushbullet msg send\", \"error\": \"false\"}") 73 | 74 | 75 | 76 | proc pushbulletParseMqtt*(payload: string) = 77 | ## Receive raw JSON from MQTT and parse it 78 | 79 | let js = parseJson(payload) 80 | 81 | if js["pushid"].getStr() == "test": 82 | pushbulletSendTest() 83 | 84 | else: 85 | pushbulletSendDb(js["pushid"].getStr()) 86 | 87 | 88 | 89 | #[ 90 | proc pushbulletSendWebsocketDb*(db: DbConn, pushID: string): string = 91 | ## Sends a push from database 92 | 93 | let push = getRow(db, sql"SELECT title, body FROM pushbullet_templates WHERE id = ?", pushID) 94 | 95 | let resp = pushbulletSendCurl("note", push[0], push[1]) 96 | return pushbulletHistory(db, resp, push[0], push[1]) 97 | ]# 98 | 99 | #[ 100 | proc pushbulletSendWebsocket*(pushType, title, body: string): string = 101 | ## Get certificate expiration date in special format 102 | ## 103 | ## Returns true and output if success, 104 | ## Returns false and output if error 105 | 106 | let resp = pushbulletSendCurl("note", title, body) 107 | return pushbulletHistory(db, resp, title, body) 108 | ]# 109 | 110 | proc pushbulletUpdateApi*() = 111 | pushbulletAPI = getValue(dbPushbullet, sql"SELECT api FROM pushbullet_settings WHERE id = ?", "1") 112 | 113 | 114 | proc pushbulletNewApi*(api: string) = 115 | exec(dbPushbullet, sql"UPDATE pushbullet_settings SET api = ? WHERE id = ?", api, "1") 116 | pushbulletAPI = api 117 | 118 | 119 | pushbulletUpdateApi() -------------------------------------------------------------------------------- /nimhapkg/modules/rpi/rpi_utils.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import wiringPiNim 4 | import asyncdispatch 5 | import strutils 6 | import json 7 | import db_sqlite 8 | 9 | from osproc import execProcess 10 | 11 | import ../../resources/database/database 12 | import ../../resources/mqtt/mqtt_func 13 | import ../../resources/utils/log_utils 14 | 15 | type 16 | RpiTemplate = tuple[id: string, name: string, pin: string, pinMode: string, pinPull: string, digitalAction: string, analogAction: string, value: string] 17 | 18 | var rpiTemplate: seq[RpiTemplate] = @[] 19 | 20 | var dbRpi = conn("dbRpi.db") 21 | 22 | proc rpiLoadTemplates*() = 23 | ## Load the RPi templates 24 | 25 | rpiTemplate = @[] 26 | 27 | let allRpi = getAllRows(dbRpi, sql"SELECT id, name, pin, pinMode, pinPull, digitalAction, analogAction, value FROM rpi_templates") 28 | 29 | for rpi in allRpi: 30 | rpiTemplate.add((id: rpi[0], name: rpi[1], pin: rpi[2], pinMode: rpi[3], pinPull: rpi[4], digitalAction: rpi[5], analogAction: rpi[6], value: rpi[7])) 31 | 32 | 33 | proc rpiAction*(rpiID: string): string = 34 | ## Run the RPi template 35 | 36 | for rpi in rpiTemplate: 37 | if rpi[0] != rpiID: 38 | continue 39 | 40 | if rpi[2].len() == 0: 41 | return 42 | let rpiPin = toU32(parseInt(rpi[2])) 43 | 44 | 45 | # Setup RPi 46 | #if piSetup() < 0: 47 | # return 48 | 49 | 50 | # Set pin mode 51 | if rpi[3].len() != 0: 52 | case rpi[3] 53 | of "gpio": 54 | piPinModeGPIO(rpiPin) 55 | of "pwm": 56 | piPinModePWM(rpiPin) 57 | of "output": 58 | piPinModeOutput(rpiPin) 59 | of "input": 60 | piPinModeInput(rpiPin) 61 | else: 62 | discard 63 | 64 | 65 | # Pull pin 66 | if rpi[4].len() != 0: 67 | case rpi[4] 68 | of "off": 69 | piPullOff(rpiPin) 70 | of "down": 71 | piPullDown(rpiPin) 72 | of "up": 73 | piPullUp(rpiPin) 74 | else: 75 | discard 76 | 77 | 78 | # Read or write 79 | # Digital: 80 | let value = toU32(parseInt(rpi[1])) 81 | if rpi[5].len() != 0: 82 | case rpi[5] 83 | of "pwm": 84 | piDigitalPWM(rpiPin, value) 85 | of "write": 86 | piDigitalWrite(rpiPin, value) 87 | of "read": 88 | return $piDigitalRead(rpiPin) 89 | else: 90 | discard 91 | 92 | # Analog: 93 | elif rpi[6].len() != 0: 94 | case rpi[6] 95 | of "write": 96 | analogWrite(rpiPin, value) 97 | of "read": 98 | return $analogRead(rpiPin) 99 | else: 100 | discard 101 | 102 | 103 | proc rpiParseMqtt*(payload: string) {.async.} = 104 | let js = parseJson(payload) 105 | 106 | if js["action"].getStr() == "runtemplate": 107 | let rpiID = js["rpiid"].getStr() 108 | let rpiOutput = rpiAction(rpiID) 109 | 110 | mqttSend("rss", "wss/to", "{\"handler\": \"action\", \"element\": \"rpi\", \"action\": \"template\", \"output\": \"" & rpiOutput & "\"}") 111 | 112 | elif js["action"].getStr() == "write": 113 | let rpiID = js["rpiid"].getStr() 114 | let rpiOutput = rpiAction(rpiID) 115 | 116 | 117 | 118 | proc rpiInit() = 119 | ## Init RPi setup 120 | 121 | let gpioExists = execProcess("gpio -readall") 122 | if gpioExists.len() == 0: 123 | logit("rpi", "DEBUG", "piSetup(): Setup failed") 124 | return 125 | 126 | for line in split(gpioExists, "\n"): 127 | if "Oops" in line: 128 | logit("rpi", "DEBUG", "piSetup(): Setup failed") 129 | return 130 | 131 | rpiLoadTemplates() 132 | logit("rpi", "DEBUG", "piSetup(): Setup complete") 133 | 134 | 135 | rpiInit() -------------------------------------------------------------------------------- /nimhapkg/modules/rss/rss_reader.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch 2 | import db_sqlite 3 | import httpclient 4 | import json 5 | import parsexml 6 | import streams 7 | import strutils 8 | 9 | import ../../resources/database/database 10 | import ../../resources/mqtt/mqtt_func 11 | 12 | 13 | var db = conn() 14 | var dbRss = conn("dbRss.db") 15 | 16 | 17 | proc rssFormatHtml(field, data: string, websocketMqtt: bool): string = 18 | ## Formats RSS data to HTML 19 | 20 | var html = "" 21 | 22 | if websocketMqtt: 23 | html.add("\n
" & data & "
") 24 | else: 25 | html.add("\n
" & data & "
") 26 | 27 | return html 28 | 29 | 30 | proc rssReadUrl*(name, url: string, fields: varargs[string, `$`], skipNth = 0, websocketMqtt = false, createHTML = true): string = 31 | ## Reads a RSS feed and reads userdefined fields 32 | ## 33 | ## Currently only support for xmlElementStart (closed tags: , <pubDate>, etc.) 34 | 35 | 36 | when defined(dev): 37 | echo "RSS feed data:" 38 | echo " - RSS URL: " & url 39 | echo " - RSS fields: " & $fields 40 | echo " - RSS skip: " & $skipNth 41 | 42 | 43 | # Connect to RSS fead and get body 44 | var client = newHttpClient() 45 | let rss = getContent(client, url) 46 | 47 | 48 | # Start parsing feed 49 | var nn = newStringStream(rss) 50 | var x: XmlParser 51 | open(x, nn, "") 52 | next(x) 53 | 54 | 55 | # Defined 56 | var rssOut = "" 57 | var skip = 0 58 | var startTag = false 59 | var notStart = false 60 | let startField = fields[0] 61 | let endField = fields[fields.len() - 1] 62 | 63 | while true: 64 | x.next() 65 | try: 66 | case x.kind 67 | of xmlElementStart: 68 | 69 | # Loop through fields 70 | for field in fields: 71 | 72 | if x.elementName == field: 73 | if field == startField: 74 | notStart = false 75 | else: 76 | notStart = true 77 | 78 | if field == startField and startTag == true and notStart == true: 79 | startTag = true 80 | rssOut.add("\n</div>") 81 | 82 | # Check if <div> is needed 83 | if field == startField and startTag == false: 84 | startTag = true 85 | rssOut.add("\n<div>") 86 | 87 | x.next() 88 | 89 | while x.kind == xmlCharData: 90 | # Skip line 91 | if skip < skipNth: 92 | inc(skip) 93 | x.next() 94 | 95 | # Generate inner HTML 96 | if x.charData == "" or x.charData.replace(" ", "") == "\"": 97 | discard 98 | elif createHTML: 99 | rssOut.add(rssFormatHtml(field, x.charData, websocketMqtt)) 100 | else: 101 | rssOut.add(x.charData) 102 | 103 | x.next() 104 | 105 | # End of element 106 | if x.kind == xmlElementEnd and x.elementName == field: 107 | # Check if </div> is needed 108 | if field == endField: 109 | startTag = false 110 | rssOut.add("\n</div>") 111 | 112 | continue 113 | 114 | 115 | of xmlEof: break 116 | of xmlError: 117 | echo(errorMsg(x)) 118 | x.next() 119 | else: x.next() 120 | 121 | except AssertionError: 122 | continue 123 | 124 | except: 125 | echo "Error: Something went wrong" 126 | 127 | if websocketMqtt: 128 | rssOut = "<div class=\\\"rss-container rss-" & name & "\\\">" & rssOut & "</div>" 129 | else: 130 | rssOut = "<div class=\"rss-container rss-" & name & "\">" & rssOut & "</div>" 131 | 132 | when defined(dev): 133 | echo " - RSS output: " & rssOut 134 | 135 | return rssOut 136 | 137 | 138 | proc rssReadUrl*(feedid: string, websocketMqtt = false): string = 139 | ## Reads a RSS feed from data in database 140 | 141 | let rssData = getRow(dbRss, sql"SELECT url, fields, skip, name FROM rss_feeds WHERE id = ?", feedid) 142 | 143 | return rssReadUrl(rssData[3], rssData[0], rssData[1].split(","), parseInt(rssData[2]), websocketMqtt) 144 | 145 | 146 | proc rssFeetchToWss(feedid: string) {.async.} = 147 | ## Reads RSS and sends to WSS 148 | 149 | mqttSend("rss", "wss/to", "{\"handler\": \"action\", \"element\": \"rss\", \"action\": \"update\", \"feedid\": \"" & feedid & "\", \"data\": \"" & rssReadUrl(feedid, true).replace("\n", "") & "\"}") 150 | 151 | 152 | template jn(json: JsonNode, data: string): string = 153 | ## Avoid error in parsing JSON 154 | 155 | try: 156 | json[data].getStr() 157 | except: 158 | "" 159 | 160 | proc rssParseMqtt*(payload: string) {.async.} = 161 | 162 | let js = parseJson(payload) 163 | 164 | if jn(js, "action") == "refresh": 165 | let feedid = jn(js, "feedid") 166 | if feedid == "": 167 | return 168 | 169 | asyncCheck rssFeetchToWss(feedid) 170 | 171 | 172 | 173 | #echo rssReadUrl("https://www.archlinux.org/feeds/packages/", ["title", "pubDate"], 0) -------------------------------------------------------------------------------- /nimhapkg/modules/web/web_certs.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import osproc, strutils, db_sqlite, asyncdispatch 4 | 5 | import ../../resources/mqtt/mqtt_func 6 | 7 | from times import epochTime 8 | 9 | 10 | proc certExpiraryDaysTo*(serverAddress, port: string): string = 11 | ## Return days before a certificate expire 12 | 13 | var sslOut = execProcess("echo $(date --date \"$(openssl s_client -connect " & serverAddress & ":" & port & " -servername " & serverAddress & " < /dev/null 2>/dev/null | openssl x509 -noout -enddate | sed -n 's/notAfter=//p')\" +\"%s\")").replace("\n", "") 14 | 15 | if not isDigit(sslOut): 16 | return "" 17 | 18 | else: 19 | return split($((parseFloat(sslOut) - epochTime()) / 86400 ), ".")[0] 20 | 21 | 22 | 23 | proc certExpiraryJson*(serverAddress, port: string) {.async.} = 24 | ## Excecute openssl s_client and return dates before expiration 25 | ## 26 | ## Dot . in serveraddress send in JSON response needs to be 27 | ## removed due to the use of serveraddress in CSS class 28 | 29 | let daysToExpire = certExpiraryDaysTo(serverAddress, port) 30 | 31 | if not isDigit(daysToExpire): 32 | mqttSend("webutils", "wss/to", "{\"sslOut\": \"action\", \"element\": \"certexpiry\", \"server\": \"" & replace(serverAddress, ".", "") & "\", \"value\": \"error\"}") 33 | 34 | else: 35 | mqttSend("webutils", "wss/to", "{\"handler\": \"action\", \"element\": \"certexpiry\", \"server\": \"" & replace(serverAddress, ".", "") & "\", \"value\": \"" & daysToExpire & "\"}") 36 | 37 | 38 | 39 | proc certExpiraryAll*(db: DbConn) {.async.} = 40 | ## Get all web urls and check cert 41 | 42 | let allCerts = getAllRows(db, sql"SELECT url, port FROM certificates") 43 | 44 | for cert in allCerts: 45 | asyncCheck certExpiraryJson(cert[0], cert[1]) 46 | 47 | 48 | 49 | proc certDatabase*(db: DbConn) = 50 | ## Creates web certificates tables in database 51 | 52 | exec(db, sql""" 53 | CREATE TABLE IF NOT EXISTS certificates ( 54 | id INTEGER PRIMARY KEY, 55 | name TEXT, 56 | url TEXT, 57 | port INTEGER, 58 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 59 | );""") 60 | 61 | 62 | -------------------------------------------------------------------------------- /nimhapkg/modules/web/web_utils.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | # Copyright 2018 - Thomas T. Jarløv 4 | 5 | import osproc, strutils, asyncdispatch, json 6 | 7 | import ../../resources/database/database 8 | import ../../resources/mqtt/mqtt_func 9 | import ../web/web_certs 10 | 11 | var dbWeb = conn("dbWeb.db") 12 | 13 | proc webParseMqtt*(payload: string) {.async.} = 14 | ## Parse OS utils MQTT 15 | 16 | let js = parseJson(payload) 17 | 18 | if js["item"].getStr() == "certexpiry": 19 | if js.hasKey("server"): 20 | asyncCheck certExpiraryJson(js["server"].getStr(), js["port"].getStr()) 21 | 22 | else: 23 | asyncCheck certExpiraryAll(dbWeb) 24 | 25 | certDatabase(dbWeb) -------------------------------------------------------------------------------- /nimhapkg/modules/xiaomi/xiaomi_utils.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import json, sequtils, strutils, net, asyncdispatch, db_sqlite 4 | import multicast 5 | import nimcrypto 6 | import xiaomi 7 | 8 | import ../../resources/database/database 9 | import ../../resources/utils/log_utils 10 | import ../../resources/utils/parsers 11 | import ../../resources/mqtt/mqtt_func 12 | 13 | 14 | type 15 | Device = tuple[sid: string, name: string, model: string, alarmvalue: string] 16 | DeviceAlarm = tuple[sid: string, alarmvalue: string, trigger: string] 17 | DeviceTemplate = tuple[id: string, sid: string, value_name: string, value_data: string] 18 | Gateway = tuple[sid: string, name: string, model: string, token: string, password: string, secret: string] 19 | 20 | 21 | var devices: seq[Device] = @[] 22 | var devicesTemplates: seq[DeviceTemplate] = @[] 23 | var devicesAlarm: seq[DeviceAlarm] = @[] 24 | var gateway: Gateway 25 | 26 | 27 | ## Db connection 28 | var db = conn() 29 | var dbXiaomi = conn("dbXiaomi.db") 30 | 31 | ## Xiaomi 32 | var xiaomiGatewaySid = "" 33 | 34 | 35 | template jn(json: JsonNode, data: string): string = 36 | ## Avoid error when parsing JSON 37 | try: json[data].getStr() except:"" 38 | 39 | 40 | proc xiaomiLoadDevices() = 41 | ## Load all devices into seq 42 | 43 | devices = @[] 44 | 45 | let allDevices = getAllRows(dbXiaomi, sql"SELECT sid, name, model FROM xiaomi_devices") 46 | 47 | for row in allDevices: 48 | devices.add((sid: row[0], name: row[1], model: row[2], alarmvalue: "")) 49 | 50 | 51 | proc xiaomiLoadDevicesTemplates() = 52 | ## Load all devices into seq 53 | 54 | devicesTemplates = @[] 55 | 56 | let allDevices = getAllRows(dbXiaomi, sql"SELECT id, sid, value_name, value_data FROM xiaomi_templates") 57 | 58 | for row in allDevices: 59 | devicesTemplates.add((id: row[0], sid: row[1], value_name: row[2], value_data: row[3])) 60 | 61 | 62 | proc xiaomiLoadDevicesAlarm() = 63 | ## Load all devices which trigger the alarm into seq 64 | 65 | devicesAlarm = @[] 66 | 67 | let allDevices = getAllRows(dbXiaomi, sql"SELECT xd.sid, xdd.value_data, xdd.triggerAlarm FROM xiaomi_devices AS xd LEFT JOIN xiaomi_devices_data AS xdd ON xdd.sid = xd.sid WHERE xdd.triggerAlarm != '' AND xdd.triggerAlarm != 'false' AND xdd.triggerAlarm IS NOT NULL") 68 | 69 | for row in allDevices: 70 | devicesAlarm.add((sid: row[0], alarmvalue: row[1], trigger: row[2])) 71 | 72 | 73 | proc xiaomiGatewayCreate(gSid, gName, gToken, gPassword, gSecret: string) = 74 | ## Generates the gateway tuple 75 | 76 | gateway = (sid: gSid, name: gName, model: "gateway", token: gToken, password: gPassword, secret: gSecret) 77 | 78 | 79 | proc xiaomiGatewayUpdateSecret(gToken = gateway[3]) = 80 | ## Updates the gateways token and secret 81 | 82 | if gToken.len() == 0 or gateway[4].len() == 0: 83 | return 84 | xiaomiSecretUpdate(gateway[4], gToken) 85 | gateway[3] = gToken 86 | gateway[5] = xiaomiGatewaySecret 87 | 88 | 89 | proc xiaomiGatewayUpdatePassword*() = 90 | ## Update the gateways password. 91 | ## It is not currently need to use a proc, 92 | ## but in the future, there will be support 93 | ## for multiple gateways. 94 | 95 | gateway[4] = getValue(dbXiaomi, sql"SELECT key FROM xiaomi_api") 96 | 97 | 98 | proc xiaomiSoundPlay*(sid: string, defaultRingtone = "8") = 99 | ## Send Xiaomi command to start sound 100 | 101 | var volume = "4" 102 | var ringtone = defaultRingtone 103 | if "," in defaultRingtone: 104 | ringtone = split(defaultRingtone, ",")[0] 105 | volume = split(defaultRingtone, ",")[1] 106 | 107 | xiaomiGatewaySecret = gateway[5] 108 | xiaomiWrite(sid, "\"mid\": " & ringtone & ", \"vol\": " & volume) 109 | 110 | 111 | proc xiaomiSoundStop*(sid: string) = 112 | ## Send Xiaomi command to stop sound 113 | 114 | xiaomiGatewaySecret = gateway[5] 115 | xiaomiWrite(sid, "\"mid\": 10000") 116 | 117 | 118 | proc xiaomiGatewayLight*(sid: string, color = "0") = 119 | ## Send Xiaomi command to enable gateway light 120 | 121 | xiaomiGatewaySecret = gateway[5] 122 | xiaomiWrite(sid, "\"rgb\": " & color) 123 | 124 | 125 | proc xiaomiWriteTemplate*(id: string) = 126 | ## Write a template to the gateway 127 | 128 | for device in devicesTemplates: 129 | if device[0] == id: 130 | 131 | case device[2] 132 | of "ringtone": 133 | if device[3] == "10000": 134 | xiaomiSoundStop(device[1]) 135 | elif device[3] != "": 136 | xiaomiSoundPlay(device[1], device[3]) 137 | else: 138 | xiaomiSoundPlay(device[1]) 139 | 140 | of "rgb": 141 | if device[3] != "": 142 | xiaomiGatewayLight(device[1], device[3]) 143 | else: 144 | xiaomiGatewayLight(device[1]) 145 | 146 | else: 147 | discard 148 | 149 | break 150 | 151 | var alarmWaitForReset = false 152 | proc xiaomiCheckAlarmStatus(sid, value, xdata, alarmStatus: string) {.async.} = 153 | ## Check if the triggered device should trigger the alarm 154 | 155 | let alarmtrigger = jn(parseJson(xdata), value) 156 | 157 | for device in devicesAlarm: 158 | if device[0] == sid and device[1] == alarmtrigger and device[2] == alarmStatus: 159 | mqttSend("xiaomi", "alarm", "{\"handler\": \"action\", \"element\": \"xiaomi\", \"action\": \"triggered\", \"sid\": \"" & sid & "\", \"value\": \"" & value & "\", \"data\": " & xdata & "}") 160 | alarmWaitForReset = true 161 | logit("xiaomi", "INFO", "xiaomiCheckAlarmStatus(): ALARM = " & xdata) 162 | 163 | break 164 | 165 | 166 | proc xiaomiDiscoverUpdateDB(clearDB = false) = 167 | # Updates the database with new devices 168 | ## TODO: THIS FAILS 169 | 170 | if clearDB: 171 | exec(dbXiaomi, sql"DELETE FROM xiaomi_devices") 172 | 173 | let devicesJson = xiaomiDiscover() 174 | let devices = parseJson(devicesJson)["xiaomi_devices"] 175 | for device in items(devices): 176 | let sid = device["sid"].getStr() 177 | if getValue(dbXiaomi, sql"SELECT sid FROM xiaomi_devices WHERE sid = ?", sid) == "": 178 | exec(dbXiaomi, sql"INSERT INTO xiaomi_devices (sid, name, model, short_id) VALUES (?, ?, ?, ?)", sid, sid, device["model"].getStr(), device["short_id"].getStr()) 179 | 180 | xiaomiLoadDevices() 181 | xiaomiLoadDevicesTemplates() 182 | xiaomiLoadDevicesAlarm() 183 | 184 | 185 | proc xiaomiParseMqtt*(payload, alarmStatus: string) {.async.} = 186 | ## Parse the MQTT 187 | 188 | var js: JsonNode 189 | try: 190 | if payload.len() != 0: 191 | js = parseJson(payload) 192 | else: 193 | return 194 | except JsonParsingError: 195 | logit("xiaomi", "ERROR", "JSON parsing error") 196 | return 197 | 198 | # Get SID 199 | let sid = jn(js, "sid") 200 | 201 | # Check that payload has command response 202 | if js.hasKey("cmd"): 203 | let cmd = jn(js, "cmd") 204 | 205 | # If this is the gateway, get the token and update the secret 206 | if cmd == "heartbeat" and jn(js, "token") != "": 207 | let token = jn(js, "token") 208 | 209 | # Create the gateway 210 | if gateway[0].len() == 0: 211 | xiaomiGatewayCreate(sid, "Gateway", token, getValue(dbXiaomi, sql"SELECT key FROM xiaomi_api"), "") 212 | if gateway[4].len() == 0: 213 | logit("xiaomi", "WARNING", "No API key was found") 214 | else: 215 | xiaomiGatewayUpdateSecret() 216 | 217 | # Create gateway in DB 218 | if getValue(dbXiaomi, sql"SELECT sid FROM xiaomi_api WHERE sid = ?", sid).len() == 0: 219 | discard tryExec(dbXiaomi, sql"INSERT INTO xiaomi_devices (sid, name, model) VALUES (?, ?, ?)", sid, "Gateway", "gateway") 220 | discard tryExec(dbXiaomi, sql"INSERT INTO xiaomi_api (sid, token) VALUES (?, ?)", sid, token) 221 | 222 | logit("xiaomi", "DEBUG", "Gateway created") 223 | 224 | else: 225 | # Update the secret 226 | xiaomiGatewayUpdateSecret(token) 227 | logit("xiaomi", "DEBUG", "Gateway secret updated") 228 | 229 | return 230 | 231 | logit("xiaomi", "DEBUG", payload) 232 | 233 | # Skip data is empty 234 | let xdata = jn(js, "data") 235 | if xdata == "": 236 | return 237 | 238 | # Check output 239 | if cmd == "report" or cmd == "read_ack": 240 | var value = "" 241 | 242 | if "no_motion" in xdata: 243 | value = "no_motion" 244 | 245 | elif "motion" in xdata: 246 | value = "motion" 247 | 248 | elif "lux" in xdata: 249 | value = "lux" 250 | 251 | elif "status" in xdata: 252 | value = "status" 253 | 254 | elif "rgb" in xdata: 255 | value = "rgb" 256 | 257 | elif "illumination" in xdata: 258 | value = "illumination" 259 | 260 | # Check if the alarms needs to ring 261 | if not alarmWaitForReset and alarmStatus in ["armAway", "armHome"]: 262 | asyncCheck xiaomiCheckAlarmStatus(sid, "status", xdata, alarmStatus) 263 | 264 | if alarmStatus in ["disarmed"]: 265 | alarmWaitForReset = false 266 | 267 | # Add message 268 | mqttSend("xiaomi", "wss/to", "{\"handler\": \"action\", \"element\": \"xiaomi\", \"action\": \"read\", \"sid\": \"" & sid & "\", \"value\": \"" & value & "\", \"data\": " & xdata & "}") 269 | 270 | 271 | else: 272 | 273 | logit("xiaomi", "DEBUG", payload) 274 | 275 | case js["action"].getStr() 276 | of "discover": 277 | xiaomiDiscoverUpdateDB() 278 | 279 | of "read": 280 | xiaomiSendRead(js["sid"].getStr()) 281 | 282 | of "template": 283 | xiaomiWriteTemplate(js["value"].getStr()) 284 | 285 | of "updatepassword": 286 | xiaomiGatewayUpdatePassword() 287 | 288 | of "adddevice": 289 | xiaomiLoadDevices() 290 | 291 | of "updatedevice": 292 | xiaomiLoadDevices() 293 | xiaomiLoadDevicesTemplates() 294 | xiaomiLoadDevicesAlarm() 295 | 296 | of "deletedevice": 297 | xiaomiLoadDevices() 298 | xiaomiLoadDevicesTemplates() 299 | xiaomiLoadDevicesAlarm() 300 | 301 | of "addtemplate": 302 | xiaomiLoadDevicesTemplates() 303 | 304 | of "deletetemplate": 305 | xiaomiLoadDevicesTemplates() 306 | 307 | of "reloaddevices": 308 | xiaomiLoadDevices() 309 | xiaomiLoadDevicesTemplates() 310 | xiaomiLoadDevicesAlarm() 311 | 312 | 313 | xiaomiConnect() 314 | xiaomiLoadDevices() 315 | xiaomiLoadDevicesTemplates() 316 | xiaomiLoadDevicesAlarm() 317 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import parseCfg, db_sqlite, os, strutils 4 | import ../../resources/utils/common 5 | 6 | let dict = loadConf("database") 7 | const default_db_dir = "/var/lib/nimha/db" 8 | 9 | 10 | let db_user = dict.getSectionValue("Database","user") 11 | let db_pass = dict.getSectionValue("Database","pass") 12 | let db_name = dict.getSectionValue("Database","name") 13 | 14 | proc generateDB*(db: DbConn) = 15 | 16 | const 17 | TPassword = "VARCHAR(32)" 18 | 19 | # Person 20 | if not db.tryExec(sql(""" 21 | CREATE TABLE IF NOT EXISTS person( 22 | id INTEGER PRIMARY KEY, 23 | name VARCHAR(60) NOT NULL, 24 | password VARCHAR(300) NOT NULL, 25 | email VARCHAR(60) NOT NULL, 26 | salt VARBIN(128) NOT NULL, 27 | status VARCHAR(30) NOT NULL, 28 | timezone VARCHAR(100), 29 | secretUrl VARCHAR(250), 30 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 31 | modified timestamp NOT NULL default (STRFTIME('%s', 'now')), 32 | lastOnline timestamp NOT NULL default (STRFTIME('%s', 'now')) 33 | );"""), []): 34 | echo " - Database: person table already exists" 35 | 36 | # Session 37 | if not db.tryExec(sql(""" 38 | CREATE TABLE IF NOT EXISTS session( 39 | id INTEGER PRIMARY KEY, 40 | ip inet NOT NULL, 41 | key VARCHAR(32) NOT NULL, 42 | userid INTEGER NOT NULL, 43 | lastModified timestamp NOT NULL default (STRFTIME('%s', 'now')), 44 | FOREIGN KEY (userid) REFERENCES person(id) 45 | );""" % [TPassword]), []): 46 | echo " - Database: session table already exists" 47 | 48 | # Main program events 49 | if not db.tryExec(sql""" 50 | CREATE TABLE IF NOT EXISTS mainevents( 51 | id INTEGER PRIMARY KEY, 52 | event TEXT, 53 | element TEXT, 54 | value TEXT, 55 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 56 | );""", []): 57 | echo " - Database: mainevents table already exists" 58 | 59 | # History 60 | if not db.tryExec(sql""" 61 | CREATE TABLE IF NOT EXISTS history ( 62 | id INTEGER PRIMARY KEY, 63 | element TEXT, 64 | identifier TEXT, 65 | error TEXT, 66 | value TEXT, 67 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 68 | );""", []): 69 | echo " - Database: history table already exists" 70 | 71 | # Set WAL - https://www.sqlite.org/wal.html 72 | exec(db, sql"PRAGMA journal_mode=WAL;") 73 | 74 | proc setupDbBasedir(): string = 75 | ## Create database base directory if needed 76 | result = 77 | when defined(dev) or not defined(systemInstall): 78 | let db_folder = dict.getSectionValue("Database","folder") 79 | replace(getAppDir(), "/nimhapkg/mainmodules", "") & "/" & db_folder 80 | else: 81 | default_db_dir 82 | 83 | discard existsOrCreateDir(result) 84 | 85 | proc conn*(dbName="nimha.db"): DbConn = 86 | ## Connect to the Database, creating dirs and files as needed. 87 | let db_dir = setupDbBasedir() 88 | let db_fn = db_dir / dbName 89 | echo "Opening " & db_fn 90 | let dbexists = fileExists(db_fn) 91 | try: 92 | var db = open(connection=db_fn, user=db_user, password=db_pass, database=db_name) 93 | 94 | if not dbexists: 95 | echo "Creating tables in " & db_fn 96 | generateDB(db) 97 | 98 | return db 99 | 100 | except: 101 | echo "ERROR: Connection to DB could not be established" 102 | quit() 103 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/alarm_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | proc alarmDatabase*(db: DbConn) = 6 | ## Creates alarm tables in database 7 | 8 | # Alarm 9 | if not tryExec(db, sql""" 10 | CREATE TABLE IF NOT EXISTS alarm ( 11 | id INTEGER PRIMARY KEY, 12 | status TEXT, 13 | modified timestamp NOT NULL default (STRFTIME('%s', 'now')) 14 | );"""): 15 | echo "ERROR: Alarm table could not be created" 16 | 17 | if getAllRows(db, sql"SELECT id FROM alarm").len() <= 0: 18 | exec(db, sql"INSERT INTO alarm (status) VALUES (?)", "disarmed") 19 | 20 | # Alarm history 21 | if not tryExec(db, sql""" 22 | CREATE TABLE IF NOT EXISTS alarm_history ( 23 | id INTEGER PRIMARY KEY, 24 | userid INTEGER, 25 | status TEXT, 26 | trigger TEXT, 27 | device TEXT, 28 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 29 | FOREIGN KEY (userid) REFERENCES person(id) 30 | );"""): 31 | echo "ERROR: Alarm history table could not be created" 32 | 33 | # Alarm settings 34 | if not tryExec(db, sql""" 35 | CREATE TABLE IF NOT EXISTS alarm_settings ( 36 | id INTEGER PRIMARY KEY, 37 | element TEXT, 38 | value TEXT, 39 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 40 | );"""): 41 | echo "ERROR: Alarm settings table could not be created" 42 | 43 | if getAllRows(db, sql"SELECT id FROM alarm_settings").len() <= 0: 44 | exec(db, sql"INSERT INTO alarm_settings (element, value) VALUES (?, ?)", "countdown", "20") 45 | exec(db, sql"INSERT INTO alarm_settings (element, value) VALUES (?, ?)", "armtime", "20") 46 | 47 | # Alarm password 48 | if not tryExec(db, sql""" 49 | CREATE TABLE IF NOT EXISTS alarm_password ( 50 | id INTEGER PRIMARY KEY, 51 | userid INTEGER, 52 | name VARCHAR(300), 53 | password VARCHAR(300) NOT NULL, 54 | salt VARBIN(128) NOT NULL, 55 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 56 | FOREIGN KEY (userid) REFERENCES person(id) 57 | );"""): 58 | echo "ERROR: Alarm password table could not be created" 59 | 60 | # Alarm actions 61 | if not tryExec(db, sql""" 62 | CREATE TABLE IF NOT EXISTS alarm_actions ( 63 | id INTEGER PRIMARY KEY, 64 | alarmstate TEXT, 65 | action TEXT, 66 | action_name TEXT, 67 | action_ref TEXT, 68 | parameter1 TEXT, 69 | parameter2 TEXT, 70 | parameter3 TEXT, 71 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 72 | );"""): 73 | echo "ERROR: Alarm actions table could not be created" 74 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/cron_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc cronDatabase*(db: DbConn) = 7 | ## Creates cron tables in database 8 | 9 | # Cron actions 10 | exec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS cron_actions ( 12 | id INTEGER PRIMARY KEY, 13 | element TEXT, 14 | action_name TEXT, 15 | action_ref TEXT, 16 | time TEXT, 17 | active TEXT, 18 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 19 | );""") 20 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/filestream_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc filestreamDatabase*(db: DbConn) = 7 | ## Creates cron tables in database 8 | 9 | # Cron actions 10 | exec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS filestream ( 12 | id INTEGER PRIMARY KEY, 13 | name TEXT, 14 | url TEXT, 15 | download TEXT, 16 | html TEXT, 17 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 18 | );""") 19 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/mail_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc mailDatabase*(db: DbConn) = 7 | ## Creates mail tables in database 8 | 9 | # Mail settings 10 | if not tryExec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS mail_settings ( 12 | id INTEGER PRIMARY KEY, 13 | address TEXT, 14 | port TEXT, 15 | fromaddress TEXT, 16 | user TEXT, 17 | password TEXT, 18 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 19 | );"""): 20 | echo "ERROR: Mail settings table could not be created" 21 | 22 | if getAllRows(db, sql"SELECT id FROM mail_settings").len() <= 0: 23 | exec(db, sql"INSERT INTO mail_settings (address, port, fromaddress, user, password) VALUES (?, ?, ?, ?, ?)", "smtp.com", "537", "mail@mail.com", "mailer", "secret") 24 | 25 | # Mail templates 26 | if not tryExec(db, sql""" 27 | CREATE TABLE IF NOT EXISTS mail_templates ( 28 | id INTEGER PRIMARY KEY, 29 | name TEXT, 30 | recipient TEXT, 31 | subject TEXT, 32 | body TEXT, 33 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 34 | );"""): 35 | echo "ERROR: Mail templates table could not be created" -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/mqtt_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc mqttDatabase*(db: DbConn) = 7 | ## Creates cron tables in database 8 | 9 | # Cron actions 10 | exec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS mqtt_templates ( 12 | id INTEGER PRIMARY KEY, 13 | name TEXT, 14 | topic TEXT, 15 | message TEXT, 16 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 17 | );""") 18 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/os_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2019 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc osDatabase*(db: DbConn) = 7 | ## Creates os tables in database 8 | 9 | # Mail templates 10 | if not tryExec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS os_templates ( 12 | id INTEGER PRIMARY KEY, 13 | name TEXT, 14 | command TEXT, 15 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 16 | );"""): 17 | echo "ERROR: OS templates table could not be created" -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/owntracks_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | 7 | proc owntracksDatabase*(db: DbConn) = 8 | ## Creates Xiaomi tables in database 9 | 10 | # Devices 11 | if not tryExec(db, sql""" 12 | CREATE TABLE IF NOT EXISTS owntracks_devices ( 13 | username TEXT PRIMARY KEY, 14 | device_id TEXT, 15 | tracker_id TEXT, 16 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 17 | );"""): 18 | echo "ERROR: Owntracks device table could not be created" 19 | 20 | # Waypoints 21 | if not tryExec(db, sql""" 22 | CREATE TABLE IF NOT EXISTS owntracks_waypoints ( 23 | id INTEGER PRIMARY KEY, 24 | username TEXT, 25 | device_id TEXT, 26 | desc TEXT, 27 | lat INTEGER, 28 | lon INTEGER, 29 | rad INTEGER, 30 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 31 | FOREIGN KEY (username) REFERENCES owntracks_devices(username) 32 | );"""): 33 | echo "ERROR: Owntracks waypoints table could not be created" 34 | 35 | # History 36 | if not tryExec(db, sql""" 37 | CREATE TABLE IF NOT EXISTS owntracks_history ( 38 | id INTEGER PRIMARY KEY, 39 | username TEXT, 40 | device_id TEXT, 41 | tracker_id TEXT, 42 | lat INTEGER, 43 | lon INTEGER, 44 | conn VARCHAR(10), 45 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 46 | FOREIGN KEY (username) REFERENCES owntracks_devices(username) 47 | );"""): 48 | echo "ERROR: Owntracks history table could not be created" -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/pushbullet_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc pushbulletDatabase*(db: DbConn) = 7 | ## Creates pushbullet tables in database 8 | 9 | # Pushbullet settings 10 | exec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS pushbullet_settings ( 12 | id INTEGER PRIMARY KEY, 13 | api TEXT, 14 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 15 | );""") 16 | if getAllRows(db, sql"SELECT id FROM pushbullet_settings").len() <= 0: 17 | exec(db, sql"INSERT INTO pushbullet_settings (api) VALUES (?)", "") 18 | 19 | # Pushbullet templates 20 | exec(db, sql""" 21 | CREATE TABLE IF NOT EXISTS pushbullet_templates ( 22 | id INTEGER PRIMARY KEY, 23 | name TEXT, 24 | title TEXT, 25 | body TEXT, 26 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 27 | );""") -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/rpi_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc rpiDatabase*(db: DbConn) = 7 | ## Creates mail tables in database 8 | 9 | # Raspberry Pi templates 10 | if not tryExec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS rpi_templates ( 12 | id INTEGER PRIMARY KEY, 13 | name TEXT, 14 | pin TEXT, 15 | pinMode TEXT, 16 | pinPull TEXT, 17 | digitalAction TEXT, 18 | analogAction TEXT, 19 | value TEXT, 20 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 21 | );"""): 22 | echo "ERROR: Mail templates table could not be created" -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/rss_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | proc rssDatabase*(db: DbConn) = 7 | ## Creates RSS tables in database 8 | 9 | # RSS feeds 10 | exec(db, sql""" 11 | CREATE TABLE IF NOT EXISTS rss_feeds ( 12 | id INTEGER PRIMARY KEY, 13 | url TEXT, 14 | skip INTEGER, 15 | fields TEXT, 16 | name TEXT, 17 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 18 | );""") 19 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/modules/xiaomi_database.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite 4 | 5 | 6 | 7 | proc xiaomiDatabase*(db: DbConn) = 8 | ## Creates Xiaomi tables in database 9 | 10 | # Devices 11 | if not tryExec(db, sql""" 12 | CREATE TABLE IF NOT EXISTS xiaomi_devices ( 13 | sid TEXT PRIMARY KEY, 14 | name TEXT, 15 | model TEXT, 16 | short_id TEXT, 17 | token TEXT, 18 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')) 19 | );"""): 20 | echo "ERROR: Xiaomi device table could not be created" 21 | 22 | # Gateway API 23 | if not tryExec(db, sql""" 24 | CREATE TABLE IF NOT EXISTS xiaomi_api ( 25 | sid TEXT PRIMARY KEY, 26 | key TEXT, 27 | token TEXT, 28 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 29 | FOREIGN KEY (sid) REFERENCES xiaomi_devices(sid) 30 | );"""): 31 | echo "ERROR: Xiaomi api table could not be created" 32 | 33 | # Sensors ( to be renamed ) 34 | if not tryExec(db, sql""" 35 | CREATE TABLE IF NOT EXISTS xiaomi_devices_data ( 36 | id INTEGER PRIMARY KEY, 37 | sid TEXT, 38 | value_name TEXT, 39 | value_data TEXT, 40 | action TEXT, 41 | triggerAlarm TEXT, 42 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 43 | FOREIGN KEY (sid) REFERENCES xiaomi_devices(sid) 44 | );"""): 45 | echo "ERROR: Xiaomi device data table could not be created" 46 | 47 | # Actions 48 | if not tryExec(db, sql""" 49 | CREATE TABLE IF NOT EXISTS xiaomi_templates ( 50 | id INTEGER PRIMARY KEY, 51 | sid TEXT, 52 | name TEXT, 53 | value_name TEXT, 54 | value_data TEXT, 55 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 56 | FOREIGN KEY (sid) REFERENCES xiaomi_devices(sid) 57 | );"""): 58 | echo "ERROR: Xiaomi templates table could not be created" 59 | 60 | # History 61 | if not tryExec(db, sql""" 62 | CREATE TABLE IF NOT EXISTS xiaomi_history ( 63 | id INTEGER PRIMARY KEY, 64 | sid TEXT, 65 | cmd TEXT, 66 | token TEXT, 67 | data TEXT, 68 | creation timestamp NOT NULL default (STRFTIME('%s', 'now')), 69 | FOREIGN KEY (sid) REFERENCES xiaomi_devices(sid) 70 | );"""): 71 | echo "ERROR: Xiaomi history table could not be created" 72 | 73 | -------------------------------------------------------------------------------- /nimhapkg/resources/database/sql_safe.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import db_sqlite, random 4 | 5 | from os import sleep 6 | 7 | 8 | randomize() 9 | 10 | 11 | proc getValueSafe*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): string = 12 | try: 13 | return getValue(db, query, args) 14 | except DbError: 15 | echo(getCurrentExceptionMsg()) 16 | return "" 17 | 18 | 19 | proc getValueSafeRetryHelper*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): string = 20 | try: 21 | return getValue(db, query, args) 22 | except DbError: 23 | echo(getCurrentExceptionMsg()) 24 | return "ERROR" 25 | 26 | 27 | proc getValueSafeRetry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): string = 28 | var counter = 0 29 | var loop = true 30 | while counter != 3 and loop: 31 | let res = getValueSafeRetryHelper(db, query, args) 32 | if res != "ERROR": 33 | loop = false 34 | return res 35 | else: 36 | inc(counter) 37 | sleep(rand(50)) 38 | 39 | 40 | proc getAllRowsSafe*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[Row] = 41 | when not defined(dev): 42 | try: 43 | return getAllRows(db, query, args) 44 | except DbError: 45 | echo(getCurrentExceptionMsg()) 46 | return @[] 47 | 48 | when defined(dev): 49 | return getAllRows(db, query, args) 50 | 51 | 52 | proc getRowSafe*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = 53 | when not defined(dev): 54 | try: 55 | return getRow(db, query, args) 56 | except DbError: 57 | echo(getCurrentExceptionMsg()) 58 | return @[] 59 | 60 | when defined(dev): 61 | return getRow(db, query, args) 62 | 63 | 64 | proc execSafe*(db: DbConn; query: SqlQuery; args: varargs[string, `$`]) = 65 | when not defined(dev): 66 | try: 67 | exec(db, query, args) 68 | except DbError: 69 | echo(getCurrentExceptionMsg()) 70 | discard 71 | 72 | when defined(dev): 73 | exec(db, query, args) 74 | 75 | 76 | proc tryExecSafe*(db: DbConn; query: SqlQuery; args: varargs[string, `$`]): bool = 77 | when not defined(dev): 78 | try: 79 | return tryExec(db, query, args) 80 | except DbError: 81 | echo(getCurrentExceptionMsg()) 82 | return false 83 | 84 | when defined(dev): 85 | return tryExec(db, query, args) 86 | 87 | 88 | proc tryInsertIDSafe*(db: DbConn; query: SqlQuery; args: varargs[string, `$`]): int64 = 89 | when not defined(dev): 90 | try: 91 | return tryInsertID(db, query, args) 92 | except DbError: 93 | echo(getCurrentExceptionMsg()) 94 | discard 95 | 96 | when defined(dev): 97 | return tryInsertID(db, query, args) -------------------------------------------------------------------------------- /nimhapkg/resources/mqtt/mqtt_func.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch 2 | import osproc 3 | import parsecfg 4 | from os import getAppDir 5 | from strutils import replace 6 | import ../../resources/utils/common 7 | 8 | let dict = loadConf("mqtt_func") 9 | let s_mqttPathSub* = dict.getSectionValue("MQTT","mqttPathSub") 10 | let s_mqttPathPub* = dict.getSectionValue("MQTT","mqttPathPub") 11 | let s_mqttIp* = dict.getSectionValue("MQTT","mqttIp") 12 | let s_mqttPort* = dict.getSectionValue("MQTT","mqttPort") 13 | let s_mqttUsername* = dict.getSectionValue("MQTT","mqttUsername") 14 | let s_mqttPassword* = dict.getSectionValue("MQTT","mqttPassword") 15 | 16 | proc setupBaseCmd(): string = 17 | result = s_mqttPathPub & " -p " & s_mqttPort 18 | if s_mqttIp != "": 19 | result.add " -h " & s_mqttIp 20 | if s_mqttUsername != "": 21 | result.add " -u " & s_mqttUsername 22 | if s_mqttPassword != "": 23 | result.add " -P " & s_mqttPassword 24 | 25 | let baseCmd = setupBaseCmd() 26 | 27 | 28 | proc mqttSend*(clientID, topic, message: string) = 29 | ## Send <message> to <topic> 30 | let cmd = baseCmd & " -i " & clientID & " -t " & topic & " -m '" & message & "'" 31 | discard execCmd(cmd) 32 | 33 | 34 | proc mqttSendAsync*(clientID, topic, message: string) {.async.} = 35 | ## Send <message> to <topic> 36 | let cmd = baseCmd & " -i " & clientID & " -t " & topic & " -m '" & message & "'" 37 | discard execCmd(cmd) 38 | -------------------------------------------------------------------------------- /nimhapkg/resources/mqtt/mqtt_templates.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | # 3 | # Todo: Implement nim solution instead of curl 4 | 5 | import asyncdispatch 6 | 7 | import db_sqlite, osproc, json, strutils, parsecfg 8 | import ../database/database 9 | import ../mqtt/mqtt_func 10 | 11 | 12 | var dbMqtt = conn("dbMqtt.db") 13 | 14 | 15 | proc mqttActionSendDb*(mqttActionID: string) = 16 | ## Sends a MQTT message from database 17 | 18 | let action = getRow(dbMqtt, sql"SELECT topic, message FROM mqtt_templates WHERE id = ?", mqttActionID) 19 | 20 | asyncCheck mqttSendAsync("mqttaction", action[0], action[1]) 21 | -------------------------------------------------------------------------------- /nimhapkg/resources/users/password.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import md5, bcrypt 4 | import math, random, os 5 | from strutils import multireplace 6 | randomize() 7 | 8 | 9 | var urandom: File 10 | let useUrandom = urandom.open("/dev/urandom") 11 | 12 | 13 | proc makeSalt*(): string = 14 | ## Generate random salt. Uses cryptographically secure /dev/urandom 15 | ## on platforms where it is available, and Nim's random module in other cases. 16 | result = "" 17 | if useUrandom: 18 | var randomBytes: array[0..127, char] 19 | discard urandom.readBuffer(addr(randomBytes), 128) 20 | for ch in randomBytes: 21 | if ord(ch) in {32..126}: 22 | result.add(ch) 23 | else: 24 | for i in 0..127: 25 | result.add(chr(rand(94) + 32)) # Generate numbers from 32 to 94 + 32 = 126 26 | 27 | result = result.multireplace([("\"", "_"), ("\'", "_"), ("\\", "_")]) 28 | 29 | 30 | proc makeSessionKey*(): string = 31 | ## Creates a random key to be used to authorize a session. 32 | let random = makeSalt() 33 | return bcrypt.hash(random, genSalt(8)) 34 | 35 | 36 | proc makePassword*(password, salt: string, comparingTo = ""): string = 37 | ## Creates an MD5 hash by combining password and salt 38 | let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) 39 | result = hash(getMD5(salt & getMD5(password)), bcryptSalt) -------------------------------------------------------------------------------- /nimhapkg/resources/users/user_add.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import os, strutils, db_sqlite 4 | 5 | 6 | import ../database/database 7 | import ../users/password 8 | 9 | 10 | proc createAdminUser*(args: seq[string]) = 11 | ## Create new admin user 12 | ## Input is done through stdin 13 | 14 | var db = conn() 15 | 16 | echo("User: Checking if any Admin exists in DB") 17 | let anyAdmin = getAllRows(db, sql"SELECT id FROM person WHERE status = ?", "Admin") 18 | 19 | if anyAdmin.len() < 1: 20 | echo("User: No Admin exists. Create it!") 21 | 22 | var iName = "" 23 | var iEmail = "" 24 | var iPwd = "" 25 | 26 | for arg in args: 27 | if arg.substr(0, 2) == "-u:": 28 | iName = arg.substr(3, arg.len()) 29 | elif arg.substr(0, 2) == "-p:": 30 | iPwd = arg.substr(3, arg.len()) 31 | elif arg.substr(0, 2) == "-e:": 32 | iEmail = arg.substr(3, arg.len()) 33 | 34 | if iName == "" or iPwd == "" or iEmail == "": 35 | echo("User: Missing either name, password or email to create the admin user") 36 | return 37 | 38 | let salt = makeSalt() 39 | let password = makePassword(iPwd, salt) 40 | 41 | discard insertID(db, sql"INSERT INTO person (name, email, password, salt, status) VALUES (?, ?, ?, ?, ?)", $iName, $iEmail, password, salt, "Admin") 42 | 43 | echo("User: Admin added! Moving on..") 44 | else: 45 | echo("User: Admin user already exists. Skipping it.") -------------------------------------------------------------------------------- /nimhapkg/resources/users/user_check.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | from jester import Request 3 | 4 | type 5 | Userrank* = enum 6 | Normal 7 | Moderator 8 | Admin 9 | Deactivated 10 | NotLoggedin 11 | 12 | type 13 | Session* = object of RootObj 14 | loggedIn*: bool 15 | username*, userpass*, email*: string 16 | 17 | TData* = ref object of Session 18 | req*: jester.Request 19 | userid*: string # User ID 20 | timezone*: string # User timezone 21 | rank*: Userrank # User status (rank) 22 | 23 | 24 | -------------------------------------------------------------------------------- /nimhapkg/resources/utils/common.nim: -------------------------------------------------------------------------------- 1 | 2 | import parsecfg 3 | from os import getAppDir, `/` 4 | from strutils import replace 5 | 6 | proc loadConf*(modulename: string): Config = 7 | ## Load config for the main daemon or a module 8 | var fn = "" 9 | when defined(dev) or not defined(systemInstall): 10 | if modulename == "": 11 | # main daemon 12 | fn = getAppDir() & "/config/nimha_dev.cfg" 13 | else: 14 | fn = replace(getAppDir(), "/nimhapkg/mainmodules", "") & "/config/nimha_dev.cfg" 15 | else: 16 | fn = "/etc/nimha/nimha.cfg" 17 | 18 | echo("Reading cfg file " & fn) 19 | loadConfig(fn) 20 | 21 | #installpath 22 | const systmp = "/var/run/nimha/tmp" 23 | 24 | proc getTmpDir*(modulename = ""): string = 25 | ## Temporary directory, not persistent across restarts 26 | when defined(dev) or not defined(systemInstall): 27 | replace(getAppDir(), "/nimhapkg/mainmodules", "") / "/tmp" 28 | else: 29 | if modulename == "": 30 | systmp / "nimha" 31 | else: 32 | # TODO: check for path traversal? 33 | systmp / modulename 34 | 35 | proc getNimbleCache*(): string = 36 | ## Get Nimble cache 37 | when defined(dev) or not defined(systemInstall): 38 | replace(getAppDir(), "/nimhapkg/mainmodules", "") / "/nimblecache" 39 | else: 40 | #installpath 41 | "/var/run/nimha/nimblecache" 42 | 43 | -------------------------------------------------------------------------------- /nimhapkg/resources/utils/dates.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import strutils, times 4 | 5 | 6 | 7 | const monthNames = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 8 | 9 | 10 | proc currentDatetime*(formatting: string): string= 11 | ## Getting the current local time 12 | 13 | if formatting == "full": 14 | result = format(local(getTime()), "yyyy-MM-dd HH:mm:ss") 15 | elif formatting == "date": 16 | result = format(local(getTime()), "yyyy-MM-dd") 17 | elif formatting == "compact": 18 | result = format(local(getTime()), "yyyyMMdd") 19 | elif formatting == "year": 20 | result = format(local(getTime()), "yyyy") 21 | elif formatting == "month": 22 | result = format(local(getTime()), "MM") 23 | elif formatting == "day": 24 | result = format(local(getTime()), "dd") 25 | elif formatting == "time": 26 | result = format(local(getTime()), "HH:mm:ss") 27 | 28 | 29 | 30 | proc getDaysInMonthU*(month, year: int): int = 31 | ## Gets the number of days in the month and year 32 | ## 33 | ## Examples: 34 | ## 35 | runnableExamples: 36 | doAssert getDaysInMonthU(02, 2018) == 28 37 | doAssert getDaysInMonthU(10, 2020) == 31 38 | if month notin {1..12}: 39 | echo "Wrong format" 40 | else: 41 | result = getDaysInMonth(Month(month), year) 42 | 43 | 44 | 45 | 46 | proc dateEpoch*(date, format: string): int64 = 47 | ## Transform a date in user format to epoch 48 | ## Does not utilize timezone 49 | ## 50 | ## Examples: 51 | ## 52 | runnableExamples: 53 | doAssert dateEpoch("2018-02-18", "YYYY-MM-DD") == "1518908400" 54 | 55 | try: 56 | case format 57 | of "YYYYMMDD": 58 | return toUnix(toTime(parse(date, "yyyyMMdd"))) 59 | of "YYYY-MM-DD": 60 | return toUnix(toTime(parse(date, "yyyy-MM-dd"))) 61 | of "YYYY-MM-DD HH:mm": 62 | return toUnix(toTime(parse(date, "yyyy-MM-dd HH:mm"))) 63 | of "DD-MM-YYYY": 64 | return toUnix(toTime(parse(date, "dd-MM-yyyy"))) 65 | else: 66 | return 0 67 | except: 68 | 69 | return 0 70 | 71 | 72 | 73 | proc epochDate*(epochTime, format: string, timeZone = "0"): string = 74 | ## Transform epoch to user formatted date 75 | ## 76 | ## Examples: 77 | ## 78 | runnableExamples: 79 | doAssert epochDate("1522995050", "YYYY-MM-DD HH:mm", "2") == "2018-04-06 - 08:10" 80 | 81 | 82 | if epochTime == "": 83 | return "" 84 | 85 | try: 86 | case format 87 | of "YYYY": 88 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 89 | return toTime.substr(0, 3) 90 | 91 | of "YYYY_MM_DD-HH_mm": 92 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 93 | return toTime.substr(0, 3) & "_" & toTime.substr(5, 6) & "_" & toTime.substr(8, 9) & "-" & toTime.substr(11, 12) & "_" & toTime.substr(14, 15) 94 | 95 | of "YYYY MM DD": 96 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 97 | return toTime.substr(0, 3) & " " & toTime.substr(5, 6) & " " & toTime.substr(8, 9) 98 | 99 | of "YYYY-MM-DD": 100 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 101 | return toTime.substr(0, 9) 102 | 103 | of "YYYY-MM-DD HH:mm": 104 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 105 | return toTime.substr(0, 9) & " - " & toTime.substr(11, 15) 106 | 107 | of "DD MMM HH:mm": 108 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 109 | return toTime.substr(8, 9) & " " & monthNames[parseInt(toTime.substr(5, 6))] & " " & toTime.substr(11, 15) 110 | 111 | of "DD MM YYYY": 112 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 113 | return toTime.substr(8, 9) & " " & toTime.substr(5, 6) & " " & toTime.substr(0, 3) 114 | 115 | of "DD": 116 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 117 | return toTime.substr(8, 9) 118 | 119 | of "MMM DD": 120 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 121 | return monthNames[parseInt(toTime.substr(5, 6))] & " " & toTime.substr(8, 9) 122 | 123 | of "MMM": 124 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 125 | return monthNames[parseInt(toTime.substr(5, 6))] 126 | 127 | else: 128 | let toTime = $(utc(fromUnix(parseInt(epochTime))) + initInterval(hours=parseInt(timeZone))) 129 | return toTime.substr(0, 9) 130 | 131 | except: 132 | return "" 133 | 134 | -------------------------------------------------------------------------------- /nimhapkg/resources/utils/log_utils.nim: -------------------------------------------------------------------------------- 1 | import os, logging, strutils, re, times 2 | 3 | let logFile = replace(getAppDir(), "/nimhapkg/mainmodules", "") & "/log/log.log" 4 | 5 | discard existsOrCreateDir(replace(getAppDir(), "/nimhapkg/mainmodules", "") & "/log") 6 | if not fileExists(logFile): open(logFile, fmWrite).close() 7 | 8 | #var console_logger = newConsoleLogger(fmtStr = verboseFmtStr) # Logs to terminal. 9 | when defined(release): 10 | var rolling_file_logger = newRollingFileLogger(logFile, levelThreshold = lvlWarn, mode = fmReadWriteExisting, fmtStr = verboseFmtStr) 11 | else: 12 | var rolling_file_logger = newRollingFileLogger(logFile, mode = fmReadWriteExisting, fmtStr = verboseFmtStr) 13 | 14 | addHandler(rolling_file_logger) 15 | 16 | template echoLog(element, level, msg: string) = 17 | echo $now() & " [" & level & "] [" & element & "] " & msg 18 | 19 | 20 | proc logit*(element, level, msg: string) = 21 | ## Debug information 22 | 23 | if level in ["WARNING"]: 24 | warn("[" & element & "] - " & msg) 25 | 26 | if level in ["WARNING", "ERROR"]: 27 | error("[" & element & "] - " & msg) 28 | 29 | when defined(logoutput) or defined(logxiaomi): 30 | if element == "xiaomi": echoLog(element, level, msg) 31 | 32 | when defined(logoutput) or defined(logcron): 33 | if element == "cron": echoLog(element, level, msg) 34 | 35 | when defined(logoutput) or defined(logwsgateway): 36 | if element == "WSgateway": echoLog(element, level, msg) 37 | 38 | when defined(logoutput) or defined(loggateway): 39 | if element == "gateway": echoLog(element, level, msg) 40 | 41 | when defined(logoutput) or defined(logwebsocket): 42 | if element == "websocket": echoLog(element, level, msg) 43 | 44 | when defined(logoutput) or defined(logalarm): 45 | if element == "alarm": echoLog(element, level, msg) -------------------------------------------------------------------------------- /nimhapkg/resources/utils/parsers.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import json 4 | 5 | 6 | template jn*(json: JsonNode, data: string): string = 7 | ## Avoid error in parsing JSON 8 | 9 | try: 10 | json[data].getStr() 11 | except: 12 | "" 13 | 14 | template jnInt*(json: JsonNode, data: string): int = 15 | ## Avoid error in parsing JSON 16 | 17 | try: 18 | json[data].getInt() 19 | except: 20 | 0 21 | 22 | template jnFloat*(json: JsonNode, data: string): float = 23 | ## Avoid error in parsing JSON 24 | 25 | try: 26 | json[data].getFloat() 27 | except: 28 | 0 29 | 30 | template js*(data: string): JsonNode = 31 | ## Avoid error in parsing JSON 32 | 33 | try: 34 | parseJson(data) 35 | except: 36 | parseJson("{}") -------------------------------------------------------------------------------- /nimhapkg/resources/www/google_recaptcha.nim: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - Thomas T. Jarløv 2 | 3 | import recaptcha, parsecfg, asyncdispatch 4 | 5 | from os import getAppDir 6 | from strutils import replace 7 | import ../../resources/utils/common 8 | 9 | 10 | var 11 | useCaptcha*: bool 12 | captcha*: ReCaptcha 13 | 14 | 15 | # Using config.ini 16 | let dict = loadConf("google_recaptcha") 17 | 18 | # Web settings 19 | let recaptchaSecretKey = dict.getSectionValue("reCAPTCHA","Secretkey") 20 | let recaptchaSiteKey* = dict.getSectionValue("reCAPTCHA","Sitekey") 21 | 22 | 23 | proc setupReCapthca*() = 24 | # Activate Google reCAPTCHA 25 | if len(recaptchaSecretKey) > 0 and len(recaptchaSiteKey) > 0: 26 | useCaptcha = true 27 | captcha = initReCaptcha(recaptchaSecretKey, recaptchaSiteKey) 28 | 29 | else: 30 | useCaptcha = false 31 | 32 | 33 | proc checkReCaptcha*(antibot, userIP: string): Future[bool] {.async.} = 34 | if useCaptcha: 35 | var captchaValid: bool = false 36 | try: 37 | captchaValid = await captcha.verify(antibot, userIP) 38 | except: 39 | captchaValid = false 40 | 41 | if not captchaValid: 42 | return false 43 | 44 | else: 45 | return true 46 | 47 | else: 48 | return true 49 | -------------------------------------------------------------------------------- /nimhapkg/tmpl/alarm.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | #proc genAlarm(c: var TData): string = 7 | # result = "" 8 | <head> 9 | ${genMainHead(c)} 10 | </head> 11 | 12 | <body> 13 | <header> 14 | ${genMainHeader()} 15 | </header> 16 | 17 | <main> 18 | <div id="pageType" data-userid="${c.userid}" data-userstatus="${c.rank}" data-type="alarm" style="display: none;"></div> 19 | <div class="wrapper"> 20 | ${genMainSidebar()} 21 | 22 | <div id="pagewrapper"> 23 | <div id="alarm"> 24 | 25 | <h1>Alarm</h1> 26 | 27 | <p>Go to the <a href="/settings/alarmlog">alarm log here</a>.</p> 28 | 29 | # let alarmCountdown = getValue(dbAlarm, sql"SELECT value FROM alarm_settings WHERE element = ?", "countdown") 30 | # let alarmArmtime = getValue(dbAlarm, sql"SELECT value FROM alarm_settings WHERE element = ?", "armtime") 31 | <div class="alarmDetails"> 32 | <h3>Settings</h3> 33 | <div> 34 | <label>Alarm countdown (seconds)</label> 35 | <input class="alarmCountdown form-control form-control-sm" value="$alarmCountdown" /> 36 | <button class="btn btn-primary alarmCountdownUpdate">Update</button> 37 | </div> 38 | 39 | <br> 40 | 41 | <div> 42 | <label>Arm time (time to get out)</label> 43 | <input class="alarmArmtime form-control form-control-sm" value="$alarmArmtime" /> 44 | <button class="btn btn-primary alarmArmtimeUpdate">Update</button> 45 | </div> 46 | </div> 47 | 48 | <hr> 49 | 50 | <div class="alarmPasswords"> 51 | # let allPasswords = getAllRows(dbAlarm, sql"SELECT id, name, creation FROM alarm_password") 52 | 53 | <h3>Alarm password</h3> 54 | <h5>Users with access to manage the alarm (only digits!)</h5> 55 | <h5>Admin user can change status without password.</h5> 56 | 57 | # if c.rank == Admin: 58 | # let allUsers = getAllRows(db, sql"SELECT id, name FROM person") 59 | <form method="GET" action="/alarm/do?action=adduser"> 60 | <input name="action" class="hide" value="adduser" /> 61 | <select name="userid" class="form-control form-control-sm"> 62 | # for person in allUsers: 63 | <option value="${person[0]}">${person[1]}</option> 64 | # end for 65 | </select> 66 | <input name="password" class="password form-control form-control-sm" type="password" placeholder="Password" style="text-align: center" required /> 67 | <button type="submit" class="btn btn-primary">Save new user</button> 68 | </form> 69 | # end if 70 | 71 | # if allPasswords.len() > 0: 72 | <table class="alarmPasswords table table-bordered table-hover"> 73 | <thead> 74 | <tr class="thead-dark"> 75 | <th>Username</th> 76 | <th>Creation</th> 77 | <th></th> 78 | </tr> 79 | </thead> 80 | <tbody> 81 | # for device in allPasswords: 82 | <tr class="device"> 83 | <td> 84 | ${device[1]} 85 | </td> 86 | <td> 87 | ${epochDate(device[2], "YYYY-MM-DD HH:mm")} 88 | </td> 89 | <td data-userid="${device[0]}" class="btn btn-danger alarmDeletePassword"> 90 | Del 91 | </td> 92 | </tr> 93 | # end for 94 | </tbody> 95 | </table> 96 | # else: 97 | <div>Theres no users with assigned passwords</div> 98 | # end if 99 | </div> 100 | 101 | <hr> 102 | 103 | <div class="alarmActions"> 104 | # let allActions = getAllRows(dbAlarm, sql"SELECT id, alarmstate, action, action_name, action_ref FROM alarm_actions ORDER BY action") 105 | # 106 | # let pushAlarm = getAllRows(dbPushbullet, sql"SELECT id, name FROM pushbullet_templates") 107 | # 108 | # let mailAlarm = getAllRows(dbMail, sql"SELECT id, name FROM mail_templates") 109 | # 110 | # let mqttActions = getAllRows(dbMqtt, sql"SELECT id, name FROM mqtt_templates") 111 | # 112 | # let osActions = getAllRows(dbOs, sql"SELECT id, name FROM os_templates") 113 | # 114 | # let rpiActions = getAllRows(dbRpi, sql"SELECT id, name FROM rpi_templates") 115 | # 116 | # let xiaomiAlarm = getAllRows(dbXiaomi, sql"SELECT xda.id, xda.name, xd.name FROM xiaomi_templates AS xda LEFT JOIN xiaomi_devices AS xd ON xd.sid = xda.sid ORDER BY xda.name") 117 | # 118 | # var select = "" 119 | # 120 | # for push in pushAlarm: 121 | # select.add("<option value='" & push[0] & "' data-element='pushbullet'>Pushbullet: " & push[1] & "</option>") 122 | # end for 123 | # 124 | # for mail in mailAlarm: 125 | # select.add("<option value='" & mail[0] & "' data-element='mail'>Mail: " & mail[1] & "</option>") 126 | # end for 127 | # 128 | # for os in osActions: 129 | # select.add("<option value='" & os[0] & "' data-element='os'>OS: " & os[1] & "</option>") 130 | # end for 131 | # 132 | # for mqtt in mqttActions: 133 | # select.add("<option value='" & mqtt[0] & "' data-element='mqtt'>MQTT: " & mqtt[1] & "</option>") 134 | # end for 135 | # 136 | # for rpi in rpiActions: 137 | # select.add("<option value='" & rpi[0] & "' data-element='rpi'>RPi: " & rpi[1] & "</option>") 138 | # end for 139 | # 140 | # for xiaomi in xiaomiAlarm: 141 | # select.add("<option value='" & xiaomi[0] & "' data-element='xiaomi'>Xiaomi: " & xiaomi[1] & " (" & xiaomi[2] & ")</option>") 142 | # end for 143 | 144 | <h3>Alarm actions</h3> 145 | <table class="alarmActions table table-bordered table-hover"> 146 | <thead> 147 | <tr class="thead-dark"> 148 | <th>Alarm state</th> 149 | <th>Action</th> 150 | <th>Name</th> 151 | <th></th> 152 | </tr> 153 | <tr class="alarmItemAdd"> 154 | <td> 155 | <select name="alarmstate" class="alarmstate form-control form-control-sm"> 156 | <option value="disarmed">Disarmed</option> 157 | <option value="armAway">Arm away</option> 158 | <option value="armHome">Arm home</option> 159 | <option value="triggered">Triggered</option> 160 | <option value="ringing">Ringing</option> 161 | </select> 162 | </td> 163 | <td> 164 | <select name="alarmaction" class="alarmaction form-control form-control-sm"> 165 | $select 166 | </select> 167 | </td> 168 | <td> 169 | 170 | </td> 171 | <td class="btn btn-success alarmActionAdd"> 172 | Add 173 | </td> 174 | </tr> 175 | </thead> 176 | 177 | <tbody> 178 | # var rowBefore = "" 179 | # 180 | # for action in allActions: 181 | # if rowBefore != action[2]: 182 | <tr><td colspan="4" style="background-color: #bababa;"></td></tr> 183 | # end if 184 | # 185 | # rowBefore = action[2] 186 | 187 | <tr> 188 | <td>${action[1]}</td> 189 | <td>${action[2]}</td> 190 | <td>${action[3]}</td> 191 | <td data-actionid="${action[0]}" class="btn btn-danger alarmDeleteAction">Del</td> 192 | </tr> 193 | # end for 194 | </tbody> 195 | </table> 196 | </div> 197 | </div> 198 | </div> 199 | </div> 200 | </main> 201 | 202 | <footer> 203 | ${genMainFooter()} 204 | </footer> 205 | 206 | ${genMainNotify()} 207 | 208 | </body> 209 | 210 | 211 | #end proc 212 | # 213 | # 214 | # 215 | # 216 | # 217 | #proc genAlarmCode(c: var TData): string = 218 | # result = "" 219 | <head> 220 | ${genMainHead(c)} 221 | </head> 222 | 223 | <body> 224 | <nav id="navbar" style="display: none;"></nav> 225 | <main> 226 | <div id="pageType" data-userid="${c.userid}" data-userstatus="${c.rank}" data-type="alarmNumpad" style="display: none;"></div> 227 | 228 | <div class="wrapper"> 229 | ${genAlarmNumpad(c, true)} 230 | </div> 231 | 232 | </main> 233 | 234 | ${genMainNotify()} 235 | 236 | </body> 237 | #end if -------------------------------------------------------------------------------- /nimhapkg/tmpl/alarm_numpad.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | # 8 | # 9 | # 10 | #proc genAlarmNumpad(c: var TData, onlyCode = false): string = 11 | # result = "" 12 | # let alarmStatus = getValue(dbAlarm, sql"SELECT status FROM alarm WHERE id = ?", "1") 13 | # 14 | # var alarmPretty = "" 15 | # if alarmStatus == "disarmed": 16 | # alarmPretty = "Disarmed" 17 | # elif alarmStatus == "armAway": 18 | # alarmPretty = "Armed away" 19 | # elif alarmStatus == "armAway": 20 | # alarmPretty = "Armed away" 21 | # elif alarmStatus == "ringing": 22 | # alarmPretty = "Ringing" 23 | # elif alarmStatus == "triggered": 24 | # alarmPretty = "Triggered" 25 | # else: 26 | # alarmPretty = alarmStatus 27 | # end if 28 | 29 | 30 | <div class="modal fade" id="alarmModel" tabindex="-1" role="dialog" aria-labelledby="alarmModalLabel" aria-hidden="true"> 31 | <div class="modal-dialog" role="document"> 32 | <div class="modal-content"> 33 | <div class="modal-header"> 34 | <h5 class="modal-title" id="alarmModalLabel">Alarm password</h5> 35 | <button type="button" class="close" data-dismiss="modal" aria-label="Close"> 36 | <span aria-hidden="true">×</span> 37 | </button> 38 | </div> 39 | <div class="modal-body"> 40 | 41 | <div id="alarmNumpad" data-status="" data-onlycode="$onlyCode"> 42 | <div class="container-fluid"> 43 | <div class="row"> 44 | <div class="col-12 inner"> 45 | 46 | # if onlyCode: 47 | <label>Current status: <span class="currentAlarm">${alarmPretty}</span></label> 48 | <select class="onlyCode form-control form-control-sm"> 49 | <option value="disarmed">Disarm alarm</option> 50 | <option value="armAway">Arm away</option> 51 | <option value="armHome">Arm home</option> 52 | </select> 53 | <br> 54 | # end if 55 | 56 | # if c.rank != Admin: 57 | <div class="row"> 58 | <div class="col-12"> 59 | <input type="password" disabled="disabled" class="form-control form-control-sm password" /> 60 | </div> 61 | </div> 62 | 63 | <!-- Row 1 --> 64 | <div class="row"> 65 | <div class="col-4"> 66 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="1">1</button> 67 | </div> 68 | 69 | <div class="col-4"> 70 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="2">2</button> 71 | </div> 72 | 73 | <div class="col-4"> 74 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="3">3</button> 75 | </div> 76 | </div> 77 | 78 | <!-- Row 2 --> 79 | <div class="row"> 80 | <div class="col-4"> 81 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="4">4</button> 82 | </div> 83 | 84 | <div class="col-4"> 85 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="5">5</button> 86 | </div> 87 | 88 | <div class="col-4"> 89 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="6">6</button> 90 | </div> 91 | </div> 92 | 93 | <!-- Row 3 --> 94 | <div class="row"> 95 | <div class="col-4"> 96 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="7">7</button> 97 | </div> 98 | 99 | <div class="col-4"> 100 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="8">8</button> 101 | </div> 102 | 103 | <div class="col-4"> 104 | <button type="button" class="btn btn-primary btn-sm alarmpad" data-num="9">9</button> 105 | </div> 106 | </div> 107 | 108 | # end if 109 | 110 | </div> 111 | </div> 112 | </div> 113 | </div> 114 | 115 | </div> 116 | <div class="modal-footer"> 117 | <button type="button" class="btn btn-secondary alarmSubmitCancel" data-dismiss="modal">Close</button> 118 | <button type="button" class="btn btn-success alarmSubmit">Submit</button> 119 | </div> 120 | </div> 121 | </div> 122 | </div> 123 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/certificates.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | #proc genCertificates(c: var TData): string = 8 | # result = "" 9 | <head> 10 | ${genMainHead(c)} 11 | </head> 12 | 13 | <body> 14 | <header> 15 | ${genMainHeader()} 16 | </header> 17 | 18 | <main> 19 | <div id="pageType" data-userid="${c.userid}" data-type="certificates" style="display: none;"></div> 20 | <div class="wrapper"> 21 | ${genMainSidebar()} 22 | 23 | # let allCerts = getAllRows(dbWeb, sql"SELECT id, name, url, port, creation FROM certificates ORDER BY name ASC") 24 | 25 | <div id="pagewrapper"> 26 | <div id="certificates"> 27 | 28 | <h1>Certificates</h1> 29 | 30 | <div class="certList"> 31 | <table class="certList table"> 32 | <thead> 33 | <tr class="thead-dark"> 34 | <th>Name</th> 35 | <th>URL</th> 36 | <th>Port</th> 37 | <th>Creation</th> 38 | <th></th> 39 | </tr> 40 | <tr class="certItemEdit"> 41 | <td> 42 | <input class="name" /> 43 | </td> 44 | <td> 45 | <input class="url" /> 46 | </td> 47 | <td> 48 | <input class="port" /> 49 | </td> 50 | <td> 51 | <input disabled="disabled" /> 52 | </td> 53 | <td class="btn btn-success certAddNew"> 54 | Add 55 | </td> 56 | </tr> 57 | </thead> 58 | <tbody> 59 | # for cert in allCerts: 60 | <tr class="device" data-id="${cert[0]}"> 61 | <td> 62 | ${cert[1]} 63 | </td> 64 | <td> 65 | ${cert[2]} 66 | </td> 67 | <td> 68 | ${cert[3]} 69 | </td> 70 | <td> 71 | ${epochDate(cert[4], "YYYY-MM-DD HH:mm")} 72 | </td> 73 | <td data-certid="${cert[0]}" class="btn btn-danger certDelete"> 74 | Del 75 | </td> 76 | </tr> 77 | # end for 78 | </tbody> 79 | </table> 80 | </div> 81 | </div> 82 | </div> 83 | </div> 84 | </main> 85 | 86 | <footer> 87 | ${genMainFooter()} 88 | </footer> 89 | 90 | ${genMainNotify()} 91 | 92 | </body> 93 | 94 | 95 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/cron.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | #proc genCron(c: var TData): string = 7 | # result = "" 8 | <head> 9 | ${genMainHead(c)} 10 | </head> 11 | 12 | <body> 13 | <header> 14 | ${genMainHeader()} 15 | </header> 16 | 17 | <main> 18 | <div id="pageType" data-userid="${c.userid}" data-type="cron" style="display: none;"></div> 19 | <div class="wrapper"> 20 | ${genMainSidebar()} 21 | 22 | <div id="pagewrapper"> 23 | <div id="cron"> 24 | 25 | <h1>Cron jobs</h1> 26 | 27 | <div class="cronActions"> 28 | # let allActions = getAllRows(dbCron, sql"SELECT id, element, action_name, action_ref, time, active FROM cron_actions") 29 | # 30 | # let pushAlarm = getAllRows(dbPushbullet, sql"SELECT id, name FROM pushbullet_templates") 31 | # 32 | # let mailAlarm = getAllRows(dbMail, sql"SELECT id, name FROM mail_templates") 33 | # 34 | # let mqttActions = getAllRows(dbMqtt, sql"SELECT id, name FROM mqtt_templates") 35 | # 36 | # let osActions = getAllRows(dbOs, sql"SELECT id, name FROM os_templates") 37 | # 38 | # let rpiActions = getAllRows(dbRpi, sql"SELECT id, name FROM rpi_templates") 39 | # 40 | # let xiaomiAlarm = getAllRows(dbXiaomi, sql"SELECT xda.id, xda.name, xd.name FROM xiaomi_templates AS xda LEFT JOIN xiaomi_devices AS xd ON xd.sid = xda.sid ORDER BY xda.name") 41 | # 42 | # var select = "" 43 | # 44 | # for push in pushAlarm: 45 | # select.add("<option value='" & push[0] & "' data-element='pushbullet'>Pushbullet: " & push[1] & "</option>") 46 | # end for 47 | # 48 | # for mail in mailAlarm: 49 | # select.add("<option value='" & mail[0] & "' data-element='mail'>Mail: " & mail[1] & "</option>") 50 | # end for 51 | # 52 | # for mqtt in mqttActions: 53 | # select.add("<option value='" & mqtt[0] & "' data-element='mqtt'>MQTT: " & mqtt[1] & "</option>") 54 | # end for 55 | # 56 | # for os in osActions: 57 | # select.add("<option value='" & os[0] & "' data-element='os'>OS: " & os[1] & "</option>") 58 | # end for 59 | # 60 | # for rpi in rpiActions: 61 | # select.add("<option value='" & rpi[0] & "' data-element='rpi'>RPi: " & rpi[1] & "</option>") 62 | # end for 63 | # 64 | # for xiaomi in xiaomiAlarm: 65 | # select.add("<option value='" & xiaomi[0] & "' data-element='xiaomi'>Xiaomi: " & xiaomi[1] & " (" & xiaomi[2] & ")</option>") 66 | # end for 67 | 68 | <p>Time is set with 24H format and specified with hour and minut: HH:mm (e.g. 22:02, 23:55)</p> 69 | 70 | <table class="cronActions table table-bordered table-hover"> 71 | <thead> 72 | <tr class="thead-dark"> 73 | <th>Element</th> 74 | <th>Name</th> 75 | <th>Time</th> 76 | <th></th> 77 | </tr> 78 | <tr class="cronItemAdd"> 79 | <td colspan="2"> 80 | <select name="cronaction" class="cronaction form-control form-control-sm"> 81 | $select 82 | </select> 83 | </td> 84 | <td class="time"> 85 | <input name="crontime" class="crontime form-control form-control-sm" /> 86 | </td> 87 | <td class="btn btn-success cronActionAdd"> 88 | Add 89 | </td> 90 | </tr> 91 | </thead> 92 | 93 | <tbody> 94 | # var rowBefore = "" 95 | # 96 | # for action in allActions: 97 | # if rowBefore != action[2]: 98 | <tr><td colspan="4" style="background-color: #bababa;"></td></tr> 99 | # end if 100 | # 101 | # rowBefore = action[2] 102 | 103 | <tr> 104 | <td>${action[1]}</td> 105 | <td>${action[2]}</td> 106 | <td class="time">${action[4]}</td> 107 | <td data-cronid="${action[0]}" class="btn btn-danger cronDeleteAction">Del</td> 108 | </tr> 109 | # end for 110 | </tbody> 111 | </table> 112 | </div> 113 | </div> 114 | </div> 115 | </div> 116 | </main> 117 | 118 | <footer> 119 | ${genMainFooter()} 120 | </footer> 121 | 122 | ${genMainNotify()} 123 | 124 | </body> 125 | 126 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/dashboard.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | #proc cardAlarm(c: var TData): string = 7 | # result = "" 8 | <div data-id="cardAlarm" class="cardAlarm item col-12 col-sm-6 col-md-6 col-lg-3"> 9 | <div class="inner"> 10 | # let alarmStatus = getValue(dbAlarm, sql"SELECT status FROM alarm") 11 | # template alarmClass(status: string): string = 12 | # if status == alarmStatus: 13 | # if alarmStatus == "disarmed": 14 | # "badge-success" 15 | # else: 16 | # "badge-danger" 17 | # end if 18 | # else: 19 | # "badge-secondary" 20 | # end if 21 | # end template 22 | # 23 | # template alarmText(status: string): string = 24 | # if status == alarmStatus: 25 | # "True" 26 | # else: 27 | # "False" 28 | # end if 29 | # end template 30 | # 31 | # var canActivate = "" 32 | 33 | <div class="alarm"> 34 | <div class="heading"> 35 | <div class="icon-handle"></div> 36 | <div>Alarm status</div> 37 | </div> 38 | <div class="alarmInner"> 39 | <div class="disarmed"> 40 | <label>Disarmed</label> 41 | <span class="status disarmed badge ${alarmClass("disarmed")}">${alarmText("disarmed")}</span> 42 | # canActivate = "" 43 | # if alarmText("disarmed") == "True": 44 | # canActivate = "hide" 45 | # end if 46 | <button data-status="disarmed" class="$canActivate activate badge badge-warning">Activate</button> 47 | </div> 48 | <div class="armAway"> 49 | <label>Armed away</label> 50 | <span class="status armAway badge ${alarmClass("armAway")}">${alarmText("armAway")}</span> 51 | # canActivate = "" 52 | # if alarmText("armAway") == "True": 53 | # canActivate = "hide" 54 | # end if 55 | <button data-status="armAway" class="$canActivate activate badge badge-warning">Activate</button> 56 | </div> 57 | <div class="armHome"> 58 | <label>Armed home</label> 59 | <span class="status armHome badge ${alarmClass("armHome")}">${alarmText("armHome")}</span> 60 | # canActivate = "" 61 | # if alarmText("armHome") == "True": 62 | # canActivate = "hide" 63 | # end if 64 | <button data-status="armHome" class="$canActivate activate badge badge-warning">Activate</button> 65 | </div> 66 | <div class="triggered"> 67 | <label>Triggered</label> 68 | <span class="status triggered badge ${alarmClass("triggered")}">${alarmText("triggered")}</span> 69 | </div> 70 | <div class="ringing"> 71 | <label>Ringing</label> 72 | <span class="status ringing badge ${alarmClass("ringing")}">${alarmText("ringing")}</span> 73 | </div> 74 | </div> 75 | </div> 76 | </div> 77 | </div> 78 | #end proc 79 | # 80 | # 81 | # 82 | # 83 | #proc cardCertificates(c: var TData): string = 84 | # result = "" 85 | # let certificates = getAllRows(dbWeb, sql"SELECT name, url, port FROM certificates ORDER BY name ASC") 86 | # if certificates.len() > 0: 87 | <div data-id="cardCertificates" class="cardCertificates item col-12 col-sm-6 col-md-6 col-lg-3"> 88 | <div class="inner"> 89 | <div class="certexpiry"> 90 | <div class="heading"> 91 | <div class="icon-handle"></div> 92 | <div>Cert expiration</div> 93 | </div> 94 | <div class="certexpiryInner"> 95 | # for cert in certificates: 96 | 97 | <div class="cert"> 98 | <label>${cert[0]}</label> 99 | <div class="days ${replace(cert[1], ".", "")}"></div> 100 | <span data-server="${cert[1]}" data-port="${cert[2]}" class="certRefresh badge badge-primary">Refresh</span> 101 | </div> 102 | 103 | # end for 104 | </div> 105 | </div> 106 | </div> 107 | </div> 108 | # end if 109 | #end proc 110 | # 111 | # 112 | # 113 | # 114 | #proc cardPushbullet(c: var TData): string = 115 | # result = "" 116 | # let pushAll = getAllRows(dbPushbullet, sql"SELECT id, name FROM pushbullet_templates") 117 | # if pushAll.len() > 0: 118 | <div data-id="cardPushbullet" class="cardPushbullet item col-12 col-sm-6 col-md-6 col-lg-3"> 119 | <div class="inner"> 120 | <div class="pushbullet"> 121 | <div class="heading"> 122 | <div class="icon-handle"></div> 123 | <div>Pushbullet</div> 124 | </div> 125 | <div class="pushbulletInner"> 126 | # for push in pushAll: 127 | <div> 128 | <label>${push[1]}</label> 129 | <span data-pushid="${push[0]}" class="badge badge-primary pushbulletSend">Send</span> 130 | </div> 131 | # end for 132 | </div> 133 | </div> 134 | </div> 135 | </div> 136 | # end if 137 | #end proc 138 | # 139 | # 140 | # 141 | # 142 | #proc cardXiaomiDevice(c: var TData): string = 143 | # result = "" 144 | # let xiaomiDevices = getAllRows(dbXiaomi, sql"SELECT xdd.sid, xd.name, xdd.value_name, xdd.action FROM xiaomi_devices_data AS xdd LEFT JOIN xiaomi_devices AS xd ON xd.sid = xdd.sid WHERE xdd.triggerAlarm = ? ORDER BY xd.name ASC", "false") 145 | # if xiaomiDevices.len() > 0: 146 | <div data-id="cardXiaomiDevice" class="cardXiaomiDevice item col-12 col-sm-6 col-md-6 col-lg-3"> 147 | <div class="inner"> 148 | <div class="xiaomi"> 149 | <div class="heading"> 150 | <div class="icon-handle"></div> 151 | <div>Xiaomi device status</div> 152 | </div> 153 | <div class="xiaomiInner"> 154 | # for device in xiaomiDevices: 155 | <div class="${device[0]} device ${device[2]}"> 156 | <label>${device[1]}</label> 157 | <div class="value"></div> 158 | <span data-sid="${device[0]}" data-action="${device[3]}" data-value="${device[2]}" class="xiaomiRefresh badge badge-primary">Refresh</span> 159 | </div> 160 | # end for 161 | </div> 162 | </div> 163 | </div> 164 | </div> 165 | # end if 166 | #end proc 167 | # 168 | # 169 | # 170 | # 171 | #proc cardOwntracks(c: var TData): string = 172 | # result = "" 173 | # if getAllRows(dbOwntracks, sql"SELECT id FROM owntracks_history").len() > 0: 174 | <div data-id="cardOwntracks" class="cardOwntracks item col-12 col-sm-6 col-md-6 col-lg-3"> 175 | <div class="inner"> 176 | <div class="owntracks"> 177 | <div class="heading"> 178 | <div class="icon-handle"></div> 179 | <div>Owntracks map <span class="owntracksRefresh badge badge-primary">Refresh</span></div> 180 | </div> 181 | <div class="owntracksInner"> 182 | <div id="map" style="width: 100%; height: 100%"></div> 183 | </div> 184 | </div> 185 | </div> 186 | </div> 187 | # end if 188 | #end proc 189 | # 190 | # 191 | # 192 | # 193 | #proc cardfilestream(c: var TData, cardNumber = ""): string = 194 | # result = "" 195 | # var filestreams: seq[Row] 196 | # if cardNumber == "": 197 | # filestreams = getAllRows(dbFile, sql"SELECT id, name, url, download, html FROM filestream") 198 | # else: 199 | # filestreams = getAllRows(dbFile, sql"SELECT id, name, url, download, html FROM filestream WHERE id = ?", cardNumber) 200 | # end if 201 | # for stream in filestreams: 202 | <div data-id="cardFilestream-${stream[0]}" class="cardFilestream item col-12 col-sm-6 col-md-6 col-lg-3"> 203 | <div class="inner"> 204 | <div class="filestream"> 205 | <div class="heading"> 206 | <div class="icon-handle"></div> 207 | <div> 208 | <span class="heading">${stream[1]}</span> 209 | <span data-streamid="${stream[0]}" class="filestreamToggle badge badge-primary">Toggle</span> 210 | <span data-streamid="${stream[0]}" class="filestreamUpdate badge badge-primary">Update</span> 211 | <span data-streamid="${stream[0]}" class="filestreamAutoUpdate badge badge-primary">6sec update</span> 212 | </div> 213 | </div> 214 | # var htmlType = "img" 215 | # if stream[4] != "" or stream[4] != "img": 216 | # htmlType = stream[4] 217 | # end if 218 | # if stream[3] == "true": 219 | <$htmlType id="filestream-${stream[0]}" data-url="/filestream/download?url=${stream[2]}" data-toggle="play" src="/filestream/download?url=${stream[2]}"> 220 | # else: 221 | <$htmlType id="filestream-${stream[0]}" data-url="${stream[2]}" data-toggle="play" src="${stream[2]}"> 222 | # end if 223 | </div> 224 | </div> 225 | </div> 226 | # end for 227 | #end proc 228 | # 229 | # 230 | # 231 | # 232 | #proc cardRss(c: var TData): string = 233 | # result = "" 234 | # let rssFeeds = getAllRows(dbRss, sql"SELECT id, url, skip, fields, name FROM rss_feeds") 235 | # for feed in rssFeeds: 236 | <div data-id="cardRss" class="cardRss item col-12 col-sm-6 col-md-6 col-lg-3"> 237 | <div class="inner"> 238 | <div class="rss"> 239 | <div class="heading"> 240 | <div class="icon-handle"></div> 241 | <div>RSS - ${feed[4]} <span data-feedid="${feed[0]}" class="rssRefresh badge badge-primary">Refresh</span></div> 242 | </div> 243 | <div id="rss-${feed[0]}" class="rssInner scrollbar"> 244 | ${rssReadUrl(feed[4], feed[1], feed[3].split(","), parseInt(feed[2]))} 245 | </div> 246 | </div> 247 | </div> 248 | </div> 249 | # end for 250 | #end proc 251 | # 252 | # 253 | # 254 | # 255 | #proc cardWebsocketUsers(c: var TData): string = 256 | # result = "" 257 | <div data-id="cardWebsocketUsers" class="cardWebsocketUsers item col-12 col-sm-6 col-md-6 col-lg-3"> 258 | <div class="inner"> 259 | <div class="wsusers"> 260 | <div class="heading"> 261 | <div class="icon-handle"></div> 262 | <div>Websocket users</div> 263 | </div> 264 | <div class="wsusersInner"> 265 | </div> 266 | <div class="user clone" style="display: none"> 267 | </div> 268 | </div> 269 | </div> 270 | </div> 271 | #end proc 272 | # 273 | # 274 | # 275 | # 276 | #proc cardOsStats(c: var TData): string = 277 | # result = "" 278 | <div data-id="cardOsStats" class="cardOsStats item col-12 col-sm-6 col-md-6 col-lg-3"> 279 | <div class="inner"> 280 | <div class="osstats"> 281 | <div class="heading"> 282 | <div class="icon-handle"></div> 283 | <div> 284 | <span class="heading">OS stats</span> 285 | <span id="osstatsRefresh" class="badge badge-primary">Refresh</span> 286 | </div> 287 | </div> 288 | <div class="osstatsInner"> 289 | <div class="stats"> 290 | <label>Free mem: </label> <span class="freemem"></span> 291 | </div> 292 | <div class="stats"> 293 | <label>Used mem: </label> <span class="usedmem"></span> 294 | </div> 295 | <div class="stats"> 296 | <label>Free swap: </label> <span class="freeswap"></span> 297 | </div> 298 | <div class="stats"> 299 | <label>Used swap: </label> <span class="usedswap"></span> 300 | </div> 301 | <div class="stats"> 302 | <label>Host IP: </label> <span class="hostip"></span> 303 | </div> 304 | <div class="stats"> 305 | <label>Active connections: </label> <span class="connections"></span> 306 | </div> 307 | </div> 308 | </div> 309 | </div> 310 | </div> 311 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/filestream.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | #proc genFilestream(c: var TData): string = 7 | # result = "" 8 | <head> 9 | ${genMainHead(c)} 10 | </head> 11 | 12 | <body> 13 | <header> 14 | ${genMainHeader()} 15 | </header> 16 | 17 | <main> 18 | <div id="pageType" data-userid="${c.userid}" data-type="filestream" style="display: none;"></div> 19 | <div class="wrapper"> 20 | ${genMainSidebar()} 21 | 22 | <div id="pagewrapper"> 23 | <div id="filestream"> 24 | 25 | <h1>Filestreams</h1> 26 | 27 | <p>This enables a stream of a file to your dashboard. This could be a image or video.</p> 28 | 29 | <p>If you are serving a image from a camera at home - which should not be exposed to the internet, you can set the <kbd>Download</kbd> to <kbd>true</kbd>. Then the NimHA will download the image and serve it.</p> 30 | 31 | <p>As a standard, the HTML type will be <kbd>img</kbd>.</p> 32 | 33 | <div class="filestream"> 34 | 35 | <table class="filestream table table-bordered table-hover"> 36 | <thead> 37 | <tr class="thead-dark"> 38 | <th>Name</th> 39 | <th>URL</th> 40 | <th>Download</th> 41 | <th>HTML type</th> 42 | <th></th> 43 | </tr> 44 | <tr class="filestreamItemAdd"> 45 | <td> 46 | <input name="streamname" class="streamname form-control form-control-sm" /> 47 | </td> 48 | <td> 49 | <input name="streamurl" class="streamurl form-control form-control-sm" /> 50 | </td> 51 | <td> 52 | <input name="streamdownload" class="streamdownload form-control form-control-sm" /> 53 | </td> 54 | <td> 55 | <input name="streamhtml" class="streamhtml form-control form-control-sm" /> 56 | </td> 57 | <td class="btn btn-success filestreamAdd"> 58 | Add 59 | </td> 60 | </tr> 61 | </thead> 62 | 63 | <tbody> 64 | 65 | # let filestreams = getAllRows(dbFile, sql"SELECT id, name, url, download, html FROM filestream") 66 | # for stream in filestreams: 67 | <tr> 68 | <td>${stream[1]}</td> 69 | <td>${stream[2]}</td> 70 | <td>${stream[3]}</td> 71 | <td>${stream[4]}</td> 72 | <td data-streamid="${stream[0]}" class="btn btn-danger streamDelete">Del</td> 73 | </tr> 74 | # end for 75 | 76 | </tbody> 77 | </table> 78 | </div> 79 | </div> 80 | </div> 81 | </div> 82 | </main> 83 | 84 | <footer> 85 | ${genMainFooter()} 86 | </footer> 87 | 88 | ${genMainNotify()} 89 | 90 | </body> 91 | 92 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/mail.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | #proc genMail(c: var TData): string = 8 | # result = "" 9 | <head> 10 | ${genMainHead(c)} 11 | </head> 12 | 13 | <body> 14 | <header> 15 | ${genMainHeader()} 16 | </header> 17 | 18 | <main> 19 | <div id="pageType" data-userid="${c.userid}" data-type="mail" style="display: none;"></div> 20 | <div class="wrapper"> 21 | ${genMainSidebar()} 22 | 23 | # let mailSettings = getRow(dbMail, sql"SELECT address, port, fromaddress, user, password FROM mail_settings WHERE id = ?", "1") 24 | 25 | <div id="pagewrapper"> 26 | <div id="mail"> 27 | 28 | <h1>Mail</h1> 29 | 30 | <div class="mailSettings"> 31 | 32 | <hr> 33 | 34 | <h3>Mail test</h3> 35 | 36 | <div> 37 | <label>Send testmail</label> 38 | <input class="form-control form-control-sm testmail recipient" /> 39 | <button class="btn btn-primary mailTestmail">Send</button> 40 | </div> 41 | 42 | <hr> 43 | 44 | <h3>Mail settings</h3> 45 | 46 | <table class="mailSettings table"> 47 | <thead> 48 | <tr class="thead-dark"> 49 | <th>Address</th> 50 | <th>Port</th> 51 | <th>From</th> 52 | <th>User</th> 53 | <th>Password</th> 54 | <th></th> 55 | </tr> 56 | <tr class="mailSettingsEdit"> 57 | <td> 58 | <input class="form-control form-control-sm address" /> 59 | </td> 60 | <td> 61 | <input class="form-control form-control-sm port" /> 62 | </td> 63 | <td> 64 | <input class="form-control form-control-sm from" /> 65 | </td> 66 | <td> 67 | <input class="form-control form-control-sm user" /> 68 | </td> 69 | <td> 70 | <input class="form-control form-control-sm password" /> 71 | </td> 72 | <td class="btn btn-success mailSettingsUpdate"> 73 | Update 74 | </td> 75 | </tr> 76 | </thead> 77 | <tbody> 78 | <tr class="mail"> 79 | <td> 80 | ${mailSettings[0]} 81 | </td> 82 | <td> 83 | ${mailSettings[1]} 84 | </td> 85 | <td> 86 | ${mailSettings[2]} 87 | </td> 88 | <td> 89 | ${mailSettings[3]} 90 | </td> 91 | <td> 92 | ******** 93 | </td> 94 | <td></td> 95 | </tr> 96 | </tbody> 97 | </table> 98 | </div> 99 | 100 | <hr> 101 | 102 | <div class="mailTemplates"> 103 | # let mailTemplates = getAllRows(dbMail, sql"SELECT id, name, recipient, subject, body FROM mail_templates") 104 | 105 | <h3>Mail templates</h3> 106 | 107 | <table class="mailTemplates table"> 108 | <thead> 109 | <tr class="thead-dark"> 110 | <th>Name</th> 111 | <th>Recipient</th> 112 | <th>Subject</th> 113 | <th>Body</th> 114 | <th></th> 115 | </tr> 116 | <tr class="mailTemplatesEdit"> 117 | <td> 118 | <input class="form-control form-control-sm name" /> 119 | </td> 120 | <td> 121 | <input class="form-control form-control-sm recipient" /> 122 | </td> 123 | <td> 124 | <input class="form-control form-control-sm subject" /> 125 | </td> 126 | <td> 127 | <input class="form-control form-control-sm body" /> 128 | </td> 129 | <td class="btn btn-success mailTemplateAdd"> 130 | Add 131 | </td> 132 | </tr> 133 | </thead> 134 | <tbody> 135 | # for mail in mailTemplates: 136 | <tr class="mail"> 137 | <td> 138 | ${mail[1]} 139 | </td> 140 | <td> 141 | ${mail[2]} 142 | </td> 143 | <td> 144 | ${mail[3]} 145 | </td> 146 | <td> 147 | ${mail[4]} 148 | </td> 149 | <td data-mail="${mail[0]}" class="btn btn-danger mailTemplateDelete"> 150 | Del 151 | </td> 152 | </tr> 153 | # end for 154 | </tbody> 155 | </table> 156 | </div> 157 | 158 | </div> 159 | </div> 160 | </div> 161 | </main> 162 | 163 | <footer> 164 | ${genMainFooter()} 165 | </footer> 166 | 167 | ${genMainNotify()} 168 | 169 | </body> 170 | 171 | 172 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/main.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | # 8 | # 9 | # 10 | #proc genMainHead(c: var TData): string = 11 | # result = "" 12 | <meta name="description" content="Nim Website Creator"> 13 | <meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1.0" /> 14 | 15 | <link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-touch-icon.png"> 16 | <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon/favicon-32x32.png"> 17 | <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon/favicon-16x16.png"> 18 | <link rel="manifest" href="/images/favicon/site.webmanifest"> 19 | <link rel="mask-icon" href="/images/favicon/safari-pinned-tab.svg" color="#5bbad5"> 20 | <link rel="shortcut icon" href="/images/favicon/favicon.ico"> 21 | 22 | <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> 23 | <link rel="stylesheet" href="/css/style.css"> 24 | 25 | <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous" defer></script> 26 | <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous" defer></script> 27 | <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous" defer></script> 28 | 29 | # if c.loggedIn: 30 | <script src="https://cdn.jsdelivr.net/npm/js-cookie@2.2.0/src/js.cookie.min.js" defer></script> 31 | 32 | # if gMapsApi == "": 33 | <script src="http://maps.google.com/maps/api/js" type="text/javascript" defer></script> 34 | # else: 35 | <script src="https://maps.google.com/maps/api/js?key=$gMapsApi" type="text/javascript" defer></script> 36 | # end if 37 | 38 | <script src="/js/script.js" defer></script> 39 | # end if 40 | #end proc 41 | # 42 | # 43 | #proc genMainHeader(): string = 44 | # result = "" 45 | <nav id="navbar" class="navbar navbar-expand-md navbar-dark"> 46 | <div class="heading"> 47 | <a id="sidebarToggle" class="navbar-brand">Nim Home Assistant</a> 48 | </div> 49 | </nav> 50 | #end proc 51 | # 52 | # 53 | #proc genMainSidebar(): string = 54 | # result = "" 55 | <nav id="sidebar"> 56 | <div class="sidebar-header"> 57 | <h3>Menu</h3> 58 | </div> 59 | <ul class="list-unstyled components"> 60 | <li><a href="/">Home</a></li> 61 | <li> 62 | <a href="#submenu1" data-toggle="collapse" aria-expanded="false">Pages</a> 63 | <ul class="collapse list-unstyled" id="submenu1"> 64 | <li><a href="/alarm">- Alarm</a></li> 65 | <li><a href="/certificates">- Certificates</a></li> 66 | <li><a href="/cron">- Cron jobs</a></li> 67 | <li><a href="/mail">- Mail</a></li> 68 | <li><a href="/filestream">- Filestream</a></li> 69 | <li><a href="/mqtt">- MQTT</a></li> 70 | <li><a href="/os">- OS</a></li> 71 | <li><a href="/owntracks">- Owntracks</a></li> 72 | <li><a href="/pushbullet">- Pushbullet</a></li> 73 | <li><a href="/rpi">- Raspberry Pi</a></li> 74 | <li><a href="/rss">- RSS</a></li> 75 | <li><a href="/xiaomi/devices">- Xiaomi devices</a></li> 76 | </ul> 77 | </li> 78 | <li> 79 | <a href="#submenu2" data-toggle="collapse" aria-expanded="false">Settings</a> 80 | <ul class="collapse list-unstyled" id="submenu2"> 81 | <li><a href="/settings/users">- Users</a></li> 82 | <li><a href="/settings/system">- System commands</a></li> 83 | <li><a href="/settings/serverinfo">- Server info</a></li> 84 | <li><a href="/settings/log">- Server log</a></li> 85 | </ul> 86 | </li> 87 | <li><a href="https://github.com/ThomasTJdev/nim_homeassistant">Github</a></li> 88 | <li><a href="/logout">Logout</a></li> 89 | </ul> 90 | </nav> 91 | #end proc 92 | # 93 | # 94 | #proc genMainFooter(): string = 95 | # result = "" 96 | <div id="footerInside"> 97 | <div class="container-fluid"> 98 | <div class="row"> 99 | <div class="col-12 col-md-3 footerLeft"> 100 | <p> 101 | <p>© 2018 - <a href="https://ttj.dk"><u>Thomas T. Jarløv</u></a></p> 102 | </p> 103 | </div> 104 | <div class="col-12 col-md-6 footerMiddle"> 105 | </div> 106 | <div class="col-12 col-md-3 footerRight"> 107 | <p> 108 | <p>License: GPLv3 - <a href="https://github.com/ThomasTJdev/nim_websitecreator"><u>Github</u></a></p> 109 | </p> 110 | </div> 111 | </div> 112 | </div> 113 | </div> 114 | #end proc 115 | # 116 | # 117 | #proc genMainNotify(): string = 118 | # result = "" 119 | <div id="notification"> 120 | <div class="inner"> 121 | Notification 122 | </div> 123 | </div> 124 | #end proc 125 | # 126 | # 127 | # 128 | #proc genMain(c: var TData): string = 129 | # result = "" 130 | # 131 | # 132 | # let cardOrderRaw = "cardAlarm,cardPushbullet,cardCertificates,cardXiaomiDevice,cardOwntracks,cardFilestream,cardRss,cardWebsocketUsers,cardOsStats" 133 | # var cardOrder = "" 134 | # 135 | # if not c.req.cookies.hasKey("dashboardCardOrder"): 136 | # cardOrder = cardOrderRaw 137 | # else: 138 | # ## Uses "%2C" since comma can not be saved in cookie 139 | # var cardOrdertmp = replace(c.req.cookies["dashboardCardOrder"], "%2C", ",") 140 | # cardOrder = foldl(deduplicate(split(cardOrdertmp,",")), a & (b & ","), "") 141 | # end if 142 | # 143 | <head> 144 | <meta name="description" content="Nim Website Creator"> 145 | <meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1.0" /> 146 | 147 | <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> 148 | <link rel="stylesheet" href="/css/style.css"> 149 | 150 | <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous" defer></script> 151 | <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous" defer></script> 152 | <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous" defer></script> 153 | 154 | # if c.loggedIn: 155 | <script src="https://cdn.jsdelivr.net/npm/js-cookie@2.2.0/src/js.cookie.min.js" defer></script> 156 | 157 | # if gMapsApi == "": 158 | <script src="http://maps.google.com/maps/api/js" type="text/javascript" defer></script> 159 | # else: 160 | <script src="https://maps.google.com/maps/api/js?key=$gMapsApi" type="text/javascript" defer></script> 161 | # end if 162 | 163 | <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js" defer></script> 164 | <script src="//cdnjs.cloudflare.com/ajax/libs/Sortable/1.6.0/Sortable.min.js" defer></script> 165 | 166 | <script src="/js/script.js" defer></script> 167 | # end if 168 | </head> 169 | 170 | <body> 171 | <header> 172 | <nav id="navbar" class="navbar navbar-expand-md navbar-dark"> 173 | <a id="sidebarToggle" class="navbar-brand" href="#!">Nim Home Assistant</a> 174 | <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText"> 175 | <span class="navbar-toggler-icon"></span> 176 | </button> 177 | <div class="collapse navbar-collapse" id="navbarText"> 178 | <ul class="navbar-nav ml-auto"> 179 | <li class="nav-item"> 180 | <a class="nav-link disabled" href="#">Card order:</a> 181 | </li> 182 | <li class="nav-item dropdown"> 183 | <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="font-weight: 700;"> 184 | Options 185 | </a> 186 | <div class="dropdown-menu" aria-labelledby="navbarDropdown"> 187 | <a class="dropdown-item orderReset" href="#">Reset order</a> 188 | <div class="dropdown-divider"></div> 189 | 190 | # for card in split(cardOrderRaw, ","): 191 | # let cardName = replace(card, "card", "") 192 | # if card in split(cardOrder, ","): 193 | <a class="dropdown-item cardToggle" data-card="$card" data-visible="true" href="#!"><span style="color: green; font-weight: 700; margin-right: 10px;">ON</span>$cardName</a> 194 | # else: 195 | <a class="dropdown-item cardToggle" data-card="$card" data-visible="false" href="#!"><span style="color: red; font-weight: 700; margin-right: 10px;">OFF</span>$cardName</a> 196 | # end if 197 | # end for 198 | 199 | </div> 200 | </li> 201 | <li class="nav-item"> 202 | <a id="orderTrash" class="nav-link trash" href="#"><img src="/images/icon_trash.png"></a> 203 | </li> 204 | </ul> 205 | </div> 206 | </nav> 207 | </header> 208 | 209 | <main> 210 | <div id="pageType" data-userid="${c.userid}" data-userstatus="${c.rank}" data-type="dashboard" style="display: none;"></div> 211 | <div class="wrapper"> 212 | ${genMainSidebar()} 213 | 214 | <!-- 215 | Main area 216 | --> 217 | <div id="pagewrapper"> 218 | <div class="container-fluid"> 219 | <div id="sortableCards" class="row"> 220 | 221 | # for position in split(cardOrder, ","): 222 | # 223 | # if position == "cardAlarm": 224 | ${cardAlarm(c)} 225 | # 226 | # elif position == "cardCertificates": 227 | ${cardCertificates(c)} 228 | # 229 | # elif position == "cardPushbullet": 230 | ${cardPushbullet(c)} 231 | # 232 | # elif position == "cardXiaomiDevice": 233 | ${cardXiaomiDevice(c)} 234 | # 235 | # elif position == "cardOwntracks": 236 | ${cardOwntracks(c)} 237 | # 238 | # elif position == "cardFilestream": 239 | ${cardFilestream(c)} 240 | # 241 | # elif match(position, re"cardFilestream"): 242 | ${cardfilestream(c, replace(position, "cardFilestream-", ""))} 243 | # 244 | # elif position == "cardRss": 245 | ${cardRss(c)} 246 | # 247 | # elif position == "cardWebsocketUsers": 248 | ${cardWebsocketUsers(c)} 249 | # 250 | # elif position == "cardOsStats": 251 | ${cardOsStats(c)} 252 | # 253 | # else: 254 | # discard 255 | # 256 | # end if 257 | # end for 258 | 259 | 260 | 261 | </div> 262 | </div> 263 | </div> 264 | 265 | 266 | 267 | </div> 268 | </main> 269 | ${genAlarmNumpad(c)} 270 | 271 | <footer> 272 | ${genMainFooter()} 273 | </footer> 274 | 275 | ${genMainNotify()} 276 | 277 | </body> 278 | 279 | 280 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/mqtt.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | #proc genMqtt(c: var TData): string = 7 | # result = "" 8 | <head> 9 | ${genMainHead(c)} 10 | </head> 11 | 12 | <body> 13 | <header> 14 | ${genMainHeader()} 15 | </header> 16 | 17 | <main> 18 | <div id="pageType" data-userid="${c.userid}" data-type="mqtt" style="display: none;"></div> 19 | <div class="wrapper"> 20 | ${genMainSidebar()} 21 | 22 | <div id="pagewrapper"> 23 | <div id="mqtt"> 24 | 25 | <h1>MQTT</h1> 26 | 27 | <div> 28 | <h2>Send MQTT test message</h2> 29 | <form method="GET" action="/mqtt/do" style="max-width: 300px;"> 30 | <input name="action" value="sendtest" style="display: none;"> 31 | <input name="topic" placeholder="MQTT topic" class="form-control"> 32 | <input name="message" placeholder="MQTT message" class="form-control"> 33 | <button type="submit">Send test message</button> 34 | </form> 35 | </div> 36 | 37 | <div> 38 | <h2>Add MQTT templates</h2> 39 | 40 | <div class="mqtt"> 41 | <p>Insert topic in double quotes, e.g. "myTopic"</p> 42 | 43 | <table class="mqtt table table-bordered table-hover"> 44 | <thead> 45 | <tr class="thead-dark"> 46 | <th>Name</th> 47 | <th>Topic</th> 48 | <th>Message</th> 49 | <th></th> 50 | </tr> 51 | <tr class="mqttItemAdd"> 52 | <td> 53 | <input name="mqttname" class="mqttname form-control form-control-sm" /> 54 | </td> 55 | <td> 56 | <input name="mqtttopic" class="mqtttopic form-control form-control-sm" /> 57 | </td> 58 | <td> 59 | <input name="mqttmessage" class="mqttmessage form-control form-control-sm" /> 60 | </td> 61 | <td class="btn btn-success mqttActionAdd"> 62 | Add 63 | </td> 64 | </tr> 65 | </thead> 66 | 67 | <tbody> 68 | 69 | # let mqttActions = getAllRows(dbMqtt, sql"SELECT id, name, topic, message FROM mqtt_templates") 70 | # for action in mqttActions: 71 | <tr> 72 | <td>${action[1]}</td> 73 | <td>${action[2]}</td> 74 | <td>${action[3]}</td> 75 | <td data-actionid="${action[0]}" class="btn btn-danger mqttActionDelete">Del</td> 76 | </tr> 77 | # end for 78 | 79 | </tbody> 80 | </table> 81 | </div> 82 | </div> 83 | 84 | </div> 85 | </div> 86 | </div> 87 | </main> 88 | 89 | <footer> 90 | ${genMainFooter()} 91 | </footer> 92 | 93 | ${genMainNotify()} 94 | 95 | </body> 96 | 97 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/os.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | #proc genOs(c: var TData): string = 8 | # result = "" 9 | <head> 10 | ${genMainHead(c)} 11 | </head> 12 | 13 | <body> 14 | <header> 15 | ${genMainHeader()} 16 | </header> 17 | 18 | <main> 19 | <div id="pageType" data-userid="${c.userid}" data-type="os" style="display: none;"></div> 20 | <div class="wrapper"> 21 | ${genMainSidebar()} 22 | 23 | <div id="pagewrapper"> 24 | <div id="os"> 25 | 26 | <h1>OS utils</h1> 27 | 28 | <hr> 29 | 30 | <div class="osTemplates"> 31 | # let osTemplates = getAllRows(dbOs, sql"SELECT id, name, command FROM os_templates") 32 | 33 | <h3>OS test command</h3> 34 | <i>Command is only shown on terminal output.</i> 35 | <p>Do not test blocking commands! They will block NimHA, and you need to restart NimHA. Blocking commands are allowed below as templates, but not in test-runs.</p> 36 | <div class="osTest"> 37 | <label>Run command</label> 38 | <input class="form-control form-control-sm testos command" /> 39 | <button class="btn btn-primary osTestCommand">Run</button> 40 | </div> 41 | 42 | <hr> 43 | 44 | <h3>OS templates</h3> 45 | <table class="osTemplates table"> 46 | <thead> 47 | <tr class="thead-dark"> 48 | <th>Name</th> 49 | <th>Command</th> 50 | <th></th> 51 | </tr> 52 | <tr class="osTemplatesEdit"> 53 | <td> 54 | <input class="form-control form-control-sm name" /> 55 | </td> 56 | <td> 57 | <input class="form-control form-control-sm command" /> 58 | </td> 59 | <td class="btn btn-success osTemplateAdd"> 60 | Add 61 | </td> 62 | </tr> 63 | </thead> 64 | <tbody> 65 | # for os in osTemplates: 66 | <tr class="os"> 67 | <td> 68 | ${os[1]} 69 | </td> 70 | <td> 71 | ${os[2]} 72 | </td> 73 | <td data-os="${os[0]}" class="btn btn-danger osTemplateDelete"> 74 | Del 75 | </td> 76 | </tr> 77 | # end for 78 | </tbody> 79 | </table> 80 | </div> 81 | 82 | </div> 83 | </div> 84 | </div> 85 | </main> 86 | 87 | <footer> 88 | ${genMainFooter()} 89 | </footer> 90 | 91 | ${genMainNotify()} 92 | 93 | </body> 94 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/owntracks.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | # 8 | # 9 | #proc genOwntracks(c: var TData): string = 10 | # result = "" 11 | <head> 12 | ${genMainHead(c)} 13 | </head> 14 | 15 | <body> 16 | <header> 17 | ${genMainHeader()} 18 | </header> 19 | 20 | <main> 21 | <div id="pageType" data-userid="${c.userid}" data-type="owntracksdevices" style="display: none;"></div> 22 | <div class="wrapper"> 23 | ${genMainSidebar()} 24 | 25 | # let allWaypoints = getAllRows(dbOwntracks, sql"SELECT owntracks_waypoints.id, owntracks_waypoints.username, owntracks_waypoints.device_id, owntracks_waypoints.desc, owntracks_waypoints.lat, owntracks_waypoints.lon, owntracks_waypoints.rad, owntracks_waypoints.creation FROM owntracks_waypoints LEFT JOIN owntracks_devices ON owntracks_devices.username = owntracks_waypoints.username ORDER BY owntracks_devices.username ASC") 26 | # 27 | # let allDevices = getAllRows(dbOwntracks, sql"SELECT username, device_id, tracker_id, creation FROM owntracks_devices ORDER BY username DESC") 28 | 29 | <div id="pagewrapper"> 30 | 31 | <div id="owntracksDevices"> 32 | 33 | <h1>Owntracks</h1> 34 | 35 | <div class="deviceList"> 36 | <h3>Devices</h3> 37 | <table class="deviceList table"> 38 | <thead> 39 | <tr class="thead-dark"> 40 | <th>Username</th> 41 | <th>Device ID</th> 42 | <th>Tracker ID</th> 43 | <th>Creation</th> 44 | <th>Saved locations</th> 45 | <th></th> 46 | <th></th> 47 | </tr> 48 | </thead> 49 | <tbody> 50 | # for device in allDevices: 51 | <tr class="device"> 52 | <td> 53 | ${device[0]} 54 | </td> 55 | <td> 56 | ${device[1]} 57 | </td> 58 | <td> 59 | ${device[2]} 60 | </td> 61 | <td> 62 | ${epochDate(device[3], "YYYY-MM-DD HH:mm")} 63 | </td> 64 | <td> 65 | ${getAllRows(dbOwntracks, sql"SELECT id FROM owntracks_history WHERE username = ? AND device_id = ?", device[0], device[1]).len()} 66 | </td> 67 | <td data-username="${device[0]}" data-deviceid="${device[1]}" class="btn-primary owntracksClearhistoryDevice" style="cursor: pointer;"> 68 | Del history 69 | </td> 70 | <td data-username="${device[0]}" data-deviceid="${device[1]}" class="btn btn-danger owntracksDeleteDevice"> 71 | Del 72 | </td> 73 | </tr> 74 | # end for 75 | </tbody> 76 | </table> 77 | </div> 78 | 79 | <hr> 80 | 81 | <div class="waypoints"> 82 | <h3>Waypoints</h3> 83 | <table class="waypoints table table-bordered table-hover"> 84 | <thead> 85 | <tr class="thead-dark"> 86 | <th>Username</th> 87 | <th>Device ID</th> 88 | <th>Description</th> 89 | <th>Latitude</th> 90 | <th>Longitude</th> 91 | <th>Radius</th> 92 | <th>Creation</th> 93 | <th></th> 94 | </tr> 95 | </thead> 96 | 97 | <tbody> 98 | # for device in allWaypoints: 99 | <tr class="waypoints ${device[0]}"> 100 | <td> 101 | ${device[1]} 102 | </td> 103 | <td> 104 | ${device[2]} 105 | </td> 106 | <td> 107 | ${device[3]} 108 | </td> 109 | <td> 110 | ${device[4]} 111 | </td> 112 | <td> 113 | ${device[5]} 114 | </td> 115 | <td> 116 | ${device[6]} 117 | </td> 118 | <td> 119 | ${epochDate(device[7], "YYYY-MM-DD HH:mm")} 120 | </td> 121 | <td data-waypointid="${device[0]}" class="btn btn-danger owntracksDeleteWaypoint"> 122 | Del 123 | </td> 124 | </tr> 125 | # end for 126 | </tbody> 127 | </table> 128 | </div> 129 | </div> 130 | </div> 131 | </div> 132 | </main> 133 | 134 | <footer> 135 | ${genMainFooter()} 136 | </footer> 137 | 138 | ${genMainNotify()} 139 | 140 | </body> 141 | 142 | 143 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/pushbullet.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | #proc genPushbullet(c: var TData): string = 8 | # result = "" 9 | <head> 10 | ${genMainHead(c)} 11 | </head> 12 | 13 | <body> 14 | <header> 15 | ${genMainHeader()} 16 | </header> 17 | 18 | <main> 19 | <div id="pageType" data-userid="${c.userid}" data-type="pushbullet" style="display: none;"></div> 20 | <div class="wrapper"> 21 | ${genMainSidebar()} 22 | 23 | <div id="pagewrapper"> 24 | <div id="pushbullet"> 25 | 26 | <h1>Pushbullet</h1> 27 | 28 | <div class="pushbulletApi"> 29 | 30 | # let pushApi = getValue(dbPushbullet, sql"SELECT api FROM pushbullet_settings WHERE id = ?", "1") 31 | 32 | <hr> 33 | 34 | <h3>API</h3> 35 | 36 | <div> 37 | <label>API key</label> 38 | <input class="form-control form-control-sm api key" value="$pushApi" placeholder="Your API key" /> 39 | <button class="btn btn-primary pushbulletApiUpdate">Update</button> 40 | </div> 41 | 42 | </div> 43 | 44 | <hr> 45 | 46 | <div class="pushbulletTest"> 47 | 48 | <h3>Test connection</h3> 49 | 50 | <div> 51 | <label>Send a test message</label> 52 | <button class="btn btn-primary" id="pushbulletTest" style="display: block; margin-top: 0px;">Test connection</button> 53 | </div> 54 | 55 | </div> 56 | 57 | <hr> 58 | 59 | <div class="pushbulletTemplates"> 60 | # let pushTemplates = getAllRows(dbPushbullet, sql"SELECT id, name, title, body FROM pushbullet_templates") 61 | 62 | <h3>Pushbullet templates</h3> 63 | 64 | <table class="pushbulletTemplates table"> 65 | <thead> 66 | <tr class="thead-dark"> 67 | <th>Name</th> 68 | <th>Title</th> 69 | <th>Body</th> 70 | <th></th> 71 | </tr> 72 | <tr class="pushbulletTemplatesEdit"> 73 | <td> 74 | <input class="form-control form-control-sm name" /> 75 | </td> 76 | <td> 77 | <input class="form-control form-control-sm title" /> 78 | </td> 79 | <td> 80 | <input class="form-control form-control-sm body" /> 81 | </td> 82 | <td class="btn btn-success pushbulletTemplateAdd"> 83 | Add 84 | </td> 85 | </tr> 86 | </thead> 87 | <tbody> 88 | # for push in pushTemplates: 89 | <tr class="pushbullet"> 90 | <td> 91 | ${push[1]} 92 | </td> 93 | <td> 94 | ${push[2]} 95 | </td> 96 | <td> 97 | ${push[3]} 98 | </td> 99 | <td data-pushid="${push[0]}" class="btn btn-danger pushbulletTemplateDelete"> 100 | Del 101 | </td> 102 | </tr> 103 | # end for 104 | </tbody> 105 | </table> 106 | </div> 107 | 108 | </div> 109 | </div> 110 | </div> 111 | </main> 112 | 113 | <footer> 114 | ${genMainFooter()} 115 | </footer> 116 | 117 | ${genMainNotify()} 118 | 119 | </body> 120 | 121 | 122 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/rpi.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | #proc genRpi(c: var TData): string = 8 | # result = "" 9 | <head> 10 | ${genMainHead(c)} 11 | </head> 12 | 13 | <body> 14 | <header> 15 | ${genMainHeader()} 16 | </header> 17 | 18 | <main> 19 | <div id="pageType" data-userid="${c.userid}" data-type="rpi" style="display: none;"></div> 20 | <div class="wrapper"> 21 | ${genMainSidebar()} 22 | 23 | <div id="pagewrapper"> 24 | <div id="rpi"> 25 | 26 | <h1>Raspberry Pi</h1> 27 | 28 | <hr> 29 | 30 | <div class="rpiTemplates"> 31 | # let allRpi = getAllRows(dbRpi, sql"SELECT id, name, pin, pinMode, pinPull, digitalAction, analogAction, value FROM rpi_templates") 32 | 33 | <h3>Raspberry Pi templates</h3> 34 | 35 | <p>The GPIO are mapped to Broadcom. Checkout <a href="https://pinout.xyz/">https://pinout.xyz/</a> to get the pins numbers.</p> 36 | 37 | <p>The <kbd>mode</kbd> can be set to: input, output, GPIO and PWM.</p> 38 | 39 | <p>The <kbd>pull</kbd> can be set to off, down and up.</p> 40 | 41 | <p>The <kbd>digital</kbd> can be set to write, pwm (write) and read. When set to read, you need to specify the pin to read on, in the <kbd>value</kbd> field.</p> 42 | 43 | <p>The <kbd>analog</kbd> can be set to write and read. When set to read, you need to specify the pin to read on, in the <kbd>value</kbd> field.</p> 44 | 45 | <p></p> 46 | 47 | <table class="rpiTemplates table"> 48 | <thead> 49 | <tr class="thead-dark"> 50 | <th>Name</th> 51 | <th>Pin</th> 52 | <th>Mode</th> 53 | <th>Pull</th> 54 | <th>Digital</th> 55 | <th>Analog</th> 56 | <th>Value</th> 57 | <th></th> 58 | <th></th> 59 | </tr> 60 | <tr class="rpiTemplatesEdit"> 61 | <td> 62 | <input class="form-control form-control-sm name" /> 63 | </td> 64 | <td> 65 | <input class="form-control form-control-sm pin" /> 66 | </td> 67 | <td> 68 | <input class="form-control form-control-sm mode" /> 69 | </td> 70 | <td> 71 | <input class="form-control form-control-sm pull" /> 72 | </td> 73 | <td> 74 | <input class="form-control form-control-sm digital" /> 75 | </td> 76 | <td> 77 | <input class="form-control form-control-sm analog" /> 78 | </td> 79 | <td> 80 | <input class="form-control form-control-sm value" /> 81 | </td> 82 | <td colspan="2" class="btn btn-success rpiTemplateAdd"> 83 | Add 84 | </td> 85 | </tr> 86 | </thead> 87 | <tbody> 88 | # for rpi in allRpi: 89 | <tr class="rpi"> 90 | <td> 91 | ${rpi[1]} 92 | </td> 93 | <td> 94 | ${rpi[2]} 95 | </td> 96 | <td> 97 | ${rpi[3]} 98 | </td> 99 | <td> 100 | ${rpi[4]} 101 | </td> 102 | <td> 103 | ${rpi[5]} 104 | </td> 105 | <td> 106 | ${rpi[6]} 107 | </td> 108 | <td> 109 | ${rpi[7]} 110 | </td> 111 | <td data-rpi="${rpi[0]}" class="btn btn-primary rpiTemplateRun"> 112 | Run 113 | </td> 114 | <td data-rpi="${rpi[0]}" class="btn btn-danger rpiTemplateDelete"> 115 | Del 116 | </td> 117 | </tr> 118 | # end for 119 | </tbody> 120 | </table> 121 | </div> 122 | 123 | </div> 124 | </div> 125 | </div> 126 | </main> 127 | 128 | <footer> 129 | ${genMainFooter()} 130 | </footer> 131 | 132 | ${genMainNotify()} 133 | 134 | </body> 135 | 136 | 137 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/rss.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | # 7 | #proc genRss(c: var TData, testFeed = ""): string = 8 | # result = "" 9 | <head> 10 | ${genMainHead(c)} 11 | </head> 12 | 13 | <body> 14 | <header> 15 | ${genMainHeader()} 16 | </header> 17 | 18 | <main> 19 | <div id="pageType" data-userid="${c.userid}" data-type="rss" style="display: none;"></div> 20 | <div class="wrapper"> 21 | ${genMainSidebar()} 22 | 23 | <div id="pagewrapper"> 24 | <div id="rss"> 25 | 26 | <h1>RSS</h1> 27 | 28 | <hr> 29 | 30 | <div class="rssTestFeed"> 31 | 32 | <h3>Test a feed</h3> 33 | 34 | <div> 35 | <form action="/rss/do" method="GET"> 36 | <input style="display: none" name="action" value="testfeed" /> 37 | <div> 38 | <label>URL:</label> 39 | <input name="url" class="form-control" required /> 40 | </div> 41 | <div> 42 | <label>Skip x lines:</label> 43 | <input name="skip" class="form-control" value="0" required /> 44 | </div> 45 | <div> 46 | <label>Fields (comma separated):</label> 47 | <input name="fields" class="form-control" required /> 48 | </div> 49 | 50 | <button type="submit" class="btn btn-primary" id="rssTestFeed">Test feed</button> 51 | </form> 52 | 53 | <br> 54 | 55 | # if testFeed != "": 56 | <div class="rssTestOutputRaw" style="display: none;"> 57 | $testFeed 58 | </div> 59 | <div class="rssTestOutput"> 60 | 61 | </div> 62 | # end if 63 | 64 | </div> 65 | 66 | </div> 67 | 68 | <hr> 69 | 70 | <div class="rssFeeds"> 71 | # let rssFeeds = getAllRows(dbRss, sql"SELECT id, url, skip, fields, name FROM rss_feeds") 72 | 73 | <h3>RSS feeds</h3> 74 | 75 | <p>Fields needs to comma separated and listed in chronological order, as they appear in the RSS feed.</p> 76 | <p>The skip x lines will skip the first x hits according to your fields.</p> 77 | 78 | <br> 79 | 80 | <p>Examples</p> 81 | <ul> 82 | <li>URL: https://forum.nim-lang.org/threadActivity.xml</li> 83 | <li>Skip: 2</li> 84 | <li>Fields: title,updated</li> 85 | </ul> 86 | <ul> 87 | <li>URL: https://www.archlinux.org/feeds/packages/</li> 88 | <li>Skip: 0</li> 89 | <li>Fields: title,pubDate</li> 90 | </ul> 91 | 92 | <table class="rssFeeds table"> 93 | <thead> 94 | <tr class="thead-dark"> 95 | <th>Name</th> 96 | <th>URL</th> 97 | <th style="max-width: 120px;">Skip x lines</th> 98 | <th>Fields</th> 99 | <th></th> 100 | </tr> 101 | <tr class="rssFeedsEdit"> 102 | <td> 103 | <input class="form-control form-control-sm name" /> 104 | </td> 105 | <td> 106 | <input class="form-control form-control-sm url" /> 107 | </td> 108 | <td style="width: 120px;"> 109 | <input class="form-control form-control-sm skip" value="0" style="text-align: center;" /> 110 | </td> 111 | <td> 112 | <input class="form-control form-control-sm fields" /> 113 | </td> 114 | <td class="btn btn-success rssFeedsAdd"> 115 | Add 116 | </td> 117 | </tr> 118 | </thead> 119 | <tbody> 120 | # for feed in rssFeeds: 121 | <tr class="rss"> 122 | <td> 123 | ${feed[4]} 124 | </td> 125 | <td> 126 | ${feed[1]} 127 | </td> 128 | <td style="text-align: center;"> 129 | ${feed[2]} 130 | </td> 131 | <td> 132 | ${feed[3]} 133 | </td> 134 | <td data-feedid="${feed[0]}" class="btn btn-danger rssFeedDelete"> 135 | Del 136 | </td> 137 | </tr> 138 | # end for 139 | </tbody> 140 | </table> 141 | </div> 142 | 143 | </div> 144 | </div> 145 | </div> 146 | </main> 147 | 148 | <footer> 149 | ${genMainFooter()} 150 | </footer> 151 | 152 | ${genMainNotify()} 153 | 154 | </body> 155 | 156 | 157 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/settings.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | #proc genSystemCommands(c: var TData): string = 7 | # result = "" 8 | <head> 9 | ${genMainHead(c)} 10 | </head> 11 | 12 | <body> 13 | <header> 14 | ${genMainHeader()} 15 | </header> 16 | 17 | <main> 18 | <div id="pageType" data-userid="${c.userid}" data-type="settings" style="display: none;"></div> 19 | <div class="wrapper"> 20 | ${genMainSidebar()} 21 | 22 | <div id="pagewrapper"> 23 | <div id="settings"> 24 | 25 | <h1>System commands</h1> 26 | 27 | <p>Main settings for NimHA. Only the <kbd>admin</kbd> user can use the tools.</p> 28 | 29 | <div class="settings restart"> 30 | <h3>System</h3> 31 | 32 | <div class="btn-group-vertical"> 33 | <a href="/settings/restart?module=nimha" class="btn btn-danger">Kill NimHA</a> 34 | <a href="/settings/restart?module=system" class="btn btn-danger">Reboot system</a> 35 | </div> 36 | 37 | </div> 38 | 39 | <div class="settings restart"> 40 | <h3>Restart main modules</h3> 41 | 42 | <p>You can restart the different modules if necessary.</p> 43 | 44 | <div class="btn-group-vertical"> 45 | <a href="/settings/restart?module=cron" class="btn btn-danger">Cron</a> 46 | <a href="/settings/restart?module=gateway" class="btn btn-danger">Gateway</a> 47 | <a href="/settings/restart?module=gatewayws" class="btn btn-danger">Gateway websocket</a> 48 | <a href="/settings/restart?module=webinterface" class="btn btn-danger">Webserver</a> 49 | <a href="/settings/restart?module=websocket" class="btn btn-danger">Websocket</a> 50 | <a href="/settings/restart?module=xiaomi" class="btn btn-danger">Xiaomi</a> 51 | </div> 52 | 53 | </div> 54 | </div> 55 | </div> 56 | </div> 57 | </main> 58 | 59 | <footer> 60 | ${genMainFooter()} 61 | </footer> 62 | 63 | ${genMainNotify()} 64 | 65 | </body> 66 | #end proc 67 | # 68 | # 69 | # 70 | #import httpclient #HACK: Do NOT move to main,leave it here. 71 | # 72 | #proc genServerInfo(c: var TData): string = 73 | # result = "" 74 | # 75 | # let hostn = getHostname() 76 | # let uptim = execCmdEx("uptime --pretty").output.strip 77 | # let disks = execCmdEx("df --human-readable --local --output=avail " & getCurrentDir()).output.strip 78 | # const uname = staticExec("uname -a").strip 79 | # const distr = staticExec("lsb_release -a").strip 80 | # let pubip = newHttpClient().getContent("http://api.ipify.org").strip 81 | # 82 | <head> 83 | ${genMainHead(c)} 84 | </head> 85 | 86 | <body> 87 | <header> 88 | ${genMainHeader()} 89 | </header> 90 | 91 | <main> 92 | <div id="pageType" data-userid="${c.userid}" data-type="settings" style="display: none;"></div> 93 | <div class="wrapper"> 94 | ${genMainSidebar()} 95 | 96 | <div id="pagewrapper"> 97 | <div id="settings"> 98 | 99 | <h1>Server info</h1> 100 | 101 | <!-- Credit NimWC (Nim Website Creator) - https://github.com/ThomasTJdev/nim_websitecreator --> 102 | <table border=1 class="table serverinfo"> 103 | <thead> 104 | <tr> 105 | <th style="width: 200px;">Name</th> 106 | <th>Value</th> 107 | </tr> 108 | </thead> 109 | <tfoot> 110 | <tr> 111 | <th>Name</th> 112 | <th>Value</th> 113 | </tr> 114 | </tfoot> 115 | <tbody class="is-family-monospace"> 116 | <tr> 117 | <td> <b>System</b> </td> <td> $uname </td> 118 | </tr> 119 | <tr> 120 | <td> <b>Distro</b> </td> <td> $distr </td> 121 | </tr> 122 | <tr> 123 | <td> <b>Uptime</b> </td> <td> $uptim </td> 124 | </tr> 125 | <tr> 126 | <td> <b>Public IP</b> </td> <td> $pubip </td> 127 | </tr> 128 | <tr> 129 | <td> <b>Disk</b> </td> <td> $disks </td> 130 | </tr> 131 | <tr> 132 | <td> <b>Hostname</b> </td> <td> $hostn </td> 133 | </tr> 134 | <tr> 135 | <td> <b>Compile Date</b> </td> <td> $CompileDate </td> 136 | </tr> 137 | <tr> 138 | <td> <b>Compile Time</b> </td> <td> $CompileTime </td> 139 | </tr> 140 | <tr> 141 | <td> <b>Nim Version</b> </td> <td> $NimVersion </td> 142 | </tr> 143 | <tr> 144 | <td> <b>CPU</b> </td> <td> $hostCPU.toUpperAscii </td> 145 | </tr> 146 | <tr> 147 | <td> <b>CPU Count</b> </td> <td>${countProcessors()}</td> 148 | </tr> 149 | <tr> 150 | <td> <b>OS</b> </td> <td>$hostOS.toUpperAscii</td> 151 | </tr> 152 | <tr> 153 | <td> <b>Endian</b> </td> <td>$cpuEndian</td> 154 | </tr> 155 | <tr> 156 | <td> <b>Temp Directory</b> </td> <td>${getTempDir()}</td> 157 | </tr> 158 | <tr> 159 | <td> <b>Current Directory</b> </td> <td>${getCurrentDir()}</td> 160 | </tr> 161 | <tr> 162 | <td> <b>Log File</b> </td> <td>${defaultFilename()}</td> 163 | </tr> 164 | <tr> 165 | <td> <b>App Directory</b> </td> <td>${getAppDir()}</td> 166 | </tr> 167 | <tr> 168 | <td> <b>Biggest Integer</b> </td> <td>$int.high</td> 169 | </tr> 170 | <tr> 171 | <td> <b>Server DateTime</b> </td> <td>${now()}</td> 172 | </tr> 173 | <tr> 174 | <td> <b>SSL enabled</b> </td> <td>${defined(ssl)}</td> 175 | </tr> 176 | <tr> 177 | <td> <b>ReCaptcha enabled</b> </td> <td>$useCaptcha</td> 178 | </tr> 179 | <tr> 180 | <td> <b>Release Build</b> </td> <td>${defined(release)}</td> 181 | </tr> 182 | <tr> 183 | <td> <b>Force Recompile enabled</b> </td> <td>${defined(rc)}</td> 184 | </tr> 185 | <tr> 186 | <td> <b>Development Mode enabled</b> </td> <td>${defined(dev)}</td> 187 | </tr> 188 | <tr> 189 | <td> <b>Free Memory</b> </td> <td>${getFreeMem()}</td> 190 | </tr> 191 | <tr> 192 | <td> <b>Total Memory</b> </td> <td>${getTotalMem()}</td> 193 | </tr> 194 | <tr> 195 | <td> <b>Occupied Memory</b> </td> <td>${getOccupiedMem()}</td> 196 | </tr> 197 | <tr> 198 | <td> <b>Garbage Collector</b> </td> <td>${GC_getStatistics()}</td> 199 | </tr> 200 | </tbody> 201 | </table> 202 | 203 | 204 | </div> 205 | </div> 206 | </div> 207 | </main> 208 | 209 | <footer> 210 | ${genMainFooter()} 211 | </footer> 212 | 213 | ${genMainNotify()} 214 | 215 | </body> 216 | #end proc 217 | # 218 | # 219 | # 220 | # 221 | #proc genServerLog(c: var TData): string = 222 | # result = "" 223 | # 224 | # let logcontent = readFile(getAppDir().replace(re"/nimhapkg.*", "") & "/log/log.log") 225 | <head> 226 | ${genMainHead(c)} 227 | </head> 228 | 229 | <body> 230 | <header> 231 | ${genMainHeader()} 232 | </header> 233 | 234 | <main> 235 | <div id="pageType" data-userid="${c.userid}" data-type="settings" style="display: none;"></div> 236 | <div class="wrapper"> 237 | ${genMainSidebar()} 238 | 239 | <div id="pagewrapper"> 240 | <div id="settings"> 241 | 242 | <h1>Server log</h1> 243 | 244 | 245 | <h1 class="has-text-centered">Logs</h1> 246 | <textarea class="form-control" name="logs" id="logs" title="Log Size: $logcontent.splitLines.len Lines." dir="auto" rows=20 readonly autofocus spellcheck style="width:99% !important;height:90% !important"> 247 | $logcontent.strip 248 | </textarea> 249 | <br> 250 | <a title="Copy Logs" onclick="document.querySelector('#logs').select();document.execCommand('copy')"> 251 | <button class="btn btn-secondary">Copy</button> 252 | </a> 253 | </div> 254 | </div> 255 | </div> 256 | </main> 257 | 258 | <footer> 259 | ${genMainFooter()} 260 | </footer> 261 | 262 | ${genMainNotify()} 263 | 264 | </body> 265 | #end proc 266 | # 267 | # 268 | # 269 | # 270 | #proc genAlarmLog(c: var TData): string = 271 | # result = "" 272 | # 273 | # let logcontent = getAllRows(dbAlarm, sql"SELECT id, userid, status, trigger, device, creation FROM alarm_history ORDER BY creation DESC") 274 | <head> 275 | ${genMainHead(c)} 276 | </head> 277 | 278 | <body> 279 | <header> 280 | ${genMainHeader()} 281 | </header> 282 | 283 | <main> 284 | <div id="pageType" data-userid="${c.userid}" data-type="settings" style="display: none;"></div> 285 | <div class="wrapper"> 286 | ${genMainSidebar()} 287 | 288 | <div id="pagewrapper"> 289 | <div id="settings"> 290 | 291 | <h1>Alarm log</h1> 292 | 293 | <h1 class="has-text-centered">Logs</h1> 294 | <textarea class="form-control" name="logs" id="logs" dir="auto" rows=20 readonly autofocus spellcheck style="width:99% !important;height:90% !important"> 295 | # for log in logcontent: 296 | ${log} 297 | # end for 298 | </textarea> 299 | <br> 300 | <a title="Copy Logs" onclick="document.querySelector('#logs').select();document.execCommand('copy')"> 301 | <button class="btn btn-secondary">Copy</button> 302 | </a> 303 | </div> 304 | </div> 305 | </div> 306 | </main> 307 | 308 | <footer> 309 | ${genMainFooter()} 310 | </footer> 311 | 312 | ${genMainNotify()} 313 | 314 | </body> 315 | #end proc -------------------------------------------------------------------------------- /nimhapkg/tmpl/users.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | # 3 | # 4 | # 5 | # 6 | #proc genLogin(c: var TData, errorMsg = ""): string = 7 | # result = "" 8 | <head> 9 | ${genMainHead(c)} 10 | </head> 11 | 12 | <body> 13 | <header> 14 | ${genMainHeader()} 15 | </header> 16 | 17 | <main> 18 | <div id="pageType" data-userid="${c.userid}" data-type="login" style="display: none;"></div> 19 | <div class="wrapper"> 20 | 21 | <div id="pagewrapper"> 22 | <div id="login"> 23 | 24 | <h1>Login</h1> 25 | 26 | # if errorMsg != "": 27 | <h3 style="color: red;">$errorMsg</h3> 28 | # end if 29 | 30 | <form method="POST" action="/dologin"> 31 | <input type="email" name="email" class="form-control" placeholder="Email" /> 32 | <input type="password" name="password" class="form-control" placeholder="Password" /> 33 | 34 | #if useCaptcha: 35 | <div id="recaptcha"> 36 | <div class="g-recaptcha" data-sitekey="${recaptchaSiteKey}" data-theme="light" style="transform:scale(0.93);-webkit-transform:scale(0.93);transform-origin:0 0;-webkit-transform-origin:0 0;"></div> 37 | <script src="https://www.google.com/recaptcha/api.js" async defer></script> 38 | </div> 39 | #end if 40 | 41 | <button type="submit" class="btn btn-primary">Login</button> 42 | </form> 43 | 44 | </div> 45 | </div> 46 | </div> 47 | </main> 48 | 49 | <footer> 50 | ${genMainFooter()} 51 | </footer> 52 | 53 | </body> 54 | #end proc 55 | # 56 | # 57 | #proc genUsers(c: var TData): string = 58 | # result = "" 59 | <head> 60 | ${genMainHead(c)} 61 | </head> 62 | 63 | <body> 64 | <header> 65 | ${genMainHeader()} 66 | </header> 67 | 68 | <main> 69 | <div id="pageType" data-userid="${c.userid}" data-type="settings" style="display: none;"></div> 70 | <div class="wrapper"> 71 | ${genMainSidebar()} 72 | 73 | <div id="pagewrapper"> 74 | <div id="settings"> 75 | 76 | <h1>Users</h1> 77 | 78 | <p>You can only add an <kbd>admin</kbd> user from the command line.</p> 79 | 80 | <hr> 81 | 82 | <div class="settings users"> 83 | <h3>Add user</h3> 84 | 85 | <form method="POST" action="/settings/users/add"> 86 | <div> 87 | <input name="email" type="email" placeholder="Email" class="form-control" required> 88 | </div> 89 | <div> 90 | <input name="name" type="text" placeholder="Name" class="form-control" required> 91 | </div> 92 | <div> 93 | <input name="password" type="password" placeholder="Password" class="form-control" required> 94 | </div> 95 | <br> 96 | <input type="submit" value="Add user" class="btn btn-primary"> 97 | </form> 98 | 99 | </div> 100 | 101 | <hr> 102 | 103 | <div class="settings userlist"> 104 | <h3>Users</h3> 105 | 106 | # let usersAll = getAllRows(db, sql"SELECT name, email, status, lastOnline FROM person;") 107 | <table class="table"> 108 | <thead> 109 | <tr class="thead-dark"> 110 | <th>Name</th> 111 | <th>Email</th> 112 | <th>Status</th> 113 | <th>Last login</th> 114 | <th>Del</th> 115 | </tr> 116 | </thead> 117 | <tbody> 118 | # for user in usersAll: 119 | <tr> 120 | <td>${user[0]}</td> 121 | <td>${user[1]}</td> 122 | <td>${user[2]}</td> 123 | <td>${epochDate(user[3], "YYYY-MM-DD HH:mm")}</td> 124 | # if user[2] == "Admin": 125 | <td></td> 126 | # else: 127 | <td class="btn btn-danger"><a href="/settings/users/delete?email=${user[1]}">Del</a></td> 128 | # end if 129 | </tr> 130 | # end for 131 | </tbody> 132 | </table> 133 | </div> 134 | </div> 135 | </div> 136 | </div> 137 | </main> 138 | 139 | <footer> 140 | ${genMainFooter()} 141 | </footer> 142 | 143 | ${genMainNotify()} 144 | 145 | </body> 146 | #end proc -------------------------------------------------------------------------------- /private/screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/private/screenshots/dashboard.png -------------------------------------------------------------------------------- /public/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <browserconfig> 3 | <msapplication> 4 | <tile> 5 | <square150x150logo src="/images/favicon/mstile-150x150.png"/> 6 | <TileColor>#2b5797</TileColor> 7 | </tile> 8 | </msapplication> 9 | </browserconfig> 10 | -------------------------------------------------------------------------------- /public/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/favicon/favicon.ico -------------------------------------------------------------------------------- /public/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /public/images/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" 3 | "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> 4 | <svg version="1.0" xmlns="http://www.w3.org/2000/svg" 5 | width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000" 6 | preserveAspectRatio="xMidYMid meet"> 7 | <metadata> 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | </metadata> 10 | <g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)" 11 | fill="#000000" stroke="none"> 12 | <path d="M375 1822 c-37 -5 -55 -19 -55 -42 0 -10 -6 -23 -14 -29 -11 -9 -13 13 | -26 -9 -67 4 -37 2 -54 -6 -54 -6 0 -11 12 -11 27 0 14 -3 24 -6 21 -3 -4 -2 14 | -28 2 -55 7 -41 9 -45 15 -23 8 28 7 36 14 -105 15 -289 20 -334 39 -348 11 15 | -8 16 -19 12 -25 -11 -18 4 -37 15 -21 9 13 2 -180 -7 -209 -2 -7 3 -38 12 16 | -70 9 -31 19 -93 24 -138 4 -45 15 -95 24 -112 23 -44 21 -99 -4 -112 -11 -6 17 | -20 -17 -20 -25 0 -8 -5 -15 -12 -15 -15 0 -128 -121 -128 -137 0 -7 -6 -13 18 | -13 -13 -14 0 -39 -45 -63 -112 l-13 -38 754 0 c427 0 755 4 755 9 0 5 -7 14 19 | -15 21 -8 7 -15 19 -15 26 0 8 -4 14 -10 14 -5 0 -10 8 -10 17 0 16 -17 54 20 | -78 176 l-23 47 30 6 c30 6 48 29 30 39 -5 4 -7 11 -3 16 3 5 8 21 10 36 3 15 21 | 10 33 16 40 5 7 21 54 34 105 31 120 42 324 19 368 -19 37 -19 74 0 90 8 7 15 22 | 19 15 27 0 7 7 16 15 19 8 4 15 16 16 28 0 20 1 20 8 1 5 -12 8 6 7 50 0 39 23 | -5 74 -11 78 -7 6 -6 10 2 13 8 4 10 17 7 37 -4 24 -3 28 5 17 15 -22 25 -5 24 | 14 27 -11 34 -5 152 7 133 12 -20 5 102 -7 124 -6 10 -8 26 -6 36 8 29 -95 57 25 | -112 30 -3 -5 -18 -10 -33 -10 -27 -1 -89 -29 -64 -30 7 0 10 -5 6 -11 -3 -6 26 | -17 -8 -30 -5 -25 7 -34 -8 -11 -17 7 -4 3 -8 -10 -12 -14 -5 -23 -15 -23 -27 27 | 0 -10 -3 -17 -8 -15 -4 3 -21 -15 -37 -39 -17 -24 -35 -42 -42 -39 -7 2 -15 28 | -1 -19 -7 -4 -7 -3 -8 4 -4 7 4 12 2 12 -4 0 -6 -7 -13 -15 -16 -8 -3 -23 -21 29 | -34 -40 -13 -22 -25 -32 -35 -28 -13 5 -77 -31 -84 -48 -5 -11 -100 -10 -120 30 | 1 -37 19 -99 23 -115 7 -21 -22 -97 -28 -122 -10 -10 8 -26 14 -35 14 -22 0 31 | -70 51 -70 74 0 12 -5 16 -16 12 -12 -5 -14 -1 -9 15 5 15 4 19 -4 15 -6 -4 32 | -11 -1 -11 6 0 7 -10 18 -22 23 -12 6 -26 19 -32 30 -6 11 -15 23 -19 26 -4 3 33 | -7 10 -6 15 3 20 -2 28 -10 15 -15 -24 -72 46 -64 78 5 22 4 24 -8 14 -11 -9 34 | -20 -6 -42 13 -16 14 -25 29 -21 36 5 8 0 9 -14 6 -15 -4 -22 -2 -22 8 0 19 35 | -29 30 -65 26z"/> 36 | <path d="M1520 1760 c0 -13 11 -13 30 0 12 8 11 10 -7 10 -13 0 -23 -4 -23 37 | -10z"/> 38 | <path d="M1281 1514 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/> 39 | <path d="M1752 1490 c0 -14 2 -19 5 -12 2 6 2 18 0 25 -3 6 -5 1 -5 -13z"/> 40 | <path d="M1745 1260 c-4 -17 -5 -34 -2 -36 2 -3 7 10 11 27 3 18 4 34 2 36 -2 41 | 3 -7 -10 -11 -27z"/> 42 | <path d="M322 1110 c0 -14 2 -19 5 -12 2 6 2 18 0 25 -3 6 -5 1 -5 -13z"/> 43 | <path d="M1738 1030 c2 -22 8 -40 13 -40 5 0 9 18 9 40 0 26 -5 40 -13 40 -9 44 | 0 -11 -12 -9 -40z"/> 45 | <path d="M320 840 c0 -5 9 -10 21 -10 11 0 17 5 14 10 -3 6 -13 10 -21 10 -8 46 | 0 -14 -4 -14 -10z"/> 47 | <path d="M1636 525 c-3 -9 0 -15 9 -15 16 0 20 16 6 24 -5 3 -11 -1 -15 -9z"/> 48 | </g> 49 | </svg> 50 | -------------------------------------------------------------------------------- /public/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/images/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#ffffff", 12 | "background_color": "#ffffff", 13 | "display": "standalone" 14 | } 15 | -------------------------------------------------------------------------------- /public/images/icon_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/icon_handle.png -------------------------------------------------------------------------------- /public/images/icon_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/icon_pause.png -------------------------------------------------------------------------------- /public/images/icon_trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasTJdev/nim_homeassistant/ae0a445a82295e7da13d0ded692351b065e9251a/public/images/icon_trash.png --------------------------------------------------------------------------------