Bump pycoolmasternet-async and add filter and error code support to CoolMastetNet (#84548)

* Add filter and error code support to CoolMastetNet

* Create separate entities

* Remove async_add_entities_for_platform

* Fixed call to async_add_entities

* Avoid using test global
This commit is contained in:
amitfin 2023-01-03 19:00:45 +02:00 committed by GitHub
parent 34798189ca
commit 11b03b5669
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 339 additions and 44 deletions

View file

@ -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:

View file

@ -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

View file

@ -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()

View file

@ -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):

View file

@ -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()

View file

@ -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"]

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"