Compare commits

...
Sign in to create a new pull request.

5 commits
dev ... mill

Author SHA1 Message Date
Daniel Hjelseth Høyer
3ea6d3b139
Merge branch 'dev' into mill 2024-11-13 18:39:20 +01:00
Daniel Hjelseth Høyer
29c68f3615 mill
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2024-11-13 18:37:24 +01:00
Daniel Hjelseth Høyer
182f8b33f7 tests
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2024-11-12 07:54:59 +01:00
Daniel Hjelseth Høyer
a40eeefc8c
typo 2024-11-12 06:42:26 +01:00
Daniel Hjelseth Høyer
e07ca1a1bc Mill, new features
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2024-11-09 13:35:16 +01:00
10 changed files with 227 additions and 22 deletions

View file

@ -14,6 +14,7 @@ from homeassistant.components.climate import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONF_IP_ADDRESS, CONF_IP_ADDRESS,
CONF_USERNAME, CONF_USERNAME,
@ -23,12 +24,16 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import (
ATTR_AWAY_TEMP, ATTR_AWAY_TEMP,
ATTR_COMFORT_TEMP, ATTR_COMFORT_TEMP,
ATTR_MAX_HEATING_POWER,
ATTR_ROOM_NAME, ATTR_ROOM_NAME,
ATTR_SLEEP_TEMP, ATTR_SLEEP_TEMP,
CLOUD, CLOUD,
@ -38,6 +43,7 @@ from .const import (
MANUFACTURER, MANUFACTURER,
MAX_TEMP, MAX_TEMP,
MIN_TEMP, MIN_TEMP,
SERVICE_MAX_HEATING_POWER,
SERVICE_SET_ROOM_TEMP, SERVICE_SET_ROOM_TEMP,
) )
from .coordinator import MillDataUpdateCoordinator from .coordinator import MillDataUpdateCoordinator
@ -84,6 +90,21 @@ async def async_setup_entry(
DOMAIN, SERVICE_SET_ROOM_TEMP, set_room_temp, schema=SET_ROOM_TEMP_SCHEMA DOMAIN, SERVICE_SET_ROOM_TEMP, set_room_temp, schema=SET_ROOM_TEMP_SCHEMA
) )
async def max_heating_power(entity: MillHeater, service: ServiceCall) -> None:
"""Limit heating power."""
await mill_data_coordinator.mill_data_connection.max_heating_power(
entity.heater_id, service.data[ATTR_MAX_HEATING_POWER]
)
async_get_current_platform().async_register_entity_service(
SERVICE_MAX_HEATING_POWER,
schema={
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_MAX_HEATING_POWER): vol.Range(min=0, max=2000),
},
func=max_heating_power,
)
class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity): class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
"""Representation of a Mill Thermostat device.""" """Representation of a Mill Thermostat device."""
@ -111,7 +132,7 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
self._available = False self._available = False
self._id = heater.device_id self.heater_id = heater.device_id
self._attr_unique_id = heater.device_id self._attr_unique_id = heater.device_id
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, heater.device_id)}, identifiers={(DOMAIN, heater.device_id)},
@ -127,7 +148,10 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return return
await self.coordinator.mill_data_connection.set_heater_temp( await self.coordinator.mill_data_connection.set_heater_temp(
self._id, float(temperature) self.heater_id, float(temperature)
)
await self.coordinator.mill_data_connection.fetch_historic_energy_usage(
self.heater_id
) )
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
@ -135,12 +159,12 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
"""Set new target hvac mode.""" """Set new target hvac mode."""
if hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.HEAT:
await self.coordinator.mill_data_connection.heater_control( await self.coordinator.mill_data_connection.heater_control(
self._id, power_status=True self.heater_id, power_status=True
) )
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
elif hvac_mode == HVACMode.OFF: elif hvac_mode == HVACMode.OFF:
await self.coordinator.mill_data_connection.heater_control( await self.coordinator.mill_data_connection.heater_control(
self._id, power_status=False self.heater_id, power_status=False
) )
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
@ -152,7 +176,7 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
self._update_attr(self.coordinator.data[self._id]) self._update_attr(self.coordinator.data[self.heater_id])
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback

View file

@ -2,6 +2,7 @@
ATTR_AWAY_TEMP = "away_temp" ATTR_AWAY_TEMP = "away_temp"
ATTR_COMFORT_TEMP = "comfort_temp" ATTR_COMFORT_TEMP = "comfort_temp"
ATTR_MAX_HEATING_POWER = "max_heating_power"
ATTR_ROOM_NAME = "room_name" ATTR_ROOM_NAME = "room_name"
ATTR_SLEEP_TEMP = "sleep_temp" ATTR_SLEEP_TEMP = "sleep_temp"
BATTERY = "battery" BATTERY = "battery"
@ -9,6 +10,8 @@ CLOUD = "Cloud"
CONNECTION_TYPE = "connection_type" CONNECTION_TYPE = "connection_type"
CONSUMPTION_TODAY = "day_consumption" CONSUMPTION_TODAY = "day_consumption"
CONSUMPTION_YEAR = "year_consumption" CONSUMPTION_YEAR = "year_consumption"
CONTROL_SIGNAL = "control_signal"
CURRENT_POWER = "current_power"
DOMAIN = "mill" DOMAIN = "mill"
ECO2 = "eco2" ECO2 = "eco2"
HUMIDITY = "humidity" HUMIDITY = "humidity"
@ -17,5 +20,6 @@ MANUFACTURER = "Mill"
MAX_TEMP = 35 MAX_TEMP = 35
MIN_TEMP = 5 MIN_TEMP = 5
SERVICE_SET_ROOM_TEMP = "set_room_temperature" SERVICE_SET_ROOM_TEMP = "set_room_temperature"
SERVICE_MAX_HEATING_POWER = "max_heating_power"
TEMPERATURE = "current_temp" TEMPERATURE = "current_temp"
TVOC = "tvoc" TVOC = "tvoc"

View file

@ -4,12 +4,22 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import cast
from mill import Mill from mill import Heater, Mill
from mill_local import Mill as MillLocal from mill_local import Mill as MillLocal
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
statistics_during_period,
)
from homeassistant.const import UnitOfEnergy
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util, slugify
from .const import DOMAIN from .const import DOMAIN
@ -28,11 +38,98 @@ class MillDataUpdateCoordinator(DataUpdateCoordinator):
) -> None: ) -> None:
"""Initialize global Mill data updater.""" """Initialize global Mill data updater."""
self.mill_data_connection = mill_data_connection self.mill_data_connection = mill_data_connection
self._last_stats_time = dt_util.utcnow() - timedelta(days=1)
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
name=DOMAIN, name=DOMAIN,
update_method=mill_data_connection.fetch_heater_and_sensor_data,
update_interval=update_interval, update_interval=update_interval,
) )
async def _async_update_data(self) -> dict:
"""Update data via API."""
data = await self.mill_data_connection.fetch_heater_and_sensor_data()
if isinstance(self.mill_data_connection, Mill):
await self._insert_statistics()
return data
async def _insert_statistics(self) -> None:
"""Insert Mill statistics."""
now = dt_util.utcnow()
if self._last_stats_time > now - timedelta(hours=1):
return
for dev_id, heater in self.mill_data_connection.devices.items():
if not isinstance(heater, Heater):
continue
statistic_id = f"{DOMAIN}:energy_{slugify(dev_id)}"
last_stats = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, statistic_id, True, set()
)
if not last_stats or not last_stats.get(statistic_id):
hourly_data = (
await self.mill_data_connection.fetch_historic_energy_usage(dev_id)
)
hourly_data = dict(sorted(hourly_data.items(), key=lambda x: x[0]))
_sum = 0.0
last_stats_time = None
else:
hourly_data = (
await self.mill_data_connection.fetch_historic_energy_usage(
dev_id,
n_days=(
now
- dt_util.utc_from_timestamp(
last_stats[statistic_id][0]["start"]
)
).days
+ 2,
)
)
if not hourly_data:
return
hourly_data = dict(sorted(hourly_data.items(), key=lambda x: x[0]))
start_time = next(iter(hourly_data))
stats = await get_instance(self.hass).async_add_executor_job(
statistics_during_period,
self.hass,
start_time,
None,
{statistic_id},
"hour",
None,
{"sum", "state"},
)
stat = stats[statistic_id][0]
_sum = cast(float, stat["sum"])
last_stats_time = dt_util.utc_from_timestamp(stat["start"])
statistics = []
for start, state in hourly_data.items():
if state is None:
continue
if last_stats_time and (start < last_stats_time or start > now):
continue
_sum += state
statistics.append(
StatisticData(
start=start,
state=state,
sum=_sum,
)
)
metadata = StatisticMetaData(
has_mean=False,
has_sum=True,
name=f"{heater.name}",
source=DOMAIN,
statistic_id=statistic_id,
unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
)
async_add_external_statistics(self.hass, metadata, statistics)
self._last_stats_time = now.replace(minute=0, second=0)

View file

@ -2,6 +2,9 @@
"services": { "services": {
"set_room_temperature": { "set_room_temperature": {
"service": "mdi:thermometer" "service": "mdi:thermometer"
},
"max_heating_power": {
"service": "mdi:power"
} }
} }
} }

View file

@ -3,6 +3,7 @@
"name": "Mill", "name": "Mill",
"codeowners": ["@danielhiversen"], "codeowners": ["@danielhiversen"],
"config_flow": true, "config_flow": true,
"dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/mill", "documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["mill", "mill_local"], "loggers": ["mill", "mill_local"],

View file

@ -33,6 +33,8 @@ from .const import (
CONNECTION_TYPE, CONNECTION_TYPE,
CONSUMPTION_TODAY, CONSUMPTION_TODAY,
CONSUMPTION_YEAR, CONSUMPTION_YEAR,
CONTROL_SIGNAL,
CURRENT_POWER,
DOMAIN, DOMAIN,
ECO2, ECO2,
HUMIDITY, HUMIDITY,
@ -57,6 +59,19 @@ HEATER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
SensorEntityDescription(
key=CURRENT_POWER,
translation_key="current_power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=CONTROL_SIGNAL,
translation_key="control_signal",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
) )
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
@ -94,6 +109,16 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
), ),
) )
SOCKET_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=HUMIDITY,
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
*HEATER_SENSOR_TYPES,
)
LOCAL_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( LOCAL_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="control_signal", key="control_signal",
@ -145,7 +170,9 @@ async def async_setup_entry(
) )
for mill_device in mill_data_coordinator.data.values() for mill_device in mill_data_coordinator.data.values()
for entity_description in ( for entity_description in (
HEATER_SENSOR_TYPES SOCKET_SENSOR_TYPES
if isinstance(mill_device, mill.Socket)
else HEATER_SENSOR_TYPES
if isinstance(mill_device, mill.Heater) if isinstance(mill_device, mill.Heater)
else SENSOR_TYPES else SENSOR_TYPES
) )

View file

@ -23,3 +23,20 @@ set_room_temperature:
min: 0 min: 0
max: 100 max: 100
unit_of_measurement: "°" unit_of_measurement: "°"
max_heating_power:
fields:
entity_id:
required: true
example: "climate.house"
selector:
entity:
integration: mill
domain: climate
max_heating_power:
required: true
selector:
number:
min: 0
max: 2000
unit_of_measurement: "W"

View file

@ -53,6 +53,20 @@
} }
}, },
"services": { "services": {
"max_heating_power": {
"description": "Sets max power heater can use.",
"fields": {
"entity_id": {
"description": "Entity id of heater to change.",
"name": "Entity id"
},
"max_heating_power": {
"description": "Max heating power.",
"name": "Max heating power"
}
},
"name": "Set max heating power"
},
"set_room_temperature": { "set_room_temperature": {
"name": "Set room temperature", "name": "Set room temperature",
"description": "Sets Mill room temperatures.", "description": "Sets Mill room temperatures.",

View file

@ -4,6 +4,7 @@ from unittest.mock import patch
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.mill.const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL from homeassistant.components.mill.const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL
from homeassistant.components.recorder import Recorder
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -11,7 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_show_config_form(hass: HomeAssistant) -> None: async def test_show_config_form(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test show configuration form.""" """Test show configuration form."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -21,7 +22,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None:
assert result["step_id"] == "user" assert result["step_id"] == "user"
async def test_create_entry(hass: HomeAssistant) -> None: async def test_create_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test create entry from user input.""" """Test create entry from user input."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -56,7 +57,9 @@ async def test_create_entry(hass: HomeAssistant) -> None:
} }
async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: async def test_flow_entry_already_exists(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test user input for config_entry that already exists.""" """Test user input for config_entry that already exists."""
test_data = { test_data = {
@ -96,7 +99,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_connection_error(hass: HomeAssistant) -> None: async def test_connection_error(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test connection error.""" """Test connection error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -125,7 +128,7 @@ async def test_connection_error(hass: HomeAssistant) -> None:
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}
async def test_local_create_entry(hass: HomeAssistant) -> None: async def test_local_create_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test create entry from user input.""" """Test create entry from user input."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -165,7 +168,9 @@ async def test_local_create_entry(hass: HomeAssistant) -> None:
assert result["data"] == test_data assert result["data"] == test_data
async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: async def test_local_flow_entry_already_exists(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test user input for config_entry that already exists.""" """Test user input for config_entry that already exists."""
test_data = { test_data = {
@ -215,7 +220,9 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None:
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_local_connection_error(hass: HomeAssistant) -> None: async def test_local_connection_error(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test connection error.""" """Test connection error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View file

@ -4,6 +4,7 @@ import asyncio
from unittest.mock import patch from unittest.mock import patch
from homeassistant.components import mill from homeassistant.components import mill
from homeassistant.components.recorder import Recorder
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -11,7 +12,9 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_setup_with_cloud_config(hass: HomeAssistant) -> None: async def test_setup_with_cloud_config(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test setup of cloud config.""" """Test setup of cloud config."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=mill.DOMAIN, domain=mill.DOMAIN,
@ -31,7 +34,9 @@ async def test_setup_with_cloud_config(hass: HomeAssistant) -> None:
assert len(mock_connect.mock_calls) == 1 assert len(mock_connect.mock_calls) == 1
async def test_setup_with_cloud_config_fails(hass: HomeAssistant) -> None: async def test_setup_with_cloud_config_fails(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test setup of cloud config.""" """Test setup of cloud config."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=mill.DOMAIN, domain=mill.DOMAIN,
@ -47,7 +52,9 @@ async def test_setup_with_cloud_config_fails(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.SETUP_RETRY assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_with_cloud_config_times_out(hass: HomeAssistant) -> None: async def test_setup_with_cloud_config_times_out(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test setup of cloud config will retry if timed out.""" """Test setup of cloud config will retry if timed out."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=mill.DOMAIN, domain=mill.DOMAIN,
@ -63,7 +70,9 @@ async def test_setup_with_cloud_config_times_out(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.SETUP_RETRY assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_with_old_cloud_config(hass: HomeAssistant) -> None: async def test_setup_with_old_cloud_config(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test setup of old cloud config.""" """Test setup of old cloud config."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=mill.DOMAIN, domain=mill.DOMAIN,
@ -82,7 +91,9 @@ async def test_setup_with_old_cloud_config(hass: HomeAssistant) -> None:
assert len(mock_connect.mock_calls) == 1 assert len(mock_connect.mock_calls) == 1
async def test_setup_with_local_config(hass: HomeAssistant) -> None: async def test_setup_with_local_config(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test setup of local config.""" """Test setup of local config."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=mill.DOMAIN, domain=mill.DOMAIN,
@ -119,7 +130,7 @@ async def test_setup_with_local_config(hass: HomeAssistant) -> None:
assert len(mock_connect.mock_calls) == 1 assert len(mock_connect.mock_calls) == 1
async def test_unload_entry(hass: HomeAssistant) -> None: async def test_unload_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test removing mill client.""" """Test removing mill client."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=mill.DOMAIN, domain=mill.DOMAIN,