diff --git a/CODEOWNERS b/CODEOWNERS index 7e43aaba2f9..e5b6ed9798a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -999,8 +999,8 @@ homeassistant/components/updater/* @home-assistant/core tests/components/updater/* @home-assistant/core homeassistant/components/upnp/* @StevenLooman @ehendrix23 tests/components/upnp/* @StevenLooman @ehendrix23 -homeassistant/components/uptimerobot/* @ludeeus -tests/components/uptimerobot/* @ludeeus +homeassistant/components/uptimerobot/* @ludeeus @chemelli74 +tests/components/uptimerobot/* @ludeeus @chemelli74 homeassistant/components/usb/* @bdraco tests/components/usb/* @bdraco homeassistant/components/usgs_earthquakes_feed/* @exxamalte diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index 7637a61c593..e98ae7b514c 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] +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTRIBUTION: Final = "Data provided by UptimeRobot" diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index d19ada33158..17241dba196 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -6,7 +6,7 @@ "pyuptimerobot==21.11.0" ], "codeowners": [ - "@ludeeus" + "@ludeeus", "@chemelli74" ], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py new file mode 100644 index 00000000000..77f32e20b4b --- /dev/null +++ b/homeassistant/components/uptimerobot/sensor.py @@ -0,0 +1,68 @@ +"""UptimeRobot sensor platform.""" +from __future__ import annotations + +from typing import TypedDict + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import UptimeRobotDataUpdateCoordinator +from .const import DOMAIN +from .entity import UptimeRobotEntity + + +class StatusValue(TypedDict): + """Sensor details.""" + + value: str + icon: str + + +SENSORS_INFO = { + 0: StatusValue(value="pause", icon="mdi:television-pause"), + 1: StatusValue(value="not_checked_yet", icon="mdi:television"), + 2: StatusValue(value="up", icon="mdi:television-shimmer"), + 8: StatusValue(value="seems_down", icon="mdi:television-off"), + 9: StatusValue(value="down", icon="mdi:television-off"), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """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 + ], + ) + + +class UptimeRobotSensor(UptimeRobotEntity, SensorEntity): + """Representation of a UptimeRobot sensor.""" + + @property + def native_value(self) -> str: + """Return the status of the monitor.""" + return SENSORS_INFO[self.monitor.status]["value"] + + @property + def icon(self) -> str: + """Return the status of the monitor.""" + return SENSORS_INFO[self.monitor.status]["icon"] diff --git a/homeassistant/components/uptimerobot/strings.sensor.json b/homeassistant/components/uptimerobot/strings.sensor.json new file mode 100644 index 00000000000..a8177bab9cf --- /dev/null +++ b/homeassistant/components/uptimerobot/strings.sensor.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "pause": "Pause", + "not_checked_yet": "Not checked yet", + "up": "Up", + "seems_down": "Seems down", + "down": "Down" + } + } + } diff --git a/homeassistant/components/uptimerobot/translations/sensor.en.json b/homeassistant/components/uptimerobot/translations/sensor.en.json new file mode 100644 index 00000000000..196fd3d203c --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.en.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Down", + "not_checked_yet": "Not checked yet", + "pause": "Pause", + "seems_down": "Seems down", + "up": "Up" + } + } +} \ No newline at end of file diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 86198487851..6ec0e7aef7d 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -46,7 +46,10 @@ MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA = { "source": config_entries.SOURCE_USER, } -UPTIMEROBOT_TEST_ENTITY = "binary_sensor.test_monitor" +STATE_UP = "up" + +UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY = "binary_sensor.test_monitor" +UPTIMEROBOT_SENSOR_TEST_ENTITY = "sensor.test_monitor" class MockApiResponseKey(str, Enum): @@ -94,7 +97,8 @@ async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY).state == STATE_UP assert mock_entry.state == config_entries.ConfigEntryState.LOADED return mock_entry diff --git a/tests/components/uptimerobot/test_binary_sensor.py b/tests/components/uptimerobot/test_binary_sensor.py index 25ca76a2914..0cf0c3a6fbe 100644 --- a/tests/components/uptimerobot/test_binary_sensor.py +++ b/tests/components/uptimerobot/test_binary_sensor.py @@ -15,7 +15,7 @@ from homeassistant.util import dt from .common import ( MOCK_UPTIMEROBOT_MONITOR, - UPTIMEROBOT_TEST_ENTITY, + UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY, setup_uptimerobot_integration, ) @@ -26,7 +26,7 @@ async def test_presentation(hass: HomeAssistant) -> None: """Test the presenstation of UptimeRobot binary_sensors.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + entity = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) assert entity.state == STATE_ON assert entity.attributes["device_class"] == BinarySensorDeviceClass.CONNECTIVITY @@ -38,7 +38,7 @@ async def test_unaviable_on_update_failure(hass: HomeAssistant) -> None: """Test entity unaviable on update failure.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + entity = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) assert entity.state == STATE_ON with patch( @@ -48,5 +48,5 @@ async def test_unaviable_on_update_failure(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + entity = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) assert entity.state == STATE_UNAVAILABLE diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 3a11319f230..94dd7e8af4c 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -20,7 +20,7 @@ from homeassistant.util import dt from .common import ( MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, MOCK_UPTIMEROBOT_MONITOR, - UPTIMEROBOT_TEST_ENTITY, + UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY, MockApiResponseKey, mock_uptimerobot_api_response, setup_uptimerobot_integration, @@ -68,7 +68,7 @@ async def test_reauthentication_trigger_after_setup( """Test reauthentication trigger.""" mock_config_entry = await setup_uptimerobot_integration(hass) - binary_sensor = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + binary_sensor = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED assert binary_sensor.state == STATE_ON @@ -81,7 +81,10 @@ async def test_reauthentication_trigger_after_setup( await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + assert ( + hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state + == STATE_UNAVAILABLE + ) assert "Authentication failed while fetching uptimerobot data" in caplog.text @@ -107,7 +110,7 @@ async def test_integration_reload(hass: HomeAssistant): entry = hass.config_entries.async_get_entry(mock_entry.entry_id) assert entry.state == config_entries.ConfigEntryState.LOADED - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON async def test_update_errors(hass: HomeAssistant, caplog: LogCaptureFixture): @@ -120,7 +123,10 @@ async def test_update_errors(hass: HomeAssistant, caplog: LogCaptureFixture): ): async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + assert ( + hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state + == STATE_UNAVAILABLE + ) with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", @@ -128,7 +134,7 @@ async def test_update_errors(hass: HomeAssistant, caplog: LogCaptureFixture): ): async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", @@ -136,7 +142,10 @@ async def test_update_errors(hass: HomeAssistant, caplog: LogCaptureFixture): ): async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + assert ( + hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state + == STATE_UNAVAILABLE + ) assert "Error fetching uptimerobot data: test error from API" in caplog.text @@ -152,8 +161,8 @@ async def test_device_management(hass: HomeAssistant): assert devices[0].identifiers == {(DOMAIN, "1234")} assert devices[0].name == "Test monitor" - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON - assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2") is None + assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY}_2") is None with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", @@ -169,8 +178,10 @@ async def test_device_management(hass: HomeAssistant): assert devices[0].identifiers == {(DOMAIN, "1234")} assert devices[1].identifiers == {(DOMAIN, "12345")} - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON - assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2").state == STATE_ON + assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert ( + hass.states.get(f"{UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY}_2").state == STATE_ON + ) with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", @@ -183,5 +194,5 @@ async def test_device_management(hass: HomeAssistant): assert len(devices) == 1 assert devices[0].identifiers == {(DOMAIN, "1234")} - assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON - assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2") is None + assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY}_2") is None diff --git a/tests/components/uptimerobot/test_sensor.py b/tests/components/uptimerobot/test_sensor.py new file mode 100644 index 00000000000..3e833af9bd4 --- /dev/null +++ b/tests/components/uptimerobot/test_sensor.py @@ -0,0 +1,50 @@ +"""Test UptimeRobot sensor.""" + +from unittest.mock import patch + +from pyuptimerobot import UptimeRobotAuthenticationException + +from homeassistant.components.uptimerobot.const import COORDINATOR_UPDATE_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from .common import ( + MOCK_UPTIMEROBOT_MONITOR, + STATE_UP, + UPTIMEROBOT_SENSOR_TEST_ENTITY, + setup_uptimerobot_integration, +) + +from tests.common import async_fire_time_changed + +SENSOR_ICON = "mdi:television-shimmer" + + +async def test_presentation(hass: HomeAssistant) -> None: + """Test the presenstation of UptimeRobot sensors.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) + + assert entity.state == STATE_UP + assert entity.attributes["icon"] == SENSOR_ICON + assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] + + +async def test_unaviable_on_update_failure(hass: HomeAssistant) -> None: + """Test entity unaviable on update failure.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) + assert entity.state == STATE_UP + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotAuthenticationException, + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) + assert entity.state == STATE_UNAVAILABLE