diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index 57874a6db3e..cc7215349a6 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -1,23 +1,20 @@ """Support to control a Salda Smarty XP/XV ventilation unit.""" -from datetime import timedelta import ipaddress import logging -from pysmarty2 import Smarty import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME, Platform from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, SIGNAL_UPDATE_SMARTY +from .const import DOMAIN +from .coordinator import SmartyConfigEntry, SmartyCoordinator _LOGGER = logging.getLogger(__name__) @@ -35,8 +32,6 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = [Platform.BINARY_SENSOR, Platform.FAN, Platform.SENSOR] -type SmartyConfigEntry = ConfigEntry[Smarty] - async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: """Create a smarty system.""" @@ -89,27 +84,11 @@ async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: async def async_setup_entry(hass: HomeAssistant, entry: SmartyConfigEntry) -> bool: """Set up the Smarty environment from a config entry.""" - def _setup_smarty() -> Smarty: - smarty = Smarty(host=entry.data[CONF_HOST]) - smarty.update() - return smarty + coordinator = SmartyCoordinator(hass) - smarty = await hass.async_add_executor_job(_setup_smarty) + await coordinator.async_config_entry_first_refresh() - entry.runtime_data = smarty - - async def poll_device_update(event_time) -> None: - """Update Smarty device.""" - _LOGGER.debug("Updating Smarty device") - if await hass.async_add_executor_job(smarty.update): - _LOGGER.debug("Update success") - async_dispatcher_send(hass, SIGNAL_UPDATE_SMARTY) - else: - _LOGGER.debug("Update failed") - - entry.async_on_unload( - async_track_time_interval(hass, poll_device_update, timedelta(seconds=30)) - ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index c9fe516a526..3934b7510ad 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -4,17 +4,15 @@ from __future__ import annotations import logging -from pysmarty2 import Smarty - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import SIGNAL_UPDATE_SMARTY, SmartyConfigEntry +from .coordinator import SmartyConfigEntry, SmartyCoordinator _LOGGER = logging.getLogger(__name__) @@ -26,88 +24,76 @@ async def async_setup_entry( ) -> None: """Set up the Smarty Binary Sensor Platform.""" - smarty = entry.runtime_data - entry_id = entry.entry_id + coordinator = entry.runtime_data sensors = [ - AlarmSensor(entry.title, smarty, entry_id), - WarningSensor(entry.title, smarty, entry_id), - BoostSensor(entry.title, smarty, entry_id), + AlarmSensor(coordinator), + WarningSensor(coordinator), + BoostSensor(coordinator), ] - async_add_entities(sensors, True) + async_add_entities(sensors) -class SmartyBinarySensor(BinarySensorEntity): +class SmartyBinarySensor(CoordinatorEntity[SmartyCoordinator], BinarySensorEntity): """Representation of a Smarty Binary Sensor.""" - _attr_should_poll = False - def __init__( self, + coordinator: SmartyCoordinator, name: str, device_class: BinarySensorDeviceClass | None, - smarty: Smarty, ) -> None: """Initialize the entity.""" - self._attr_name = name + super().__init__(coordinator) + self._attr_name = f"{coordinator.config_entry.title} {name}" self._attr_device_class = device_class - self._smarty = smarty - - async def async_added_to_hass(self) -> None: - """Call to update.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback) - - @callback - def _update_callback(self) -> None: - """Call update method.""" - self.async_schedule_update_ha_state(True) class BoostSensor(SmartyBinarySensor): """Boost State Binary Sensor.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Alarm Sensor Init.""" - super().__init__(name=f"{name} Boost State", device_class=None, smarty=smarty) - self._attr_unique_id = f"{entry_id}_boost" + super().__init__(coordinator, name="Boost State", device_class=None) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_boost" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_is_on = self._smarty.boost + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.coordinator.client.boost class AlarmSensor(SmartyBinarySensor): """Alarm Binary Sensor.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Alarm Sensor Init.""" super().__init__( - name=f"{name} Alarm", + coordinator, + name="Alarm", device_class=BinarySensorDeviceClass.PROBLEM, - smarty=smarty, ) - self._attr_unique_id = f"{entry_id}_alarm" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_alarm" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_is_on = self._smarty.alarm + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.coordinator.client.alarm class WarningSensor(SmartyBinarySensor): """Warning Sensor.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Warning Sensor Init.""" super().__init__( - name=f"{name} Warning", + coordinator, + name="Warning", device_class=BinarySensorDeviceClass.PROBLEM, - smarty=smarty, ) - self._attr_unique_id = f"{entry_id}_warning" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_warning" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_is_on = self._smarty.warning + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.coordinator.client.warning diff --git a/homeassistant/components/smarty/const.py b/homeassistant/components/smarty/const.py index b241a10afc9..926c4233750 100644 --- a/homeassistant/components/smarty/const.py +++ b/homeassistant/components/smarty/const.py @@ -1,5 +1,3 @@ """Constants for the Smarty component.""" DOMAIN = "smarty" - -SIGNAL_UPDATE_SMARTY = "smarty_update" diff --git a/homeassistant/components/smarty/coordinator.py b/homeassistant/components/smarty/coordinator.py new file mode 100644 index 00000000000..20d7995a644 --- /dev/null +++ b/homeassistant/components/smarty/coordinator.py @@ -0,0 +1,36 @@ +"""Smarty Coordinator.""" + +from datetime import timedelta +import logging + +from pysmarty2 import Smarty + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + +type SmartyConfigEntry = ConfigEntry[SmartyCoordinator] + + +class SmartyCoordinator(DataUpdateCoordinator[None]): + """Smarty Coordinator.""" + + config_entry: SmartyConfigEntry + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize.""" + super().__init__( + hass, + logger=_LOGGER, + name="Smarty", + update_interval=timedelta(seconds=30), + ) + self.client = Smarty(host=self.config_entry.data[CONF_HOST]) + + async def _async_update_data(self) -> None: + """Fetch data from Smarty.""" + if not await self.hass.async_add_executor_job(self.client.update): + raise UpdateFailed("Failed to update Smarty data") diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index ca6474c05f5..898d53ebf89 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -9,15 +9,16 @@ from typing import Any from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, ) from homeassistant.util.scaling import int_states_in_range -from . import SIGNAL_UPDATE_SMARTY, SmartyConfigEntry +from . import SmartyConfigEntry +from .coordinator import SmartyCoordinator _LOGGER = logging.getLogger(__name__) @@ -32,16 +33,15 @@ async def async_setup_entry( ) -> None: """Set up the Smarty Fan Platform.""" - smarty = entry.runtime_data + coordinator = entry.runtime_data - async_add_entities([SmartyFan(entry.title, smarty, entry.entry_id)], True) + async_add_entities([SmartyFan(coordinator)]) -class SmartyFan(FanEntity): +class SmartyFan(CoordinatorEntity[SmartyCoordinator], FanEntity): """Representation of a Smarty Fan.""" _attr_icon = "mdi:air-conditioner" - _attr_should_poll = False _attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_OFF @@ -49,12 +49,13 @@ class SmartyFan(FanEntity): ) _enable_turn_on_off_backwards_compatibility = False - def __init__(self, name, smarty, entry_id): + def __init__(self, coordinator: SmartyCoordinator) -> None: """Initialize the entity.""" - self._attr_name = name + super().__init__(coordinator) + self._attr_name = coordinator.config_entry.title self._smarty_fan_speed = 0 - self._smarty = smarty - self._attr_unique_id = entry_id + self._smarty = coordinator.client + self._attr_unique_id = coordinator.config_entry.entry_id @property def is_on(self) -> bool: @@ -108,17 +109,8 @@ class SmartyFan(FanEntity): self._smarty_fan_speed = 0 self.schedule_update_ha_state() - async def async_added_to_hass(self) -> None: - """Call to update fan.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback - ) - ) - @callback - def _update_callback(self) -> None: + def _handle_coordinator_update(self) -> None: """Call update method.""" - _LOGGER.debug("Updating state") self._smarty_fan_speed = self._smarty.fan_speed - self.async_write_ha_state() + super()._handle_coordinator_update() diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index c727dcd4fdd..6a4c1eb8597 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -2,19 +2,17 @@ from __future__ import annotations -import datetime as dt +from datetime import datetime, timedelta import logging -from pysmarty2 import Smarty - from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import UnitOfTemperature -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util -from . import SIGNAL_UPDATE_SMARTY, SmartyConfigEntry +from .coordinator import SmartyConfigEntry, SmartyCoordinator _LOGGER = logging.getLogger(__name__) @@ -26,162 +24,153 @@ async def async_setup_entry( ) -> None: """Set up the Smarty Sensor Platform.""" - smarty = entry.runtime_data - entry_id = entry.entry_id + coordinator = entry.runtime_data sensors = [ - SupplyAirTemperatureSensor(entry.title, smarty, entry_id), - ExtractAirTemperatureSensor(entry.title, smarty, entry_id), - OutdoorAirTemperatureSensor(entry.title, smarty, entry_id), - SupplyFanSpeedSensor(entry.title, smarty, entry_id), - ExtractFanSpeedSensor(entry.title, smarty, entry_id), - FilterDaysLeftSensor(entry.title, smarty, entry_id), + SupplyAirTemperatureSensor(coordinator), + ExtractAirTemperatureSensor(coordinator), + OutdoorAirTemperatureSensor(coordinator), + SupplyFanSpeedSensor(coordinator), + ExtractFanSpeedSensor(coordinator), + FilterDaysLeftSensor(coordinator), ] - async_add_entities(sensors, True) + async_add_entities(sensors) -class SmartySensor(SensorEntity): +class SmartySensor(CoordinatorEntity[SmartyCoordinator], SensorEntity): """Representation of a Smarty Sensor.""" - _attr_should_poll = False - def __init__( self, + coordinator: SmartyCoordinator, name: str, + key: str, device_class: SensorDeviceClass | None, - smarty: Smarty, unit_of_measurement: str | None, ) -> None: """Initialize the entity.""" - self._attr_name = name + super().__init__(coordinator) + self._attr_name = f"{coordinator.config_entry.title} {name}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{key}" self._attr_native_value = None self._attr_device_class = device_class self._attr_native_unit_of_measurement = unit_of_measurement - self._smarty = smarty - - async def async_added_to_hass(self) -> None: - """Call to update.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback) - - @callback - def _update_callback(self) -> None: - """Call update method.""" - self.async_schedule_update_ha_state(True) class SupplyAirTemperatureSensor(SmartySensor): """Supply Air Temperature Sensor.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Supply Air Temperature Init.""" super().__init__( - name=f"{name} Supply Air Temperature", + coordinator, + name="Supply Air Temperature", + key="supply_air_temperature", device_class=SensorDeviceClass.TEMPERATURE, unit_of_measurement=UnitOfTemperature.CELSIUS, - smarty=smarty, ) - self._attr_unique_id = f"{entry_id}_supply_air_temperature" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_native_value = self._smarty.supply_air_temperature + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return self.coordinator.client.supply_air_temperature class ExtractAirTemperatureSensor(SmartySensor): """Extract Air Temperature Sensor.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Supply Air Temperature Init.""" super().__init__( - name=f"{name} Extract Air Temperature", + coordinator, + name="Extract Air Temperature", + key="extract_air_temperature", device_class=SensorDeviceClass.TEMPERATURE, unit_of_measurement=UnitOfTemperature.CELSIUS, - smarty=smarty, ) - self._attr_unique_id = f"{entry_id}_extract_air_temperature" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_native_value = self._smarty.extract_air_temperature + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return self.coordinator.client.extract_air_temperature class OutdoorAirTemperatureSensor(SmartySensor): """Extract Air Temperature Sensor.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Outdoor Air Temperature Init.""" super().__init__( - name=f"{name} Outdoor Air Temperature", + coordinator, + name="Outdoor Air Temperature", + key="outdoor_air_temperature", device_class=SensorDeviceClass.TEMPERATURE, unit_of_measurement=UnitOfTemperature.CELSIUS, - smarty=smarty, ) - self._attr_unique_id = f"{entry_id}_outdoor_air_temperature" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_native_value = self._smarty.outdoor_air_temperature + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return self.coordinator.client.outdoor_air_temperature class SupplyFanSpeedSensor(SmartySensor): """Supply Fan Speed RPM.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Supply Fan Speed RPM Init.""" super().__init__( - name=f"{name} Supply Fan Speed", + coordinator, + name="Supply Fan Speed", + key="supply_fan_speed", device_class=None, unit_of_measurement=None, - smarty=smarty, ) - self._attr_unique_id = f"{entry_id}_supply_fan_speed" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_native_value = self._smarty.supply_fan_speed + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return self.coordinator.client.supply_fan_speed class ExtractFanSpeedSensor(SmartySensor): """Extract Fan Speed RPM.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Extract Fan Speed RPM Init.""" super().__init__( - name=f"{name} Extract Fan Speed", + coordinator, + name="Extract Fan Speed", + key="extract_fan_speed", device_class=None, unit_of_measurement=None, - smarty=smarty, ) - self._attr_unique_id = f"{entry_id}_extract_fan_speed" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - self._attr_native_value = self._smarty.extract_fan_speed + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return self.coordinator.client.extract_fan_speed class FilterDaysLeftSensor(SmartySensor): """Filter Days Left.""" - def __init__(self, name: str, smarty: Smarty, entry_id: str) -> None: + def __init__(self, coordinator: SmartyCoordinator) -> None: """Filter Days Left Init.""" super().__init__( - name=f"{name} Filter Days Left", + coordinator, + name="Filter Days Left", + key="filter_days_left", device_class=SensorDeviceClass.TIMESTAMP, unit_of_measurement=None, - smarty=smarty, ) self._days_left = 91 - self._attr_unique_id = f"{entry_id}_filter_days_left" - def update(self) -> None: - """Update state.""" - _LOGGER.debug("Updating sensor %s", self._attr_name) - days_left = self._smarty.filter_timer + @property + def native_value(self) -> datetime | None: + """Return the state of the sensor.""" + days_left = self.coordinator.client.filter_timer if days_left is not None and days_left != self._days_left: - self._attr_native_value = dt_util.now() + dt.timedelta(days=days_left) self._days_left = days_left + return dt_util.now() + timedelta(days=days_left) + return None diff --git a/tests/components/smarty/conftest.py b/tests/components/smarty/conftest.py index eff76a7994d..24f358aa9cf 100644 --- a/tests/components/smarty/conftest.py +++ b/tests/components/smarty/conftest.py @@ -27,7 +27,7 @@ def mock_smarty() -> Generator[AsyncMock]: """Mock a Smarty client.""" with ( patch( - "homeassistant.components.smarty.Smarty", + "homeassistant.components.smarty.coordinator.Smarty", autospec=True, ) as mock_client, patch(