diff --git a/.coveragerc b/.coveragerc index 714ad039ef0..a92c092e39f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -452,6 +452,7 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py homeassistant/components/mikrotik/device_tracker.py + homeassistant/components/mill/__init__.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/minecraft_server/__init__.py diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 157ea345efd..652153f9a8c 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1 +1,25 @@ """The mill component.""" +import logging + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Mill platform.""" + return True + + +async def async_setup_entry(hass, entry): + """Set up the Mill heater.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_forward_entry_unload( + config_entry, "climate" + ) + return unload_ok diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 814694b0f77..ee7fc8cffb7 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -4,7 +4,7 @@ import logging from mill import Mill import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, TEMP_CELSIUS, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -29,6 +30,7 @@ from .const import ( ATTR_ROOM_NAME, ATTR_SLEEP_TEMP, DOMAIN, + MANUFACTURER, MAX_TEMP, MIN_TEMP, SERVICE_SET_ROOM_TEMP, @@ -38,10 +40,6 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - SET_ROOM_TEMP_SCHEMA = vol.Schema( { vol.Required(ATTR_ROOM_NAME): cv.string, @@ -52,16 +50,15 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Mill heater.""" +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Mill climate.""" mill_data_connection = Mill( - config[CONF_USERNAME], - config[CONF_PASSWORD], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], websession=async_get_clientsession(hass), ) if not await mill_data_connection.connect(): - _LOGGER.error("Failed to connect to Mill") - return + raise ConfigEntryNotReady await mill_data_connection.find_all_heaters() @@ -218,3 +215,19 @@ class MillHeater(ClimateEntity): async def async_update(self): """Retrieve latest state.""" self._heater = await self._conn.update_device(self._heater.device_id) + + @property + def device_id(self): + """Return the ID of the physical device this sensor is part of.""" + return self._heater.device_id + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(DOMAIN, self.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": f"generation {1 if self._heater.is_gen1 else 2}", + } + return device_info diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py new file mode 100644 index 00000000000..08eb0f5c536 --- /dev/null +++ b/homeassistant/components/mill/config_flow.py @@ -0,0 +1,55 @@ +"""Adds config flow for Mill integration.""" +import logging + +from mill import Mill +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Mill integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors={}, + ) + + username = user_input[CONF_USERNAME].replace(" ", "") + password = user_input[CONF_PASSWORD].replace(" ", "") + + mill_data_connection = Mill( + username, password, websession=async_get_clientsession(self.hass), + ) + + errors = {} + + if not await mill_data_connection.connect(): + errors["connection_error"] = "connection_error" + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors, + ) + + unique_id = username + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=unique_id, data={CONF_USERNAME: username, CONF_PASSWORD: password}, + ) diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py index 65c67b72b6e..b0ba7065e0a 100644 --- a/homeassistant/components/mill/const.py +++ b/homeassistant/components/mill/const.py @@ -4,6 +4,7 @@ ATTR_AWAY_TEMP = "away_temp" ATTR_COMFORT_TEMP = "comfort_temp" ATTR_ROOM_NAME = "room_name" ATTR_SLEEP_TEMP = "sleep_temp" +MANUFACTURER = "Mill" MAX_TEMP = 35 MIN_TEMP = 5 DOMAIN = "mill" diff --git a/homeassistant/components/mill/strings.json b/homeassistant/components/mill/strings.json new file mode 100644 index 00000000000..f9126c30f78 --- /dev/null +++ b/homeassistant/components/mill/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + } + } +} diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e5cee7be57..45ea03adab1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,6 +373,9 @@ meteofrance==0.3.7 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.mill +millheater==0.3.4 + # homeassistant.components.minio minio==4.0.9 diff --git a/tests/components/mill/__init__.py b/tests/components/mill/__init__.py new file mode 100644 index 00000000000..4bbe1b4fb08 --- /dev/null +++ b/tests/components/mill/__init__.py @@ -0,0 +1 @@ +"""Tests for Mill.""" diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py new file mode 100644 index 00000000000..54b8dbc9b59 --- /dev/null +++ b/tests/components/mill/test_config_flow.py @@ -0,0 +1,86 @@ +"""Tests for Mill config flow.""" +import pytest + +from homeassistant.components.mill.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mill_setup", autouse=True) +def mill_setup_fixture(): + """Patch mill setup entry.""" + with patch("homeassistant.components.mill.async_setup_entry", return_value=True): + yield + + +async def test_show_config_form(hass): + """Test show configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + +async def test_create_entry(hass): + """Test create entry from user input.""" + test_data = { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + } + + with patch("mill.Mill.connect", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "create_entry" + assert result["title"] == test_data[CONF_USERNAME] + assert result["data"] == test_data + + +async def test_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists.""" + + test_data = { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + } + + first_entry = MockConfigEntry( + domain="mill", data=test_data, unique_id=test_data[CONF_USERNAME], + ) + first_entry.add_to_hass(hass) + + with patch("mill.Mill.connect", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass): + """Test connection error.""" + + test_data = { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + } + + first_entry = MockConfigEntry( + domain="mill", data=test_data, unique_id=test_data[CONF_USERNAME], + ) + first_entry.add_to_hass(hass) + + with patch("mill.Mill.connect", return_value=False): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "form" + assert result["errors"]["connection_error"] == "connection_error"