diff --git a/.coveragerc b/.coveragerc index 83e6971cc6a..3645eb00d33 100644 --- a/.coveragerc +++ b/.coveragerc @@ -126,7 +126,9 @@ omit = homeassistant/components/comfoconnect/* homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.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/cpuspeed/sensor.py homeassistant/components/crimereports/sensor.py diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index b27ae5f25b4..530427d33ad 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -1 +1,22 @@ -"""The coolmaster component.""" +"""The Coolmaster integration.""" + + +async def async_setup(hass, config): + """Set up Coolmaster components.""" + return True + + +async def async_setup_entry(hass, entry): + """Set up Coolmaster from a config entry.""" + hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, "climate")) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a Coolmaster config entry.""" + await hass.async_add_job( + hass.config_entries.async_forward_entry_unload(entry, "climate") + ) + + return True diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 71115a9eebb..a52431dd89b 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -3,9 +3,8 @@ import logging from pycoolmasternet import CoolMasterNet -import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -23,21 +22,11 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -import homeassistant.helpers.config_validation as cv + +from .const import CONF_SUPPORTED_MODES, DOMAIN SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE -DEFAULT_PORT = 10102 - -AVAILABLE_MODES = [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, -] - CM_TO_HA_STATE = { "heat": HVAC_MODE_HEAT, "cool": HVAC_MODE_COOL, @@ -50,17 +39,6 @@ HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()} FAN_MODES = ["low", "med", "high", "auto"] -CONF_SUPPORTED_MODES = "supported_modes" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): vol.All( - cv.ensure_list, [vol.In(AVAILABLE_MODES)] - ), - } -) - _LOGGER = logging.getLogger(__name__) @@ -69,18 +47,17 @@ def _build_entity(device, supported_modes): return CoolmasterClimate(device, supported_modes) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the CoolMasterNet climate platform.""" - - supported_modes = config.get(CONF_SUPPORTED_MODES) - host = config[CONF_HOST] - port = config[CONF_PORT] + 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 = cool.devices() + devices = await hass.async_add_executor_job(cool.devices) all_devices = [_build_entity(device, supported_modes) for device in devices] - add_entities(all_devices, True) + async_add_devices(all_devices, True) class CoolmasterClimate(ClimateDevice): @@ -118,6 +95,16 @@ class CoolmasterClimate(ClimateDevice): else: self._unit = TEMP_FAHRENHEIT + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "CoolAutomation", + "model": "CoolMasterNet", + } + @property def unique_id(self): """Return unique ID for this device.""" diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py new file mode 100644 index 00000000000..543b4c239c8 --- /dev/null +++ b/homeassistant/components/coolmaster/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow to configure Coolmaster.""" + +from pycoolmasternet import CoolMasterNet +import voluptuous as vol + +from homeassistant import core, config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +# pylint: disable=unused-import +from .const import AVAILABLE_MODES, CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN + +MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES} + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) + + +async def validate_connection(hass: core.HomeAssistant, host): + """Validate that we can connect to the Coolmaster instance.""" + cool = CoolMasterNet(host, port=DEFAULT_PORT) + devices = await hass.async_add_executor_job(cool.devices) + return len(devices) > 0 + + +class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Coolmaster config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def _async_get_entry(self, data): + supported_modes = [ + key for (key, value) in data.items() if key in AVAILABLE_MODES and value + ] + return self.async_create_entry( + title=data[CONF_HOST], + data={ + CONF_HOST: data[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + CONF_SUPPORTED_MODES: supported_modes, + }, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = {} + + host = user_input[CONF_HOST] + + try: + result = await validate_connection(self.hass, host) + if not result: + errors["base"] = "no_units" + except (ConnectionRefusedError, TimeoutError): + errors["base"] = "connection_error" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + return self._async_get_entry(user_input) diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py new file mode 100644 index 00000000000..d4cfea73820 --- /dev/null +++ b/homeassistant/components/coolmaster/const.py @@ -0,0 +1,25 @@ +"""Constants for the Coolmaster integration.""" + +from homeassistant.components.climate.const import ( + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +) + +DOMAIN = "coolmaster" + +DEFAULT_PORT = 10102 + +CONF_SUPPORTED_MODES = "supported_modes" + +AVAILABLE_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, +] diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 69ab8ee3c4b..124a1e4a5b9 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,6 +1,7 @@ { "domain": "coolmaster", "name": "Coolmaster", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": [ "pycoolmasternet==0.0.4" diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json new file mode 100644 index 00000000000..d309f8c9c93 --- /dev/null +++ b/homeassistant/components/coolmaster/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "CoolMasterNet", + "step": { + "user": { + "title": "Setup your CoolMasterNet connection details.", + "data": { + "host": "Host", + "off": "Can be turned off", + "heat": "Support heat mode", + "cool": "Support cool mode", + "heat_cool": "Support automatic heat/cool mode", + "dry": "Support dry mode", + "fan_only": "Support fan only mode" + } + } + }, + "error": { + "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.", + "no_units": "Could not find any HVAC units in CoolMasterNet host." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bf63869bc9b..4668528fedb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -14,6 +14,7 @@ FLOWS = [ "axis", "cast", "cert_expiry", + "coolmaster", "daikin", "deconz", "dialogflow", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e61732e632b..2bdd47cd946 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -398,6 +398,9 @@ pybotvac==0.0.17 # homeassistant.components.cast pychromecast==4.0.1 +# homeassistant.components.coolmaster +pycoolmasternet==0.0.4 + # homeassistant.components.daikin pydaikin==1.6.1 diff --git a/tests/components/coolmaster/__init__.py b/tests/components/coolmaster/__init__.py new file mode 100644 index 00000000000..a7e1bf08c99 --- /dev/null +++ b/tests/components/coolmaster/__init__.py @@ -0,0 +1 @@ +"""Tests for the Coolmaster component.""" diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py new file mode 100644 index 00000000000..d49858fcf05 --- /dev/null +++ b/tests/components/coolmaster/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Coolmaster config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES + +# from homeassistant.components.coolmaster.config_flow import validate_connection + +from tests.common import mock_coro + + +def _flow_data(): + options = {"host": "1.1.1.1"} + for mode in AVAILABLE_MODES: + options[mode] = True + return options + + +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} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.coolmaster.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 10102, + "supported_modes": AVAILABLE_MODES, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_timeout(hass): + """Test we handle a connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_connection_refused(hass): + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=ConnectionRefusedError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_no_units(hass): + """Test we handle no units found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(False), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "no_units"}