From 35261a908971eafbf138087bdd03c3285d859944 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 18 Mar 2022 12:18:19 +0100 Subject: [PATCH] Add switch platform to UptimeRobot (#65394) * Add switch platfor mto UptimeRobot * Add tests * Apply review comment * review comments part 2 * review comments part 3 * Fix tests after swapping logic on/off * Fix reauth test * Check for read-only key * Fix reauth for switch platform * mypy * cleanup * cleanup part 2 * Fixes + review comments * Tests * Apply more review comments * Required changes * fix test * Remove if * 100% tests coverage * Check readonly key in config_flow * Fix strings & translation * Add guard for 'monitor' keys * allign tests * Wrong API key message reworded --- .../components/uptimerobot/__init__.py | 10 +- .../components/uptimerobot/binary_sensor.py | 22 ++- .../components/uptimerobot/config_flow.py | 9 +- homeassistant/components/uptimerobot/const.py | 2 +- .../components/uptimerobot/entity.py | 10 +- .../components/uptimerobot/sensor.py | 24 ++- .../components/uptimerobot/strings.json | 5 +- .../components/uptimerobot/switch.py | 75 ++++++++ .../uptimerobot/translations/en.json | 5 +- tests/components/uptimerobot/common.py | 19 ++- .../uptimerobot/test_config_flow.py | 24 +++ tests/components/uptimerobot/test_init.py | 34 ++++ tests/components/uptimerobot/test_switch.py | 160 ++++++++++++++++++ 13 files changed, 357 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/uptimerobot/switch.py create mode 100644 tests/components/uptimerobot/test_switch.py diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 06da8a1b4b1..6d9be1b2364 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -26,9 +26,12 @@ from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLA async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UptimeRobot from a config entry.""" hass.data.setdefault(DOMAIN, {}) - uptime_robot_api = UptimeRobot( - entry.data[CONF_API_KEY], async_get_clientsession(hass) - ) + key: str = entry.data[CONF_API_KEY] + if key.startswith("ur") or key.startswith("m"): + raise ConfigEntryAuthFailed( + "Wrong API key type detected, use the 'main' API key" + ) + uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass)) dev_reg = await async_get_registry(hass) hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeRobotDataUpdateCoordinator( @@ -58,6 +61,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): """Data update coordinator for UptimeRobot.""" data: list[UptimeRobotMonitor] + config_entry: ConfigEntry def __init__( self, diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 40f850c5376..248212a8345 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -23,18 +23,16 @@ async def async_setup_entry( """Set up the UptimeRobot binary_sensors.""" coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - UptimeRobotBinarySensor( - coordinator, - BinarySensorEntityDescription( - key=str(monitor.id), - name=monitor.friendly_name, - device_class=BinarySensorDeviceClass.CONNECTIVITY, - ), - monitor=monitor, - ) - for monitor in coordinator.data - ], + UptimeRobotBinarySensor( + coordinator, + BinarySensorEntityDescription( + key=str(monitor.id), + name=monitor.friendly_name, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + monitor=monitor, + ) + for monitor in coordinator.data ) diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 3f08e7e692e..5b6ac1d4880 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -34,9 +34,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate the user input allows us to connect.""" errors: dict[str, str] = {} response: UptimeRobotApiResponse | UptimeRobotApiError | None = None - uptime_robot_api = UptimeRobot( - data[CONF_API_KEY], async_get_clientsession(self.hass) - ) + key: str = data[CONF_API_KEY] + if key.startswith("ur") or key.startswith("m"): + LOGGER.error("Wrong API key type detected, use the 'main' API key") + errors["base"] = "not_main_key" + return errors, None + uptime_robot_api = UptimeRobot(key, async_get_clientsession(self.hass)) try: response = await uptime_robot_api.async_get_account_details() diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index e98ae7b514c..e89c1c38e0e 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -13,7 +13,7 @@ LOGGER: Logger = getLogger(__package__) COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10) DOMAIN: Final = "uptimerobot" -PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] ATTRIBUTION: Final = "Data provided by UptimeRobot" diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 318e5a5094e..6f7c616b7a4 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -5,11 +5,9 @@ from pyuptimerobot import UptimeRobotMonitor from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import UptimeRobotDataUpdateCoordinator from .const import ATTR_TARGET, ATTRIBUTION, DOMAIN @@ -17,10 +15,11 @@ class UptimeRobotEntity(CoordinatorEntity): """Base UptimeRobot entity.""" _attr_attribution = ATTRIBUTION + coordinator: UptimeRobotDataUpdateCoordinator def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: UptimeRobotDataUpdateCoordinator, description: EntityDescription, monitor: UptimeRobotMonitor, ) -> None: @@ -40,6 +39,7 @@ class UptimeRobotEntity(CoordinatorEntity): ATTR_TARGET: self.monitor.url, } self._attr_unique_id = str(self.monitor.id) + self.api = coordinator.api @property def _monitors(self) -> list[UptimeRobotMonitor]: diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py index 77f32e20b4b..0e450bf24b7 100644 --- a/homeassistant/components/uptimerobot/sensor.py +++ b/homeassistant/components/uptimerobot/sensor.py @@ -38,19 +38,17 @@ async def async_setup_entry( """Set up the UptimeRobot sensors.""" coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - UptimeRobotSensor( - coordinator, - SensorEntityDescription( - key=str(monitor.id), - name=monitor.friendly_name, - entity_category=EntityCategory.DIAGNOSTIC, - device_class="uptimerobot__monitor_status", - ), - monitor=monitor, - ) - for monitor in coordinator.data - ], + UptimeRobotSensor( + coordinator, + SensorEntityDescription( + key=str(monitor.id), + name=monitor.friendly_name, + entity_category=EntityCategory.DIAGNOSTIC, + device_class="uptimerobot__monitor_status", + ), + monitor=monitor, + ) + for monitor in coordinator.data ) diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index 2946f2e2d5d..5ead21a50cd 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -2,14 +2,14 @@ "config": { "step": { "user": { - "description": "You need to supply a read-only API key from UptimeRobot", + "description": "You need to supply the 'main' API key from UptimeRobot", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "You need to supply a new read-only API key from UptimeRobot", + "description": "You need to supply a new 'main' API key from UptimeRobot", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } @@ -19,6 +19,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", "unknown": "[%key:common::config_flow::error::unknown%]", + "not_main_key": "Wrong API key type detected, use the 'main' API key", "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration." }, "abort": { diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py new file mode 100644 index 00000000000..619f72ae47f --- /dev/null +++ b/homeassistant/components/uptimerobot/switch.py @@ -0,0 +1,75 @@ +"""UptimeRobot switch platform.""" +from __future__ import annotations + +from typing import Any + +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import UptimeRobotDataUpdateCoordinator +from .const import API_ATTR_OK, DOMAIN, LOGGER +from .entity import UptimeRobotEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the UptimeRobot switches.""" + coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + UptimeRobotSwitch( + coordinator, + SwitchEntityDescription( + key=str(monitor.id), + name=f"{monitor.friendly_name} Active", + device_class=SwitchDeviceClass.SWITCH, + ), + monitor=monitor, + ) + for monitor in coordinator.data + ) + + +class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity): + """Representation of a UptimeRobot switch.""" + + _attr_icon = "mdi:cog" + + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + return bool(self.monitor.status != 0) + + async def _async_edit_monitor(self, **kwargs: Any) -> None: + """Edit monitor status.""" + try: + response = await self.api.async_edit_monitor(**kwargs) + except UptimeRobotAuthenticationException: + LOGGER.debug("API authentication error, calling reauth") + self.coordinator.config_entry.async_start_reauth(self.hass) + return + except UptimeRobotException as exception: + LOGGER.error("API exception: %s", exception) + return + + if response.status != API_ATTR_OK: + LOGGER.error("API exception: %s", response.error.message, exc_info=True) + return + + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn on switch.""" + await self._async_edit_monitor(id=self.monitor.id, status=0) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn off switch.""" + await self._async_edit_monitor(id=self.monitor.id, status=1) diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index a78af34102d..f4fe398a195 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", + "not_main_key": "Wrong API key type detected, use the 'main' API key", "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration.", "unknown": "Unexpected error" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API Key" }, - "description": "You need to supply a new read-only API key from UptimeRobot", + "description": "You need to supply a new 'main' API key from UptimeRobot", "title": "Reauthenticate Integration" }, "user": { "data": { "api_key": "API Key" }, - "description": "You need to supply a read-only API key from UptimeRobot" + "description": "You need to supply the 'main' API key from UptimeRobot" } } } diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 6ec0e7aef7d..2003f411358 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -20,7 +20,8 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -MOCK_UPTIMEROBOT_API_KEY = "0242ac120003" +MOCK_UPTIMEROBOT_API_KEY = "u0242ac120003" +MOCK_UPTIMEROBOT_API_KEY_READ_ONLY = "ur0242ac120003" MOCK_UPTIMEROBOT_EMAIL = "test@test.test" MOCK_UPTIMEROBOT_UNIQUE_ID = "1234567890" @@ -37,6 +38,14 @@ MOCK_UPTIMEROBOT_MONITOR = { "type": 1, "url": "http://example.com", } +MOCK_UPTIMEROBOT_MONITOR_PAUSED = { + "id": 1234, + "friendly_name": "Test monitor", + "status": 0, + "type": 1, + "url": "http://example.com", +} + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA = { "domain": DOMAIN, @@ -45,11 +54,19 @@ MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA = { "unique_id": MOCK_UPTIMEROBOT_UNIQUE_ID, "source": config_entries.SOURCE_USER, } +MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA_KEY_READ_ONLY = { + "domain": DOMAIN, + "title": MOCK_UPTIMEROBOT_EMAIL, + "data": {"platform": DOMAIN, "api_key": MOCK_UPTIMEROBOT_API_KEY_READ_ONLY}, + "unique_id": MOCK_UPTIMEROBOT_UNIQUE_ID, + "source": config_entries.SOURCE_USER, +} STATE_UP = "up" UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY = "binary_sensor.test_monitor" UPTIMEROBOT_SENSOR_TEST_ENTITY = "sensor.test_monitor" +UPTIMEROBOT_SWITCH_TEST_ENTITY = "switch.test_monitor_active" class MockApiResponseKey(str, Enum): diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 7daa59df111..c477e1bbc65 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.data_entry_flow import ( from .common import ( MOCK_UPTIMEROBOT_ACCOUNT, MOCK_UPTIMEROBOT_API_KEY, + MOCK_UPTIMEROBOT_API_KEY_READ_ONLY, MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, MOCK_UPTIMEROBOT_UNIQUE_ID, MockApiResponseKey, @@ -56,6 +57,29 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_read_only(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY_READ_ONLY}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"]["base"] == "not_main_key" + + @pytest.mark.parametrize( "exception,error_key", [ diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 94dd7e8af4c..8efa51e05a6 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -19,6 +19,7 @@ from homeassistant.util import dt from .common import ( MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA_KEY_READ_ONLY, MOCK_UPTIMEROBOT_MONITOR, UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY, MockApiResponseKey, @@ -62,6 +63,39 @@ async def test_reauthentication_trigger_in_setup( ) +async def test_reauthentication_trigger_key_read_only( + hass: HomeAssistant, caplog: LogCaptureFixture +): + """Test reauthentication trigger.""" + mock_config_entry = MockConfigEntry( + **MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA_KEY_READ_ONLY + ) + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + + assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert ( + mock_config_entry.reason + == "Wrong API key type detected, use the 'main' API key" + ) + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id + + assert ( + "Config entry 'test@test.test' for uptimerobot integration could not authenticate" + in caplog.text + ) + + async def test_reauthentication_trigger_after_setup( hass: HomeAssistant, caplog: LogCaptureFixture ): diff --git a/tests/components/uptimerobot/test_switch.py b/tests/components/uptimerobot/test_switch.py new file mode 100644 index 00000000000..82ea06ce836 --- /dev/null +++ b/tests/components/uptimerobot/test_switch.py @@ -0,0 +1,160 @@ +"""Test UptimeRobot switch.""" + +from unittest.mock import patch + +from pyuptimerobot import UptimeRobotAuthenticationException + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from .common import ( + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_MONITOR, + MOCK_UPTIMEROBOT_MONITOR_PAUSED, + UPTIMEROBOT_SWITCH_TEST_ENTITY, + MockApiResponseKey, + mock_uptimerobot_api_response, + setup_uptimerobot_integration, +) + +from tests.common import MockConfigEntry + + +async def test_presentation(hass: HomeAssistant) -> None: + """Test the presenstation of UptimeRobot sensors.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + + assert entity.state == STATE_ON + assert entity.attributes["icon"] == "mdi:cog" + assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] + + +async def test_switch_off(hass: HomeAssistant) -> None: + """Test entity unaviable on update failure.""" + + mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response( + data=[MOCK_UPTIMEROBOT_MONITOR_PAUSED] + ), + ), patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(), + ): + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_OFF + + +async def test_switch_on(hass: HomeAssistant) -> None: + """Test entity unaviable on update failure.""" + + mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), + ), patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(), + ): + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + +async def test_authentication_error(hass: HomeAssistant, caplog) -> None: + """Test authentication error turning switch on/off.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + side_effect=UptimeRobotAuthenticationException, + ), patch( + "homeassistant.config_entries.ConfigEntry.async_start_reauth" + ) as config_entry_reauth: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + assert config_entry_reauth.assert_called + + +async def test_refresh_data(hass: HomeAssistant, caplog) -> None: + """Test authentication error turning switch on/off.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "homeassistant.helpers.update_coordinator.DataUpdateCoordinator.async_request_refresh" + ) as coordinator_refresh: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + assert coordinator_refresh.assert_called + + +async def test_switch_api_failure(hass: HomeAssistant, caplog) -> None: + """Test general exception turning switch on/off.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + assert "API exception" in caplog.text