diff --git a/.coveragerc b/.coveragerc index 1adede05ca4..e1ef57b9817 100644 --- a/.coveragerc +++ b/.coveragerc @@ -641,7 +641,6 @@ 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/mill/sensor.py diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 73480563c29..c087fe0d853 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,16 +1,19 @@ """The mill component.""" +from __future__ import annotations + from datetime import timedelta import logging from mill import Mill +from mill_local import Mill as MillLocal -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL _LOGGER = logging.getLogger(__name__) @@ -23,8 +26,9 @@ class MillDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, + update_interval: timedelta | None = None, *, - mill_data_connection: Mill, + mill_data_connection: Mill | MillLocal, ) -> None: """Initialize global Mill data updater.""" self.mill_data_connection = mill_data_connection @@ -34,26 +38,42 @@ class MillDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER, name=DOMAIN, update_method=mill_data_connection.fetch_heater_and_sensor_data, - update_interval=timedelta(seconds=30), + update_interval=update_interval, ) async def async_setup_entry(hass, entry): """Set up the Mill heater.""" - mill_data_connection = Mill( - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - websession=async_get_clientsession(hass), - ) + hass.data.setdefault(DOMAIN, {LOCAL: {}, CLOUD: {}}) + + if entry.data.get(CONNECTION_TYPE) == LOCAL: + mill_data_connection = MillLocal( + entry.data[CONF_IP_ADDRESS], + websession=async_get_clientsession(hass), + ) + update_interval = timedelta(seconds=15) + key = entry.data[CONF_IP_ADDRESS] + conn_type = LOCAL + else: + mill_data_connection = Mill( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + websession=async_get_clientsession(hass), + ) + update_interval = timedelta(seconds=30) + key = entry.data[CONF_USERNAME] + conn_type = CLOUD + if not await mill_data_connection.connect(): raise ConfigEntryNotReady - - hass.data[DOMAIN] = MillDataUpdateCoordinator( + data_coordinator = MillDataUpdateCoordinator( hass, mill_data_connection=mill_data_connection, + update_interval=update_interval, ) - await hass.data[DOMAIN].async_config_entry_first_refresh() + hass.data[DOMAIN][conn_type][key] = data_coordinator + await data_coordinator.async_config_entry_first_refresh() hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index ad50bf437bb..80611cf9f96 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -12,7 +12,13 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_IP_ADDRESS, + CONF_USERNAME, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -23,15 +29,16 @@ from .const import ( ATTR_COMFORT_TEMP, ATTR_ROOM_NAME, ATTR_SLEEP_TEMP, + CLOUD, + CONNECTION_TYPE, DOMAIN, + LOCAL, MANUFACTURER, MAX_TEMP, MIN_TEMP, SERVICE_SET_ROOM_TEMP, ) -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - SET_ROOM_TEMP_SCHEMA = vol.Schema( { vol.Required(ATTR_ROOM_NAME): cv.string, @@ -44,8 +51,12 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill climate.""" + if entry.data.get(CONNECTION_TYPE) == LOCAL: + mill_data_coordinator = hass.data[DOMAIN][LOCAL][entry.data[CONF_IP_ADDRESS]] + async_add_entities([LocalMillHeater(mill_data_coordinator)]) + return - mill_data_coordinator = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]] entities = [ MillHeater(mill_data_coordinator, mill_device) @@ -75,7 +86,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP - _attr_supported_features = SUPPORT_FLAGS + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS @@ -169,3 +180,47 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._attr_hvac_mode = HVAC_MODE_HEAT else: self._attr_hvac_mode = HVAC_MODE_OFF + + +class LocalMillHeater(CoordinatorEntity, ClimateEntity): + """Representation of a Mill Thermostat device.""" + + _attr_hvac_mode = HVAC_MODE_HEAT + _attr_hvac_modes = [HVAC_MODE_HEAT] + _attr_max_temp = MAX_TEMP + _attr_min_temp = MIN_TEMP + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_target_temperature_step = PRECISION_WHOLE + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, coordinator): + """Initialize the thermostat.""" + super().__init__(coordinator) + self._attr_name = coordinator.mill_data_connection.name + self._update_attr() + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return + await self.coordinator.mill_data_connection.set_target_temperature( + int(temperature) + ) + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + self.async_write_ha_state() + + @callback + def _update_attr(self) -> None: + data = self.coordinator.data + self._attr_target_temperature = data["set_temperature"] + self._attr_current_temperature = data["ambient_temperature"] + + if data["current_power"] > 0: + self._attr_hvac_action = CURRENT_HVAC_HEAT + else: + self._attr_hvac_action = CURRENT_HVAC_IDLE diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py index 7970e2946f2..9f7dd5d5cdb 100644 --- a/homeassistant/components/mill/config_flow.py +++ b/homeassistant/components/mill/config_flow.py @@ -1,16 +1,13 @@ """Adds config flow for Mill integration.""" from mill import Mill +from mill_local import Mill as MillLocal import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN - -DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} -) +from .const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -20,10 +17,68 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" + data_schema = vol.Schema( + { + vol.Required(CONNECTION_TYPE, default=CLOUD): vol.In( + ( + CLOUD, + LOCAL, + ) + ) + } + ) + if user_input is None: return self.async_show_form( step_id="user", - data_schema=DATA_SCHEMA, + data_schema=data_schema, + ) + + if user_input[CONNECTION_TYPE] == LOCAL: + return await self.async_step_local() + return await self.async_step_cloud() + + async def async_step_local(self, user_input=None): + """Handle the local step.""" + data_schema = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) + if user_input is None: + return self.async_show_form( + step_id="local", + data_schema=data_schema, + ) + + mill_data_connection = MillLocal( + user_input[CONF_IP_ADDRESS], + websession=async_get_clientsession(self.hass), + ) + + await self.async_set_unique_id(mill_data_connection.device_ip) + self._abort_if_unique_id_configured() + + if not await mill_data_connection.connect(): + return self.async_show_form( + step_id="local", + data_schema=data_schema, + errors={"base": "cannot_connect"}, + ) + + return self.async_create_entry( + title=user_input[CONF_IP_ADDRESS], + data={ + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONNECTION_TYPE: LOCAL, + }, + ) + + async def async_step_cloud(self, user_input=None): + """Handle the cloud step.""" + data_schema = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ) + if user_input is None: + return self.async_show_form( + step_id="cloud", + data_schema=data_schema, errors={}, ) @@ -39,10 +94,10 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if not await mill_data_connection.connect(): - errors["cannot_connect"] = "cannot_connect" + errors["base"] = "cannot_connect" return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, + step_id="cloud", + data_schema=data_schema, errors=errors, ) @@ -53,5 +108,9 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=unique_id, - data={CONF_USERNAME: username, CONF_PASSWORD: password}, + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONNECTION_TYPE: CLOUD, + }, ) diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py index 25a57117a65..c42747920bf 100644 --- a/homeassistant/components/mill/const.py +++ b/homeassistant/components/mill/const.py @@ -5,11 +5,14 @@ ATTR_COMFORT_TEMP = "comfort_temp" ATTR_ROOM_NAME = "room_name" ATTR_SLEEP_TEMP = "sleep_temp" BATTERY = "battery" +CLOUD = "Cloud" +CONNECTION_TYPE = "connection_type" CONSUMPTION_TODAY = "day_consumption" CONSUMPTION_YEAR = "year_consumption" DOMAIN = "mill" ECO2 = "eco2" HUMIDITY = "humidity" +LOCAL = "Local" MANUFACTURER = "Mill" MAX_TEMP = 35 MIN_TEMP = 5 diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 7347cf16daa..a2507251524 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,8 +2,8 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.8.0"], + "requirements": ["millheater==0.8.0", "mill-local==0.1.0"], "codeowners": ["@danielhiversen"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "local_polling" } diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 1b9e84eafa8..64db26c371c 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + CONF_USERNAME, ENERGY_KILO_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, @@ -28,11 +29,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( BATTERY, + CLOUD, + CONNECTION_TYPE, CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, ECO2, HUMIDITY, + LOCAL, MANUFACTURER, TEMPERATURE, TVOC, @@ -95,8 +99,10 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill sensor.""" + if entry.data.get(CONNECTION_TYPE) == LOCAL: + return - mill_data_coordinator = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]] entities = [ MillSensor( diff --git a/homeassistant/components/mill/strings.json b/homeassistant/components/mill/strings.json index ab09f3f59b6..5f4cec1336e 100644 --- a/homeassistant/components/mill/strings.json +++ b/homeassistant/components/mill/strings.json @@ -8,10 +8,22 @@ }, "step": { "user": { + "data": { + "connection_type": "Select connection type" + }, + "description": "Select connection type. Local requires generation 3 heaters" + }, + "cloud": { "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "local": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + }, + "description": "Local IP address of the device." } } } diff --git a/homeassistant/components/mill/translations/en.json b/homeassistant/components/mill/translations/en.json index bb7d67f03b4..ee66706832e 100644 --- a/homeassistant/components/mill/translations/en.json +++ b/homeassistant/components/mill/translations/en.json @@ -7,11 +7,23 @@ "cannot_connect": "Failed to connect" }, "step": { - "user": { + "cloud": { "data": { "password": "Password", "username": "Username" } + }, + "local": { + "data": { + "ip_address": "IP Address" + }, + "description": "Local IP address of the device." + }, + "user": { + "data": { + "connection_type": "Select connection type" + }, + "description": "Select connection type. Local requires generation 3 heaters" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 24162ca05bb..28a79154886 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1010,6 +1010,9 @@ micloud==0.4 # homeassistant.components.miflora miflora==0.7.0 +# homeassistant.components.mill +mill-local==0.1.0 + # homeassistant.components.mill millheater==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0b4a99dfdf..c65cf8fcc7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -611,6 +611,9 @@ mficlient==0.3.0 # homeassistant.components.xiaomi_miio micloud==0.4 +# homeassistant.components.mill +mill-local==0.1.0 + # homeassistant.components.mill millheater==0.8.0 diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ce35b3d9708..ff2f7393c82 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -1,47 +1,57 @@ """Tests for Mill config flow.""" from unittest.mock import patch -import pytest - from homeassistant import config_entries -from homeassistant.components.mill.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.mill.const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import RESULT_TYPE_FORM 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": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == 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", - } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM with patch("mill.Mill.connect", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + }, ) + await hass.async_block_till_done() assert result["type"] == "create_entry" - assert result["title"] == test_data[CONF_USERNAME] - assert result["data"] == test_data + assert result["title"] == "user" + assert result["data"] == { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + CONNECTION_TYPE: CLOUD, + } async def test_flow_entry_already_exists(hass): @@ -59,10 +69,26 @@ async def test_flow_entry_already_exists(hass): ) first_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + with patch("mill.Mill.connect", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -70,23 +96,152 @@ async def test_flow_entry_already_exists(hass): async def test_connection_error(hass): """Test connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + with patch("mill.Mill.connect", return_value=False): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_local_create_entry(hass): + """Test create entry from user input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM test_data = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pswd", + CONF_IP_ADDRESS: "192.168.1.59", + } + + with patch( + "mill_local.Mill.connect", + return_value={ + "name": "panel heater gen. 3", + "version": "0x210927", + "operation_key": "", + "status": "ok", + }, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + test_data[CONNECTION_TYPE] = LOCAL + assert result["type"] == "create_entry" + assert result["title"] == test_data[CONF_IP_ADDRESS] + assert result["data"] == test_data + + +async def test_local_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists.""" + + test_data = { + CONF_IP_ADDRESS: "192.168.1.59", } first_entry = MockConfigEntry( domain="mill", data=test_data, - unique_id=test_data[CONF_USERNAME], + unique_id=test_data[CONF_IP_ADDRESS], ) 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": config_entries.SOURCE_USER}, data=test_data + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + CONF_IP_ADDRESS: "192.168.1.59", + } + + with patch( + "mill_local.Mill.connect", + return_value={ + "name": "panel heater gen. 3", + "version": "0x210927", + "operation_key": "", + "status": "ok", + }, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, ) - assert result["type"] == "form" - assert result["errors"]["cannot_connect"] == "cannot_connect" + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_local_connection_error(hass): + """Test connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + CONF_IP_ADDRESS: "192.168.1.59", + } + + with patch( + "mill_local.Mill.connect", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/mill/test_init.py b/tests/components/mill/test_init.py new file mode 100644 index 00000000000..f92b4689ebf --- /dev/null +++ b/tests/components/mill/test_init.py @@ -0,0 +1,121 @@ +"""Tests for Mill init.""" + +from unittest.mock import patch + +from homeassistant.components import mill +from homeassistant.config_entries import ConfigEntryState +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_coro + + +async def test_setup_with_cloud_config(hass): + """Test setup of cloud config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + mill.CONNECTION_TYPE: mill.CLOUD, + }, + ) + entry.add_to_hass(hass) + with patch( + "mill.Mill.fetch_heater_and_sensor_data", return_value={} + ) as mock_fetch, patch("mill.Mill.connect", return_value=True) as mock_connect: + assert await async_setup_component(hass, "mill", entry) + assert len(mock_fetch.mock_calls) == 1 + assert len(mock_connect.mock_calls) == 1 + + +async def test_setup_with_cloud_config_fails(hass): + """Test setup of cloud config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + mill.CONNECTION_TYPE: mill.CLOUD, + }, + ) + entry.add_to_hass(hass) + with patch("mill.Mill.connect", return_value=False): + assert await async_setup_component(hass, "mill", entry) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_with_old_cloud_config(hass): + """Test setup of old cloud config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + }, + ) + entry.add_to_hass(hass) + with patch("mill.Mill.fetch_heater_and_sensor_data", return_value={}), patch( + "mill.Mill.connect", return_value=True + ) as mock_connect: + assert await async_setup_component(hass, "mill", entry) + + assert len(mock_connect.mock_calls) == 1 + + +async def test_setup_with_local_config(hass): + """Test setup of local config.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_IP_ADDRESS: "192.168.1.59", + mill.CONNECTION_TYPE: mill.LOCAL, + }, + ) + entry.add_to_hass(hass) + with patch( + "mill_local.Mill.fetch_heater_and_sensor_data", + return_value={ + "ambient_temperature": 20, + "set_temperature": 22, + "current_power": 0, + }, + ) as mock_fetch, patch( + "mill_local.Mill.connect", + return_value={ + "name": "panel heater gen. 3", + "version": "0x210927", + "operation_key": "", + "status": "ok", + }, + ) as mock_connect: + assert await async_setup_component(hass, "mill", entry) + + assert len(mock_fetch.mock_calls) == 1 + assert len(mock_connect.mock_calls) == 1 + + +async def test_unload_entry(hass): + """Test removing mill client.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + mill.CONNECTION_TYPE: mill.CLOUD, + }, + ) + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as unload_entry, patch( + "mill.Mill.fetch_heater_and_sensor_data", return_value={} + ), patch( + "mill.Mill.connect", return_value=True + ): + assert await async_setup_component(hass, "mill", entry) + + assert await hass.config_entries.async_unload(entry.entry_id) + + assert unload_entry.call_count == 2 + assert entry.entry_id not in hass.data[mill.DOMAIN]