From 9f12cf1d5ac120adf12973b605ceb8ec202c2454 Mon Sep 17 00:00:00 2001 From: Trevor Date: Wed, 26 Apr 2017 11:30:18 -0500 Subject: [PATCH 1/9] Add radarr.py --- homeassistant/components/sensor/radarr.py | 244 ++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 homeassistant/components/sensor/radarr.py diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py new file mode 100644 index 00000000000000..9d5e9683b09499 --- /dev/null +++ b/homeassistant/components/sensor/radarr.py @@ -0,0 +1,244 @@ +""" +Support for Radarr. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.radarr/ +""" +import logging +import time +from datetime import datetime + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_API_KEY, CONF_HOST, CONF_PORT) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.const import CONF_SSL +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA + +_LOGGER = logging.getLogger(__name__) + +CONF_DAYS = 'days' +CONF_INCLUDED = 'include_paths' +CONF_UNIT = 'unit' +CONF_URLBASE = 'urlbase' + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 7878 +DEFAULT_URLBASE = '' +DEFAULT_DAYS = '1' +DEFAULT_UNIT = 'GB' + +SENSOR_TYPES = { + 'diskspace': ['Disk Space', 'GB', 'mdi:harddisk'], + 'upcoming': ['Upcoming', 'Movies', 'mdi:television'], + 'wanted': ['Wanted', 'Movies', 'mdi:television'], + 'movies': ['Movies', 'Movies', 'mdi:television'], + 'commands': ['Commands', 'Commands', 'mdi:code-braces'], + 'status': ['Status', 'Status', 'mdi:information'] +} + +ENDPOINTS = { + 'diskspace': 'http{0}://{1}:{2}/{3}api/diskspace?apikey={4}', + 'upcoming': + 'http{0}://{1}:{2}/{3}api/calendar?apikey={4}&start={5}&end={6}', + 'wanted': 'http{0}://{1}:{2}/{3}api/movie?apikey={4}', + 'movies': 'http{0}://{1}:{2}/{3}api/movie?apikey={4}', + 'commands': 'http{0}://{1}:{2}/{3}api/command?apikey={4}', + 'status': 'http{0}://{1}:{2}/{3}api/system/status?apikey={4}' +} + +# Support to Yottabytes for the future, why not +BYTE_SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES.keys()))]), + vol.Optional(CONF_INCLUDED, default=[]): cv.ensure_list, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): cv.string, + vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.string, + vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): vol.In(BYTE_SIZES) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Radarr platform.""" + conditions = config.get(CONF_MONITORED_CONDITIONS) + add_devices( + [RadarrSensor(hass, config, sensor) for sensor in conditions] + ) + return True + + +class RadarrSensor(Entity): + """Implemention of the Radarr sensor.""" + + def __init__(self, hass, conf, sensor_type): + """Create Radarr entity.""" + from pytz import timezone + self.conf = conf + self.host = conf.get(CONF_HOST) + self.port = conf.get(CONF_PORT) + self.urlbase = conf.get(CONF_URLBASE) + if self.urlbase: + self.urlbase = "%s/" % self.urlbase.strip('/') + self.apikey = conf.get(CONF_API_KEY) + self.included = conf.get(CONF_INCLUDED) + self.days = int(conf.get(CONF_DAYS)) + self.ssl = 's' if conf.get(CONF_SSL) else '' + + # Object data + self.data = [] + self._tz = timezone(str(hass.config.time_zone)) + self.type = sensor_type + self._name = SENSOR_TYPES[self.type][0] + if self.type == 'diskspace': + self._unit = conf.get(CONF_UNIT) + else: + self._unit = SENSOR_TYPES[self.type][1] + self._icon = SENSOR_TYPES[self.type][2] + + # Update sensor + self._available = False + self.update() + + def update(self): + """Update the data for the sensor.""" + start = get_date(self._tz) + end = get_date(self._tz, self.days) + try: + res = requests.get( + ENDPOINTS[self.type].format( + self.ssl, self.host, self.port, + self.urlbase, self.apikey, start, end), + timeout=5) + except OSError: + _LOGGER.error('Host %s is not available', self.host) + self._available = False + self._state = None + return + + if res.status_code == 200: + if self.type in ['wanted', 'upcoming', 'movies', 'commands']: + if self.type == 'movies': + self.data = list( + filter( + lambda x: x['downloaded'] == True, + res.json() + ) + ) + elif self.type == 'wanted': + self.data = list( + filter( + lambda x: x['downloaded'] == False, + res.json() + ) + ) + else: + self.data = res.json() + self._state = len(self.data) + elif self.type == 'diskspace': + # If included paths are not provided, use all data + if self.included == []: + self.data = res.json() + else: + # Filter to only show lists that are included + self.data = list( + filter( + lambda x: x['path'] in self.included, + res.json() + ) + ) + self._state = '{:.2f}'.format( + to_unit( + sum([data['freeSpace'] for data in self.data]), + self._unit + ) + ) + elif self.type == 'status': + self.data = res.json() + self._state = self.data['version'] + self._available = True + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format('Radarr', self._name) + + @property + def state(self): + """Return sensor state.""" + return self._state + + @property + def available(self): + """Return sensor availability.""" + return self._available + + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + attributes = {} + if self.type == 'upcoming': + for movie in self.data: + attributes[to_key(movie)] = get_release_date(movie) + elif self.type == 'wanted': + for movie in self.data: + attributes[to_key(movie)] = movie['status'] + elif self.type == 'commands': + for command in self.data: + attributes[command['name']] = command['state'] + elif self.type == 'diskspace': + for data in self.data: + attributes[data['path']] = '{:.2f}/{:.2f}{} ({:.2f}%)'.format( + to_unit(data['freeSpace'], self._unit), + to_unit(data['totalSpace'], self._unit), + self._unit, ( + to_unit(data['freeSpace'], self._unit) / + to_unit(data['totalSpace'], self._unit) * 100 + ) + ) + elif self.type == 'movies': + for movie in self.data: + attributes[to_key(movie)] = movie['hasFile'] + elif self.type == 'status': + attributes = self.data + + return attributes + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + + +def get_date(zone, offset=0): + """Get date based on timezone and offset of days.""" + day = 60 * 60 * 24 + return datetime.date( + datetime.fromtimestamp(time.time() + day*offset, tz=zone) + ) + +def get_release_date(data): + date = data['physicalRelease'] + if not date: + date = data['inCinemas'] + return date + +def to_key(data): + return '{} ({})'.format(data['title'], data['year']) + +def to_unit(value, unit): + """Convert bytes to give unit.""" + return value / 1024**BYTE_SIZES.index(unit) + From 6ce25fd58789a59248d6920ffb842166e429a207 Mon Sep 17 00:00:00 2001 From: Trevor Date: Wed, 26 Apr 2017 11:48:09 -0500 Subject: [PATCH 2/9] Update radarr.py --- homeassistant/components/sensor/radarr.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py index 9d5e9683b09499..046a48466f0786 100644 --- a/homeassistant/components/sensor/radarr.py +++ b/homeassistant/components/sensor/radarr.py @@ -128,14 +128,14 @@ def update(self): if self.type == 'movies': self.data = list( filter( - lambda x: x['downloaded'] == True, + lambda x: x['downloaded'], res.json() ) ) elif self.type == 'wanted': self.data = list( filter( - lambda x: x['downloaded'] == False, + lambda x: not x['downloaded'], res.json() ) ) @@ -213,7 +213,7 @@ def device_state_attributes(self): attributes[to_key(movie)] = movie['hasFile'] elif self.type == 'status': attributes = self.data - + return attributes @property @@ -228,17 +228,19 @@ def get_date(zone, offset=0): return datetime.date( datetime.fromtimestamp(time.time() + day*offset, tz=zone) ) - + + def get_release_date(data): date = data['physicalRelease'] if not date: date = data['inCinemas'] return date + def to_key(data): return '{} ({})'.format(data['title'], data['year']) + def to_unit(value, unit): """Convert bytes to give unit.""" return value / 1024**BYTE_SIZES.index(unit) - From 445724259b2ea1cd0b88bf3f5a7ddd7fc374ed24 Mon Sep 17 00:00:00 2001 From: Trevor Date: Mon, 1 May 2017 19:21:18 -0500 Subject: [PATCH 3/9] Update radarr.py --- homeassistant/components/sensor/radarr.py | 52 ++++++++--------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py index 046a48466f0786..47be5aa4103325 100644 --- a/homeassistant/components/sensor/radarr.py +++ b/homeassistant/components/sensor/radarr.py @@ -6,15 +6,15 @@ """ import logging import time -from datetime import datetime +from datetime import datetime, timedelta import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_API_KEY, CONF_HOST, CONF_PORT) -from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.const import CONF_SSL +from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_SSL) from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -31,6 +31,8 @@ DEFAULT_DAYS = '1' DEFAULT_UNIT = 'GB' +SCAN_INTERVAL = timedelta(minutes=10) + SENSOR_TYPES = { 'diskspace': ['Disk Space', 'GB', 'mdi:harddisk'], 'upcoming': ['Upcoming', 'Movies', 'mdi:television'], @@ -41,13 +43,12 @@ } ENDPOINTS = { - 'diskspace': 'http{0}://{1}:{2}/{3}api/diskspace?apikey={4}', + 'diskspace': 'http{0}://{1}:{2}/{3}api/diskspace', 'upcoming': - 'http{0}://{1}:{2}/{3}api/calendar?apikey={4}&start={5}&end={6}', - 'wanted': 'http{0}://{1}:{2}/{3}api/movie?apikey={4}', - 'movies': 'http{0}://{1}:{2}/{3}api/movie?apikey={4}', - 'commands': 'http{0}://{1}:{2}/{3}api/command?apikey={4}', - 'status': 'http{0}://{1}:{2}/{3}api/system/status?apikey={4}' + 'http{0}://{1}:{2}/{3}api/calendar?start={4}&end={5}', + 'movies': 'http{0}://{1}:{2}/{3}api/movie', + 'commands': 'http{0}://{1}:{2}/{3}api/command', + 'status': 'http{0}://{1}:{2}/{3}api/system/status' } # Support to Yottabytes for the future, why not @@ -55,7 +56,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES.keys()))]), + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), vol.Optional(CONF_INCLUDED, default=[]): cv.ensure_list, vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, @@ -86,7 +87,7 @@ def __init__(self, hass, conf, sensor_type): self.port = conf.get(CONF_PORT) self.urlbase = conf.get(CONF_URLBASE) if self.urlbase: - self.urlbase = "%s/" % self.urlbase.strip('/') + self.urlbase = '%s/'.format(self.urlbase.strip('/')) self.apikey = conf.get(CONF_API_KEY) self.included = conf.get(CONF_INCLUDED) self.days = int(conf.get(CONF_DAYS)) @@ -115,7 +116,8 @@ def update(self): res = requests.get( ENDPOINTS[self.type].format( self.ssl, self.host, self.port, - self.urlbase, self.apikey, start, end), + self.urlbase, start, end), + headers={'X-Api-Key': self.apikey}, timeout=5) except OSError: _LOGGER.error('Host %s is not available', self.host) @@ -124,23 +126,8 @@ def update(self): return if res.status_code == 200: - if self.type in ['wanted', 'upcoming', 'movies', 'commands']: - if self.type == 'movies': - self.data = list( - filter( - lambda x: x['downloaded'], - res.json() - ) - ) - elif self.type == 'wanted': - self.data = list( - filter( - lambda x: not x['downloaded'], - res.json() - ) - ) - else: - self.data = res.json() + if self.type in ['upcoming', 'movies', 'commands']: + self.data = res.json() self._state = len(self.data) elif self.type == 'diskspace': # If included paths are not provided, use all data @@ -192,9 +179,6 @@ def device_state_attributes(self): if self.type == 'upcoming': for movie in self.data: attributes[to_key(movie)] = get_release_date(movie) - elif self.type == 'wanted': - for movie in self.data: - attributes[to_key(movie)] = movie['status'] elif self.type == 'commands': for command in self.data: attributes[command['name']] = command['state'] @@ -210,7 +194,7 @@ def device_state_attributes(self): ) elif self.type == 'movies': for movie in self.data: - attributes[to_key(movie)] = movie['hasFile'] + attributes[to_key(movie)] = movie['downloaded'] elif self.type == 'status': attributes = self.data From 429accc9059071b08d16f42dcc2046f9232c5422 Mon Sep 17 00:00:00 2001 From: Trevor Date: Tue, 2 May 2017 00:22:14 -0500 Subject: [PATCH 4/9] Add test_radarr.py --- tests/components/sensor/test_radarr.py | 446 +++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 tests/components/sensor/test_radarr.py diff --git a/tests/components/sensor/test_radarr.py b/tests/components/sensor/test_radarr.py new file mode 100644 index 00000000000000..7fb4e492b431d9 --- /dev/null +++ b/tests/components/sensor/test_radarr.py @@ -0,0 +1,446 @@ +"""The tests for the radarr platform.""" +import unittest +import time +from datetime import datetime + +import pytest + +from homeassistant.components.sensor import radarr + +from tests.common import get_test_home_assistant + + +def mocked_exception(*args, **kwargs): + """Mock exception thrown by requests.get.""" + raise OSError + + +def mocked_requests_get(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + today = datetime.date(datetime.fromtimestamp(time.time())) + url = str(args[0]) + if 'api/calendar' in url: + return MockResponse([ + { + "title": "Resident Evil: The Final Chapter", + "sortTitle": "resident evil final chapter", + "sizeOnDisk": 0, + "status": "announced", + "overview": "Alice, Jill, Claire, Chris, Leon, Ada, and Wesker rush to The Hive, where The Red Queen plots total destruction over the human race.", + "inCinemas": "2017-01-25T00:00:00Z", + "physicalRelease": "2017-01-27T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "/radarr/MediaCover/12/poster.jpg?lastWrite=636208663600000000" + }, + { + "coverType": "banner", + "url": "/radarr/MediaCover/12/banner.jpg?lastWrite=636208663600000000" + } + ], + "website": "", + "downloaded": "false", + "year": 2017, + "hasFile": "false", + "youTubeTrailerId": "B5yxr7lmxhg", + "studio": "Impact Pictures", + "path": "/path/to/Resident Evil The Final Chapter (2017)", + "profileId": 3, + "monitored": "false", + "runtime": 106, + "lastInfoSync": "2017-01-24T14:52:40.315434Z", + "cleanTitle": "residentevilfinalchapter", + "imdbId": "tt2592614", + "tmdbId": 173897, + "titleSlug": "resident-evil-the-final-chapter-2017", + "genres": [ + "Action", + "Horror", + "Science Fiction" + ], + "tags": [], + "added": "2017-01-24T14:52:39.989964Z", + "ratings": { + "votes": 363, + "value": 4.3 + }, + "alternativeTitles": [ + "Resident Evil: Rising" + ], + "qualityProfileId": 3, + "id": 12 + } + ], 200) + elif 'api/command' in url: + return MockResponse([ + { + "name": "RescanMovie", + "startedOn": "0001-01-01T00:00:00Z", + "stateChangeTime": "2014-02-05T05:09:09.2366139Z", + "sendUpdatesToClient": "true", + "state": "pending", + "id": 24 + } + ], 200) + elif 'api/movie' in url: + return MockResponse([ + { + "title": "Assassin's Creed", + "sortTitle": "assassins creed", + "sizeOnDisk": 0, + "status": "released", + "overview": "Lynch discovers he is a descendant of the secret Assassins society through unlocked genetic memories that allow him to relive the adventures of his ancestor, Aguilar, in 15th Century Spain. After gaining incredible knowledge and skills he’s poised to take on the oppressive Knights Templar in the present day.", + "inCinemas": "2016-12-21T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "/radarr/MediaCover/1/poster.jpg?lastWrite=636200219330000000" + }, + { + "coverType": "banner", + "url": "/radarr/MediaCover/1/banner.jpg?lastWrite=636200219340000000" + } + ], + "website": "https://www.ubisoft.com/en-US/", + "downloaded": "false", + "year": 2016, + "hasFile": "false", + "youTubeTrailerId": "pgALJgMjXN4", + "studio": "20th Century Fox", + "path": "/path/to/Assassin's Creed (2016)", + "profileId": 6, + "monitored": "true", + "runtime": 115, + "lastInfoSync": "2017-01-23T22:05:32.365337Z", + "cleanTitle": "assassinscreed", + "imdbId": "tt2094766", + "tmdbId": 121856, + "titleSlug": "assassins-creed-121856", + "genres": [ + "Action", + "Adventure", + "Fantasy", + "Science Fiction" + ], + "tags": [], + "added": "2017-01-14T20:18:52.938244Z", + "ratings": { + "votes": 711, + "value": 5.2 + }, + "alternativeTitles": [ + "Assassin's Creed: The IMAX Experience" + ], + "qualityProfileId": 6, + "id": 1 + } + ], 200) + elif 'api/diskspace' in url: + return MockResponse([ + { + "path": "/data", + "label": "", + "freeSpace": 282500067328, + "totalSpace": 499738734592 + } + ], 200) + elif 'api/system/status' in url: + return MockResponse({ + "version": "0.2.0.210", + "buildTime": "2017-01-22T23:12:49Z", + "isDebug": "false", + "isProduction": "true", + "isAdmin": "false", + "isUserInteractive": "false", + "startupPath": "/path/to/radarr", + "appData": "/path/to/radarr/data", + "osVersion": "4.8.13.1", + "isMonoRuntime": "true", + "isMono": "true", + "isLinux": "true", + "isOsx": "false", + "isWindows": "false", + "branch": "develop", + "authentication": "forms", + "sqliteVersion": "3.16.2", + "urlBase": "", + "runtimeVersion": "4.6.1 (Stable 4.6.1.3/abb06f1 Mon Oct 3 07:57:59 UTC 2016)" + }, 200) + else: + return MockResponse({ + "error": "Unauthorized" + }, 401) + + +class TestRadarrSetup(unittest.TestCase): + """Test the Radarr platform.""" + + # pylint: disable=invalid-name + DEVICES = [] + + def add_devices(self, devices): + """Mock add devices.""" + for device in devices: + self.DEVICES.append(device) + + def setUp(self): + """Initialize values for this testcase class.""" + self.DEVICES = [] + self.hass = get_test_home_assistant() + self.hass.config.time_zone = 'America/Los_Angeles' + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_diskspace_no_paths(self, req_mock): + """Test getting all disk space.""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [], + 'monitored_conditions': [ + 'diskspace' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual('263.10', device.state) + self.assertEqual('mdi:harddisk', device.icon) + self.assertEqual('GB', device.unit_of_measurement) + self.assertEqual('Radarr Disk Space', device.name) + self.assertEqual( + '263.10/465.42GB (56.53%)', + device.device_state_attributes["/data"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_diskspace_paths(self, req_mock): + """Test getting diskspace for included paths.""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'diskspace' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual('263.10', device.state) + self.assertEqual('mdi:harddisk', device.icon) + self.assertEqual('GB', device.unit_of_measurement) + self.assertEqual('Radarr Disk Space', device.name) + self.assertEqual( + '263.10/465.42GB (56.53%)', + device.device_state_attributes["/data"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_commands(self, req_mock): + """Test getting running commands.""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'commands' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:code-braces', device.icon) + self.assertEqual('Commands', device.unit_of_measurement) + self.assertEqual('Radarr Commands', device.name) + self.assertEqual( + 'pending', + device.device_state_attributes["RescanMovie"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_movies(self, req_mock): + """Test getting the number of movies.""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'movies' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Movies', device.unit_of_measurement) + self.assertEqual('Radarr Movies', device.name) + self.assertEqual( + 'false', + device.device_state_attributes["Assassin's Creed (2016)"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_upcoming_multiple_days(self, req_mock): + """Test the upcoming movies for multiple days.""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Movies', device.unit_of_measurement) + self.assertEqual('Radarr Upcoming', device.name) + self.assertEqual( + '2017-01-27T00:00:00Z', + device.device_state_attributes["Resident Evil: The Final Chapter (2017)"] + ) + + @pytest.mark.skip + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_upcoming_today(self, req_mock): + """Test filtering for a single day. + + Radarr needs to respond with at least 2 days + """ + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Movies', device.unit_of_measurement) + self.assertEqual('Radarr Upcoming', device.name) + self.assertEqual( + '2017-01-27T00:00:00Z', + device.device_state_attributes["Resident Evil: The Final Chapter (2017)"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_system_status(self, req_mock): + """Test getting system status""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'status' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual('0.2.0.210', device.state) + self.assertEqual('mdi:information', device.icon) + self.assertEqual('Radarr Status', device.name) + self.assertEqual( + '4.8.13.1', + device.device_state_attributes['osVersion']) + + @pytest.mark.skip + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_ssl(self, req_mock): + """Test SSL being enabled.""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ], + "ssl": "true" + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('s', device.ssl) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Movies', device.unit_of_measurement) + self.assertEqual('Radarr Upcoming', device.name) + self.assertEqual( + '2017-01-27T00:00:00Z', + device.device_state_attributes["Resident Evil: The Final Chapter (2017)"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_exception) + def test_exception_handling(self, req_mock): + """Test exception being handled.""" + config = { + 'platform': 'radarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + radarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(None, device.state) \ No newline at end of file From e817f66810aa4ac33c1e05dd8b11371c5f176fb1 Mon Sep 17 00:00:00 2001 From: Trevor Date: Tue, 2 May 2017 00:43:11 -0500 Subject: [PATCH 5/9] Update test_radarr.py --- tests/components/sensor/test_radarr.py | 31 +++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/components/sensor/test_radarr.py b/tests/components/sensor/test_radarr.py index 7fb4e492b431d9..ac906c64e5e858 100644 --- a/tests/components/sensor/test_radarr.py +++ b/tests/components/sensor/test_radarr.py @@ -29,26 +29,27 @@ def json(self): """Return the json of the response.""" return self.json_data - today = datetime.date(datetime.fromtimestamp(time.time())) url = str(args[0]) if 'api/calendar' in url: return MockResponse([ { - "title": "Resident Evil: The Final Chapter", + "title": "Resident Evil", "sortTitle": "resident evil final chapter", "sizeOnDisk": 0, "status": "announced", - "overview": "Alice, Jill, Claire, Chris, Leon, Ada, and Wesker rush to The Hive, where The Red Queen plots total destruction over the human race.", + "overview": "Alice, Jill, Claire, Chris, Leon, Ada, and...", "inCinemas": "2017-01-25T00:00:00Z", "physicalRelease": "2017-01-27T00:00:00Z", "images": [ { "coverType": "poster", - "url": "/radarr/MediaCover/12/poster.jpg?lastWrite=636208663600000000" + "url": "/radarr/MediaCover/12/poster.jpg" + + "?lastWrite=636208663600000000" }, { "coverType": "banner", - "url": "/radarr/MediaCover/12/banner.jpg?lastWrite=636208663600000000" + "url": "/radarr/MediaCover/12/banner.jpg" + + "?lastWrite=636208663600000000" } ], "website": "", @@ -102,16 +103,18 @@ def json(self): "sortTitle": "assassins creed", "sizeOnDisk": 0, "status": "released", - "overview": "Lynch discovers he is a descendant of the secret Assassins society through unlocked genetic memories that allow him to relive the adventures of his ancestor, Aguilar, in 15th Century Spain. After gaining incredible knowledge and skills he’s poised to take on the oppressive Knights Templar in the present day.", + "overview": "Lynch discovers he is a descendant of...", "inCinemas": "2016-12-21T00:00:00Z", "images": [ { "coverType": "poster", - "url": "/radarr/MediaCover/1/poster.jpg?lastWrite=636200219330000000" + "url": "/radarr/MediaCover/1/poster.jpg" + + "?lastWrite=636200219330000000" }, { "coverType": "banner", - "url": "/radarr/MediaCover/1/banner.jpg?lastWrite=636200219340000000" + "url": "/radarr/MediaCover/1/banner.jpg" + + "?lastWrite=636200219340000000" } ], "website": "https://www.ubisoft.com/en-US/", @@ -177,7 +180,9 @@ def json(self): "authentication": "forms", "sqliteVersion": "3.16.2", "urlBase": "", - "runtimeVersion": "4.6.1 (Stable 4.6.1.3/abb06f1 Mon Oct 3 07:57:59 UTC 2016)" + "runtimeVersion": "4.6.1 " + + "(Stable 4.6.1.3/abb06f1 " + + "Mon Oct 3 07:57:59 UTC 2016)" }, 200) else: return MockResponse({ @@ -336,7 +341,7 @@ def test_upcoming_multiple_days(self, req_mock): self.assertEqual('Radarr Upcoming', device.name) self.assertEqual( '2017-01-27T00:00:00Z', - device.device_state_attributes["Resident Evil: The Final Chapter (2017)"] + device.device_state_attributes["Resident Evil (2017)"] ) @pytest.mark.skip @@ -367,7 +372,7 @@ def test_upcoming_today(self, req_mock): self.assertEqual('Radarr Upcoming', device.name) self.assertEqual( '2017-01-27T00:00:00Z', - device.device_state_attributes["Resident Evil: The Final Chapter (2017)"] + device.device_state_attributes["Resident Evil (2017)"] ) @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) @@ -422,7 +427,7 @@ def test_ssl(self, req_mock): self.assertEqual('Radarr Upcoming', device.name) self.assertEqual( '2017-01-27T00:00:00Z', - device.device_state_attributes["Resident Evil: The Final Chapter (2017)"] + device.device_state_attributes["Resident Evil (2017)"] ) @unittest.mock.patch('requests.get', side_effect=mocked_exception) @@ -443,4 +448,4 @@ def test_exception_handling(self, req_mock): radarr.setup_platform(self.hass, config, self.add_devices, None) for device in self.DEVICES: device.update() - self.assertEqual(None, device.state) \ No newline at end of file + self.assertEqual(None, device.state) From b96ccb5875d8c90a6df49f56272cb8bfa0268441 Mon Sep 17 00:00:00 2001 From: Trevor Date: Tue, 2 May 2017 00:45:04 -0500 Subject: [PATCH 6/9] Update test_radarr.py --- tests/components/sensor/test_radarr.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/components/sensor/test_radarr.py b/tests/components/sensor/test_radarr.py index ac906c64e5e858..0b8059c880f071 100644 --- a/tests/components/sensor/test_radarr.py +++ b/tests/components/sensor/test_radarr.py @@ -1,7 +1,5 @@ """The tests for the radarr platform.""" import unittest -import time -from datetime import datetime import pytest @@ -44,12 +42,12 @@ def json(self): { "coverType": "poster", "url": "/radarr/MediaCover/12/poster.jpg" - + "?lastWrite=636208663600000000" + + "?lastWrite=636208663600000000" }, { "coverType": "banner", "url": "/radarr/MediaCover/12/banner.jpg" - + "?lastWrite=636208663600000000" + + "?lastWrite=636208663600000000" } ], "website": "", From 9fb2f6b18344ad1004cd404286320928b17746bd Mon Sep 17 00:00:00 2001 From: Trevor Date: Thu, 4 May 2017 18:56:09 -0500 Subject: [PATCH 7/9] Update radarr.py --- homeassistant/components/sensor/radarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py index 47be5aa4103325..144485a448820e 100644 --- a/homeassistant/components/sensor/radarr.py +++ b/homeassistant/components/sensor/radarr.py @@ -87,7 +87,7 @@ def __init__(self, hass, conf, sensor_type): self.port = conf.get(CONF_PORT) self.urlbase = conf.get(CONF_URLBASE) if self.urlbase: - self.urlbase = '%s/'.format(self.urlbase.strip('/')) + self.urlbase = '{}/'.format(self.urlbase.strip('/')) self.apikey = conf.get(CONF_API_KEY) self.included = conf.get(CONF_INCLUDED) self.days = int(conf.get(CONF_DAYS)) From 8cf438835213b43cd5234c7d59daec4b46020a41 Mon Sep 17 00:00:00 2001 From: Trevor Date: Thu, 4 May 2017 19:23:53 -0500 Subject: [PATCH 8/9] Update .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 365a3a23fc1a9a..c6c3caa2003e21 100644 --- a/.coveragerc +++ b/.coveragerc @@ -399,6 +399,7 @@ omit = homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/qnap.py + homeassistant/components/sensor/radarr.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sensehat.py From 9a7cd5febd934b7d998bc7d157049e84ebb3154b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Jun 2017 23:34:53 -0700 Subject: [PATCH 9/9] Fix hound. --- tests/components/sensor/test_radarr.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/components/sensor/test_radarr.py b/tests/components/sensor/test_radarr.py index 0b8059c880f071..b0259b6352afb8 100644 --- a/tests/components/sensor/test_radarr.py +++ b/tests/components/sensor/test_radarr.py @@ -41,13 +41,13 @@ def json(self): "images": [ { "coverType": "poster", - "url": "/radarr/MediaCover/12/poster.jpg" - + "?lastWrite=636208663600000000" + "url": ("/radarr/MediaCover/12/poster.jpg" + "?lastWrite=636208663600000000") }, { "coverType": "banner", - "url": "/radarr/MediaCover/12/banner.jpg" - + "?lastWrite=636208663600000000" + "url": ("/radarr/MediaCover/12/banner.jpg" + "?lastWrite=636208663600000000") } ], "website": "", @@ -106,13 +106,13 @@ def json(self): "images": [ { "coverType": "poster", - "url": "/radarr/MediaCover/1/poster.jpg" - + "?lastWrite=636200219330000000" + "url": ("/radarr/MediaCover/1/poster.jpg" + "?lastWrite=636200219330000000") }, { "coverType": "banner", - "url": "/radarr/MediaCover/1/banner.jpg" - + "?lastWrite=636200219340000000" + "url": ("/radarr/MediaCover/1/banner.jpg" + "?lastWrite=636200219340000000") } ], "website": "https://www.ubisoft.com/en-US/", @@ -178,9 +178,9 @@ def json(self): "authentication": "forms", "sqliteVersion": "3.16.2", "urlBase": "", - "runtimeVersion": "4.6.1 " - + "(Stable 4.6.1.3/abb06f1 " - + "Mon Oct 3 07:57:59 UTC 2016)" + "runtimeVersion": ("4.6.1 " + "(Stable 4.6.1.3/abb06f1 " + "Mon Oct 3 07:57:59 UTC 2016)") }, 200) else: return MockResponse({