From 827501659c926ace3741425760b1294d2e93b48e Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 14 Oct 2021 04:05:06 +0200 Subject: [PATCH] Nut: Use coordinator data, code cleanup and add test coverage (#57643) --- .coveragerc | 1 - CODEOWNERS | 2 +- homeassistant/components/nut/__init__.py | 10 +-- homeassistant/components/nut/const.py | 1 - homeassistant/components/nut/manifest.json | 2 +- homeassistant/components/nut/sensor.py | 27 +++----- tests/components/nut/test_sensor.py | 73 +++++++++++++++++++++- 7 files changed, 83 insertions(+), 33 deletions(-) diff --git a/.coveragerc b/.coveragerc index 18139bddf08..d09bab53760 100644 --- a/.coveragerc +++ b/.coveragerc @@ -731,7 +731,6 @@ omit = homeassistant/components/nuki/const.py homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py - homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py homeassistant/components/obihai/* diff --git a/CODEOWNERS b/CODEOWNERS index ef702109461..bed454c62a7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -361,7 +361,7 @@ homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt @pvizeli @pree homeassistant/components/numato/* @clssn homeassistant/components/number/* @home-assistant/core @Shulyaka -homeassistant/components/nut/* @bdraco +homeassistant/components/nut/* @bdraco @ollo69 homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index ea57c5994c6..6c8b5c69e80 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -28,7 +27,6 @@ from .const import ( PYNUT_FIRMWARE, PYNUT_MANUFACTURER, PYNUT_MODEL, - PYNUT_NAME, PYNUT_UNIQUE_ID, UNDO_UPDATE_LISTENER, ) @@ -61,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(data.update) if not data.status: raise UpdateFailed("Error fetching UPS state") + return data.status coordinator = DataUpdateCoordinator( hass, @@ -72,11 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - status = data.status - - if not status: - _LOGGER.error("NUT Sensor has no data, unable to set up") - raise ConfigEntryNotReady + status = coordinator.data _LOGGER.debug("NUT Sensors Available: %s", status) @@ -95,7 +90,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYNUT_MANUFACTURER: _manufacturer_from_status(status), PYNUT_MODEL: _model_from_status(status), PYNUT_FIRMWARE: _firmware_from_status(status), - PYNUT_NAME: data.name, UNDO_UPDATE_LISTENER: undo_listener, } diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 3861c608631..250d999ce35 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -45,7 +45,6 @@ PYNUT_UNIQUE_ID = "unique_id" PYNUT_MANUFACTURER = "manufacturer" PYNUT_MODEL = "model" PYNUT_FIRMWARE = "firmware" -PYNUT_NAME = "name" SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.status.display": SensorEntityDescription( diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 388858b93f0..2489078ebd6 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -3,7 +3,7 @@ "name": "Network UPS Tools (NUT)", "documentation": "https://www.home-assistant.io/integrations/nut", "requirements": ["pynut2==2.1.2"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco", "@ollo69"], "config_flow": true, "zeroconf": ["_nut._tcp.local."], "iot_class": "local_polling" diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 703e9ddd4ec..48253674be8 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -20,7 +20,6 @@ from .const import ( PYNUT_FIRMWARE, PYNUT_MANUFACTURER, PYNUT_MODEL, - PYNUT_NAME, PYNUT_UNIQUE_ID, SENSOR_TYPES, STATE_TYPES, @@ -37,10 +36,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): manufacturer = pynut_data[PYNUT_MANUFACTURER] model = pynut_data[PYNUT_MODEL] firmware = pynut_data[PYNUT_FIRMWARE] - name = pynut_data[PYNUT_NAME] coordinator = pynut_data[COORDINATOR] data = pynut_data[PYNUT_DATA] - status = data.status + status = coordinator.data enabled_resources = [ resource.lower() for resource in config_entry.data[CONF_RESOURCES] @@ -55,7 +53,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): NUTSensor( coordinator, data, - name.title(), SENSOR_TYPES[sensor_type], unique_id, manufacturer, @@ -76,7 +73,6 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self, coordinator: DataUpdateCoordinator, data: PyNUTData, - name: str, sensor_description: SensorEntityDescription, unique_id: str, manufacturer: str | None, @@ -90,20 +86,16 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._manufacturer = manufacturer self._firmware = firmware self._model = model - self._device_name = name - self._data = data + self._device_name = data.name.title() self._unique_id = unique_id - self._attr_entity_registry_enabled_default = enabled_default - self._attr_name = f"{name} {sensor_description.name}" - if unique_id is not None: - self._attr_unique_id = f"{unique_id}_{sensor_description.key}" + self._attr_entity_registry_enabled_default = enabled_default + self._attr_name = f"{self._device_name} {sensor_description.name}" + self._attr_unique_id = f"{unique_id}_{sensor_description.key}" @property def device_info(self): """Device info for the ups.""" - if not self._unique_id: - return None device_info = { "identifiers": {(DOMAIN, self._unique_id)}, "name": self._device_name, @@ -119,17 +111,14 @@ class NUTSensor(CoordinatorEntity, SensorEntity): @property def native_value(self): """Return entity state from ups.""" - if not self._data.status: - return None + status = self.coordinator.data if self.entity_description.key == KEY_STATUS_DISPLAY: - return _format_display_state(self._data.status) - return self._data.status.get(self.entity_description.key) + return _format_display_state(status) + return status.get(self.entity_description.key) def _format_display_state(status): """Return UPS display state.""" - if status is None: - return STATE_TYPES["OFF"] try: return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split()) except KeyError: diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index a5715ff9c8e..4b1e1cc8a9a 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -1,9 +1,20 @@ """The sensor tests for the nut platform.""" -from homeassistant.const import PERCENTAGE +from unittest.mock import patch + +from homeassistant.components.nut.const import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_RESOURCES, + PERCENTAGE, + STATE_UNKNOWN, +) from homeassistant.helpers import entity_registry as er -from .util import async_init_integration +from .util import _get_mock_pynutclient, async_init_integration + +from tests.common import MockConfigEntry async def test_pr3000rt2u(hass): @@ -204,6 +215,64 @@ async def test_blazer_usb(hass): ) +async def test_state_sensors(hass): + """Test creation of status display sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "mock", + CONF_PORT: "mock", + CONF_RESOURCES: ["ups.status", "ups.status.display"], + }, + ) + entry.add_to_hass(hass) + + mock_pynut = _get_mock_pynutclient( + list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OL"} + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.ups1_status") + state2 = hass.states.get("sensor.ups1_status_data") + assert state1.state == "Online" + assert state2.state == "OL" + + +async def test_unknown_state_sensors(hass): + """Test creation of unknown status display sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "mock", + CONF_PORT: "mock", + CONF_RESOURCES: ["ups.status", "ups.status.display"], + }, + ) + entry.add_to_hass(hass) + + mock_pynut = _get_mock_pynutclient( + list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OQ"} + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.ups1_status") + state2 = hass.states.get("sensor.ups1_status_data") + assert state1.state == STATE_UNKNOWN + assert state2.state == "OQ" + + async def test_stale_options(hass): """Test creation of sensors with stale options to remove."""