From 1fc4a04c5d7e32de7cbc12dffb1f320a37ead6a0 Mon Sep 17 00:00:00 2001 From: Edwin van de Pol Date: Mon, 24 Jun 2019 11:50:37 +0200 Subject: [PATCH] Version 1.0.0 --- CHANGELOG.md | 3 + LICENSE | 13 + README.md | 62 ++++ app.js | 22 ++ app.json | 367 ++++++++++++++++++++++ assets/clock.svg | 54 ++++ assets/download-speed.svg | 60 ++++ assets/file.svg | 46 +++ assets/harddisk.svg | 1 + assets/icon.svg | 42 +++ assets/images/large.png | Bin 0 -> 7512 bytes assets/images/small.png | Bin 0 -> 5939 bytes drivers/nzbdriver/assets/icon.svg | 42 +++ drivers/nzbdriver/assets/images/large.png | Bin 0 -> 8676 bytes drivers/nzbdriver/assets/images/small.png | Bin 0 -> 3141 bytes drivers/nzbdriver/device.js | 289 +++++++++++++++++ drivers/nzbdriver/driver.js | 126 ++++++++ drivers/nzbdriver/pair/start.html | 68 ++++ lib/Api.js | 169 ++++++++++ locales/en.json | 13 + locales/nl.json | 13 + 21 files changed, 1390 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100644 app.json create mode 100644 assets/clock.svg create mode 100644 assets/download-speed.svg create mode 100644 assets/file.svg create mode 100644 assets/harddisk.svg create mode 100644 assets/icon.svg create mode 100644 assets/images/large.png create mode 100644 assets/images/small.png create mode 100644 drivers/nzbdriver/assets/icon.svg create mode 100644 drivers/nzbdriver/assets/images/large.png create mode 100644 drivers/nzbdriver/assets/images/small.png create mode 100644 drivers/nzbdriver/device.js create mode 100644 drivers/nzbdriver/driver.js create mode 100644 drivers/nzbdriver/pair/start.html create mode 100644 lib/Api.js create mode 100644 locales/en.json create mode 100644 locales/nl.json 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 0000000000000000000000000000000000000000..3cf1f541f20b9012f044bce66171759d0d2ec94f GIT binary patch literal 7512 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 GzWYC-*rSR7 literal 0 HcmV?d00001 diff --git a/assets/images/small.png b/assets/images/small.png new file mode 100644 index 0000000000000000000000000000000000000000..13e9e8908d96846a381da93be8a7c75a18e577be GIT binary patch literal 5939 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..58b3d4757d35cb07f8572c0cd17d5d7716d9eb28 GIT binary patch literal 8676 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 H9>@I;gJBWN literal 0 HcmV?d00001 diff --git a/drivers/nzbdriver/assets/images/small.png b/drivers/nzbdriver/assets/images/small.png new file mode 100644 index 0000000000000000000000000000000000000000..4c9ecf385ec096c6e7b593cc512ae9856f686b61 GIT binary patch literal 3141 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 literal 0 HcmV?d00001 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 @@ + + +
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + diff --git a/lib/Api.js b/lib/Api.js new file mode 100644 index 0000000..e79a0f9 --- /dev/null +++ b/lib/Api.js @@ -0,0 +1,169 @@ +'use strict'; + +const Homey = require('homey'); + +const url = require('url'); + +class Api { + + /* + |--------------------------------------------------------------------------- + | Constructor + |--------------------------------------------------------------------------- + */ + + constructor (device_data) { + + // Defaults + this.user = device_data.user || 'nzbget'; + this.pass = device_data.pass || 'tegbzn6789'; + this.port = device_data.port || 6789; + this.url = device_data.url || 'http://127.0.0.1'; + + var urlObj = url.parse(this.url); + + this.auth = this.user + ':' + this.pass; + this.host = urlObj.hostname; + this.agent = this.url.startsWith('https') ? require('https') : require('http'); + } + + /* + |--------------------------------------------------------------------------- + | Request + |--------------------------------------------------------------------------- + | + | This method will fetch and return the result of the NZBGet API. + | + */ + + request (options) { + return new Promise((resolve, reject) => { + + options = options || {}; + + this.path = options.path || '/jsonrpc'; + this.httpmethod = options.httpmethod || 'POST'; + + var requestData = {}, params = [], method = '', jsonrpc = '2.0', id = this._getRandomId(); + + if (options) { + if (options.hasOwnProperty('method')) { + method = options.method; + } + if (options.hasOwnProperty('params')) { + params = options.params; + } + if (options.hasOwnProperty('jsonrpc')) { + jsonrpc = options.jsonrpc; + } + if (options.hasOwnProperty('id')) { + id = options.id; + } + } + + requestData.id = id; + requestData.method = method; + requestData.params = params; + requestData.jsonrpc = jsonrpc; + + requestData = JSON.stringify(requestData); + + if (this.httpmethod === 'GET') { + requestData = require('querystring').escape(requestData); + } + + var requestOptions = { + host: this.host, + port: this.port, + path: this.path, + agent: this.agent.globalAgent, + method: this.httpmethod, + auth: this.auth, + headers: { + 'content-type': (this.httpmethod === 'POST') ? 'application/x-www-form-urlencoded' : 'application/json', + 'content-length': requestData.length + } + }; + + if (this.httpmethod === 'GET') { + requestOptions.path = requestOptions.path + requestData; + } + + var request = this.agent.request(requestOptions); + + request.on('error', (error) => { + console.log(`--- HTTP error ---`); + console.log(error); + reject(new Error(Homey.__('error.connection'))); + }); + + request.on('response', (response) => { + var buffer = ''; + + response.on('data', (bytes) => { + buffer += bytes; + }); + + response.on('end', () => { + var result; + var data = buffer; + + if (response.statusCode === 401) { + reject(new Error(Homey.__('error.login'))); + } + else if (response.statusCode === 403) { + reject(new Error(Homey.__('error.access'))); + } + else if (response.statusCode === 500) { + reject(new Error(Homey.__('error.internal'))); + } + else if (response.statusCode === 200 || response.statusCode === 304) { + if (data.length > 0) { + try { + result = JSON.parse(data); + + if (result.error) { + console.log(result); + reject(new Error(result.error.message)); + } + else if (!result.result) { + console.log(result); + reject(new Error(Homey.__('error.unknown'))); + } + } catch (error) { + console.log(error); + reject(new Error(`JSON error:` + error)); + } + } + } else { + reject(new Error(`${Homey.__('error.unknown')}: ${response.statusCode}`)); + } + + resolve(result); + }); + }); + + if (this.httpmethod === 'POST') { + request.write(requestData); + } + + request.end(); + }); + } + + /* + |--------------------------------------------------------------------------- + | getRandomId + |--------------------------------------------------------------------------- + | + | This method will generate and return a random integer. + | + */ + + _getRandomId () { + return parseInt(Math.random() * 100000); + } + +}; + +module.exports = Api; diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..a0f861f --- /dev/null +++ b/locales/en.json @@ -0,0 +1,13 @@ +{ + "error": { + "access": "Access has been denied", + "connection": "Could not connect to device", + "internal": "An internal server error occurred", + "login": "Username or password is invalid", + "unknown": "An unknown error occured" + }, + "login": "Login", + "password": "Password", + "port": "Port", + "username": "Username" +} \ No newline at end of file diff --git a/locales/nl.json b/locales/nl.json new file mode 100644 index 0000000..0f83e68 --- /dev/null +++ b/locales/nl.json @@ -0,0 +1,13 @@ +{ + "error": { + "access": "De toegang is geweigerd", + "connection": "Kon geen verbinding met apparaat maken", + "internal": "Er is een interne serverfout opgetreden", + "login": "Gebruikersnaam of wachtwoord is ongeldig", + "unknown": "Er is een onbekende fout opgetreden" + }, + "login": "Inloggen", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" +} \ No newline at end of file