mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Modern Forms integration initial pass - Fan (#51317)
* Modern Forms integration initial pass * cleanup of typing and nits * Stripped PR down to Fan only * Review cleanup * Set sleep_time to be required for service * Adjust minimum sleep time to one minute. * Code review changes * cleanup icon init a little
This commit is contained in:
parent
51fa28aac3
commit
01d4140177
@ -300,6 +300,7 @@ homeassistant/components/minecraft_server/* @elmurato
|
|||||||
homeassistant/components/minio/* @tkislan
|
homeassistant/components/minio/* @tkislan
|
||||||
homeassistant/components/mobile_app/* @robbiet480
|
homeassistant/components/mobile_app/* @robbiet480
|
||||||
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
|
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
|
||||||
|
homeassistant/components/modern_forms/* @wonderslug
|
||||||
homeassistant/components/monoprice/* @etsinko @OnFreund
|
homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||||
homeassistant/components/moon/* @fabaff
|
homeassistant/components/moon/* @fabaff
|
||||||
homeassistant/components/motion_blinds/* @starkillerOG
|
homeassistant/components/motion_blinds/* @starkillerOG
|
||||||
|
176
homeassistant/components/modern_forms/__init__.py
Normal file
176
homeassistant/components/modern_forms/__init__.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
"""The Modern Forms integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiomodernforms import (
|
||||||
|
ModernFormsConnectionError,
|
||||||
|
ModernFormsDevice,
|
||||||
|
ModernFormsError,
|
||||||
|
)
|
||||||
|
from aiomodernforms.models import Device as ModernFormsDeviceState
|
||||||
|
|
||||||
|
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, DOMAIN
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
PLATFORMS = [
|
||||||
|
FAN_DOMAIN,
|
||||||
|
]
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up a Modern Forms device from a config entry."""
|
||||||
|
|
||||||
|
# Create Modern Forms instance for this entry
|
||||||
|
coordinator = ModernFormsDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
|
if not coordinator.last_update_success:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
if entry.unique_id is None:
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry, unique_id=coordinator.data.info.mac_address
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up all platforms for this device/entry.
|
||||||
|
for platform in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload Modern Forms config entry."""
|
||||||
|
|
||||||
|
# Unload entities for this entry/device.
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*(
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||||
|
for platform in PLATFORMS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
del hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
if not hass.data[DOMAIN]:
|
||||||
|
del hass.data[DOMAIN]
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
def modernforms_exception_handler(func):
|
||||||
|
"""Decorate Modern Forms calls to handle Modern Forms exceptions.
|
||||||
|
|
||||||
|
A decorator that wraps the passed in function, catches Modern Forms errors,
|
||||||
|
and handles the availability of the device in the data coordinator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def handler(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
await func(self, *args, **kwargs)
|
||||||
|
self.coordinator.update_listeners()
|
||||||
|
|
||||||
|
except ModernFormsConnectionError as error:
|
||||||
|
_LOGGER.error("Error communicating with API: %s", error)
|
||||||
|
self.coordinator.last_update_success = False
|
||||||
|
self.coordinator.update_listeners()
|
||||||
|
|
||||||
|
except ModernFormsError as error:
|
||||||
|
_LOGGER.error("Invalid response from API: %s", error)
|
||||||
|
|
||||||
|
return handler
|
||||||
|
|
||||||
|
|
||||||
|
class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceState]):
|
||||||
|
"""Class to manage fetching Modern Forms data from single endpoint."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
*,
|
||||||
|
host: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize global Modern Forms data updater."""
|
||||||
|
self.modernforms = ModernFormsDevice(
|
||||||
|
host, session=async_get_clientsession(hass)
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=SCAN_INTERVAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_listeners(self) -> None:
|
||||||
|
"""Call update on all listeners."""
|
||||||
|
for update_callback in self._listeners:
|
||||||
|
update_callback()
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> ModernFormsDevice:
|
||||||
|
"""Fetch data from Modern Forms."""
|
||||||
|
try:
|
||||||
|
return await self.modernforms.update(
|
||||||
|
full_update=not self.last_update_success
|
||||||
|
)
|
||||||
|
except ModernFormsError as error:
|
||||||
|
raise UpdateFailed(f"Invalid response from API: {error}") from error
|
||||||
|
|
||||||
|
|
||||||
|
class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator]):
|
||||||
|
"""Defines a Modern Forms device entity."""
|
||||||
|
|
||||||
|
coordinator: ModernFormsDataUpdateCoordinator
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
entry_id: str,
|
||||||
|
coordinator: ModernFormsDataUpdateCoordinator,
|
||||||
|
name: str,
|
||||||
|
icon: str | None = None,
|
||||||
|
enabled_default: bool = True,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Modern Forms entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_enabled_default = enabled_default
|
||||||
|
self._entry_id = entry_id
|
||||||
|
self._attr_icon = icon
|
||||||
|
self._attr_name = name
|
||||||
|
self._unsub_dispatcher = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device information about this Modern Forms device."""
|
||||||
|
return {
|
||||||
|
ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, # type: ignore
|
||||||
|
ATTR_NAME: self.coordinator.data.info.device_name,
|
||||||
|
ATTR_MANUFACTURER: "Modern Forms",
|
||||||
|
ATTR_MODEL: self.coordinator.data.info.fan_type,
|
||||||
|
ATTR_SW_VERSION: f"{self.coordinator.data.info.firmware_version} / {self.coordinator.data.info.main_mcu_firmware_version}",
|
||||||
|
}
|
120
homeassistant/components/modern_forms/config_flow.py
Normal file
120
homeassistant/components/modern_forms/config_flow.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"""Config flow for Modern Forms."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiomodernforms import ModernFormsConnectionError, ModernFormsDevice
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import (
|
||||||
|
CONN_CLASS_LOCAL_POLL,
|
||||||
|
SOURCE_ZEROCONF,
|
||||||
|
ConfigFlow,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a ModernForms config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle setup by user for Modern Forms integration."""
|
||||||
|
return await self._handle_config_flow(user_input)
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: DiscoveryInfoType
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle zeroconf discovery."""
|
||||||
|
host = discovery_info["hostname"].rstrip(".")
|
||||||
|
name, _ = host.rsplit(".")
|
||||||
|
|
||||||
|
self.context.update(
|
||||||
|
{
|
||||||
|
CONF_HOST: discovery_info["host"],
|
||||||
|
CONF_NAME: name,
|
||||||
|
CONF_MAC: discovery_info["properties"].get(CONF_MAC),
|
||||||
|
"title_placeholders": {"name": name},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare configuration flow
|
||||||
|
return await self._handle_config_flow(discovery_info, True)
|
||||||
|
|
||||||
|
async def async_step_zeroconf_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle a flow initiated by zeroconf."""
|
||||||
|
return await self._handle_config_flow(user_input)
|
||||||
|
|
||||||
|
async def _handle_config_flow(
|
||||||
|
self, user_input: dict[str, Any] | None = None, prepare: bool = False
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Config flow handler for ModernForms."""
|
||||||
|
source = self.context.get("source")
|
||||||
|
|
||||||
|
# Request user input, unless we are preparing discovery flow
|
||||||
|
if user_input is None:
|
||||||
|
user_input = {}
|
||||||
|
if not prepare:
|
||||||
|
if source == SOURCE_ZEROCONF:
|
||||||
|
return self._show_confirm_dialog()
|
||||||
|
return self._show_setup_form()
|
||||||
|
|
||||||
|
if source == SOURCE_ZEROCONF:
|
||||||
|
user_input[CONF_HOST] = self.context.get(CONF_HOST)
|
||||||
|
user_input[CONF_MAC] = self.context.get(CONF_MAC)
|
||||||
|
|
||||||
|
if user_input.get(CONF_MAC) is None or not prepare:
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
device = ModernFormsDevice(user_input[CONF_HOST], session=session)
|
||||||
|
try:
|
||||||
|
device = await device.update()
|
||||||
|
except ModernFormsConnectionError:
|
||||||
|
if source == SOURCE_ZEROCONF:
|
||||||
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
return self._show_setup_form({"base": "cannot_connect"})
|
||||||
|
user_input[CONF_MAC] = device.info.mac_address
|
||||||
|
user_input[CONF_NAME] = device.info.device_name
|
||||||
|
|
||||||
|
# Check if already configured
|
||||||
|
await self.async_set_unique_id(user_input[CONF_MAC])
|
||||||
|
self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
|
||||||
|
|
||||||
|
title = device.info.device_name
|
||||||
|
if source == SOURCE_ZEROCONF:
|
||||||
|
title = self.context.get(CONF_NAME)
|
||||||
|
|
||||||
|
if prepare:
|
||||||
|
return await self.async_step_zeroconf_confirm()
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=title,
|
||||||
|
data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_setup_form(self, errors: dict | None = None) -> FlowResult:
|
||||||
|
"""Show the setup form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
|
errors=errors or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_confirm_dialog(self, errors: dict | None = None) -> FlowResult:
|
||||||
|
"""Show the confirm dialog to the user."""
|
||||||
|
name = self.context.get(CONF_NAME)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="zeroconf_confirm",
|
||||||
|
description_placeholders={"name": name},
|
||||||
|
errors=errors or {},
|
||||||
|
)
|
30
homeassistant/components/modern_forms/const.py
Normal file
30
homeassistant/components/modern_forms/const.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""Constants for the Modern Forms integration."""
|
||||||
|
|
||||||
|
DOMAIN = "modern_forms"
|
||||||
|
|
||||||
|
ATTR_IDENTIFIERS = "identifiers"
|
||||||
|
ATTR_MANUFACTURER = "manufacturer"
|
||||||
|
ATTR_MODEL = "model"
|
||||||
|
ATTR_OWNER = "owner"
|
||||||
|
ATTR_IDENTITY = "identity"
|
||||||
|
ATTR_MCU_FIRMWARE_VERSION = "mcu_firmware_version"
|
||||||
|
ATTR_FIRMWARE_VERSION = "firmware_version"
|
||||||
|
|
||||||
|
SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal." "{}"
|
||||||
|
SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal." "{}"
|
||||||
|
SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal." "{}"
|
||||||
|
|
||||||
|
CONF_ON_UNLOAD = "ON_UNLOAD"
|
||||||
|
|
||||||
|
OPT_BRIGHTNESS = "brightness"
|
||||||
|
OPT_ON = "on"
|
||||||
|
OPT_SPEED = "speed"
|
||||||
|
|
||||||
|
# Services
|
||||||
|
SERVICE_SET_LIGHT_SLEEP_TIMER = "set_light_sleep_timer"
|
||||||
|
SERVICE_CLEAR_LIGHT_SLEEP_TIMER = "clear_light_sleep_timer"
|
||||||
|
SERVICE_SET_FAN_SLEEP_TIMER = "set_fan_sleep_timer"
|
||||||
|
SERVICE_CLEAR_FAN_SLEEP_TIMER = "clear_fan_sleep_timer"
|
||||||
|
|
||||||
|
ATTR_SLEEP_TIME = "sleep_time"
|
||||||
|
CLEAR_TIMER = 0
|
180
homeassistant/components/modern_forms/fan.py
Normal file
180
homeassistant/components/modern_forms/fan.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
"""Support for Modern Forms Fan Fans."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from aiomodernforms.const import FAN_POWER_OFF, FAN_POWER_ON
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import callback
|
||||||
|
import homeassistant.helpers.entity_platform as entity_platform
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
from homeassistant.util.percentage import (
|
||||||
|
int_states_in_range,
|
||||||
|
percentage_to_ranged_value,
|
||||||
|
ranged_value_to_percentage,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
ModernFormsDataUpdateCoordinator,
|
||||||
|
ModernFormsDeviceEntity,
|
||||||
|
modernforms_exception_handler,
|
||||||
|
)
|
||||||
|
from .const import (
|
||||||
|
ATTR_SLEEP_TIME,
|
||||||
|
CLEAR_TIMER,
|
||||||
|
DOMAIN,
|
||||||
|
OPT_ON,
|
||||||
|
OPT_SPEED,
|
||||||
|
SERVICE_CLEAR_FAN_SLEEP_TIMER,
|
||||||
|
SERVICE_SET_FAN_SLEEP_TIMER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
|
||||||
|
) -> None:
|
||||||
|
"""Set up a Modern Forms platform from config entry."""
|
||||||
|
|
||||||
|
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
|
config_entry.entry_id
|
||||||
|
]
|
||||||
|
|
||||||
|
platform = entity_platform.async_get_current_platform()
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_SET_FAN_SLEEP_TIMER,
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_SLEEP_TIME): vol.All(
|
||||||
|
vol.Coerce(int), vol.Range(min=1, max=1440)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"async_set_fan_sleep_timer",
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_CLEAR_FAN_SLEEP_TIMER,
|
||||||
|
{},
|
||||||
|
"async_clear_fan_sleep_timer",
|
||||||
|
)
|
||||||
|
|
||||||
|
update_func = partial(
|
||||||
|
async_update_fan, config_entry, coordinator, {}, async_add_entities
|
||||||
|
)
|
||||||
|
coordinator.async_add_listener(update_func)
|
||||||
|
update_func()
|
||||||
|
|
||||||
|
|
||||||
|
class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
|
||||||
|
"""Defines a Modern Forms light."""
|
||||||
|
|
||||||
|
SPEED_RANGE = (1, 6) # off is not included
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Modern Forms light."""
|
||||||
|
super().__init__(
|
||||||
|
entry_id=entry_id,
|
||||||
|
coordinator=coordinator,
|
||||||
|
name=f"{coordinator.data.info.device_name} Fan",
|
||||||
|
)
|
||||||
|
self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_fan"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> int:
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_DIRECTION | SUPPORT_SET_SPEED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage(self) -> int | None:
|
||||||
|
"""Return the current speed percentage."""
|
||||||
|
percentage = 0
|
||||||
|
if bool(self.coordinator.data.state.fan_on):
|
||||||
|
percentage = ranged_value_to_percentage(
|
||||||
|
self.SPEED_RANGE, self.coordinator.data.state.fan_speed
|
||||||
|
)
|
||||||
|
return percentage
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_direction(self) -> str:
|
||||||
|
"""Return the current direction of the fan."""
|
||||||
|
return self.coordinator.data.state.fan_direction
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed_count(self) -> int:
|
||||||
|
"""Return the number of speeds the fan supports."""
|
||||||
|
return int_states_in_range(self.SPEED_RANGE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return the state of the fan."""
|
||||||
|
return bool(self.coordinator.data.state.fan_on)
|
||||||
|
|
||||||
|
@modernforms_exception_handler
|
||||||
|
async def async_set_direction(self, direction: str) -> None:
|
||||||
|
"""Set the direction of the fan."""
|
||||||
|
await self.coordinator.modernforms.fan(direction=direction)
|
||||||
|
|
||||||
|
@modernforms_exception_handler
|
||||||
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
|
"""Set the speed percentage of the fan."""
|
||||||
|
if percentage > 0:
|
||||||
|
await self.async_turn_on(percentage=percentage)
|
||||||
|
else:
|
||||||
|
await self.async_turn_off()
|
||||||
|
|
||||||
|
@modernforms_exception_handler
|
||||||
|
async def async_turn_on(
|
||||||
|
self,
|
||||||
|
speed: int | None = None,
|
||||||
|
percentage: int | None = None,
|
||||||
|
preset_mode: int | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Turn on the fan."""
|
||||||
|
data = {OPT_ON: FAN_POWER_ON}
|
||||||
|
|
||||||
|
if percentage:
|
||||||
|
data[OPT_SPEED] = round(
|
||||||
|
percentage_to_ranged_value(self.SPEED_RANGE, percentage)
|
||||||
|
)
|
||||||
|
await self.coordinator.modernforms.fan(**data)
|
||||||
|
|
||||||
|
@modernforms_exception_handler
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the fan off."""
|
||||||
|
await self.coordinator.modernforms.fan(on=FAN_POWER_OFF)
|
||||||
|
|
||||||
|
@modernforms_exception_handler
|
||||||
|
async def async_set_fan_sleep_timer(
|
||||||
|
self,
|
||||||
|
sleep_time: int,
|
||||||
|
) -> None:
|
||||||
|
"""Set a Modern Forms light sleep timer."""
|
||||||
|
await self.coordinator.modernforms.fan(sleep=sleep_time * 60)
|
||||||
|
|
||||||
|
@modernforms_exception_handler
|
||||||
|
async def async_clear_fan_sleep_timer(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""Clear a Modern Forms fan sleep timer."""
|
||||||
|
await self.coordinator.modernforms.fan(sleep=CLEAR_TIMER)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_fan(
|
||||||
|
entry: ConfigEntry,
|
||||||
|
coordinator: ModernFormsDataUpdateCoordinator,
|
||||||
|
current: dict[str, ModernFormsFanEntity],
|
||||||
|
async_add_entities,
|
||||||
|
) -> None:
|
||||||
|
"""Update Modern Forms Fan info."""
|
||||||
|
if not current:
|
||||||
|
current[entry.entry_id] = ModernFormsFanEntity(
|
||||||
|
entry_id=entry.entry_id, coordinator=coordinator
|
||||||
|
)
|
||||||
|
async_add_entities([current[entry.entry_id]])
|
17
homeassistant/components/modern_forms/manifest.json
Normal file
17
homeassistant/components/modern_forms/manifest.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"domain": "modern_forms",
|
||||||
|
"name": "Modern Forms",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/modern_forms",
|
||||||
|
"requirements": [
|
||||||
|
"aiomodernforms==0.1.5"
|
||||||
|
],
|
||||||
|
"zeroconf": [
|
||||||
|
{"type":"_easylink._tcp.local.", "name":"wac*"}
|
||||||
|
],
|
||||||
|
"dependencies": [],
|
||||||
|
"codeowners": [
|
||||||
|
"@wonderslug"
|
||||||
|
],
|
||||||
|
"iot_class": "local_polling"
|
||||||
|
}
|
28
homeassistant/components/modern_forms/services.yaml
Normal file
28
homeassistant/components/modern_forms/services.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
set_fan_sleep_timer:
|
||||||
|
name: Set fan sleep timer
|
||||||
|
description: Set a sleep timer on a Modern Forms fan.
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: modern_forms
|
||||||
|
domain: fan
|
||||||
|
fields:
|
||||||
|
sleep_time:
|
||||||
|
name: Sleep Time
|
||||||
|
description: Number of seconds to set the timer.
|
||||||
|
required: true
|
||||||
|
example: "900"
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 1440
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: minutes
|
||||||
|
mode: slider
|
||||||
|
|
||||||
|
clear_fan_sleep_timer:
|
||||||
|
name: Clear fan sleep timer
|
||||||
|
description: Clear the sleep timer on a Modern Forms fan.
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: modern_forms
|
||||||
|
domain: fan
|
28
homeassistant/components/modern_forms/strings.json
Normal file
28
homeassistant/components/modern_forms/strings.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"title": "Modern Forms",
|
||||||
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "Set up your Modern Forms fan to integrate with Home Assistant.",
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||||
|
},
|
||||||
|
"zeroconf_confirm": {
|
||||||
|
"description": "Do you want to add the Modern Forms fan named `{name}` to Home Assistant?",
|
||||||
|
"title": "Discovered Modern Forms fan device"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
homeassistant/components/modern_forms/translations/en.json
Normal file
28
homeassistant/components/modern_forms/translations/en.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured",
|
||||||
|
"cannot_connect": "Failed to connect"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect"
|
||||||
|
},
|
||||||
|
"flow_title": "{name}",
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "Do you want to start set up?"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host"
|
||||||
|
},
|
||||||
|
"description": "Set up your Modern Forms fan to integrate with Home Assistant."
|
||||||
|
},
|
||||||
|
"zeroconf_confirm": {
|
||||||
|
"description": "Do you want to add the Modern Forms fan named `{name}` to Home Assistant?",
|
||||||
|
"title": "Discovered Modern Forms fan device"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Modern Forms"
|
||||||
|
}
|
@ -156,6 +156,7 @@ FLOWS = [
|
|||||||
"mill",
|
"mill",
|
||||||
"minecraft_server",
|
"minecraft_server",
|
||||||
"mobile_app",
|
"mobile_app",
|
||||||
|
"modern_forms",
|
||||||
"monoprice",
|
"monoprice",
|
||||||
"motion_blinds",
|
"motion_blinds",
|
||||||
"motioneye",
|
"motioneye",
|
||||||
|
@ -60,6 +60,12 @@ ZEROCONF = {
|
|||||||
"domain": "devolo_home_control"
|
"domain": "devolo_home_control"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"_easylink._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "modern_forms",
|
||||||
|
"name": "wac*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"_elg._tcp.local.": [
|
"_elg._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "elgato"
|
"domain": "elgato"
|
||||||
|
@ -205,6 +205,9 @@ aiolip==1.1.4
|
|||||||
# homeassistant.components.lyric
|
# homeassistant.components.lyric
|
||||||
aiolyric==1.0.7
|
aiolyric==1.0.7
|
||||||
|
|
||||||
|
# homeassistant.components.modern_forms
|
||||||
|
aiomodernforms==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.keyboard_remote
|
# homeassistant.components.keyboard_remote
|
||||||
aionotify==0.2.0
|
aionotify==0.2.0
|
||||||
|
|
||||||
|
@ -130,6 +130,9 @@ aiolip==1.1.4
|
|||||||
# homeassistant.components.lyric
|
# homeassistant.components.lyric
|
||||||
aiolyric==1.0.7
|
aiolyric==1.0.7
|
||||||
|
|
||||||
|
# homeassistant.components.modern_forms
|
||||||
|
aiomodernforms==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.notion
|
# homeassistant.components.notion
|
||||||
aionotion==1.1.0
|
aionotion==1.1.0
|
||||||
|
|
||||||
|
65
tests/components/modern_forms/__init__.py
Normal file
65
tests/components/modern_forms/__init__.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""Tests for the Modern Forms integration."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from aiomodernforms.const import COMMAND_QUERY_STATIC_DATA
|
||||||
|
|
||||||
|
from homeassistant.components.modern_forms.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
|
||||||
|
|
||||||
|
|
||||||
|
async def modern_forms_call_mock(method, url, data):
|
||||||
|
"""Set up the basic returns based on info or status request."""
|
||||||
|
if COMMAND_QUERY_STATIC_DATA in data:
|
||||||
|
fixture = "modern_forms/device_info.json"
|
||||||
|
else:
|
||||||
|
fixture = "modern_forms/device_status.json"
|
||||||
|
response = AiohttpClientMockResponse(
|
||||||
|
method=method, url=url, json=json.loads(load_fixture(fixture))
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def modern_forms_no_light_call_mock(method, url, data):
|
||||||
|
"""Set up the basic returns based on info or status request."""
|
||||||
|
if COMMAND_QUERY_STATIC_DATA in data:
|
||||||
|
fixture = "modern_forms/device_info_no_light.json"
|
||||||
|
else:
|
||||||
|
fixture = "modern_forms/device_status_no_light.json"
|
||||||
|
response = AiohttpClientMockResponse(
|
||||||
|
method=method, url=url, json=json.loads(load_fixture(fixture))
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def init_integration(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
rgbw: bool = False,
|
||||||
|
skip_setup: bool = False,
|
||||||
|
mock_type: Callable = modern_forms_call_mock,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the Modern Forms integration in Home Assistant."""
|
||||||
|
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://192.168.1.123:80/mf",
|
||||||
|
side_effect=mock_type,
|
||||||
|
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_HOST: "192.168.1.123", CONF_MAC: "AA:BB:CC:DD:EE:FF"}
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
if not skip_setup:
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return entry
|
198
tests/components/modern_forms/test_config_flow.py
Normal file
198
tests/components/modern_forms/test_config_flow.py
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
"""Tests for the Modern Forms config flow."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from aiomodernforms import ModernFormsConnectionError
|
||||||
|
|
||||||
|
from homeassistant.components.modern_forms.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import (
|
||||||
|
RESULT_TYPE_ABORT,
|
||||||
|
RESULT_TYPE_CREATE_ENTRY,
|
||||||
|
RESULT_TYPE_FORM,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import init_integration
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_user_flow_implementation(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test the full manual user flow from start to finish."""
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://192.168.1.123:80/mf",
|
||||||
|
text=load_fixture("modern_forms/device_info.json"),
|
||||||
|
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("step_id") == "user"
|
||||||
|
assert result.get("type") == RESULT_TYPE_FORM
|
||||||
|
assert "flow_id" in result
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_HOST: "192.168.1.123"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("title") == "ModernFormsFan"
|
||||||
|
assert "data" in result
|
||||||
|
assert result.get("type") == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["data"][CONF_HOST] == "192.168.1.123"
|
||||||
|
assert result["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_zeroconf_flow_implementation(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test the full manual user flow from start to finish."""
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://192.168.1.123:80/mf",
|
||||||
|
text=load_fixture("modern_forms/device_info.json"),
|
||||||
|
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}},
|
||||||
|
)
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(flows) == 1
|
||||||
|
|
||||||
|
assert result.get("description_placeholders") == {CONF_NAME: "example"}
|
||||||
|
assert result.get("step_id") == "zeroconf_confirm"
|
||||||
|
assert result.get("type") == RESULT_TYPE_FORM
|
||||||
|
assert "flow_id" in result
|
||||||
|
|
||||||
|
flow = flows[0]
|
||||||
|
assert "context" in flow
|
||||||
|
assert flow["context"][CONF_HOST] == "192.168.1.123"
|
||||||
|
assert flow["context"][CONF_NAME] == "example"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2.get("title") == "example"
|
||||||
|
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
|
||||||
|
assert "data" in result2
|
||||||
|
assert result2["data"][CONF_HOST] == "192.168.1.123"
|
||||||
|
assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF"
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"homeassistant.components.modern_forms.ModernFormsDevice.update",
|
||||||
|
side_effect=ModernFormsConnectionError,
|
||||||
|
)
|
||||||
|
async def test_connection_error(
|
||||||
|
update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test we show user form on Modern Forms connection error."""
|
||||||
|
aioclient_mock.post("http://example.com/mf", exc=aiohttp.ClientError)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data={CONF_HOST: "example.com"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") == RESULT_TYPE_FORM
|
||||||
|
assert result.get("step_id") == "user"
|
||||||
|
assert result.get("errors") == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"homeassistant.components.modern_forms.ModernFormsDevice.update",
|
||||||
|
side_effect=ModernFormsConnectionError,
|
||||||
|
)
|
||||||
|
async def test_zeroconf_connection_error(
|
||||||
|
update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort zeroconf flow on Modern Forms connection error."""
|
||||||
|
aioclient_mock.post("http://192.168.1.123/mf", exc=aiohttp.ClientError)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") == RESULT_TYPE_ABORT
|
||||||
|
assert result.get("reason") == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"homeassistant.components.modern_forms.ModernFormsDevice.update",
|
||||||
|
side_effect=ModernFormsConnectionError,
|
||||||
|
)
|
||||||
|
async def test_zeroconf_confirm_connection_error(
|
||||||
|
update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort zeroconf flow on Modern Forms connection error."""
|
||||||
|
aioclient_mock.post("http://192.168.1.123:80/mf", exc=aiohttp.ClientError)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={
|
||||||
|
"source": SOURCE_ZEROCONF,
|
||||||
|
CONF_HOST: "example.com",
|
||||||
|
CONF_NAME: "test",
|
||||||
|
},
|
||||||
|
data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") == RESULT_TYPE_ABORT
|
||||||
|
assert result.get("reason") == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_device_exists_abort(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort zeroconf flow if Modern Forms device already configured."""
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://192.168.1.123:80/mf",
|
||||||
|
text=load_fixture("modern_forms/device_info.json"),
|
||||||
|
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||||
|
)
|
||||||
|
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data={CONF_HOST: "192.168.1.123"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") == RESULT_TYPE_ABORT
|
||||||
|
assert result.get("reason") == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_with_mac_device_exists_abort(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort zeroconf flow if a Modern Forms device already configured."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data={
|
||||||
|
"host": "192.168.1.123",
|
||||||
|
"hostname": "example.local.",
|
||||||
|
"properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") == RESULT_TYPE_ABORT
|
||||||
|
assert result.get("reason") == "already_configured"
|
213
tests/components/modern_forms/test_fan.py
Normal file
213
tests/components/modern_forms/test_fan.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
"""Tests for the Modern Forms fan platform."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from aiomodernforms import ModernFormsConnectionError
|
||||||
|
|
||||||
|
from homeassistant.components.fan import (
|
||||||
|
ATTR_DIRECTION,
|
||||||
|
ATTR_PERCENTAGE,
|
||||||
|
DIRECTION_FORWARD,
|
||||||
|
DIRECTION_REVERSE,
|
||||||
|
DOMAIN as FAN_DOMAIN,
|
||||||
|
SERVICE_SET_DIRECTION,
|
||||||
|
SERVICE_SET_PERCENTAGE,
|
||||||
|
)
|
||||||
|
from homeassistant.components.modern_forms.const import (
|
||||||
|
ATTR_SLEEP_TIME,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CLEAR_FAN_SLEEP_TIMER,
|
||||||
|
SERVICE_SET_FAN_SLEEP_TIMER,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.components.modern_forms import init_integration
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fan_state(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test the creation and values of the Modern Forms fans."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
state = hass.states.get("fan.modernformsfan_fan")
|
||||||
|
assert state
|
||||||
|
assert state.attributes.get(ATTR_PERCENTAGE) == 50
|
||||||
|
assert state.attributes.get(ATTR_DIRECTION) == DIRECTION_FORWARD
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
entry = entity_registry.async_get("fan.modernformsfan_fan")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "AA:BB:CC:DD:EE:FF_fan"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_change_state(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||||
|
) -> None:
|
||||||
|
"""Test the change of state of the Modern Forms fan."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
fan_mock.assert_called_once_with(
|
||||||
|
on=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "fan.modernformsfan_fan",
|
||||||
|
ATTR_PERCENTAGE: 100,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
fan_mock.assert_called_once_with(on=True, speed=6)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sleep_timer_services(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||||
|
) -> None:
|
||||||
|
"""Test the change of state of the Modern Forms segments."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_FAN_SLEEP_TIMER,
|
||||||
|
{ATTR_ENTITY_ID: "fan.modernformsfan_fan", ATTR_SLEEP_TIME: 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
fan_mock.assert_called_once_with(sleep=60)
|
||||||
|
|
||||||
|
with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CLEAR_FAN_SLEEP_TIMER,
|
||||||
|
{ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
fan_mock.assert_called_once_with(sleep=0)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_change_direction(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||||
|
) -> None:
|
||||||
|
"""Test the change of state of the Modern Forms segments."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_SET_DIRECTION,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "fan.modernformsfan_fan",
|
||||||
|
ATTR_DIRECTION: DIRECTION_REVERSE,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
fan_mock.assert_called_once_with(
|
||||||
|
direction=DIRECTION_REVERSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_percentage(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||||
|
) -> None:
|
||||||
|
"""Test the change of percentage for the Modern Forms fan."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PERCENTAGE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "fan.modernformsfan_fan",
|
||||||
|
ATTR_PERCENTAGE: 100,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
fan_mock.assert_called_once_with(
|
||||||
|
on=True,
|
||||||
|
speed=6,
|
||||||
|
)
|
||||||
|
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PERCENTAGE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "fan.modernformsfan_fan",
|
||||||
|
ATTR_PERCENTAGE: 0,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
fan_mock.assert_called_once_with(on=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fan_error(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||||
|
) -> None:
|
||||||
|
"""Test error handling of the Modern Forms fans."""
|
||||||
|
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
|
||||||
|
aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("fan.modernformsfan_fan")
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert "Invalid response from API" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fan_connection_error(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test error handling of the Moder Forms fans."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch(
|
||||||
|
"homeassistant.components.modern_forms.ModernFormsDevice.fan",
|
||||||
|
side_effect=ModernFormsConnectionError,
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("fan.modernformsfan_fan")
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
60
tests/components/modern_forms/test_init.py
Normal file
60
tests/components/modern_forms/test_init.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""Tests for the Modern Forms integration."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from aiomodernforms import ModernFormsConnectionError
|
||||||
|
|
||||||
|
from homeassistant.components.modern_forms.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.components.modern_forms import (
|
||||||
|
init_integration,
|
||||||
|
modern_forms_no_light_call_mock,
|
||||||
|
)
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"homeassistant.components.modern_forms.ModernFormsDevice.update",
|
||||||
|
side_effect=ModernFormsConnectionError,
|
||||||
|
)
|
||||||
|
async def test_config_entry_not_ready(
|
||||||
|
mock_update: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test the Modern Forms configuration entry not ready."""
|
||||||
|
entry = await init_integration(hass, aioclient_mock)
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unload_config_entry(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test the Modern Forms configuration entry unloading."""
|
||||||
|
entry = await init_integration(hass, aioclient_mock)
|
||||||
|
assert hass.data[DOMAIN]
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not hass.data.get(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setting_unique_id(hass, aioclient_mock):
|
||||||
|
"""Test we set unique ID if not set yet."""
|
||||||
|
entry = await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
assert hass.data[DOMAIN]
|
||||||
|
assert entry.unique_id == "AA:BB:CC:DD:EE:FF"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fan_only_device(hass, aioclient_mock):
|
||||||
|
"""Test we set unique ID if not set yet."""
|
||||||
|
await init_integration(
|
||||||
|
hass, aioclient_mock, mock_type=modern_forms_no_light_call_mock
|
||||||
|
)
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
fan_entry = entity_registry.async_get("fan.modernformsfan_fan")
|
||||||
|
assert fan_entry
|
||||||
|
light_entry = entity_registry.async_get("light.modernformsfan_light")
|
||||||
|
assert light_entry is None
|
15
tests/fixtures/modern_forms/device_info.json
vendored
Normal file
15
tests/fixtures/modern_forms/device_info.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"clientId": "MF_000000000000",
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"lightType": "F6IN-120V-R1-30",
|
||||||
|
"fanType": "1818-56",
|
||||||
|
"fanMotorType": "DC125X25",
|
||||||
|
"productionLotNumber": "",
|
||||||
|
"productSku": "",
|
||||||
|
"owner": "someone@somewhere.com",
|
||||||
|
"federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b",
|
||||||
|
"deviceName": "ModernFormsFan",
|
||||||
|
"firmwareVersion": "01.03.0025",
|
||||||
|
"mainMcuFirmwareVersion": "01.03.3008",
|
||||||
|
"firmwareUrl": ""
|
||||||
|
}
|
14
tests/fixtures/modern_forms/device_info_no_light.json
vendored
Normal file
14
tests/fixtures/modern_forms/device_info_no_light.json
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"clientId": "MF_000000000000",
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"fanType": "1818-56",
|
||||||
|
"fanMotorType": "DC125X25",
|
||||||
|
"productionLotNumber": "",
|
||||||
|
"productSku": "",
|
||||||
|
"owner": "someone@somewhere.com",
|
||||||
|
"federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b",
|
||||||
|
"deviceName": "ModernFormsFan",
|
||||||
|
"firmwareVersion": "01.03.0025",
|
||||||
|
"mainMcuFirmwareVersion": "01.03.3008",
|
||||||
|
"firmwareUrl": ""
|
||||||
|
}
|
17
tests/fixtures/modern_forms/device_status.json
vendored
Normal file
17
tests/fixtures/modern_forms/device_status.json
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"adaptiveLearning": false,
|
||||||
|
"awayModeEnabled": false,
|
||||||
|
"clientId": "MF_000000000000",
|
||||||
|
"decommission": false,
|
||||||
|
"factoryReset": false,
|
||||||
|
"fanDirection": "forward",
|
||||||
|
"fanOn": true,
|
||||||
|
"fanSleepTimer": 0,
|
||||||
|
"fanSpeed": 3,
|
||||||
|
"lightBrightness": 50,
|
||||||
|
"lightOn": true,
|
||||||
|
"lightSleepTimer": 0,
|
||||||
|
"resetRfPairList": false,
|
||||||
|
"rfPairModeActive": false,
|
||||||
|
"schedule": ""
|
||||||
|
}
|
14
tests/fixtures/modern_forms/device_status_no_light.json
vendored
Normal file
14
tests/fixtures/modern_forms/device_status_no_light.json
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"adaptiveLearning": false,
|
||||||
|
"awayModeEnabled": false,
|
||||||
|
"clientId": "MF_000000000000",
|
||||||
|
"decommission": false,
|
||||||
|
"factoryReset": false,
|
||||||
|
"fanDirection": "forward",
|
||||||
|
"fanOn": true,
|
||||||
|
"fanSleepTimer": 0,
|
||||||
|
"fanSpeed": 3,
|
||||||
|
"resetRfPairList": false,
|
||||||
|
"rfPairModeActive": false,
|
||||||
|
"schedule": ""
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user