diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index ef2b5328f96..6e244abfd84 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -14,7 +14,7 @@ from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/coolmaster/binary_sensor.py b/homeassistant/components/coolmaster/binary_sensor.py new file mode 100644 index 00000000000..6d25d7ababf --- /dev/null +++ b/homeassistant/components/coolmaster/binary_sensor.py @@ -0,0 +1,47 @@ +"""Binary Sensor platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet binary_sensor platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterCleanFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterCleanFilter(CoolmasterEntity, BinarySensorEntity): + """Representation of a unit's filter state (true means need to be cleaned).""" + + _attr_has_entity_name = True + entity_description = BinarySensorEntityDescription( + key="clean_filter", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Clean filter", + icon="mdi:air-filter", + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self._unit.clean_filter diff --git a/homeassistant/components/coolmaster/button.py b/homeassistant/components/coolmaster/button.py new file mode 100644 index 00000000000..6f9a31576a2 --- /dev/null +++ b/homeassistant/components/coolmaster/button.py @@ -0,0 +1,42 @@ +"""Button platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet button platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterResetFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterResetFilter(CoolmasterEntity, ButtonEntity): + """Reset the clean filter timer (once filter was cleaned).""" + + _attr_has_entity_name = True + entity_description = ButtonEntityDescription( + key="reset_filter", + entity_category=EntityCategory.CONFIG, + name="Reset filter", + icon="mdi:air-filter", + ) + + async def async_press(self) -> None: + """Press the button.""" + await self._unit.reset_filter() + await self.coordinator.async_refresh() diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 933072aac9d..27c139af824 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -9,12 +9,11 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity CM_TO_HA_STATE = { "heat": HVACMode.HEAT, @@ -31,60 +30,32 @@ FAN_MODES = ["low", "med", "high", "auto"] _LOGGER = logging.getLogger(__name__) -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: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the CoolMasterNet climate platform.""" - supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - all_devices = [ - _build_entity(coordinator, unit_id, unit, supported_modes, info) - for (unit_id, unit) in coordinator.data.items() - ] - - async_add_devices(all_devices) + supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) + async_add_entities( + CoolmasterClimate(coordinator, unit_id, info, supported_modes) + for unit_id in coordinator.data + ) -class CoolmasterClimate(CoordinatorEntity, ClimateEntity): +class CoolmasterClimate(CoolmasterEntity, ClimateEntity): """Representation of a coolmaster climate device.""" _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) - def __init__(self, coordinator, unit_id, unit, supported_modes, info): + def __init__(self, coordinator, unit_id, info, supported_modes): """Initialize the climate device.""" - super().__init__(coordinator) - self._unit_id = unit_id - self._unit = unit + super().__init__(coordinator, unit_id, info) self._hvac_modes = supported_modes - self._info = info - - @callback - def _handle_coordinator_update(self): - self._unit = self.coordinator.data[self._unit_id] - super()._handle_coordinator_update() - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - manufacturer="CoolAutomation", - model="CoolMasterNet", - name=self.name, - sw_version=self._info["version"], - ) @property def unique_id(self): diff --git a/homeassistant/components/coolmaster/entity.py b/homeassistant/components/coolmaster/entity.py new file mode 100644 index 00000000000..65f21b77534 --- /dev/null +++ b/homeassistant/components/coolmaster/entity.py @@ -0,0 +1,38 @@ +"""Base entity for Coolmaster integration.""" +from pycoolmasternet_async.coolmasternet import CoolMasterNetUnit + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import CoolmasterDataUpdateCoordinator +from .const import DOMAIN + + +class CoolmasterEntity(CoordinatorEntity[CoolmasterDataUpdateCoordinator]): + """Representation of a Coolmaster entity.""" + + def __init__( + self, + coordinator: CoolmasterDataUpdateCoordinator, + unit_id: str, + info: dict[str, str], + ) -> None: + """Initiate CoolmasterEntity.""" + super().__init__(coordinator) + self._unit_id: str = unit_id + self._unit: CoolMasterNetUnit = coordinator.data[self._unit_id] + self._attr_device_info: DeviceInfo = DeviceInfo( + identifiers={(DOMAIN, unit_id)}, + manufacturer="CoolAutomation", + model="CoolMasterNet", + name=unit_id, + sw_version=info["version"], + ) + if hasattr(self, "entity_description"): + self._attr_unique_id: str = f"{unit_id}-{self.entity_description.key}" + + @callback + def _handle_coordinator_update(self) -> None: + self._unit = self.coordinator.data[self._unit_id] + super()._handle_coordinator_update() diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index a56a97f272e..8980850ca49 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -3,7 +3,7 @@ "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": ["pycoolmasternet-async==0.1.2"], + "requirements": ["pycoolmasternet-async==0.1.5"], "codeowners": ["@OnFreund"], "iot_class": "local_polling", "loggers": ["pycoolmasternet_async"] diff --git a/homeassistant/components/coolmaster/sensor.py b/homeassistant/components/coolmaster/sensor.py new file mode 100644 index 00000000000..ef550360f84 --- /dev/null +++ b/homeassistant/components/coolmaster/sensor.py @@ -0,0 +1,42 @@ +"""Sensor platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet sensor platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterCleanFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterCleanFilter(CoolmasterEntity, SensorEntity): + """Representation of a unit's error code.""" + + _attr_has_entity_name = True + entity_description = SensorEntityDescription( + key="error_code", + entity_category=EntityCategory.DIAGNOSTIC, + name="Error code", + icon="mdi:alert", + ) + + @property + def native_value(self) -> str: + """Return the error code or OK.""" + return self._unit.error_code or "OK" diff --git a/requirements_all.txt b/requirements_all.txt index 89d28bcdb7a..164c7e0ffed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1527,7 +1527,7 @@ pycocotools==2.0.1 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.2 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fca85c2c78f..719ad0921c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1091,7 +1091,7 @@ pychromecast==13.0.4 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.2 +pycoolmasternet-async==0.1.5 # homeassistant.components.daikin pydaikin==2.8.0 diff --git a/tests/components/coolmaster/conftest.py b/tests/components/coolmaster/conftest.py new file mode 100644 index 00000000000..c46a33aaa97 --- /dev/null +++ b/tests/components/coolmaster/conftest.py @@ -0,0 +1,98 @@ +"""Fixtures for the Coolmaster integration.""" +from __future__ import annotations + +import copy +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components.coolmaster.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +DEFAULT_INFO: dict[str, str] = { + "version": "1", +} + +DEFUALT_UNIT_DATA: dict[str, Any] = { + "is_on": False, + "thermostat": 20, + "temperature": 25, + "fan_speed": "low", + "mode": "cool", + "error_code": None, + "clean_filter": False, + "swing": None, + "temperature_unit": "celsius", +} + +TEST_UNITS: dict[dict[str, Any]] = { + "L1.100": {**DEFUALT_UNIT_DATA}, + "L1.101": { + **DEFUALT_UNIT_DATA, + **{ + "is_on": True, + "clean_filter": True, + "error_code": "Err1", + }, + }, +} + + +class CoolMasterNetUnitMock: + """Mock for CoolMasterNetUnit.""" + + def __init__(self, unit_id: str, attributes: dict[str, Any]) -> None: + """Initialize the CoolMasterNetUnitMock.""" + self.unit_id = unit_id + self._attributes = attributes + for key, value in attributes.items(): + setattr(self, key, value) + + async def reset_filter(self): + """Report that the air filter was cleaned and reset the timer.""" + self._attributes["clean_filter"] = False + + +class CoolMasterNetMock: + """Mock for CoolMasterNet.""" + + def __init__(self, *_args: Any) -> None: + """Initialize the CoolMasterNetMock.""" + self._data = copy.deepcopy(TEST_UNITS) + + async def info(self) -> dict[str, Any]: + """Return info about the bridge device.""" + return DEFAULT_INFO + + async def status(self) -> dict[str, CoolMasterNetUnitMock]: + """Return the units.""" + return { + key: CoolMasterNetUnitMock(key, attributes) + for key, attributes in self._data.items() + } + + +@pytest.fixture +async def load_int(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Coolmaster integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.2.3.4", + "port": 1234, + }, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.coolmaster.CoolMasterNet", + new=CoolMasterNetMock, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/coolmaster/test_binary_sensor.py b/tests/components/coolmaster/test_binary_sensor.py new file mode 100644 index 00000000000..2f5c8c5f1be --- /dev/null +++ b/tests/components/coolmaster/test_binary_sensor.py @@ -0,0 +1,14 @@ +"""The test for the Coolmaster binary sensor platform.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def test_binary_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster binary sensor.""" + assert hass.states.get("binary_sensor.l1_100_clean_filter").state == "off" + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "on" diff --git a/tests/components/coolmaster/test_button.py b/tests/components/coolmaster/test_button.py new file mode 100644 index 00000000000..67461f63087 --- /dev/null +++ b/tests/components/coolmaster/test_button.py @@ -0,0 +1,29 @@ +"""The test for the Coolmaster button platform.""" +from __future__ import annotations + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + + +async def test_button( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster button.""" + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "on" + + button = hass.states.get("button.l1_101_reset_filter") + assert button is not None + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: button.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "off" diff --git a/tests/components/coolmaster/test_sensor.py b/tests/components/coolmaster/test_sensor.py new file mode 100644 index 00000000000..3072106ec62 --- /dev/null +++ b/tests/components/coolmaster/test_sensor.py @@ -0,0 +1,14 @@ +"""The test for the Coolmaster sensor platform.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def test_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster sensor.""" + assert hass.states.get("sensor.l1_100_error_code").state == "OK" + assert hass.states.get("sensor.l1_101_error_code").state == "Err1"