diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index c7c2ea469a8..08690dc8aab 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["pyuptimerobot"], "quality_scale": "gold", - "requirements": ["pyuptimerobot==24.0.1"] + "requirements": ["pyuptimerobot==25.0.0"] } diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py index 1ea62696cc1..234aede5400 100644 --- a/homeassistant/components/uptimerobot/sensor.py +++ b/homeassistant/components/uptimerobot/sensor.py @@ -59,9 +59,12 @@ class UptimeRobotSensor(UptimeRobotEntity, SensorEntity): """Representation of a UptimeRobot sensor.""" @property - def native_value(self) -> str: + def native_value(self) -> str | None: """Return the status of the monitor.""" + if not self._monitor.status: + return None + status = self._monitor.status.lower() # The API returns "paused" # but the entity state will be "pause" to avoid a breaking change - return {"paused": "pause"}.get(status, status) # type: ignore[no-any-return] + return {"paused": "pause"}.get(status, status) diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py index bce8f06141a..0520da93505 100644 --- a/homeassistant/components/uptimerobot/switch.py +++ b/homeassistant/components/uptimerobot/switch.py @@ -2,11 +2,7 @@ from typing import Any -from pyuptimerobot import ( - UptimeRobotAuthenticationException, - UptimeRobotException, - UptimeRobotMonitor, -) +from pyuptimerobot import UptimeRobotMonitor from homeassistant.components.switch import ( SwitchDeviceClass, @@ -14,13 +10,12 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, STATUS_DOWN, STATUS_UP +from .const import STATUS_UP from .coordinator import UptimeRobotConfigEntry from .entity import UptimeRobotEntity -from .utils import new_device_listener +from .utils import new_device_listener, uptimerobot_api_call # Limit the number of parallel updates to 1 PARALLEL_UPDATES = 1 @@ -63,26 +58,14 @@ class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity): """Return True if the entity is on.""" return bool(self._monitor.status == STATUS_UP) - async def _async_edit_monitor(self, **kwargs: Any) -> None: - """Edit monitor status.""" - try: - await self.api.async_edit_monitor(**kwargs) - except UptimeRobotAuthenticationException: - self.coordinator.config_entry.async_start_reauth(self.hass) - return - except UptimeRobotException as exception: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="api_exception", - translation_placeholders={"error": "Generic UptimeRobot exception"}, - ) from exception - - await self.coordinator.async_request_refresh() - + @uptimerobot_api_call async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - await self._async_edit_monitor(monitor_id=self._monitor.id, status=STATUS_DOWN) + await self.api.async_pause_monitor(monitor_id=self._monitor.id) + await self.coordinator.async_request_refresh() + @uptimerobot_api_call async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - await self._async_edit_monitor(monitor_id=self._monitor.id, status=STATUS_UP) + await self.api.async_start_monitor(monitor_id=self._monitor.id) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/uptimerobot/utils.py b/homeassistant/components/uptimerobot/utils.py index 989f481f11b..f0ae42ac9ac 100644 --- a/homeassistant/components/uptimerobot/utils.py +++ b/homeassistant/components/uptimerobot/utils.py @@ -1,10 +1,43 @@ """Utility functions for the UptimeRobot integration.""" -from collections.abc import Callable +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate -from pyuptimerobot import UptimeRobotMonitor +from pyuptimerobot import ( + UptimeRobotAuthenticationException, + UptimeRobotException, + UptimeRobotMonitor, +) +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN from .coordinator import UptimeRobotDataUpdateCoordinator +from .entity import UptimeRobotEntity + + +def uptimerobot_api_call[_T: UptimeRobotEntity, **_P]( + func: Callable[Concatenate[_T, _P], Awaitable[None]], +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Catch UptimeRobot API call exceptions.""" + + @wraps(func) + async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except UptimeRobotAuthenticationException: + self.coordinator.config_entry.async_start_reauth(self.hass) + return + except UptimeRobotException as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_exception", + translation_placeholders={"error": "Generic UptimeRobot exception"}, + ) from exception + + return cmd_wrapper def new_device_listener( diff --git a/requirements_all.txt b/requirements_all.txt index 33526572ce5..53c1ca7d6c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2739,7 +2739,7 @@ pytrafikverket==1.1.1 pytrydan==0.8.0 # homeassistant.components.uptimerobot -pyuptimerobot==24.0.1 +pyuptimerobot==25.0.0 # homeassistant.components.vera pyvera==0.3.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56f5478ce34..edf033752b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2338,7 +2338,7 @@ pytrafikverket==1.1.1 pytrydan==0.8.0 # homeassistant.components.uptimerobot -pyuptimerobot==24.0.1 +pyuptimerobot==25.0.0 # homeassistant.components.vera pyvera==0.3.16 diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 9aba543696a..fb29d15bbf4 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -4,7 +4,8 @@ from enum import StrEnum from typing import Any from unittest.mock import patch -from pyuptimerobot import API_PATH_MONITORS, UptimeRobotApiResponse +from pyuptimerobot import UptimeRobotApiResponse +from pyuptimerobot.const import API_PATH_MONITORS from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index f2be2e2b1c3..e6b468afa26 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import patch import pytest from pyuptimerobot import ( - API_PATH_USER_ME, UptimeRobotAuthenticationException, UptimeRobotConnectionException, UptimeRobotException, ) +from pyuptimerobot.const import API_PATH_USER_ME from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN diff --git a/tests/components/uptimerobot/test_diagnostics.py b/tests/components/uptimerobot/test_diagnostics.py index 5fe7851ccb9..3d90b854113 100644 --- a/tests/components/uptimerobot/test_diagnostics.py +++ b/tests/components/uptimerobot/test_diagnostics.py @@ -3,7 +3,8 @@ import json from unittest.mock import patch -from pyuptimerobot import API_PATH_USER_ME, UptimeRobotException +from pyuptimerobot import UptimeRobotException +from pyuptimerobot.const import API_PATH_USER_ME from homeassistant.core import HomeAssistant diff --git a/tests/components/uptimerobot/test_sensor.py b/tests/components/uptimerobot/test_sensor.py index e5d6b12596d..20462a27a4f 100644 --- a/tests/components/uptimerobot/test_sensor.py +++ b/tests/components/uptimerobot/test_sensor.py @@ -6,11 +6,12 @@ from pyuptimerobot import UptimeRobotAuthenticationException from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.uptimerobot.const import COORDINATOR_UPDATE_INTERVAL -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from .common import ( + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, MOCK_UPTIMEROBOT_MONITOR, MOCK_UPTIMEROBOT_MONITOR_2, STATE_UP, @@ -19,7 +20,7 @@ from .common import ( setup_uptimerobot_integration, ) -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed async def test_presentation(hass: HomeAssistant) -> None: @@ -83,3 +84,22 @@ async def test_sensor_dynamic(hass: HomeAssistant) -> None: assert (entity := hass.states.get(entity_id_2)) assert entity.state == STATE_UP + + +async def test_sensor_monitor_status_missing( + hass: HomeAssistant, +) -> None: + """Test sensor becomes unknown when the monitor status is missing.""" + monitor_without_status = {**MOCK_UPTIMEROBOT_MONITOR, "status": None} + 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=[monitor_without_status]), + ): + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert (entity := hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_UNKNOWN diff --git a/tests/components/uptimerobot/test_switch.py b/tests/components/uptimerobot/test_switch.py index 2d1cfef8113..4974fe92f99 100644 --- a/tests/components/uptimerobot/test_switch.py +++ b/tests/components/uptimerobot/test_switch.py @@ -3,11 +3,8 @@ from unittest.mock import patch import pytest -from pyuptimerobot import ( - API_PATH_MONITOR_DETAIL, - UptimeRobotAuthenticationException, - UptimeRobotException, -) +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException +from pyuptimerobot.const import API_PATH_MONITOR_DETAIL from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.uptimerobot.const import COORDINATOR_UPDATE_INTERVAL @@ -58,7 +55,7 @@ async def test_switch_off(hass: HomeAssistant) -> None: ), ), patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", + "pyuptimerobot.UptimeRobot.async_pause_monitor", return_value=mock_uptimerobot_api_response( api_path=API_PATH_MONITOR_DETAIL, data=MOCK_UPTIMEROBOT_MONITOR_PAUSED ), @@ -90,7 +87,7 @@ async def test_switch_on(hass: HomeAssistant) -> None: return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), ), patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", + "pyuptimerobot.UptimeRobot.async_start_monitor", return_value=mock_uptimerobot_api_response( api_path=API_PATH_MONITOR_DETAIL, data=MOCK_UPTIMEROBOT_MONITOR, @@ -122,7 +119,7 @@ async def test_authentication_error( with ( patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", + "pyuptimerobot.UptimeRobot.async_start_monitor", side_effect=UptimeRobotAuthenticationException, ), patch( @@ -148,7 +145,7 @@ async def test_action_execution_failure(hass: HomeAssistant) -> None: with ( patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", + "pyuptimerobot.UptimeRobot.async_start_monitor", side_effect=UptimeRobotException, ), pytest.raises(HomeAssistantError) as exc_info, @@ -176,7 +173,7 @@ async def test_switch_api_failure(hass: HomeAssistant) -> None: with ( patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", + "pyuptimerobot.UptimeRobot.async_pause_monitor", side_effect=UptimeRobotException, ), pytest.raises(HomeAssistantError) as exc_info,