diff --git a/.coveragerc b/.coveragerc index 506f8024d5e..04fa795c966 100644 --- a/.coveragerc +++ b/.coveragerc @@ -400,7 +400,6 @@ omit = homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py - homeassistant/components/fritz/sensor.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 9811adf6829..0f3d8cb1ae0 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -308,14 +308,13 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): """Update data.""" _LOGGER.debug("Updating FRITZ!Box sensors") + status: FritzStatus = self._avm_wrapper.fritz_status try: - status: FritzStatus = self._avm_wrapper.fritz_status - self._attr_available = True + self._attr_native_value = ( + self._last_device_value + ) = self.entity_description.value_fn(status, self._last_device_value) except FritzConnectionException: _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) self._attr_available = False return - - self._attr_native_value = ( - self._last_device_value - ) = self.entity_description.value_fn(status, self._last_device_value) + self._attr_available = True diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 139a8448b08..b073335f20a 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,6 +1,6 @@ """Common stuff for AVM Fritz!Box tests.""" import logging -from unittest.mock import patch +from unittest.mock import MagicMock, patch from fritzconnection.core.processor import Service from fritzconnection.lib.fritzhosts import FritzHosts @@ -25,7 +25,7 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods """FritzConnection mocking.""" def __init__(self, services): - """Inint Mocking class.""" + """Init Mocking class.""" self.modelname = MOCK_MODELNAME self.call_action = self._call_action self._services = services @@ -36,6 +36,13 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods LOGGER.debug("-" * 80) LOGGER.debug("FritzConnectionMock - services: %s", self.services) + def call_action_side_effect(self, side_effect=None) -> None: + """Set or unset a side_effect for call_action.""" + if side_effect is not None: + self.call_action = MagicMock(side_effect=side_effect) + else: + self.call_action = self._call_action + def _call_action(self, service: str, action: str, **kwargs): LOGGER.debug( "_call_action service: %s, action: %s, **kwargs: %s", diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py new file mode 100644 index 00000000000..31e142a3e47 --- /dev/null +++ b/tests/components/fritz/test_sensor.py @@ -0,0 +1,151 @@ +"""Tests for Shelly button platform.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from fritzconnection.core.exceptions import FritzConnectionException + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.fritz.sensor import SENSOR_TYPES +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DEVICE_CLASS_TIMESTAMP, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_STATE, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry, async_fire_time_changed + +SENSOR_STATES: dict[str, dict[str, Any]] = { + "sensor.mock_title_external_ip": { + ATTR_STATE: "1.2.3.4", + ATTR_ICON: "mdi:earth", + }, + "sensor.mock_title_device_uptime": { + # ATTR_STATE: "2022-02-05T17:46:04+00:00", + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + "sensor.mock_title_connection_uptime": { + # ATTR_STATE: "2022-03-06T11:27:16+00:00", + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + "sensor.mock_title_upload_throughput": { + ATTR_STATE: "3.4", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "kB/s", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_download_throughput": { + ATTR_STATE: "67.6", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "kB/s", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_max_connection_upload_throughput": { + ATTR_STATE: "2105.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_max_connection_download_throughput": { + ATTR_STATE: "10087.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_gb_sent": { + ATTR_STATE: "1.7", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: "GB", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_gb_received": { + ATTR_STATE: "5.2", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: "GB", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_link_upload_throughput": { + ATTR_STATE: "51805.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_link_download_throughput": { + ATTR_STATE: "318557.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_link_upload_noise_margin": { + ATTR_STATE: "9.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_link_download_noise_margin": { + ATTR_STATE: "8.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_link_upload_power_attenuation": { + ATTR_STATE: "7.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_link_download_power_attenuation": { + ATTR_STATE: "12.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:download", + }, +} + + +async def test_sensor_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test setup of Fritz!Tools sesnors.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + sensors = hass.states.async_all(SENSOR_DOMAIN) + assert len(sensors) == len(SENSOR_TYPES) + + for sensor in sensors: + assert SENSOR_STATES.get(sensor.entity_id) is not None + for key, val in SENSOR_STATES[sensor.entity_id].items(): + if key == ATTR_STATE: + assert sensor.state == val + else: + assert sensor.attributes.get(key) == val + + +async def test_sensor_update_fail(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test failed update of Fritz!Tools sesnors.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + fc_class_mock().call_action_side_effect(FritzConnectionException) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300)) + await hass.async_block_till_done() + + sensors = hass.states.async_all(SENSOR_DOMAIN) + for sensor in sensors: + assert sensor.state == STATE_UNAVAILABLE