Use aiopyarr for sonarr (#65349)

This commit is contained in:
Chris Talkington 2022-02-22 11:33:10 -06:00 committed by GitHub
parent c14912471d
commit f30681dae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 464 additions and 345 deletions

View file

@ -1,13 +1,9 @@
"""Tests for the Sonarr component."""
from homeassistant.components.sonarr.const import CONF_BASE_PATH
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL
from homeassistant.const import CONF_API_KEY, CONF_URL
MOCK_REAUTH_INPUT = {CONF_API_KEY: "test-api-key-reauth"}
MOCK_USER_INPUT = {
CONF_HOST: "192.168.1.189",
CONF_PORT: 8989,
CONF_BASE_PATH: "/api",
CONF_SSL: False,
CONF_URL: "http://192.168.1.189:8989",
CONF_API_KEY: "MOCK_API_KEY",
}

View file

@ -3,15 +3,16 @@ from collections.abc import Generator
import json
from unittest.mock import MagicMock, patch
import pytest
from sonarr.models import (
Application,
CommandItem,
Episode,
QueueItem,
SeriesItem,
WantedResults,
from aiopyarr import (
Command,
Diskspace,
SonarrCalendar,
SonarrQueue,
SonarrSeries,
SonarrWantedMissing,
SystemStatus,
)
import pytest
from homeassistant.components.sonarr.const import (
CONF_BASE_PATH,
@ -33,34 +34,46 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
def sonarr_calendar():
def sonarr_calendar() -> list[SonarrCalendar]:
"""Generate a response for the calendar method."""
results = json.loads(load_fixture("sonarr/calendar.json"))
return [Episode.from_dict(result) for result in results]
return [SonarrCalendar(result) for result in results]
def sonarr_commands():
def sonarr_commands() -> list[Command]:
"""Generate a response for the commands method."""
results = json.loads(load_fixture("sonarr/command.json"))
return [CommandItem.from_dict(result) for result in results]
return [Command(result) for result in results]
def sonarr_queue():
def sonarr_diskspace() -> list[Diskspace]:
"""Generate a response for the diskspace method."""
results = json.loads(load_fixture("sonarr/diskspace.json"))
return [Diskspace(result) for result in results]
def sonarr_queue() -> SonarrQueue:
"""Generate a response for the queue method."""
results = json.loads(load_fixture("sonarr/queue.json"))
return [QueueItem.from_dict(result) for result in results]
return SonarrQueue(results)
def sonarr_series():
def sonarr_series() -> list[SonarrSeries]:
"""Generate a response for the series method."""
results = json.loads(load_fixture("sonarr/series.json"))
return [SeriesItem.from_dict(result) for result in results]
return [SonarrSeries(result) for result in results]
def sonarr_wanted():
def sonarr_system_status() -> SystemStatus:
"""Generate a response for the system status method."""
result = json.loads(load_fixture("sonarr/system-status.json"))
return SystemStatus(result)
def sonarr_wanted() -> SonarrWantedMissing:
"""Generate a response for the wanted method."""
results = json.loads(load_fixture("sonarr/wanted-missing.json"))
return WantedResults.from_dict(results)
return SonarrWantedMissing(results)
@pytest.fixture
@ -95,54 +108,38 @@ def mock_setup_entry() -> Generator[None, None, None]:
@pytest.fixture
def mock_sonarr_config_flow(
request: pytest.FixtureRequest,
) -> Generator[None, MagicMock, None]:
def mock_sonarr_config_flow() -> Generator[None, MagicMock, None]:
"""Return a mocked Sonarr client."""
fixture: str = "sonarr/app.json"
if hasattr(request, "param") and request.param:
fixture = request.param
app = Application(json.loads(load_fixture(fixture)))
with patch(
"homeassistant.components.sonarr.config_flow.Sonarr", autospec=True
"homeassistant.components.sonarr.config_flow.SonarrClient", autospec=True
) as sonarr_mock:
client = sonarr_mock.return_value
client.host = "192.168.1.189"
client.port = 8989
client.base_path = "/api"
client.tls = False
client.app = app
client.update.return_value = app
client.calendar.return_value = sonarr_calendar()
client.commands.return_value = sonarr_commands()
client.queue.return_value = sonarr_queue()
client.series.return_value = sonarr_series()
client.wanted.return_value = sonarr_wanted()
client.async_get_calendar.return_value = sonarr_calendar()
client.async_get_commands.return_value = sonarr_commands()
client.async_get_diskspace.return_value = sonarr_diskspace()
client.async_get_queue.return_value = sonarr_queue()
client.async_get_series.return_value = sonarr_series()
client.async_get_system_status.return_value = sonarr_system_status()
client.async_get_wanted.return_value = sonarr_wanted()
yield client
@pytest.fixture
def mock_sonarr(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
def mock_sonarr() -> Generator[None, MagicMock, None]:
"""Return a mocked Sonarr client."""
fixture: str = "sonarr/app.json"
if hasattr(request, "param") and request.param:
fixture = request.param
app = Application(json.loads(load_fixture(fixture)))
with patch("homeassistant.components.sonarr.Sonarr", autospec=True) as sonarr_mock:
with patch(
"homeassistant.components.sonarr.SonarrClient", autospec=True
) as sonarr_mock:
client = sonarr_mock.return_value
client.host = "192.168.1.189"
client.port = 8989
client.base_path = "/api"
client.tls = False
client.app = app
client.update.return_value = app
client.calendar.return_value = sonarr_calendar()
client.commands.return_value = sonarr_commands()
client.queue.return_value = sonarr_queue()
client.series.return_value = sonarr_series()
client.wanted.return_value = sonarr_wanted()
client.async_get_calendar.return_value = sonarr_calendar()
client.async_get_commands.return_value = sonarr_commands()
client.async_get_diskspace.return_value = sonarr_diskspace()
client.async_get_queue.return_value = sonarr_queue()
client.async_get_series.return_value = sonarr_series()
client.async_get_system_status.return_value = sonarr_system_status()
client.async_get_wanted.return_value = sonarr_wanted()
yield client

View file

@ -1,28 +0,0 @@
{
"info": {
"version": "2.0.0.1121",
"buildTime": "2014-02-08T20:49:36.5560392Z",
"isDebug": false,
"isProduction": true,
"isAdmin": true,
"isUserInteractive": false,
"startupPath": "C:\\ProgramData\\NzbDrone\\bin",
"appData": "C:\\ProgramData\\NzbDrone",
"osVersion": "6.2.9200.0",
"isMono": false,
"isLinux": false,
"isWindows": true,
"branch": "develop",
"authentication": false,
"startOfWeek": 0,
"urlBase": ""
},
"diskspace": [
{
"path": "C:\\",
"label": "",
"freeSpace": 282500067328,
"totalSpace": 499738734592
}
]
}

View file

@ -17,7 +17,6 @@
"queued": "2020-04-06T16:54:06.41945Z",
"started": "2020-04-06T16:54:06.421322Z",
"trigger": "manual",
"state": "started",
"manual": true,
"startedOn": "2020-04-06T16:54:06.41945Z",
"stateChangeTime": "2020-04-06T16:54:06.421322Z",
@ -27,7 +26,7 @@
},
{
"name": "RefreshSeries",
"state": "started",
"status": "started",
"startedOn": "2020-04-06T16:57:51.406504Z",
"stateChangeTime": "2020-04-06T16:57:51.417931Z",
"sendUpdatesToClient": true,

View file

@ -1,129 +1,140 @@
[
{
"series": {
"title": "The Andy Griffith Show",
"sortTitle": "andy griffith show",
"seasonCount": 8,
"status": "ended",
"overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.",
"network": "CBS",
"airTime": "21:30",
"images": [
{
"coverType": "fanart",
"url": "https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg"
{
"page":1,
"pageSize":10,
"sortKey":"timeleft",
"sortDirection":"ascending",
"totalRecords":1,
"records":[
{
"series":{
"title":"The Andy Griffith Show",
"sortTitle":"andy griffith show",
"seasonCount":8,
"status":"ended",
"overview":"Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.",
"network":"CBS",
"airTime":"21:30",
"images":[
{
"coverType":"fanart",
"url":"https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg"
},
{
"coverType":"banner",
"url":"https://artworks.thetvdb.com/banners/graphical/77754-g.jpg"
},
{
"coverType":"poster",
"url":"https://artworks.thetvdb.com/banners/posters/77754-4.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
},
{
"seasonNumber":7,
"monitored":true
},
{
"seasonNumber":8,
"monitored":true
}
],
"year":1960,
"path":"F:\\The Andy Griffith Show",
"profileId":5,
"seasonFolder":true,
"monitored":true,
"useSceneNumbering":false,
"runtime":25,
"tvdbId":77754,
"tvRageId":5574,
"tvMazeId":3853,
"firstAired":"1960-02-15T06:00:00Z",
"lastInfoSync":"2016-02-05T16:40:11.614176Z",
"seriesType":"standard",
"cleanTitle":"theandygriffithshow",
"imdbId":"",
"titleSlug":"the-andy-griffith-show",
"certification":"TV-G",
"genres":[
"Comedy"
],
"tags":[
],
"added":"2008-02-04T13:44:24.204583Z",
"ratings":{
"votes":547,
"value":8.6
},
{
"coverType": "banner",
"url": "https://artworks.thetvdb.com/banners/graphical/77754-g.jpg"
},
{
"coverType": "poster",
"url": "https://artworks.thetvdb.com/banners/posters/77754-4.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
},
{
"seasonNumber": 7,
"monitored": true
},
{
"seasonNumber": 8,
"monitored": true
}
],
"year": 1960,
"path": "F:\\The Andy Griffith Show",
"profileId": 5,
"seasonFolder": true,
"monitored": true,
"useSceneNumbering": false,
"runtime": 25,
"tvdbId": 77754,
"tvRageId": 5574,
"tvMazeId": 3853,
"firstAired": "1960-02-15T06:00:00Z",
"lastInfoSync": "2016-02-05T16:40:11.614176Z",
"seriesType": "standard",
"cleanTitle": "theandygriffithshow",
"imdbId": "",
"titleSlug": "the-andy-griffith-show",
"certification": "TV-G",
"genres": [
"Comedy"
],
"tags": [],
"added": "2008-02-04T13:44:24.204583Z",
"ratings": {
"votes": 547,
"value": 8.6
"qualityProfileId":5,
"id":17
},
"qualityProfileId": 5,
"id": 17
},
"episode": {
"seriesId": 17,
"episodeFileId": 0,
"seasonNumber": 1,
"episodeNumber": 1,
"title": "The New Housekeeper",
"airDate": "1960-10-03",
"airDateUtc": "1960-10-03T01:00:00Z",
"overview": "Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.",
"hasFile": false,
"monitored": false,
"absoluteEpisodeNumber": 1,
"unverifiedSceneNumbering": false,
"id": 889
},
"quality": {
"quality": {
"id": 7,
"name": "SD"
"episode":{
"seriesId":17,
"episodeFileId":0,
"seasonNumber":1,
"episodeNumber":1,
"title":"The New Housekeeper",
"airDate":"1960-10-03",
"airDateUtc":"1960-10-03T01:00:00Z",
"overview":"Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.",
"hasFile":false,
"monitored":false,
"absoluteEpisodeNumber":1,
"unverifiedSceneNumbering":false,
"id":889
},
"revision": {
"version": 1,
"real": 0
}
},
"size": 4472186820,
"title": "The.Andy.Griffith.Show.S01E01.x264-GROUP",
"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
}
]
"quality":{
"quality":{
"id":7,
"name":"SD"
},
"revision":{
"version":1,
"real":0
}
},
"size":4472186820,
"title":"The.Andy.Griffith.Show.S01E01.x264-GROUP",
"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
}
]
}

View file

@ -3,11 +3,6 @@
"title": "The Andy Griffith Show",
"alternateTitles": [],
"sortTitle": "andy griffith show",
"seasonCount": 8,
"totalEpisodeCount": 253,
"episodeCount": 0,
"episodeFileCount": 0,
"sizeOnDisk": 0,
"status": "ended",
"overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.",
"network": "CBS",
@ -158,6 +153,14 @@
"value": 8.6
},
"qualityProfileId": 2,
"statistics": {
"seasonCount": 8,
"episodeFileCount": 0,
"episodeCount": 0,
"totalEpisodeCount": 253,
"sizeOnDisk": 0,
"percentOfEpisodes": 0.0
},
"id": 105
}
]

View file

@ -0,0 +1,29 @@
{
"appName": "Sonarr",
"version": "3.0.6.1451",
"buildTime": "2022-01-23T16:51:56Z",
"isDebug": false,
"isProduction": true,
"isAdmin": false,
"isUserInteractive": false,
"startupPath": "/app/sonarr/bin",
"appData": "/config",
"osName": "ubuntu",
"osVersion": "20.04",
"isMonoRuntime": true,
"isMono": true,
"isLinux": true,
"isOsx": false,
"isWindows": false,
"mode": "console",
"branch": "develop",
"authentication": "forms",
"sqliteVersion": "3.31.1",
"urlBase": "",
"runtimeVersion": "6.12.0.122",
"runtimeName": "mono",
"startTime": "2022-02-01T22:10:11.956137Z",
"packageVersion": "3.0.6.1451-ls247",
"packageAuthor": "[linuxserver.io](https://linuxserver.io)",
"packageUpdateMechanism": "docker"
}

View file

@ -1,6 +1,6 @@
{
"page": 1,
"pageSize": 10,
"pageSize": 50,
"sortKey": "airDateUtc",
"sortDirection": "descending",
"totalRecords": 2,

View file

@ -1,7 +1,7 @@
"""Test the Sonarr config flow."""
from unittest.mock import MagicMock, patch
from sonarr import SonarrAccessRestricted, SonarrError
from aiopyarr import ArrAuthenticationException, ArrException
from homeassistant.components.sonarr.const import (
CONF_UPCOMING_DAYS,
@ -11,7 +11,7 @@ from homeassistant.components.sonarr.const import (
DOMAIN,
)
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_SOURCE, CONF_VERIFY_SSL
from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@ -38,7 +38,7 @@ async def test_cannot_connect(
hass: HomeAssistant, mock_sonarr_config_flow: MagicMock
) -> None:
"""Test we show user form on connection error."""
mock_sonarr_config_flow.update.side_effect = SonarrError
mock_sonarr_config_flow.async_get_system_status.side_effect = ArrException
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
@ -56,7 +56,9 @@ async def test_invalid_auth(
hass: HomeAssistant, mock_sonarr_config_flow: MagicMock
) -> None:
"""Test we show user form on invalid auth."""
mock_sonarr_config_flow.update.side_effect = SonarrAccessRestricted
mock_sonarr_config_flow.async_get_system_status.side_effect = (
ArrAuthenticationException
)
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
@ -74,7 +76,7 @@ async def test_unknown_error(
hass: HomeAssistant, mock_sonarr_config_flow: MagicMock
) -> None:
"""Test we show user form on unknown error."""
mock_sonarr_config_flow.update.side_effect = Exception
mock_sonarr_config_flow.async_get_system_status.side_effect = Exception
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
@ -153,7 +155,7 @@ async def test_full_user_flow_implementation(
assert result["title"] == "192.168.1.189"
assert result["data"]
assert result["data"][CONF_HOST] == "192.168.1.189"
assert result["data"][CONF_URL] == "http://192.168.1.189:8989"
async def test_full_user_flow_advanced_options(
@ -183,7 +185,7 @@ async def test_full_user_flow_advanced_options(
assert result["title"] == "192.168.1.189"
assert result["data"]
assert result["data"][CONF_HOST] == "192.168.1.189"
assert result["data"][CONF_URL] == "http://192.168.1.189:8989"
assert result["data"][CONF_VERIFY_SSL]

View file

@ -1,11 +1,19 @@
"""Tests for the Sonsrr integration."""
from unittest.mock import MagicMock, patch
from sonarr import SonarrAccessRestricted, SonarrError
from aiopyarr import ArrAuthenticationException, ArrException
from homeassistant.components.sonarr.const import DOMAIN
from homeassistant.components.sonarr.const import CONF_BASE_PATH, DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_SOURCE
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_PORT,
CONF_SOURCE,
CONF_SSL,
CONF_URL,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@ -17,7 +25,7 @@ async def test_config_entry_not_ready(
mock_sonarr: MagicMock,
) -> None:
"""Test the configuration entry not ready."""
mock_sonarr.update.side_effect = SonarrError
mock_sonarr.async_get_system_status.side_effect = ArrException
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
@ -32,7 +40,7 @@ async def test_config_entry_reauth(
mock_sonarr: MagicMock,
) -> None:
"""Test the configuration entry needing to be re-authenticated."""
mock_sonarr.update.side_effect = SonarrAccessRestricted
mock_sonarr.async_get_system_status.side_effect = ArrAuthenticationException
with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
mock_config_entry.add_to_hass(hass)
@ -77,3 +85,34 @@ async def test_unload_config_entry(
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
assert mock_config_entry.entry_id not in hass.data[DOMAIN]
async def test_migrate_config_entry(hass: HomeAssistant):
"""Test successful migration of entry data."""
legacy_config = {
CONF_API_KEY: "MOCK_API_KEY",
CONF_HOST: "1.2.3.4",
CONF_PORT: 8989,
CONF_SSL: False,
CONF_VERIFY_SSL: False,
CONF_BASE_PATH: "/base/",
}
entry = MockConfigEntry(domain=DOMAIN, data=legacy_config)
assert entry.data == legacy_config
assert entry.version == 1
assert not entry.unique_id
await entry.async_migrate(hass)
assert entry.data == {
CONF_API_KEY: "MOCK_API_KEY",
CONF_HOST: "1.2.3.4",
CONF_PORT: 8989,
CONF_SSL: False,
CONF_VERIFY_SSL: False,
CONF_BASE_PATH: "/base/",
CONF_URL: "http://1.2.3.4:8989/base",
}
assert entry.version == 2
assert not entry.unique_id

View file

@ -2,8 +2,8 @@
from datetime import timedelta
from unittest.mock import MagicMock, patch
from aiopyarr import ArrException
import pytest
from sonarr import SonarrError
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.sonarr.const import DOMAIN
@ -96,8 +96,11 @@ async def test_sensors(
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:television"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes"
assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-26"
assert state.attributes.get("The Andy Griffith Show S01E01") == "1960-10-03"
assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-27T01:30:00+00:00"
assert (
state.attributes.get("The Andy Griffith Show S01E01")
== "1960-10-03T01:00:00+00:00"
)
assert state.state == "2"
@ -141,44 +144,49 @@ async def test_availability(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(UPCOMING_ENTITY_ID)
assert hass.states.get(UPCOMING_ENTITY_ID).state == "1"
# state to unavailable
mock_sonarr.calendar.side_effect = SonarrError
mock_sonarr.async_get_calendar.side_effect = ArrException
future = now + timedelta(minutes=1)
with patch("homeassistant.util.dt.utcnow", return_value=future):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get(UPCOMING_ENTITY_ID)
assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE
# state to available
mock_sonarr.calendar.side_effect = None
mock_sonarr.async_get_calendar.side_effect = None
future += timedelta(minutes=1)
with patch("homeassistant.util.dt.utcnow", return_value=future):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get(UPCOMING_ENTITY_ID)
assert hass.states.get(UPCOMING_ENTITY_ID).state == "1"
# state to unavailable
mock_sonarr.calendar.side_effect = SonarrError
mock_sonarr.async_get_calendar.side_effect = ArrException
future += timedelta(minutes=1)
with patch("homeassistant.util.dt.utcnow", return_value=future):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get(UPCOMING_ENTITY_ID)
assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE
# state to available
mock_sonarr.calendar.side_effect = None
mock_sonarr.async_get_calendar.side_effect = None
future += timedelta(minutes=1)
with patch("homeassistant.util.dt.utcnow", return_value=future):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get(UPCOMING_ENTITY_ID)
assert hass.states.get(UPCOMING_ENTITY_ID).state == "1"