Bump pycoolmasternet-async and add coolmaster swing mode (#82809)

* Add filter and error code support to CoolMastetNet

* Create separate entities

* coolmaster swing_mode support

* Changed default to False

* Raise HomeAssistantError

* Add tests for init and climate

* Fixed bad merge

* Catch only ValueError
pull/85071/head
amitfin 2023-01-03 20:21:11 +02:00 committed by GitHub
parent ca7384f96e
commit b5664f9eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 430 additions and 37 deletions

View File

@ -195,9 +195,6 @@ omit =
homeassistant/components/control4/const.py
homeassistant/components/control4/director_utils.py
homeassistant/components/control4/light.py
homeassistant/components/coolmaster/__init__.py
homeassistant/components/coolmaster/climate.py
homeassistant/components/coolmaster/const.py
homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/crownstone/__init__.py
homeassistant/components/crownstone/const.py

View File

@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN
from .const import CONF_SWING_SUPPORT, DATA_COORDINATOR, DATA_INFO, DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -21,7 +21,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Coolmaster from a config entry."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
coolmaster = CoolMasterNet(host, port)
coolmaster = CoolMasterNet(
host, port, swing_support=entry.data.get(CONF_SWING_SUPPORT, False)
)
try:
info = await coolmaster.info()
if not info:

View File

@ -1,7 +1,11 @@
"""CoolMasterNet platform to control of CoolMasterNet Climate Devices."""
from __future__ import annotations
import logging
from typing import Any
from pycoolmasternet_async import SWING_MODES
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
@ -10,6 +14,7 @@ from homeassistant.components.climate import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN
@ -48,10 +53,6 @@ async def async_setup_entry(
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, info, supported_modes):
"""Initialize the climate device."""
super().__init__(coordinator, unit_id, info)
@ -67,6 +68,16 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity):
"""Return the name of the climate device."""
return self.unique_id
@property
def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features."""
supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
if self.swing_mode:
supported_features |= ClimateEntityFeature.SWING_MODE
return supported_features
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
@ -109,6 +120,16 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity):
"""Return the list of available fan modes."""
return FAN_MODES
@property
def swing_mode(self) -> str | None:
"""Return the swing mode setting."""
return self._unit.swing
@property
def swing_modes(self) -> list[str] | None:
"""Return swing modes if supported."""
return SWING_MODES if self.swing_mode is not None else None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
@ -122,6 +143,15 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity):
self._unit = await self._unit.set_fan_speed(fan_mode)
self.async_write_ha_state()
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new swing mode."""
_LOGGER.debug("Setting swing mode of %s to %s", self.unique_id, swing_mode)
try:
self._unit = await self._unit.set_swing(swing_mode)
except ValueError as error:
raise HomeAssistantError(error) from error
self.async_write_ha_state()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new operation mode."""
_LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, hvac_mode)

View File

@ -12,7 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .const import CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN
from .const import CONF_SUPPORTED_MODES, CONF_SWING_SUPPORT, DEFAULT_PORT, DOMAIN
AVAILABLE_MODES = [
HVACMode.OFF.value,
@ -25,7 +25,13 @@ AVAILABLE_MODES = [
MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES}
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA})
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
**MODES_SCHEMA,
vol.Required(CONF_SWING_SUPPORT, default=False): bool,
}
)
async def _validate_connection(host: str) -> bool:
@ -50,6 +56,7 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_HOST: data[CONF_HOST],
CONF_PORT: DEFAULT_PORT,
CONF_SUPPORTED_MODES: supported_modes,
CONF_SWING_SUPPORT: data[CONF_SWING_SUPPORT],
},
)

View File

@ -8,3 +8,4 @@ DOMAIN = "coolmaster"
DEFAULT_PORT = 10102
CONF_SUPPORTED_MODES = "supported_modes"
CONF_SWING_SUPPORT = "swing_support"

View File

@ -10,7 +10,8 @@
"cool": "Support cool mode",
"heat_cool": "Support automatic heat/cool mode",
"dry": "Support dry mode",
"fan_only": "Support fan only mode"
"fan_only": "Support fan only mode",
"swing_support": "Control swing mode"
}
}
},

View File

@ -13,7 +13,8 @@
"heat": "Support heat mode",
"heat_cool": "Support automatic heat/cool mode",
"host": "Host",
"off": "Can be turned off"
"off": "Can be turned off",
"swing_support": "Control swing mode"
},
"title": "Set up your CoolMasterNet connection details."
}

View File

@ -7,6 +7,7 @@ from unittest.mock import patch
import pytest
from homeassistant.components.climate import HVACMode
from homeassistant.components.coolmaster.const import DOMAIN
from homeassistant.core import HomeAssistant
@ -16,27 +17,28 @@ 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.100": {
"is_on": False,
"thermostat": 20,
"temperature": 25,
"temperature_unit": "celsius",
"fan_speed": "low",
"mode": "cool",
"error_code": None,
"clean_filter": False,
"swing": None,
},
"L1.101": {
**DEFUALT_UNIT_DATA,
**{
"is_on": True,
"clean_filter": True,
"error_code": "Err1",
},
"is_on": True,
"thermostat": 68,
"temperature": 50,
"temperature_unit": "imperial",
"fan_speed": "high",
"mode": "heat",
"error_code": "Err1",
"clean_filter": True,
"swing": "horizontal",
},
}
@ -51,17 +53,50 @@ class CoolMasterNetUnitMock:
for key, value in attributes.items():
setattr(self, key, value)
async def reset_filter(self):
async def set_fan_speed(self, value: str) -> CoolMasterNetUnitMock:
"""Set the fan speed."""
self._attributes["fan_speed"] = value
return CoolMasterNetUnitMock(self.unit_id, self._attributes)
async def set_mode(self, value: str) -> CoolMasterNetUnitMock:
"""Set the mode."""
self._attributes["mode"] = value
return CoolMasterNetUnitMock(self.unit_id, self._attributes)
async def set_thermostat(self, value: int | float) -> CoolMasterNetUnitMock:
"""Set the target temperature."""
self._attributes["thermostat"] = value
return CoolMasterNetUnitMock(self.unit_id, self._attributes)
async def set_swing(self, value: str | None) -> CoolMasterNetUnitMock:
"""Set the swing mode."""
if value == "":
raise ValueError()
self._attributes["swing"] = value
return CoolMasterNetUnitMock(self.unit_id, self._attributes)
async def turn_on(self) -> CoolMasterNetUnitMock:
"""Turn a unit on."""
self._attributes["is_on"] = True
return CoolMasterNetUnitMock(self.unit_id, self._attributes)
async def turn_off(self) -> CoolMasterNetUnitMock:
"""Turn a unit off."""
self._attributes["is_on"] = False
return CoolMasterNetUnitMock(self.unit_id, self._attributes)
async def reset_filter(self) -> CoolMasterNetUnitMock:
"""Report that the air filter was cleaned and reset the timer."""
self._attributes["clean_filter"] = False
return CoolMasterNetUnitMock(self.unit_id, self._attributes)
class CoolMasterNetMock:
"""Mock for CoolMasterNet."""
def __init__(self, *_args: Any) -> None:
def __init__(self, *_args: Any, **kwargs: Any) -> None:
"""Initialize the CoolMasterNetMock."""
self._data = copy.deepcopy(TEST_UNITS)
self._units = copy.deepcopy(TEST_UNITS)
async def info(self) -> dict[str, Any]:
"""Return info about the bridge device."""
@ -70,8 +105,8 @@ class CoolMasterNetMock:
async def status(self) -> dict[str, CoolMasterNetUnitMock]:
"""Return the units."""
return {
key: CoolMasterNetUnitMock(key, attributes)
for key, attributes in self._data.items()
unit_id: CoolMasterNetUnitMock(unit_id, attributes)
for unit_id, attributes in self._units.items()
}
@ -83,6 +118,7 @@ async def load_int(hass: HomeAssistant) -> MockConfigEntry:
data={
"host": "1.2.3.4",
"port": 1234,
"supported_modes": [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
},
)

View File

@ -0,0 +1,290 @@
"""The test for the Coolmaster climate platform."""
from __future__ import annotations
from pycoolmasternet_async import SWING_MODES
import pytest
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_FAN_MODES,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_SWING_MODE,
ATTR_SWING_MODES,
DOMAIN as CLIMATE_DOMAIN,
FAN_HIGH,
FAN_LOW,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.components.coolmaster.climate import FAN_MODES
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
async def test_climate_state(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate state."""
assert hass.states.get("climate.l1_100").state == HVACMode.OFF
assert hass.states.get("climate.l1_101").state == HVACMode.HEAT
async def test_climate_friendly_name(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate friendly name."""
assert hass.states.get("climate.l1_100").attributes[ATTR_FRIENDLY_NAME] == "L1.100"
assert hass.states.get("climate.l1_101").attributes[ATTR_FRIENDLY_NAME] == "L1.101"
async def test_climate_supported_features(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate supported features."""
assert hass.states.get("climate.l1_100").attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
assert hass.states.get("climate.l1_101").attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.SWING_MODE
)
async def test_climate_temperature(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate current temperature."""
assert hass.states.get("climate.l1_100").attributes[ATTR_CURRENT_TEMPERATURE] == 25
assert hass.states.get("climate.l1_101").attributes[ATTR_CURRENT_TEMPERATURE] == 10
async def test_climate_thermostat(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate thermostat."""
assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 20
assert hass.states.get("climate.l1_101").attributes[ATTR_TEMPERATURE] == 20
async def test_climate_hvac_modes(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate hvac modes."""
assert hass.states.get("climate.l1_100").attributes[ATTR_HVAC_MODES] == [
HVACMode.OFF,
HVACMode.COOL,
HVACMode.HEAT,
]
assert (
hass.states.get("climate.l1_101").attributes[ATTR_HVAC_MODES]
== hass.states.get("climate.l1_100").attributes[ATTR_HVAC_MODES]
)
async def test_climate_fan_mode(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate fan mode."""
assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_LOW
assert hass.states.get("climate.l1_101").attributes[ATTR_FAN_MODE] == FAN_HIGH
async def test_climate_fan_modes(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate fan modes."""
assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODES] == FAN_MODES
assert (
hass.states.get("climate.l1_101").attributes[ATTR_FAN_MODES]
== hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODES]
)
async def test_climate_swing_mode(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate swing mode."""
assert ATTR_SWING_MODE not in hass.states.get("climate.l1_100").attributes
assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "horizontal"
async def test_climate_swing_modes(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate swing modes."""
assert ATTR_SWING_MODES not in hass.states.get("climate.l1_100").attributes
assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODES] == SWING_MODES
async def test_set_temperature(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate set temperature."""
assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 20
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.l1_100",
ATTR_TEMPERATURE: 30,
},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 30
async def test_set_fan_mode(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate set fan mode."""
assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_LOW
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{
ATTR_ENTITY_ID: "climate.l1_100",
ATTR_FAN_MODE: FAN_HIGH,
},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_HIGH
async def test_set_swing_mode(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate set swing mode."""
assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "horizontal"
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE,
{
ATTR_ENTITY_ID: "climate.l1_101",
ATTR_SWING_MODE: "vertical",
},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "vertical"
async def test_set_swing_mode_error(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate set swing mode with error."""
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE,
{
ATTR_ENTITY_ID: "climate.l1_101",
ATTR_SWING_MODE: "",
},
blocking=True,
)
async def test_set_hvac_mode(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate set hvac mode."""
assert hass.states.get("climate.l1_100").state == HVACMode.OFF
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: "climate.l1_100",
ATTR_HVAC_MODE: HVACMode.HEAT,
},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("climate.l1_100").state == HVACMode.HEAT
async def test_set_hvac_mode_off(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate set hvac mode to off."""
assert hass.states.get("climate.l1_101").state == HVACMode.HEAT
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: "climate.l1_101",
ATTR_HVAC_MODE: HVACMode.OFF,
},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("climate.l1_101").state == HVACMode.OFF
async def test_turn_on(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate turn on."""
assert hass.states.get("climate.l1_100").state == HVACMode.OFF
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "climate.l1_100",
},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("climate.l1_100").state == HVACMode.COOL
async def test_turn_off(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test the Coolmaster climate turn off."""
assert hass.states.get("climate.l1_101").state == HVACMode.HEAT
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: "climate.l1_101",
},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("climate.l1_101").state == HVACMode.OFF

View File

@ -10,6 +10,7 @@ def _flow_data():
options = {"host": "1.1.1.1"}
for mode in AVAILABLE_MODES:
options[mode] = True
options["swing_support"] = False
return options
@ -39,6 +40,7 @@ async def test_form(hass):
"host": "1.1.1.1",
"port": 10102,
"supported_modes": AVAILABLE_MODES,
"swing_support": False,
}
assert len(mock_setup_entry.mock_calls) == 1

View File

@ -0,0 +1,26 @@
"""The test for the Coolmaster integration."""
from homeassistant.components.coolmaster.const import DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
async def test_load_entry(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test Coolmaster initial load."""
# 2 units times 4 entities (climate, binary_sensor, sensor, button).
assert hass.states.async_entity_ids_count() == 8
assert load_int.state is ConfigEntryState.LOADED
async def test_unload_entry(
hass: HomeAssistant,
load_int: ConfigEntry,
) -> None:
"""Test Coolmaster unloading an entry."""
assert load_int.entry_id in hass.data.get(DOMAIN)
await hass.config_entries.async_unload(load_int.entry_id)
await hass.async_block_till_done()
assert load_int.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)