From 85d6970df88f8f665ceee566dd59aaf9e69a0f21 Mon Sep 17 00:00:00 2001 From: Harris Borawski Date: Tue, 22 Nov 2016 22:32:45 -0800 Subject: [PATCH] Add Sensor for Sonarr (#4496) * Add sonarr sensor and tests for sensor * Fixed some linting errors and removed unused import * Add SSL option for those who use SSL from within Sonarr * Add requirements to all requirements, and sensor to coveragerc * remove unused variable * move methods to functions, and other lint fixes * linting fixes * linting is clean now * Remove double requirement * fix linting for docstrings, this should probably be a part of the script/lint and not just travis --- .coveragerc | 1 + homeassistant/components/sensor/sonarr.py | 237 +++++++ tests/components/sensor/test_sonarr.py | 811 ++++++++++++++++++++++ 3 files changed, 1049 insertions(+) create mode 100644 homeassistant/components/sensor/sonarr.py create mode 100644 tests/components/sensor/test_sonarr.py diff --git a/.coveragerc b/.coveragerc index 17a6fc4317c..19c02da166d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -292,6 +292,7 @@ omit = homeassistant/components/sensor/scrape.py homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/snmp.py + homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/supervisord.py diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py new file mode 100644 index 00000000000..0023755bc04 --- /dev/null +++ b/homeassistant/components/sensor/sonarr.py @@ -0,0 +1,237 @@ +"""Support for Sonarr.""" +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 +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_HOST = 'host' +CONF_PORT = 'port' +CONF_DAYS = 'days' +CONF_INCLUDED = 'include_paths' +CONF_UNIT = 'unit' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8989 +DEFAULT_DAYS = '1' +DEFAULT_UNIT = 'GB' + +SENSOR_TYPES = { + 'diskspace': ['Disk Space', 'GB', 'mdi:harddisk'], + 'queue': ['Queue', 'Episodes', 'mdi:download'], + 'upcoming': ['Upcoming', 'Episodes', 'mdi:television'], + 'wanted': ['Wanted', 'Episodes', 'mdi:television'], + 'series': ['Series', 'Shows', 'mdi:television'], + 'commands': ['Commands', 'Commands', 'mdi:code-braces'] +} + +ENDPOINTS = { + 'diskspace': 'http{0}://{1}:{2}/api/diskspace?apikey={3}', + 'queue': 'http{0}://{1}:{2}/api/queue?apikey={3}', + 'upcoming': 'http{0}://{1}:{2}/api/calendar?apikey={3}&start={4}&end={5}', + 'wanted': 'http{0}://{1}:{2}/api/wanted/missing?apikey={3}', + 'series': 'http{0}://{1}:{2}/api/series?apikey={3}', + 'commands': 'http{0}://{1}:{2}/api/command?apikey={3}' +} + +# Suport 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_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): + """Setup the Sonarr platform.""" + conditions = config.get(CONF_MONITORED_CONDITIONS) + add_devices( + [Sonarr(hass, config, sensor) for sensor in conditions] + ) + return True + + +class Sonarr(Entity): + """Implement the Sonarr sensor class.""" + + def __init__(self, hass, conf, sensor_type): + """Create sonarr entity.""" + from pytz import timezone + # Configuration data + self.conf = conf + self.host = conf.get(CONF_HOST) + self.port = conf.get(CONF_PORT) + 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._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.update() + + def update(self): + """Update the data for the sensor.""" + start = get_date(self._tz) + end = get_date(self._tz, self.days) + res = requests.get( + ENDPOINTS[self.type].format( + self.ssl, + self.host, + self.port, + self.apikey, + start, + end + ) + ) + if res.status_code == 200: + if self.type in ['upcoming', 'queue', 'series', 'commands']: + if self.days == 1 and self.type == 'upcoming': + # Sonarr API returns empty array if start and end dates are + # the same, so we need to filter to just today + self.data = list( + filter( + lambda x: x['airDate'] == str(start), + res.json() + ) + ) + else: + self.data = res.json() + self._state = len(self.data) + elif self.type == 'wanted': + data = res.json() + res = requests.get('{}&pageSize={}'.format( + ENDPOINTS[self.type].format( + self.ssl, + self.host, + self.port, + self.apikey + ), + data['totalRecords'] + )) + self.data = res.json()['records'] + 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 + ) + ) + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format("Sonarr", self._name) + + @property + def state(self): + """Return sensor state.""" + return self._state + + @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 show in self.data: + attributes[show['series']['title']] = 'S{:02d}E{:02d}'.format( + show['seasonNumber'], + show['episodeNumber'] + ) + elif self.type == 'queue': + for show in self.data: + attributes[show['series']['title'] + ' S{:02d}E{:02d}'.format( + show['episode']['seasonNumber'], + show['episode']['episodeNumber'] + )] = '{:.2f}%'.format(100*(1-(show['sizeleft']/show['size']))) + elif self.type == 'wanted': + for show in self.data: + attributes[show['series']['title'] + ' S{:02d}E{:02d}'.format( + show['seasonNumber'], + show['episodeNumber'] + )] = show['airDate'] + 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 == 'series': + for show in self.data: + attributes[show['title']] = '{}/{} Episodes'.format( + show['episodeFileCount'], + show['episodeCount'] + ) + 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 to_unit(value, unit): + """Convert bytes to give unit.""" + return value/1024**BYTE_SIZES.index(unit) diff --git a/tests/components/sensor/test_sonarr.py b/tests/components/sensor/test_sonarr.py new file mode 100644 index 00000000000..e483a3aee1e --- /dev/null +++ b/tests/components/sensor/test_sonarr.py @@ -0,0 +1,811 @@ +"""The tests for the sonarr platform.""" +import unittest +import time +from datetime import datetime +from homeassistant.components.sensor import sonarr + +from tests.common import get_test_home_assistant + + +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([ + { + "seriesId": 3, + "episodeFileId": 0, + "seasonNumber": 4, + "episodeNumber": 11, + "title": "Easy Com-mercial, Easy Go-mercial", + "airDate": str(today), + "airDateUtc": "2014-01-27T01:30:00Z", + "overview": "To compete with fellow “restaurateur,” Ji...", + "hasFile": "false", + "monitored": "true", + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "series": { + "tvdbId": 194031, + "tvRageId": 24607, + "imdbId": "tt1561755", + "title": "Bob's Burgers", + "cleanTitle": "bobsburgers", + "status": "continuing", + "overview": "Bob's Burgers follows a third-generation ...", + "airTime": "5:30pm", + "monitored": "true", + "qualityProfileId": 1, + "seasonFolder": "true", + "lastInfoSync": "2014-01-26T19:25:55.4555946Z", + "runtime": 30, + "images": [ + { + "coverType": "banner", + "url": "http://slurm.trakt.us/images/bann.jpg" + }, + { + "coverType": "poster", + "url": "http://slurm.trakt.us/images/poster00.jpg" + }, + { + "coverType": "fanart", + "url": "http://slurm.trakt.us/images/fan6.jpg" + } + ], + "seriesType": "standard", + "network": "FOX", + "useSceneNumbering": "false", + "titleSlug": "bobs-burgers", + "path": "T:\\Bob's Burgers", + "year": 0, + "firstAired": "2011-01-10T01:30:00Z", + "qualityProfile": { + "value": { + "name": "SD", + "allowed": [ + { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + { + "id": 8, + "name": "WEBDL-480p", + "weight": 2 + }, + { + "id": 2, + "name": "DVD", + "weight": 3 + } + ], + "cutoff": { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + "id": 1 + }, + "isLoaded": "true" + }, + "seasons": [ + { + "seasonNumber": 4, + "monitored": "true" + }, + { + "seasonNumber": 3, + "monitored": "true" + }, + { + "seasonNumber": 2, + "monitored": "true" + }, + { + "seasonNumber": 1, + "monitored": "true" + }, + { + "seasonNumber": 0, + "monitored": "false" + } + ], + "id": 66 + }, + "downloading": "false", + "id": 14402 + } + ], 200) + elif 'api/command' in url: + return MockResponse([ + { + "name": "RescanSeries", + "startedOn": "0001-01-01T00:00:00Z", + "stateChangeTime": "2014-02-05T05:09:09.2366139Z", + "sendUpdatesToClient": "true", + "state": "pending", + "id": 24 + } + ], 200) + elif 'api/wanted/missing' in url or 'totalRecords' in url: + return MockResponse( + { + "page": 1, + "pageSize": 15, + "sortKey": "airDateUtc", + "sortDirection": "descending", + "totalRecords": 1, + "records": [ + { + "seriesId": 1, + "episodeFileId": 0, + "seasonNumber": 5, + "episodeNumber": 4, + "title": "Archer Vice: House Call", + "airDate": "2014-02-03", + "airDateUtc": "2014-02-04T03:00:00Z", + "overview": "Archer has to stage an that ... ", + "hasFile": "false", + "monitored": "true", + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "absoluteEpisodeNumber": 50, + "series": { + "tvdbId": 110381, + "tvRageId": 23354, + "imdbId": "tt1486217", + "title": "Archer (2009)", + "cleanTitle": "archer2009", + "status": "continuing", + "overview": "At ISIS, an international spy ...", + "airTime": "7:00pm", + "monitored": "true", + "qualityProfileId": 1, + "seasonFolder": "true", + "lastInfoSync": "2014-02-05T04:39:28.550495Z", + "runtime": 30, + "images": [ + { + "coverType": "banner", + "url": "http://slurm.trakt.us//57.12.jpg" + }, + { + "coverType": "poster", + "url": "http://slurm.trakt.u/57.12-300.jpg" + }, + { + "coverType": "fanart", + "url": "http://slurm.trakt.us/image.12.jpg" + } + ], + "seriesType": "standard", + "network": "FX", + "useSceneNumbering": "false", + "titleSlug": "archer-2009", + "path": "E:\\Test\\TV\\Archer (2009)", + "year": 2009, + "firstAired": "2009-09-18T02:00:00Z", + "qualityProfile": { + "value": { + "name": "SD", + "cutoff": { + "id": 1, + "name": "SDTV" + }, + "items": [ + { + "quality": { + "id": 1, + "name": "SDTV" + }, + "allowed": "true" + }, + { + "quality": { + "id": 8, + "name": "WEBDL-480p" + }, + "allowed": "true" + }, + { + "quality": { + "id": 2, + "name": "DVD" + }, + "allowed": "true" + }, + { + "quality": { + "id": 4, + "name": "HDTV-720p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 9, + "name": "HDTV-1080p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 10, + "name": "Raw-HD" + }, + "allowed": "false" + }, + { + "quality": { + "id": 5, + "name": "WEBDL-720p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 6, + "name": "Bluray-720p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 3, + "name": "WEBDL-1080p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 7, + "name": "Bluray-1080p" + }, + "allowed": "false" + } + ], + "id": 1 + }, + "isLoaded": "true" + }, + "seasons": [ + { + "seasonNumber": 5, + "monitored": "true" + }, + { + "seasonNumber": 4, + "monitored": "true" + }, + { + "seasonNumber": 3, + "monitored": "true" + }, + { + "seasonNumber": 2, + "monitored": "true" + }, + { + "seasonNumber": 1, + "monitored": "true" + }, + { + "seasonNumber": 0, + "monitored": "false" + } + ], + "id": 1 + }, + "downloading": "false", + "id": 55 + } + ] + }, 200) + elif 'api/queue' in url: + return MockResponse([ + { + "series": { + "title": "Game of Thrones", + "sortTitle": "game thrones", + "seasonCount": 6, + "status": "continuing", + "overview": "Seven noble families fight for land ...", + "network": "HBO", + "airTime": "21:00", + "images": [ + { + "coverType": "fanart", + "url": "http://thetvdb.com/banners/fanart/-83.jpg" + }, + { + "coverType": "banner", + "url": "http://thetvdb.com/banners/-g19.jpg" + }, + { + "coverType": "poster", + "url": "http://thetvdb.com/banners/posters-34.jpg" + } + ], + "seasons": [ + { + "seasonNumber": 0, + "monitored": "false" + }, + { + "seasonNumber": 1, + "monitored": "false" + }, + { + "seasonNumber": 2, + "monitored": "true" + }, + { + "seasonNumber": 3, + "monitored": "false" + }, + { + "seasonNumber": 4, + "monitored": "false" + }, + { + "seasonNumber": 5, + "monitored": "true" + }, + { + "seasonNumber": 6, + "monitored": "true" + } + ], + "year": 2011, + "path": "/Volumes/Media/Shows/Game of Thrones", + "profileId": 5, + "seasonFolder": "true", + "monitored": "true", + "useSceneNumbering": "false", + "runtime": 60, + "tvdbId": 121361, + "tvRageId": 24493, + "tvMazeId": 82, + "firstAired": "2011-04-16T23:00:00Z", + "lastInfoSync": "2016-02-05T16:40:11.614176Z", + "seriesType": "standard", + "cleanTitle": "gamethrones", + "imdbId": "tt0944947", + "titleSlug": "game-of-thrones", + "certification": "TV-MA", + "genres": [ + "Adventure", + "Drama", + "Fantasy" + ], + "tags": [], + "added": "2015-12-28T13:44:24.204583Z", + "ratings": { + "votes": 1128, + "value": 9.4 + }, + "qualityProfileId": 5, + "id": 17 + }, + "episode": { + "seriesId": 17, + "episodeFileId": 0, + "seasonNumber": 3, + "episodeNumber": 8, + "title": "Second Sons", + "airDate": "2013-05-19", + "airDateUtc": "2013-05-20T01:00:00Z", + "overview": "King’s Landing hosts a wedding, and ...", + "hasFile": "false", + "monitored": "false", + "absoluteEpisodeNumber": 28, + "unverifiedSceneNumbering": "false", + "id": 889 + }, + "quality": { + "quality": { + "id": 7, + "name": "Bluray-1080p" + }, + "revision": { + "version": 1, + "real": 0 + } + }, + "size": 4472186820, + "title": "Game.of.Thrones.S03E08.Second.Sons.2013.1080p.", + "sizeleft": 0, + "timeleft": "00:00:00", + "estimatedCompletionTime": "2016-02-05T22:46:52.440104Z", + "status": "Downloading", + "trackedDownloadStatus": "Ok", + "statusMessages": [], + "downloadId": "SABnzbd_nzo_Mq2f_b", + "protocol": "usenet", + "id": 1503378561 + } + ], 200) + elif 'api/series' in url: + return MockResponse([ + { + "title": "Marvel's Daredevil", + "alternateTitles": [{ + "title": "Daredevil", + "seasonNumber": -1 + }], + "sortTitle": "marvels daredevil", + "seasonCount": 2, + "totalEpisodeCount": 26, + "episodeCount": 26, + "episodeFileCount": 26, + "sizeOnDisk": 79282273693, + "status": "continuing", + "overview": "Matt Murdock was blinded in a tragic accident...", + "previousAiring": "2016-03-18T04:01:00Z", + "network": "Netflix", + "airTime": "00:01", + "images": [ + { + "coverType": "fanart", + "url": "/sonarr/MediaCover/7/fanart.jpg?lastWrite=" + }, + { + "coverType": "banner", + "url": "/sonarr/MediaCover/7/banner.jpg?lastWrite=" + }, + { + "coverType": "poster", + "url": "/sonarr/MediaCover/7/poster.jpg?lastWrite=" + } + ], + "seasons": [ + { + "seasonNumber": 1, + "monitored": "false", + "statistics": { + "previousAiring": "2015-04-10T04:01:00Z", + "episodeFileCount": 13, + "episodeCount": 13, + "totalEpisodeCount": 13, + "sizeOnDisk": 22738179333, + "percentOfEpisodes": 100 + } + }, + { + "seasonNumber": 2, + "monitored": "false", + "statistics": { + "previousAiring": "2016-03-18T04:01:00Z", + "episodeFileCount": 13, + "episodeCount": 13, + "totalEpisodeCount": 13, + "sizeOnDisk": 56544094360, + "percentOfEpisodes": 100 + } + } + ], + "year": 2015, + "path": "F:\\TV_Shows\\Marvels Daredevil", + "profileId": 6, + "seasonFolder": "true", + "monitored": "true", + "useSceneNumbering": "false", + "runtime": 55, + "tvdbId": 281662, + "tvRageId": 38796, + "tvMazeId": 1369, + "firstAired": "2015-04-10T04:00:00Z", + "lastInfoSync": "2016-09-09T09:02:49.4402575Z", + "seriesType": "standard", + "cleanTitle": "marvelsdaredevil", + "imdbId": "tt3322312", + "titleSlug": "marvels-daredevil", + "certification": "TV-MA", + "genres": [ + "Action", + "Crime", + "Drama" + ], + "tags": [], + "added": "2015-05-15T00:20:32.7892744Z", + "ratings": { + "votes": 461, + "value": 8.9 + }, + "qualityProfileId": 6, + "id": 7 + } + ], 200) + elif 'api/diskspace' in url: + return MockResponse([ + { + "path": "/data", + "label": "", + "freeSpace": 282500067328, + "totalSpace": 499738734592 + } + ], 200) + else: + return MockResponse({ + "error": "Unauthorized" + }, 401) + + +class TestSonarrSetup(unittest.TestCase): + """Test the Sonarr 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' + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_diskspace_no_paths(self, req_mock): + """Tests getting all disk space""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [], + 'monitored_conditions': [ + 'diskspace' + ] + } + sonarr.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('Sonarr 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): + """Tests getting diskspace for included paths""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'diskspace' + ] + } + sonarr.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('Sonarr 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): + """Tests getting running commands""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'commands' + ] + } + sonarr.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('Sonarr Commands', device.name) + self.assertEqual( + 'pending', + device.device_state_attributes["RescanSeries"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_queue(self, req_mock): + """Tests getting downloads in the queue""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'queue' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:download', device.icon) + self.assertEqual('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Queue', device.name) + self.assertEqual( + '100.00%', + device.device_state_attributes["Game of Thrones S03E08"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_series(self, req_mock): + """Tests getting the number of series""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'series' + ] + } + sonarr.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('Shows', device.unit_of_measurement) + self.assertEqual('Sonarr Series', device.name) + self.assertEqual( + '26/26 Episodes', + device.device_state_attributes["Marvel's Daredevil"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_wanted(self, req_mock): + """Tests getting wanted episodes""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'wanted' + ] + } + sonarr.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('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Wanted', device.name) + self.assertEqual( + '2014-02-03', + device.device_state_attributes["Archer (2009) S05E04"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_upcoming_multiple_days(self, req_mock): + """Tests upcoming episodes for multiple days""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + sonarr.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('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Upcoming', device.name) + self.assertEqual( + 'S04E11', + device.device_state_attributes["Bob's Burgers"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_upcoming_today(self, req_mock): + """ + Tests filtering for a single day. + Sonarr needs to respond with at least 2 days + """ + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + sonarr.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('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Upcoming', device.name) + self.assertEqual( + 'S04E11', + device.device_state_attributes["Bob's Burgers"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_ssl(self, req_mock): + """Tests SSL being enabled""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ], + "ssl": "true" + } + sonarr.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('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Upcoming', device.name) + self.assertEqual( + 'S04E11', + device.device_state_attributes["Bob's Burgers"] + )