From d2ca00ca53de9e088082a5fc6f4d111aa33d79bc Mon Sep 17 00:00:00 2001 From: theobld-ww <60600399+theobld-ww@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:08:30 +0100 Subject: [PATCH] Refactor Watts Vision+ to generic device, in preparation for switch support (#162721) Co-authored-by: Joostlek --- homeassistant/components/watts/__init__.py | 56 ++++++++++--------- homeassistant/components/watts/climate.py | 38 +++++++------ homeassistant/components/watts/coordinator.py | 28 ++++------ homeassistant/components/watts/entity.py | 28 +++++----- 4 files changed, 76 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/watts/__init__.py b/homeassistant/components/watts/__init__.py index 8cbc90548b1..38b84c920b8 100644 --- a/homeassistant/components/watts/__init__.py +++ b/homeassistant/components/watts/__init__.py @@ -20,9 +20,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN from .coordinator import ( + WattsVisionDeviceCoordinator, + WattsVisionDeviceData, WattsVisionHubCoordinator, - WattsVisionThermostatCoordinator, - WattsVisionThermostatData, ) _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ class WattsVisionRuntimeData: auth: WattsVisionAuth hub_coordinator: WattsVisionHubCoordinator - thermostat_coordinators: dict[str, WattsVisionThermostatCoordinator] + device_coordinators: dict[str, WattsVisionDeviceCoordinator] client: WattsVisionClient @@ -44,46 +44,50 @@ type WattsVisionConfigEntry = ConfigEntry[WattsVisionRuntimeData] @callback -def _handle_new_thermostats( +def _handle_new_devices( hass: HomeAssistant, entry: WattsVisionConfigEntry, hub_coordinator: WattsVisionHubCoordinator, ) -> None: - """Check for new thermostat devices and create coordinators.""" - + """Check for new devices and create coordinators.""" current_device_ids = set(hub_coordinator.data.keys()) - known_device_ids = set(entry.runtime_data.thermostat_coordinators.keys()) + known_device_ids = set(entry.runtime_data.device_coordinators.keys()) new_device_ids = current_device_ids - known_device_ids if not new_device_ids: return - _LOGGER.info("Discovered %d new device(s): %s", len(new_device_ids), new_device_ids) - - thermostat_coordinators = entry.runtime_data.thermostat_coordinators + device_coordinators = entry.runtime_data.device_coordinators client = entry.runtime_data.client + supported_device_ids: list[str] = [] for device_id in new_device_ids: device = hub_coordinator.data[device_id] if not isinstance(device, ThermostatDevice): continue - thermostat_coordinator = WattsVisionThermostatCoordinator( + device_coordinator = WattsVisionDeviceCoordinator( hass, client, entry, hub_coordinator, device_id ) - thermostat_coordinator.async_set_updated_data( - WattsVisionThermostatData(thermostat=device) - ) - thermostat_coordinators[device_id] = thermostat_coordinator + device_coordinator.async_set_updated_data(WattsVisionDeviceData(device=device)) + device_coordinators[device_id] = device_coordinator + supported_device_ids.append(device_id) - _LOGGER.debug("Created thermostat coordinator for device %s", device_id) + _LOGGER.debug("Created device coordinator for device %s", device_id) + if not supported_device_ids: + return + + _LOGGER.info( + "Discovered %d new device(s): %s", + len(supported_device_ids), + supported_device_ids, + ) async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_new_device") async def async_setup_entry(hass: HomeAssistant, entry: WattsVisionConfigEntry) -> bool: """Set up Watts Vision from a config entry.""" - try: implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -117,24 +121,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: WattsVisionConfigEntry) await hub_coordinator.async_config_entry_first_refresh() - thermostat_coordinators: dict[str, WattsVisionThermostatCoordinator] = {} + device_coordinators: dict[str, WattsVisionDeviceCoordinator] = {} for device_id in hub_coordinator.device_ids: device = hub_coordinator.data[device_id] if not isinstance(device, ThermostatDevice): continue - thermostat_coordinator = WattsVisionThermostatCoordinator( + device_coordinator = WattsVisionDeviceCoordinator( hass, client, entry, hub_coordinator, device_id ) - thermostat_coordinator.async_set_updated_data( - WattsVisionThermostatData(thermostat=device) - ) - thermostat_coordinators[device_id] = thermostat_coordinator + device_coordinator.async_set_updated_data(WattsVisionDeviceData(device=device)) + device_coordinators[device_id] = device_coordinator entry.runtime_data = WattsVisionRuntimeData( auth=auth, hub_coordinator=hub_coordinator, - thermostat_coordinators=thermostat_coordinators, + device_coordinators=device_coordinators, client=client, ) @@ -143,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WattsVisionConfigEntry) # Listener for dynamic device detection entry.async_on_unload( hub_coordinator.async_add_listener( - lambda: _handle_new_thermostats(hass, entry, hub_coordinator) + lambda: _handle_new_devices(hass, entry, hub_coordinator) ) ) @@ -154,7 +156,7 @@ async def async_unload_entry( hass: HomeAssistant, entry: WattsVisionConfigEntry ) -> bool: """Unload a config entry.""" - for thermostat_coordinator in entry.runtime_data.thermostat_coordinators.values(): - thermostat_coordinator.unsubscribe_hub_listener() + for device_coordinator in entry.runtime_data.device_coordinators.values(): + device_coordinator.unsubscribe_hub_listener() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/watts/climate.py b/homeassistant/components/watts/climate.py index e9f21b974f5..d30e21b5275 100644 --- a/homeassistant/components/watts/climate.py +++ b/homeassistant/components/watts/climate.py @@ -20,8 +20,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import WattsVisionConfigEntry from .const import DOMAIN, HVAC_MODE_TO_THERMOSTAT, THERMOSTAT_MODE_TO_HVAC -from .coordinator import WattsVisionThermostatCoordinator -from .entity import WattsVisionThermostatEntity +from .coordinator import WattsVisionDeviceCoordinator +from .entity import WattsVisionEntity _LOGGER = logging.getLogger(__name__) @@ -34,14 +34,18 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Watts Vision climate entities from a config entry.""" - - thermostat_coordinators = entry.runtime_data.thermostat_coordinators + device_coordinators = entry.runtime_data.device_coordinators known_device_ids: set[str] = set() @callback def _check_new_thermostats() -> None: """Check for new thermostat devices.""" - current_device_ids = set(thermostat_coordinators.keys()) + thermostat_coords = { + did: coord + for did, coord in device_coordinators.items() + if isinstance(coord.data.device, ThermostatDevice) + } + current_device_ids = set(thermostat_coords.keys()) new_device_ids = current_device_ids - known_device_ids if not new_device_ids: @@ -52,13 +56,12 @@ async def async_setup_entry( len(new_device_ids), ) - new_entities = [ - WattsVisionClimate( - thermostat_coordinators[device_id], - thermostat_coordinators[device_id].data.thermostat, - ) - for device_id in new_device_ids - ] + new_entities = [] + for device_id in new_device_ids: + coord = thermostat_coords[device_id] + device = coord.data.device + assert isinstance(device, ThermostatDevice) + new_entities.append(WattsVisionClimate(coord, device)) known_device_ids.update(new_device_ids) async_add_entities(new_entities) @@ -75,7 +78,7 @@ async def async_setup_entry( ) -class WattsVisionClimate(WattsVisionThermostatEntity, ClimateEntity): +class WattsVisionClimate(WattsVisionEntity[ThermostatDevice], ClimateEntity): """Representation of a Watts Vision heater as a climate entity.""" _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE @@ -84,11 +87,10 @@ class WattsVisionClimate(WattsVisionThermostatEntity, ClimateEntity): def __init__( self, - coordinator: WattsVisionThermostatCoordinator, + coordinator: WattsVisionDeviceCoordinator, thermostat: ThermostatDevice, ) -> None: """Initialize the climate entity.""" - super().__init__(coordinator, thermostat.device_id) self._attr_min_temp = thermostat.min_allowed_temperature @@ -102,17 +104,17 @@ class WattsVisionClimate(WattsVisionThermostatEntity, ClimateEntity): @property def current_temperature(self) -> float | None: """Return the current temperature.""" - return self.thermostat.current_temperature + return self.device.current_temperature @property def target_temperature(self) -> float | None: """Return the temperature setpoint.""" - return self.thermostat.setpoint + return self.device.setpoint @property def hvac_mode(self) -> HVACMode | None: """Return hvac mode.""" - return THERMOSTAT_MODE_TO_HVAC.get(self.thermostat.thermostat_mode) + return THERMOSTAT_MODE_TO_HVAC.get(self.device.thermostat_mode) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" diff --git a/homeassistant/components/watts/coordinator.py b/homeassistant/components/watts/coordinator.py index 5dbb5571c63..7c95564cecf 100644 --- a/homeassistant/components/watts/coordinator.py +++ b/homeassistant/components/watts/coordinator.py @@ -15,7 +15,7 @@ from visionpluspython.exceptions import ( WattsVisionError, WattsVisionTimeoutError, ) -from visionpluspython.models import Device, ThermostatDevice +from visionpluspython.models import Device from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -39,10 +39,10 @@ _LOGGER = logging.getLogger(__name__) @dataclass -class WattsVisionThermostatData: - """Data class for thermostat device coordinator.""" +class WattsVisionDeviceData: + """Data class for device coordinator.""" - thermostat: ThermostatDevice + device: Device class WattsVisionHubCoordinator(DataUpdateCoordinator[dict[str, Device]]): @@ -150,10 +150,8 @@ class WattsVisionHubCoordinator(DataUpdateCoordinator[dict[str, Device]]): return list((self.data or {}).keys()) -class WattsVisionThermostatCoordinator( - DataUpdateCoordinator[WattsVisionThermostatData] -): - """Thermostat device coordinator for individual updates.""" +class WattsVisionDeviceCoordinator(DataUpdateCoordinator[WattsVisionDeviceData]): + """Device coordinator for individual updates.""" def __init__( self, @@ -163,7 +161,7 @@ class WattsVisionThermostatCoordinator( hub_coordinator: WattsVisionHubCoordinator, device_id: str, ) -> None: - """Initialize the thermostat coordinator.""" + """Initialize the device coordinator.""" super().__init__( hass, _LOGGER, @@ -185,11 +183,10 @@ class WattsVisionThermostatCoordinator( """Handle updates from hub coordinator.""" if self.hub_coordinator.data and self.device_id in self.hub_coordinator.data: device = self.hub_coordinator.data[self.device_id] - assert isinstance(device, ThermostatDevice) - self.async_set_updated_data(WattsVisionThermostatData(thermostat=device)) + self.async_set_updated_data(WattsVisionDeviceData(device=device)) - async def _async_update_data(self) -> WattsVisionThermostatData: - """Refresh specific thermostat device.""" + async def _async_update_data(self) -> WattsVisionDeviceData: + """Refresh specific device.""" if self._fast_polling_until and datetime.now() > self._fast_polling_until: self._fast_polling_until = None self.update_interval = None @@ -215,9 +212,8 @@ class WattsVisionThermostatCoordinator( if not device: raise UpdateFailed(f"Device {self.device_id} not found") - assert isinstance(device, ThermostatDevice) - _LOGGER.debug("Refreshed thermostat %s", self.device_id) - return WattsVisionThermostatData(thermostat=device) + _LOGGER.debug("Refreshed device %s", self.device_id) + return WattsVisionDeviceData(device=device) def trigger_fast_polling(self, duration: int = 60) -> None: """Activate fast polling for a specified duration after a command.""" diff --git a/homeassistant/components/watts/entity.py b/homeassistant/components/watts/entity.py index 4b429cf4c55..f36320f281c 100644 --- a/homeassistant/components/watts/entity.py +++ b/homeassistant/components/watts/entity.py @@ -2,42 +2,44 @@ from __future__ import annotations -from visionpluspython.models import ThermostatDevice +from typing import cast + +from visionpluspython.models import Device from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import WattsVisionThermostatCoordinator +from .coordinator import WattsVisionDeviceCoordinator -class WattsVisionThermostatEntity(CoordinatorEntity[WattsVisionThermostatCoordinator]): - """Base entity for Watts Vision thermostat devices.""" +class WattsVisionEntity[_T: Device](CoordinatorEntity[WattsVisionDeviceCoordinator]): + """Base entity for Watts Vision devices.""" _attr_has_entity_name = True def __init__( - self, coordinator: WattsVisionThermostatCoordinator, device_id: str + self, coordinator: WattsVisionDeviceCoordinator, device_id: str ) -> None: """Initialize the entity.""" - super().__init__(coordinator, context=device_id) self.device_id = device_id self._attr_unique_id = device_id + device = coordinator.data.device self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.device_id)}, - name=self.thermostat.device_name, + name=device.device_name, manufacturer="Watts", - model=f"Vision+ {self.thermostat.device_type}", - suggested_area=self.thermostat.room_name, + model=f"Vision+ {device.device_type}", + suggested_area=device.room_name, ) @property - def thermostat(self) -> ThermostatDevice: - """Return the thermostat device from the coordinator data.""" - return self.coordinator.data.thermostat + def device(self) -> _T: + """Return the device from the coordinator data.""" + return cast(_T, self.coordinator.data.device) @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self.coordinator.data.thermostat.is_online + return super().available and self.coordinator.data.device.is_online