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:
parent
34798189ca
commit
11b03b5669
13 changed files with 339 additions and 44 deletions
|
@ -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:
|
||||
|
|
47
homeassistant/components/coolmaster/binary_sensor.py
Normal file
47
homeassistant/components/coolmaster/binary_sensor.py
Normal 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
|
42
homeassistant/components/coolmaster/button.py
Normal file
42
homeassistant/components/coolmaster/button.py
Normal 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()
|
|
@ -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):
|
||||
|
|
38
homeassistant/components/coolmaster/entity.py
Normal file
38
homeassistant/components/coolmaster/entity.py
Normal 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()
|
|
@ -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"]
|
||||
|
|
42
homeassistant/components/coolmaster/sensor.py
Normal file
42
homeassistant/components/coolmaster/sensor.py
Normal 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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
98
tests/components/coolmaster/conftest.py
Normal file
98
tests/components/coolmaster/conftest.py
Normal 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
|
14
tests/components/coolmaster/test_binary_sensor.py
Normal file
14
tests/components/coolmaster/test_binary_sensor.py
Normal 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"
|
29
tests/components/coolmaster/test_button.py
Normal file
29
tests/components/coolmaster/test_button.py
Normal 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"
|
14
tests/components/coolmaster/test_sensor.py
Normal file
14
tests/components/coolmaster/test_sensor.py
Normal 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"
|
Loading…
Add table
Add a link
Reference in a new issue