From 27d1c1f471ce79858f50e932eff762020e5b9680 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 22 Sep 2022 12:17:04 +0200 Subject: [PATCH] Nibe Heat Pump after merge fixups (#78931) --- .../components/nibe_heatpump/__init__.py | 41 +++---- .../components/nibe_heatpump/config_flow.py | 2 +- .../components/nibe_heatpump/sensor.py | 107 ++++++++++++++---- .../components/nibe_heatpump/strings.json | 3 - .../nibe_heatpump/translations/en.json | 6 +- .../nibe_heatpump/test_config_flow.py | 10 ++ 6 files changed, 119 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 343452c9f45..7285f5ce642 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -1,6 +1,7 @@ """The Nibe Heat Pump integration.""" from __future__ import annotations +from collections import defaultdict from datetime import timedelta from nibe.coil import Coil @@ -11,14 +12,20 @@ from nibe.heatpump import HeatPump, Model from tenacity import RetryError, retry, retry_if_exception_type, stop_after_attempt from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL, Platform +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_MODEL, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import ( @@ -33,6 +40,7 @@ from .const import ( ) PLATFORMS: list[Platform] = [Platform.SENSOR] +COIL_READ_RETRIES = 5 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -40,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: heatpump = HeatPump(Model[entry.data[CONF_MODEL]]) heatpump.word_swap = entry.data[CONF_WORD_SWAP] - heatpump.initialize() + await hass.async_add_executor_job(heatpump.initialize) connection_type = entry.data[CONF_CONNECTION_TYPE] @@ -56,17 +64,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise HomeAssistantError(f"Connection type {connection_type} is not supported.") await connection.start() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, connection.stop) + ) + coordinator = Coordinator(hass, heatpump, connection) data = hass.data.setdefault(DOMAIN, {}) data[entry.entry_id] = coordinator - try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady: - await connection.stop() - raise - reg = dr.async_get(hass) reg.async_get_or_create( config_entry_id=entry.entry_id, @@ -139,13 +146,8 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): return float(value) return None - async def async_write_coil( - self, coil: Coil | None, value: int | float | str - ) -> None: + async def async_write_coil(self, coil: Coil, value: int | float | str) -> None: """Write coil and update state.""" - if not coil: - raise HomeAssistantError("No coil available") - coil.value = value coil = await self.connection.write_coil(coil) @@ -155,16 +157,17 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): async def _async_update_data(self) -> dict[int, Coil]: @retry( - retry=retry_if_exception_type(CoilReadException), stop=stop_after_attempt(2) + retry=retry_if_exception_type(CoilReadException), + stop=stop_after_attempt(COIL_READ_RETRIES), ) async def read_coil(coil: Coil): return await self.connection.read_coil(coil) - callbacks: dict[int, list[CALLBACK_TYPE]] = {} + callbacks: dict[int, list[CALLBACK_TYPE]] = defaultdict(list) for update_callback, context in list(self._listeners.values()): assert isinstance(context, set) for address in context: - callbacks.setdefault(address, []).append(update_callback) + callbacks[address].append(update_callback) result: dict[int, Coil] = {} @@ -173,7 +176,7 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): coil = self.heatpump.get_coil_by_address(address) self.data[coil.address] = result[coil.address] = await read_coil(coil) except (CoilReadException, RetryError) as exception: - self.logger.warning("Failed to update: %s", exception) + raise UpdateFailed(f"Failed to update: {exception}") from exception except CoilNotFoundException as exception: self.logger.debug("Skipping missing coil: %s", exception) diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index 14da4d478b2..28fafdb3a37 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -115,7 +115,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(self.hass, user_input) except FieldError as exception: - LOGGER.exception("Validation error") + LOGGER.debug("Validation error %s", exception) errors[exception.field] = exception.error except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index b6ea2e766a2..b0bc816dad6 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -20,7 +21,6 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, - TEMP_KELVIN, TIME_HOURS, ) from homeassistant.core import HomeAssistant @@ -29,6 +29,78 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, CoilEntity, Coordinator +UNIT_DESCRIPTIONS = { + "°C": SensorEntityDescription( + key="°C", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + "°F": SensorEntityDescription( + key="°F", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_FAHRENHEIT, + ), + "A": SensorEntityDescription( + key="A", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + "mA": SensorEntityDescription( + key="mA", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, + ), + "V": SensorEntityDescription( + key="V", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + "mV": SensorEntityDescription( + key="mV", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + ), + "Wh": SensorEntityDescription( + key="Wh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_WATT_HOUR, + ), + "kWh": SensorEntityDescription( + key="kWh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + "MWh": SensorEntityDescription( + key="MWh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + ), + "h": SensorEntityDescription( + key="h", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, + ), +} + async def async_setup_entry( hass: HomeAssistant, @@ -40,38 +112,27 @@ async def async_setup_entry( coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - Sensor(coordinator, coil) + Sensor(coordinator, coil, UNIT_DESCRIPTIONS.get(coil.unit)) for coil in coordinator.coils if not coil.is_writable and not coil.is_boolean ) -class Sensor(SensorEntity, CoilEntity): +class Sensor(CoilEntity, SensorEntity): """Sensor entity.""" - _attr_entity_category = EntityCategory.DIAGNOSTIC - - def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + def __init__( + self, + coordinator: Coordinator, + coil: Coil, + entity_description: SensorEntityDescription | None, + ) -> None: """Initialize entity.""" super().__init__(coordinator, coil, ENTITY_ID_FORMAT) - self._attr_native_unit_of_measurement = coil.unit - - unit = self.native_unit_of_measurement - if unit in {TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN}: - self._attr_device_class = SensorDeviceClass.TEMPERATURE - elif unit in {ELECTRIC_CURRENT_AMPERE, ELECTRIC_CURRENT_MILLIAMPERE}: - self._attr_device_class = SensorDeviceClass.CURRENT - elif unit in {ELECTRIC_POTENTIAL_VOLT, ELECTRIC_POTENTIAL_MILLIVOLT}: - self._attr_device_class = SensorDeviceClass.VOLTAGE - elif unit in {ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR}: - self._attr_device_class = SensorDeviceClass.ENERGY - elif unit in {TIME_HOURS}: - self._attr_device_class = SensorDeviceClass.DURATION + if entity_description: + self.entity_description = entity_description else: - self._attr_device_class = None - - if unit: - self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_native_unit_of_measurement = coil.unit def _async_read_coil(self, coil: Coil): self._attr_native_value = coil.value diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json index 5b31ba178b3..45e55b61083 100644 --- a/homeassistant/components/nibe_heatpump/strings.json +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -17,9 +17,6 @@ "address_in_use": "The selected listening port is already in use on this system.", "model": "The model selected doesn't seem to support modbus40", "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json index 17120e20d88..6d85cbcfcb3 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -1,11 +1,9 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "address": "Invalid remote IP address specified. Address must be a IPV4 address.", - "address_in_use": "The selected listening port is already in use on this system. Reconfigure your gateway device to use a different address if the conflict can not be resolved.", + "address_in_use": "The selected listening port is already in use on this system.", + "model": "The model selected doesn't seem to support modbus40", "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", "unknown": "Unexpected error", "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`." diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index 68c01bf91c8..2647102ba5a 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -91,6 +91,16 @@ async def test_address_inuse(hass: HomeAssistant, mock_connection: Mock) -> None assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"listening_port": "address_in_use"} + error.errno = errno.EACCES + mock_connection.return_value.start.side_effect = error + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_USERDATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + async def test_read_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: """Test we handle cannot connect error."""