diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index c666c39cfb3..390b807f5cf 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -1,13 +1,41 @@ """The Coolmaster integration.""" +import logging + +from pycoolmasternet_async import CoolMasterNet + +from homeassistant.components.climate import SCAN_INTERVAL +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN + +_LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up Coolmaster components.""" + hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass, entry): """Set up Coolmaster from a config entry.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + coolmaster = CoolMasterNet(host, port) + try: + info = await coolmaster.info() + if not info: + raise ConfigEntryNotReady + except (OSError, ConnectionRefusedError, TimeoutError) as error: + raise ConfigEntryNotReady() from error + coordinator = CoolmasterDataUpdateCoordinator(hass, coolmaster) + await coordinator.async_refresh() + hass.data[DOMAIN][entry.entry_id] = { + DATA_INFO: info, + DATA_COORDINATOR: coordinator, + } hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "climate") ) @@ -17,4 +45,28 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a Coolmaster config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "climate") + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "climate") + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class CoolmasterDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Coolmaster data.""" + + def __init__(self, hass, coolmaster): + """Initialize global Coolmaster data updater.""" + self._coolmaster = coolmaster + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Fetch data from Coolmaster.""" + try: + return await self._coolmaster.status() + except (OSError, ConnectionRefusedError, TimeoutError) as error: + raise UpdateFailed from error diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 6e68e858a6d..8666307d65c 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -2,8 +2,6 @@ import logging -from pycoolmasternet import CoolMasterNet - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, @@ -15,15 +13,9 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_HOST, - CONF_PORT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from .const import CONF_SUPPORTED_MODES, DOMAIN +from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE @@ -42,58 +34,63 @@ FAN_MODES = ["low", "med", "high", "auto"] _LOGGER = logging.getLogger(__name__) -def _build_entity(device, supported_modes): - _LOGGER.debug("Found device %s", device.uid) - return CoolmasterClimate(device, supported_modes) +def _build_entity(coordinator, unit_id, unit, supported_modes, info): + _LOGGER.debug("Found device %s", unit_id) + return CoolmasterClimate(coordinator, unit_id, unit, supported_modes, info) async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the CoolMasterNet climate platform.""" supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) - host = config_entry.data[CONF_HOST] - port = config_entry.data[CONF_PORT] - cool = CoolMasterNet(host, port=port) - devices = await hass.async_add_executor_job(cool.devices) + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] - all_devices = [_build_entity(device, supported_modes) for device in devices] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - async_add_devices(all_devices, True) + all_devices = [ + _build_entity(coordinator, unit_id, unit, supported_modes, info) + for (unit_id, unit) in coordinator.data.items() + ] + + async_add_devices(all_devices) class CoolmasterClimate(ClimateEntity): """Representation of a coolmaster climate device.""" - def __init__(self, device, supported_modes): + def __init__(self, coordinator, unit_id, unit, supported_modes, info): """Initialize the climate device.""" - self._device = device - self._uid = device.uid + self._coordinator = coordinator + self._unit_id = unit_id + self._unit = unit self._hvac_modes = supported_modes - self._hvac_mode = None - self._target_temperature = None - self._current_temperature = None - self._current_fan_mode = None - self._current_operation = None - self._on = None - self._unit = None + self._info = info - def update(self): - """Pull state from CoolMasterNet.""" - status = self._device.status - self._target_temperature = status["thermostat"] - self._current_temperature = status["temperature"] - self._current_fan_mode = status["fan_speed"] - self._on = status["is_on"] + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False - device_mode = status["mode"] - if self._on: - self._hvac_mode = CM_TO_HA_STATE[device_mode] - else: - self._hvac_mode = HVAC_MODE_OFF + @property + def available(self): + """Return if entity is available.""" + return self._coordinator.last_update_success - if status["unit"] == "celsius": - self._unit = TEMP_CELSIUS - else: - self._unit = TEMP_FAHRENHEIT + def _refresh_from_coordinator(self): + self._unit = self._coordinator.data[self._unit_id] + self.async_write_ha_state() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self._refresh_from_coordinator) + ) + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self._coordinator.async_request_refresh() @property def device_info(self): @@ -103,12 +100,13 @@ class CoolmasterClimate(ClimateEntity): "name": self.name, "manufacturer": "CoolAutomation", "model": "CoolMasterNet", + "sw_version": self._info["version"], } @property def unique_id(self): """Return unique ID for this device.""" - return self._uid + return self._unit_id @property def supported_features(self): @@ -123,22 +121,30 @@ class CoolmasterClimate(ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - return self._unit + if self._unit.temperature_unit == "celsius": + return TEMP_CELSIUS + + return TEMP_FAHRENHEIT @property def current_temperature(self): """Return the current temperature.""" - return self._current_temperature + return self._unit.temperature @property def target_temperature(self): """Return the temperature we are trying to reach.""" - return self._target_temperature + return self._unit.thermostat @property def hvac_mode(self): """Return hvac target hvac state.""" - return self._hvac_mode + mode = self._unit.mode + is_on = self._unit.is_on + if not is_on: + return HVAC_MODE_OFF + + return CM_TO_HA_STATE[mode] @property def hvac_modes(self): @@ -148,41 +154,45 @@ class CoolmasterClimate(ClimateEntity): @property def fan_mode(self): """Return the fan setting.""" - return self._current_fan_mode + return self._unit.fan_speed @property def fan_modes(self): """Return the list of available fan modes.""" return FAN_MODES - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: _LOGGER.debug("Setting temp of %s to %s", self.unique_id, str(temp)) - self._device.set_thermostat(str(temp)) + self._unit = await self._unit.set_thermostat(temp) + self.async_write_ha_state() - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new fan mode.""" _LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, fan_mode) - self._device.set_fan_speed(fan_mode) + self._unit = await self._unit.set_fan_speed(fan_mode) + self.async_write_ha_state() - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new operation mode.""" _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, hvac_mode) if hvac_mode == HVAC_MODE_OFF: - self.turn_off() + await self.async_turn_off() else: - self._device.set_mode(HA_STATE_TO_CM[hvac_mode]) - self.turn_on() + self._unit = await self._unit.set_mode(HA_STATE_TO_CM[hvac_mode]) + await self.async_turn_on() - def turn_on(self): + async def async_turn_on(self): """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) - self._device.turn_on() + self._unit = await self._unit.turn_on() + self.async_write_ha_state() - def turn_off(self): + async def async_turn_off(self): """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) - self._device.turn_off() + self._unit = await self._unit.turn_off() + self.async_write_ha_state() diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index c267b283118..c674146fd15 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -1,6 +1,6 @@ """Config flow to configure Coolmaster.""" -from pycoolmasternet import CoolMasterNet +from pycoolmasternet_async import CoolMasterNet import voluptuous as vol from homeassistant import config_entries, core @@ -15,9 +15,9 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) async def _validate_connection(hass: core.HomeAssistant, host): - cool = CoolMasterNet(host, port=DEFAULT_PORT) - devices = await hass.async_add_executor_job(cool.devices) - return bool(devices) + cool = CoolMasterNet(host, DEFAULT_PORT) + units = await cool.status() + return bool(units) class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -53,7 +53,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): result = await _validate_connection(self.hass, host) if not result: errors["base"] = "no_units" - except (ConnectionRefusedError, TimeoutError): + except (OSError, ConnectionRefusedError, TimeoutError): errors["base"] = "connection_error" if errors: diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py index d4cfea73820..c07cbe392ef 100644 --- a/homeassistant/components/coolmaster/const.py +++ b/homeassistant/components/coolmaster/const.py @@ -9,6 +9,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, ) +DATA_INFO = "info" +DATA_COORDINATOR = "coordinator" + DOMAIN = "coolmaster" DEFAULT_PORT = 10102 diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index bc0ebd17d40..513dc495da3 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -3,6 +3,6 @@ "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": ["pycoolmasternet==0.0.4"], + "requirements": ["pycoolmasternet-async==0.1.0"], "codeowners": ["@OnFreund"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19c428ec822..6f8b98f14d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1268,7 +1268,7 @@ pycocotools==2.0.1 pycomfoconnect==0.3 # homeassistant.components.coolmaster -pycoolmasternet==0.0.4 +pycoolmasternet-async==0.1.0 # homeassistant.components.avri pycountry==19.8.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64b7e0e08a6..71506e12d0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ pybotvac==0.0.17 pychromecast==7.2.0 # homeassistant.components.coolmaster -pycoolmasternet==0.0.4 +pycoolmasternet-async==0.1.0 # homeassistant.components.avri pycountry==19.8.18 diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 49058fc183e..88985f7f88a 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Coolmaster config flow.""" -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN from tests.async_mock import patch @@ -14,7 +14,6 @@ def _flow_data(): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -22,8 +21,8 @@ async def test_form(hass): assert result["errors"] is None with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", - return_value=[1], + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", + return_value={"test_id": "test_unit"}, ), patch( "homeassistant.components.coolmaster.async_setup", return_value=True ) as mock_setup, patch( @@ -52,7 +51,7 @@ async def test_form_timeout(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", side_effect=TimeoutError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -70,7 +69,7 @@ async def test_form_connection_refused(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", side_effect=ConnectionRefusedError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -88,8 +87,8 @@ async def test_form_no_units(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", - return_value=[], + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", + return_value={}, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], _flow_data()