diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d08ba3a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 / 2019-06-24 + +- Initial working version. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ad648ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2019 Edwin van de Pol + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb840e5 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# NZBGet for Homey + +Monitor and control your [NZBGet](https://nzbget.net/) servers. + +Homey will automatically fetch the statistics from the NZB server. + +The update interval can be changed in the *device settings*. + + +If you like this app, consider a donation to support development: + +[![Paypal donate][pp-donate-image]][pp-donate-link] + + +## Statistics +- Article cache (MB) +- Average download speed (MB/s) +- Download speed (MB/s) +- Download speed limit (MB/s) +- Number of remaining files in queue +- Total downloaded (GB) +- Total download time +- Free disk space (GB) +- Remaining download (MB) +- Server uptime + + +## Supported actions +- Pause and resume download via toggle component + +### Flowcards + +- Pause download queue +- Resume download queue +- Reload the server +- Scan incoming directory for nzb-files +- Set download speed limit +- Shutdown the server + + +## Supported settings +- Update interval (seconds) + + +## Supported languages +- English +- Dutch (Nederlands) + + +## Support / feedback +If you have any questions or feedback, please contact me on [Slack](https://athomcommunity.slack.com/team/evdpol). + +Please report issues and feature requests at the [issues section](https://github.com/edwinvdpol/net.nzbget/issues) on GitHub. + +This app is tested with NZBGet v21.0, but should work with v15.0 and newer. + + +## Changelog +[Check it out here!](https://github.com/edwinvdpol/net.nzbget/blob/master/CHANGELOG.md) + +[pp-donate-link]: https://www.paypal.me/edwinvdpol +[pp-donate-image]: https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..7ced954 --- /dev/null +++ b/app.js @@ -0,0 +1,22 @@ +'use strict'; + +const Homey = require('homey'); + +class NZBApp extends Homey.App { + + /* + |--------------------------------------------------------------------------- + | Initiate + |--------------------------------------------------------------------------- + | + | This method is called upon initialization of this application. + | + */ + + onInit () { + console.log('✓ NZBGet App running'); + } + +}; + +module.exports = NZBApp; diff --git a/app.json b/app.json new file mode 100644 index 0000000..53e5bd2 --- /dev/null +++ b/app.json @@ -0,0 +1,367 @@ +{ + "id": "net.nzbget", + "version": "1.0.0", + "compatibility": ">=2.0.0", + "sdk": 2, + "brandColor": "#3e8c25", + "name": { + "en": "NZBGet" + }, + "description": { + "en": "Monitor and control your NZBGet servers.", + "nl": "Monitor en beheer uw NZBGet servers." + }, + "category": [ + "internet" + ], + "tags": { + "en": [ + "nzbget", + "usenet", + "downloader", + "download", + "nzb", + "tool", + "files" + ], + "nl": [ + "nzbget", + "usenet", + "downloader", + "download", + "nzb", + "tool", + "bestanden" + ] + }, + "permissions": [], + "images": { + "large": "/assets/images/large.png", + "small": "/assets/images/small.png" + }, + "author": { + "name": "Edwin van de Pol", + "email": "github@edwinvandepol.nl" + }, + "contributors": { + "developers": [ + { + "name": "Edwin van de Pol", + "email": "github@edwinvandepol.nl" + } + ] + }, + "contributing": { + "donate": { + "paypal": { + "username": "edwinvdpol" + } + } + }, + "flow": { + "actions": [ + { + "id": "pausedownload", + "title": { + "en": "Pause download queue", + "nl": "Downloadwachtrij pauzeren" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=nzbdriver" + } + ] + }, + { + "id": "resumedownload", + "title": { + "en": "Resume download queue", + "nl": "Downloadwachtrij hervatten" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=nzbdriver" + } + ] + }, + { + "id": "rate", + "title": { + "en": "Set download speed limit", + "nl": "Stel download snelheidslimiet in" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=nzbdriver" + }, + { + "type": "number", + "name": "download_rate", + "min": 0, + "step": 1, + "placeholder": { + "en": "In MB/s (0 = off)", + "nl": "In MB/s (0 = uit)" + } + } + ] + }, + { + "id": "scan", + "title": { + "en": "Scan incoming directory for nzb-files", + "nl": "Inkomende map scannen voor nzb-bestanden" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=nzbdriver" + } + ] + }, + { + "id": "reload", + "title": { + "en": "Reload the server", + "nl": "Laad de server opnieuw" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=nzbdriver" + } + ] + }, + { + "id": "shutdown", + "title": { + "en": "Shutdown the server", + "nl": "Sluit de server af" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=nzbdriver" + } + ] + } + ] + }, + "drivers": [ + { + "id": "nzbdriver", + "name": { + "en": "Server" + }, + "class": "other", + "capabilities": [ + "download_enabled", + "remaining_files", + "remaining_size", + "download_rate", + "download_size", + "rate_limit", + "free_disk_space", + "average_rate", + "article_cache", + "download_time", + "uptime" + ], + "pair": [ + { + "id": "start" + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "images": { + "large": "/drivers/nzbdriver/assets/images/large.png", + "small": "/drivers/nzbdriver/assets/images/small.png" + }, + "settings": [ + { + "type": "group", + "label": { + "en": "General settings", + "nl": "Algemene instellingen" + }, + "children": [ + { + "id": "refresh_interval", + "type": "number", + "label": { + "en": "Update interval" + }, + "value": 20, + "min": 5, + "max": 1800, + "hint": { + "en": "The refresh interval of the statistics in seconds.", + "nl": "Het vernieuwingsinterval van de statistieken in seconden." + } + } + ] + } + ] + } + ], + "capabilities": { + "article_cache": { + "type": "number", + "decimals": 2, + "title": { + "en": "Article cache", + "nl": "Artikel cache" + }, + "icon": "/assets/harddisk.svg", + "insights": true, + "getable": true, + "setable": false, + "units": { + "en": "MB" + } + }, + "average_rate": { + "type": "number", + "decimals": 2, + "title": { + "en": "Average speed", + "nl": "Gemiddelde snelheid" + }, + "icon": "/assets/download-speed.svg", + "insights": true, + "getable": true, + "setable": false, + "units": { + "en": "MB/s" + } + }, + "download_enabled": { + "type": "boolean", + "title": { + "en": "Download enabled", + "nl": "Download ingeschakeld" + }, + "getable": true, + "setable": true, + "uiComponent": "toggle", + "uiQuickAction": false + }, + "download_rate": { + "type": "number", + "decimals": 2, + "title": { + "en": "Download speed", + "nl": "Download snelheid" + }, + "icon": "/assets/download-speed.svg", + "insights": true, + "getable": true, + "setable": false, + "units": { + "en": "MB/s" + } + }, + "download_size": { + "type": "number", + "decimals": 2, + "title": { + "en": "Total downloaded", + "nl": "Totaal gedownload" + }, + "icon": "/assets/harddisk.svg", + "getable": true, + "setable": false, + "units": { + "en": "GB" + } + }, + "download_time": { + "type": "string", + "title": { + "en": "Total download time", + "nl": "Totale downloadtijd" + }, + "icon": "/assets/clock.svg", + "getable": true, + "setable": false + }, + "free_disk_space": { + "type": "number", + "title": { + "en": "Free disk space", + "nl": "Vrije schijfruimte" + }, + "icon": "/assets/harddisk.svg", + "getable": true, + "setable": false, + "units": { + "en": "GB" + } + }, + "rate_limit": { + "type": "number", + "title": { + "en": "Download limit", + "nl": "Download limiet" + }, + "icon": "/assets/download-speed.svg", + "getable": true, + "setable": false, + "units": { + "en": "MB/s" + } + }, + "remaining_files": { + "type": "number", + "title": { + "en": "Files in queue", + "nl": "Bestanden in wachtrij" + }, + "icon": "/assets/file.svg", + "getable": true, + "setable": false + }, + "remaining_size": { + "type": "number", + "decimals": 2, + "title": { + "en": "Download remaining", + "nl": "Nog te downloaden" + }, + "icon": "/assets/harddisk.svg", + "getable": true, + "setable": false, + "units": { + "en": "MB" + } + }, + "uptime": { + "type": "string", + "title": { + "en": "Server uptime" + }, + "icon": "/assets/clock.svg", + "getable": true, + "setable": false + } + } +} \ No newline at end of file diff --git a/assets/clock.svg b/assets/clock.svg new file mode 100644 index 0000000..6260ae3 --- /dev/null +++ b/assets/clock.svg @@ -0,0 +1,54 @@ + + + diff --git a/assets/download-speed.svg b/assets/download-speed.svg new file mode 100644 index 0000000..e457c5d --- /dev/null +++ b/assets/download-speed.svg @@ -0,0 +1,60 @@ + + + + diff --git a/assets/file.svg b/assets/file.svg new file mode 100644 index 0000000..386317a --- /dev/null +++ b/assets/file.svg @@ -0,0 +1,46 @@ + + + + diff --git a/assets/harddisk.svg b/assets/harddisk.svg new file mode 100644 index 0000000..55cc8e9 --- /dev/null +++ b/assets/harddisk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..51cfe9a --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,42 @@ + + + + diff --git a/assets/images/large.png b/assets/images/large.png new file mode 100644 index 0000000..3cf1f54 Binary files /dev/null and b/assets/images/large.png differ diff --git a/assets/images/small.png b/assets/images/small.png new file mode 100644 index 0000000..13e9e89 Binary files /dev/null and b/assets/images/small.png differ diff --git a/drivers/nzbdriver/assets/icon.svg b/drivers/nzbdriver/assets/icon.svg new file mode 100644 index 0000000..51cfe9a --- /dev/null +++ b/drivers/nzbdriver/assets/icon.svg @@ -0,0 +1,42 @@ + + + + diff --git a/drivers/nzbdriver/assets/images/large.png b/drivers/nzbdriver/assets/images/large.png new file mode 100644 index 0000000..58b3d47 Binary files /dev/null and b/drivers/nzbdriver/assets/images/large.png differ diff --git a/drivers/nzbdriver/assets/images/small.png b/drivers/nzbdriver/assets/images/small.png new file mode 100644 index 0000000..4c9ecf3 Binary files /dev/null and b/drivers/nzbdriver/assets/images/small.png differ diff --git a/drivers/nzbdriver/device.js b/drivers/nzbdriver/device.js new file mode 100644 index 0000000..96180fc --- /dev/null +++ b/drivers/nzbdriver/device.js @@ -0,0 +1,289 @@ +'use strict'; + +const Homey = require('homey'); + +const Api = require('/lib/Api.js'); + +class NZBDevice extends Homey.Device { + + /* + |--------------------------------------------------------------------------- + | Error message + |--------------------------------------------------------------------------- + | + | Log an error message to the console, prepended by the device name. + | + */ + + error () { + console.log.bind(this, `✕ ${this.getName()}`).apply(this, arguments); + } + + /* + |--------------------------------------------------------------------------- + | Success message + |--------------------------------------------------------------------------- + | + | Log a success message to the console, prepended by the device name. + | + */ + + success () { + console.log.bind(this, `✓ ${this.getName()}`).apply(this, arguments); + } + + /* + |--------------------------------------------------------------------------- + | Initiate + |--------------------------------------------------------------------------- + | + | This method is called when a device is initiated. + | + */ + + onInit () { + this.success(`is initiated`); + + // Register capability listeners + this._registerCapabilityListeners(); + + // Create API object + this.api = new Api(this.getData()); + + // Update device statistics on startup + this._updateDevice(); + + // Enable refresh timer + this._setRefreshTimer(this.getSetting('refresh_interval')); + } + + /* + |--------------------------------------------------------------------------- + | Settings changed + |--------------------------------------------------------------------------- + | + | This method is called when the device settings are changed. + | It logs all the changed keys, including the old- and new value. + | + | When the update interval has been changed, it will update the timer. + | + */ + + onSettings (oldSettings, newSettings, changedKeys, callback) { + changedKeys.forEach( (name) => { + this.success(`setting \`${name}\` set \`${oldSettings[name]}\` => \`${newSettings[name]}\``); + + if (name === 'refresh_interval') { + this._setRefreshTimer(newSettings[name]); + } + }); + + callback(null, null); + } + + /* + |--------------------------------------------------------------------------- + | Deleted + |--------------------------------------------------------------------------- + | + | This method is called when a device is deleted. + | It logs a success message, confirming the deletion of the device. + | + */ + + onDeleted () { + clearInterval(this._deviceDataTimer); + + this.success(`is deleted`); + } + + /* + |--------------------------------------------------------------------------- + | API functions + |--------------------------------------------------------------------------- + | + | These functions are supported by the device. + | + */ + + // Pause download queue + async pausedownload () { + return this.api.request({ method: 'pausedownload' }) + .then( () => { + this.setCapabilityValue('download_enabled', false); + this.success(`paused download queue`); + }).catch( error => { + this.error(`pausedownload: ${error}`); + }); + } + + // Set download speed limit + async rate (args) { + let rate = Number(args.download_rate * 1000); + + return this.api.request({ method: 'rate', params: [rate] }) + .then( () => { + this.setCapabilityValue('rate_limit', args.download_rate); + this.success(`set download limit to ${args.download_rate} MB/s`); + }).catch( error => { + this.error(`rate: ${error}`); + }); + } + + // Reload server + async reload () { + return this.api.request({ method: 'reload' }) + .then( () => { + this.success(`reloaded`); + }).catch( error => { + this.error(`reload: ${error}`); + }); + } + + // Resume download queue + async resumedownload () { + return this.api.request({ method: 'resumedownload' }) + .then( () => { + this.setCapabilityValue('download_enabled', true); + this.success(`resumed download queue`); + }).catch( error => { + this.error(`resumedownload: ${error}`); + }); + } + + // Scan incoming directory for nzb-files + async scan () { + return this.api.request({ method: 'scan' }) + .then( () => { + this.success(`scanning incoming directory for nzb-files`); + }).catch( error => { + this.error(`scan: ${error}`); + }); + } + + // Shutdown server + async shutdown () { + return this.api.request({ method: 'shutdown' }) + .then( () => { + this.success(`shutdown`); + }).catch( error => { + this.error(`shutdown: ${error}`); + }); + } + + /* + |--------------------------------------------------------------------------- + | Update device + |--------------------------------------------------------------------------- + | + | This method is periodically called to update the device. + | + */ + + _updateDevice () { + this.api.request({ method: 'status' }) + .then( result => { + this.setAvailable(); + + var data = result.result; + + // Convert data + var average_rate = parseFloat(data.AverageDownloadRate / 1024000); + var download_enabled = (data.DownloadPaused ? false : true); + var download_rate = parseFloat(data.DownloadRate / 1024000); + var download_size = parseFloat(data.DownloadedSizeMB / 1024); + var free_disk_space = Math.floor(data.FreeDiskSpaceMB / 1024); + var rate_limit = Number(data.DownloadLimit / 1024000); + + // Capability values + this.setCapabilityValue('article_cache', parseFloat(data.ArticleCacheMB)); + this.setCapabilityValue('average_rate', average_rate); + this.setCapabilityValue('download_enabled', download_enabled); + this.setCapabilityValue('download_rate', download_rate); + this.setCapabilityValue('download_size', download_size); + this.setCapabilityValue('download_time', this._toTime(Number(data.DownloadTimeSec))); + this.setCapabilityValue('free_disk_space', free_disk_space); + this.setCapabilityValue('rate_limit', rate_limit); + this.setCapabilityValue('remaining_size', Number(data.RemainingSizeMB)); + this.setCapabilityValue('uptime', this._toTime(data.UpTimeSec)); + + }).then( () => { + this.api.request({ method: 'listfiles' }) + .then( result => { + var remaining_files = Object.keys(result.result).length; + this.setCapabilityValue('remaining_files', remaining_files); + }); + + }).catch( error => { + this.error(error); + this.setUnavailable(error); + }); + } + + /* + |--------------------------------------------------------------------------- + | Register capability listeners + |--------------------------------------------------------------------------- + | + | This method registers all capability listeners. + | + */ + + _registerCapabilityListeners () { + this.registerCapabilityListener('download_enabled', (value) => { + if (value) { + return this.resumedownload(); + } else { + return this.pausedownload(); + } + }); + } + + /* + |--------------------------------------------------------------------------- + | Set the refresh interval timer + |--------------------------------------------------------------------------- + | + | This method sets the refresh interval in seconds. + | + */ + + _setRefreshTimer (seconds) { + if (this._deviceDataTimer) { + clearInterval(this._deviceDataTimer); + } + + var refreshInterval = seconds * 1000; + + this._deviceDataTimer = setInterval( () => { + this._updateDevice(); + }, refreshInterval); + + this.success(`refresh interval set to ${seconds} seconds`); + } + + /* + |--------------------------------------------------------------------------- + | Convert seconds to time + |--------------------------------------------------------------------------- + | + | This method converts seconds to a readable format. + | + */ + + _toTime (sec) { + var sec_num = parseInt(sec, 10); + var hours = Math.floor(sec_num / 3600); + var minutes = Math.floor((sec_num - (hours * 3600)) / 60); + var seconds = sec_num - (hours * 3600) - (minutes * 60); + + if (hours < 10) { hours = "0"+hours; } + if (minutes < 10) { minutes = "0"+minutes; } + if (seconds < 10) { seconds = "0"+seconds; } + + return hours+':'+minutes+':'+seconds; + } + +}; + +module.exports = NZBDevice; diff --git a/drivers/nzbdriver/driver.js b/drivers/nzbdriver/driver.js new file mode 100644 index 0000000..939a9cd --- /dev/null +++ b/drivers/nzbdriver/driver.js @@ -0,0 +1,126 @@ +'use strict'; + +const Homey = require('homey'); + +const Api = require('/lib/Api.js'); + +let foundServer = []; +let minimalVersion = 15; + +class NZBDriver extends Homey.Driver { + + /* + |--------------------------------------------------------------------------- + | Initiate + |--------------------------------------------------------------------------- + | + | This method is called when the driver is initiated. + | + */ + + onInit () { + this._addFlowCardActions(); + } + + /* + |--------------------------------------------------------------------------- + | Pairing + |--------------------------------------------------------------------------- + | + | This method is called when a pair session starts. + | + */ + + onPair (socket) { + console.log(`✓ Pairing started`); + + socket.on('search_devices', async (pairData, callback) => { + console.log(`✓ Searching...`); + + foundServer = []; + + var api = new Api(pairData); + + api.request({ method: 'version' }) + .then( result => { + var version = parseInt(result.result); + + if (version < minimalVersion) { + throw new Error(`Version ${result.result} is not supported.`); + } + + foundServer.push({ + name: `NZBGet v${result.result}`, + data: { + url: pairData.url, + port: pairData.port, + user: pairData.user, + pass: pairData.pass + } + }); + + callback(null, true); + }).catch( error => { + console.log(`✕ ${error}`); + callback(error); + }); + }); + + socket.on('list_devices', async (data, callback) => { + console.log(`✓ Found: ${foundServer[0].name}`); + callback(null, foundServer); + }); + } + + /* + |--------------------------------------------------------------------------- + | Add flowcard actions + |--------------------------------------------------------------------------- + | + | Register flowcard actions which can be used in Homey 'Then' section. + | + */ + + async _addFlowCardActions () { + + // Pause download queue + new Homey.FlowCardAction('pausedownload').register() + .registerRunListener( async (args) => { + return args.device.pausedownload(); + }); + + // Set download speed limit + new Homey.FlowCardAction('rate').register() + .registerRunListener( async (args) => { + return args.device.rate(args); + }) + .getArgument('download_rate'); + + // Reload server + new Homey.FlowCardAction('reload').register() + .registerRunListener( async (args) => { + return args.device.reload(); + }); + + // Resume download queue + new Homey.FlowCardAction('resumedownload').register() + .registerRunListener( async (args) => { + return args.device.resumedownload(); + }); + + // Scan incoming directory for nzb-files + new Homey.FlowCardAction('scan').register() + .registerRunListener( async (args) => { + return args.device.scan(); + }); + + // Shutdown server + new Homey.FlowCardAction('shutdown').register() + .registerRunListener( async (args) => { + return args.device.shutdown(); + }); + } + +}; + +module.exports = NZBDriver; diff --git a/drivers/nzbdriver/pair/start.html b/drivers/nzbdriver/pair/start.html new file mode 100644 index 0000000..6bbc6a0 --- /dev/null +++ b/drivers/nzbdriver/pair/start.html @@ -0,0 +1,68 @@ + + +