mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +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/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
|
||||
homeassistant/components/modern_forms/* @wonderslug
|
||||
homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
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",
|
||||
"minecraft_server",
|
||||
"mobile_app",
|
||||
"modern_forms",
|
||||
"monoprice",
|
||||
"motion_blinds",
|
||||
"motioneye",
|
||||
|
@ -60,6 +60,12 @@ ZEROCONF = {
|
||||
"domain": "devolo_home_control"
|
||||
}
|
||||
],
|
||||
"_easylink._tcp.local.": [
|
||||
{
|
||||
"domain": "modern_forms",
|
||||
"name": "wac*"
|
||||
}
|
||||
],
|
||||
"_elg._tcp.local.": [
|
||||
{
|
||||
"domain": "elgato"
|
||||
|
@ -205,6 +205,9 @@ aiolip==1.1.4
|
||||
# homeassistant.components.lyric
|
||||
aiolyric==1.0.7
|
||||
|
||||
# homeassistant.components.modern_forms
|
||||
aiomodernforms==0.1.5
|
||||
|
||||
# homeassistant.components.keyboard_remote
|
||||
aionotify==0.2.0
|
||||
|
||||
|
@ -130,6 +130,9 @@ aiolip==1.1.4
|
||||
# homeassistant.components.lyric
|
||||
aiolyric==1.0.7
|
||||
|
||||
# homeassistant.components.modern_forms
|
||||
aiomodernforms==0.1.5
|
||||
|
||||
# homeassistant.components.notion
|
||||
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