Stop exposing sleep timers and adaptive learning on Modern Forms Gen4 fans (#180525)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brian Towles
2026-08-30 18:22:25 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent df4c7f0169
commit f5aaa35355
11 changed files with 157 additions and 26 deletions
@@ -20,16 +20,19 @@ async def async_setup_entry(
"""Set up Modern Forms binary sensors."""
coordinator = entry.runtime_data
binary_sensors: list[ModernFormsBinarySensor] = [
ModernFormsFanSleepTimerActive(entry.entry_id, coordinator),
]
binary_sensors: list[ModernFormsBinarySensor] = []
# Only setup light sleep timer sensor if light unit installed
if coordinator.data.info.light_type:
if coordinator.data.has_sleep_timer():
binary_sensors.append(
ModernFormsLightSleepTimerActive(entry.entry_id, coordinator)
ModernFormsFanSleepTimerActive(entry.entry_id, coordinator)
)
# Only setup light sleep timer sensor if light unit installed
if coordinator.data.info.light_type:
binary_sensors.append(
ModernFormsLightSleepTimerActive(entry.entry_id, coordinator)
)
async_add_entities(binary_sensors)
@@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
@@ -19,6 +20,7 @@ from . import modernforms_exception_handler
from .const import (
ATTR_SLEEP_TIME,
CLEAR_TIMER,
DOMAIN,
OPT_ON,
OPT_SPEED,
OPT_WIND,
@@ -186,6 +188,11 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
sleep_time: int,
) -> None:
"""Set a Modern Forms light sleep timer."""
if not self.coordinator.data.has_sleep_timer():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sleep_timer_not_supported",
)
await self.coordinator.modern_forms.fan(sleep=sleep_time * 60)
@modernforms_exception_handler
@@ -193,4 +200,9 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
self,
) -> None:
"""Clear a Modern Forms fan sleep timer."""
if not self.coordinator.data.has_sleep_timer():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sleep_timer_not_supported",
)
await self.coordinator.modern_forms.fan(sleep=CLEAR_TIMER)
@@ -8,6 +8,7 @@ import voluptuous as vol
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
@@ -19,6 +20,7 @@ from . import modernforms_exception_handler
from .const import (
ATTR_SLEEP_TIME,
CLEAR_TIMER,
DOMAIN,
OPT_BRIGHTNESS,
OPT_ON,
SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
@@ -159,6 +161,11 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
sleep_time: int,
) -> None:
"""Set a Modern Forms light sleep timer."""
if not self.coordinator.data.has_sleep_timer():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sleep_timer_not_supported",
)
await self._async_control_light(sleep=sleep_time * 60)
@modernforms_exception_handler
@@ -166,6 +173,11 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
self,
) -> None:
"""Clear a Modern Forms light sleep timer."""
if not self.coordinator.data.has_sleep_timer():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sleep_timer_not_supported",
)
await self._async_control_light(sleep=CLEAR_TIMER)
async def _async_control_light(self, **kwargs: Any) -> None:
@@ -22,16 +22,19 @@ async def async_setup_entry(
"""Set up Modern Forms sensor based on a config entry."""
coordinator = entry.runtime_data
sensors: list[ModernFormsSensor] = [
ModernFormsFanTimerRemainingTimeSensor(entry.entry_id, coordinator),
]
sensors: list[ModernFormsSensor] = []
# Only setup light sleep timer sensor if light unit installed
if coordinator.data.info.light_type:
if coordinator.data.has_sleep_timer():
sensors.append(
ModernFormsLightTimerRemainingTimeSensor(entry.entry_id, coordinator)
ModernFormsFanTimerRemainingTimeSensor(entry.entry_id, coordinator)
)
# Only setup light sleep timer sensor if light unit installed
if coordinator.data.info.light_type:
sensors.append(
ModernFormsLightTimerRemainingTimeSensor(entry.entry_id, coordinator)
)
async_add_entities(sensors)
@@ -71,6 +71,9 @@
},
"invalid_response": {
"message": "Invalid response from the Modern Forms device"
},
"sleep_timer_not_supported": {
"message": "This fan does not support sleep timers"
}
},
"services": {
@@ -19,10 +19,13 @@ async def async_setup_entry(
"""Set up Modern Forms switch based on a config entry."""
coordinator = entry.runtime_data
switches = [
switches: list[ModernFormsSwitch] = [
ModernFormsAwaySwitch(entry.entry_id, coordinator),
ModernFormsAdaptiveLearningSwitch(entry.entry_id, coordinator),
]
if coordinator.data.has_adaptive_learning():
switches.append(ModernFormsAdaptiveLearningSwitch(entry.entry_id, coordinator))
async_add_entities(switches)
@@ -5,7 +5,7 @@ from homeassistant.components.modern_forms.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
from . import init_integration, init_integration_gen4
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -43,3 +43,26 @@ async def test_binary_sensors(
state = hass.states.get("binary_sensor.modernformsfan_fan_sleep_timer_active")
assert state
assert state.state == "off"
async def test_no_sleep_timer_binary_sensors_on_gen4(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test the sleep-timer binary sensors aren't created for Gen4 fans."""
await init_integration_gen4(hass, aioclient_mock)
# Both entities are disabled by default, so checking hass.states here
# wouldn't distinguish "not created" from "created but disabled" --
# check the entity registry instead.
assert (
entity_registry.async_get("binary_sensor.modernformsfan_fan_sleep_timer_active")
is None
)
assert (
entity_registry.async_get(
"binary_sensor.modernformsfan_light_sleep_timer_active"
)
is None
)
+39
View File
@@ -43,6 +43,7 @@ from homeassistant.helpers import entity_registry as er
from . import (
init_integration,
init_integration_gen4,
modern_forms_breeze_active_call_mock,
modern_forms_breeze_call_mock,
)
@@ -395,3 +396,41 @@ async def test_turn_off_does_not_touch_wind(
)
await hass.async_block_till_done()
fan_mock.assert_called_once_with(on=False)
async def test_fan_sleep_timer_not_supported_gen4(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test setting a sleep timer on a Gen4 fan raises an error."""
await init_integration_gen4(hass, aioclient_mock)
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_SLEEP_TIMER,
{ATTR_ENTITY_ID: "fan.modernformsfan_fan", ATTR_SLEEP_TIME: 1},
blocking=True,
)
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "sleep_timer_not_supported"
async def test_clear_fan_sleep_timer_not_supported_gen4(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test clearing a sleep timer on a Gen4 fan raises an error."""
await init_integration_gen4(hass, aioclient_mock)
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
DOMAIN,
SERVICE_CLEAR_FAN_SLEEP_TIMER,
{ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
blocking=True,
)
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "sleep_timer_not_supported"
+20 -9
View File
@@ -326,28 +326,39 @@ async def test_light_change_state_gen4(
light_mock.assert_not_called()
async def test_sleep_timer_services_gen4(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
async def test_light_sleep_timer_not_supported_gen4(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test Gen4 sleep timer services pass sleep through light_fixture()."""
"""Test setting a sleep timer on a Gen4 light fixture raises an error."""
await init_integration_gen4(hass, aioclient_mock)
with patch("aiomodernforms.ModernFormsDevice.light_fixture") as light_fixture_mock:
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_LIGHT_SLEEP_TIMER,
{ATTR_ENTITY_ID: "light.modernformsfan_uplight", ATTR_SLEEP_TIME: 1},
blocking=True,
)
await hass.async_block_till_done()
light_fixture_mock.assert_called_once_with(2, sleep=60)
with patch("aiomodernforms.ModernFormsDevice.light_fixture") as light_fixture_mock:
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "sleep_timer_not_supported"
async def test_clear_light_sleep_timer_not_supported_gen4(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test clearing a sleep timer on a Gen4 light fixture raises an error."""
await init_integration_gen4(hass, aioclient_mock)
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
DOMAIN,
SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
{ATTR_ENTITY_ID: "light.modernformsfan_uplight"},
blocking=True,
)
await hass.async_block_till_done()
light_fixture_mock.assert_called_once_with(2, sleep=0)
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "sleep_timer_not_supported"
+12 -1
View File
@@ -6,7 +6,7 @@ from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS
from homeassistant.core import HomeAssistant
from . import init_integration, modern_forms_timers_set_mock
from . import init_integration, init_integration_gen4, modern_forms_timers_set_mock
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -51,3 +51,14 @@ async def test_active_sensors(
assert state
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
datetime.fromisoformat(state.state)
async def test_no_sleep_timer_sensors_on_gen4(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test the sleep-timer sensors aren't created for Gen4 fans."""
await init_integration_gen4(hass, aioclient_mock)
assert hass.states.get("sensor.modernformsfan_fan_sleep_time") is None
assert hass.states.get("sensor.modernformsfan_light_sleep_time") is None
+12 -1
View File
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import init_integration
from . import init_integration, init_integration_gen4
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -156,3 +156,14 @@ async def test_switch_connection_error(
state = hass.states.get("switch.modernformsfan_away_mode")
assert state.state == STATE_UNAVAILABLE
async def test_no_adaptive_learning_switch_on_gen4(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test the adaptive learning switch isn't created for Gen4 fans."""
await init_integration_gen4(hass, aioclient_mock)
assert hass.states.get("switch.modernformsfan_adaptive_learning") is None
assert hass.states.get("switch.modernformsfan_away_mode") is not None