mirror of
https://github.com/home-assistant/core.git
synced 2026-08-15 01:32:55 +01:00
Add flow it integration (#175076)
Co-authored-by: Erwin Douna <e.douna@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Erwin Douna
Copilot Autofix powered by AI
parent
cba75939ed
commit
fbb769fc16
@@ -216,6 +216,7 @@ homeassistant.components.filter.*
|
||||
homeassistant.components.firefly_iii.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.flexit_bacnet.*
|
||||
homeassistant.components.flow_it.*
|
||||
homeassistant.components.flux_led.*
|
||||
homeassistant.components.folder_watcher.*
|
||||
homeassistant.components.forecast_solar.*
|
||||
|
||||
Generated
+2
@@ -578,6 +578,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/flipr/ @cnico
|
||||
/homeassistant/components/flo/ @dmulcahey
|
||||
/tests/components/flo/ @dmulcahey
|
||||
/homeassistant/components/flow_it/ @albertogeniola
|
||||
/tests/components/flow_it/ @albertogeniola
|
||||
/homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor
|
||||
/tests/components/flume/ @ChrisMandich @bdraco @jeeftor
|
||||
/homeassistant/components/fluss/ @fluss @Marcello17
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""The Flow-it integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from flow_it_api.client import FlowItVMCMachine
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .coordinator import FlowItConfigEntry, FlowItCoordinator, FlowItData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.FAN,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: FlowItConfigEntry) -> bool:
|
||||
"""Set up Flow-it from a config entry."""
|
||||
|
||||
vmc = FlowItVMCMachine(
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_USERNAME],
|
||||
session=get_async_client(hass),
|
||||
)
|
||||
|
||||
coordinator = FlowItCoordinator(hass, entry, vmc)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = FlowItData(
|
||||
vmc=vmc,
|
||||
coordinator=coordinator,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: FlowItConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
vmc = entry.runtime_data.vmc
|
||||
await vmc.close()
|
||||
|
||||
return unload_ok
|
||||
@@ -0,0 +1,181 @@
|
||||
"""Config flow for Flow-it integration."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from flow_it_api.client import FlowItVMCMachine
|
||||
from flow_it_api.exceptions import FlowItAuthError, FlowItConnectionError
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DEFAULT_USERNAME, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
vmc = FlowItVMCMachine(
|
||||
data[CONF_HOST],
|
||||
data[CONF_PASSWORD],
|
||||
data[CONF_USERNAME],
|
||||
session=get_async_client(hass),
|
||||
)
|
||||
info = await vmc.get_info()
|
||||
await vmc.refresh_state()
|
||||
if TYPE_CHECKING:
|
||||
assert vmc.state is not None
|
||||
return {
|
||||
"title": info.hostname,
|
||||
"mac_address": vmc.state.name,
|
||||
}
|
||||
|
||||
|
||||
class FlowItConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Flow-it."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovery_info: dict[str, Any] = {}
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
|
||||
if not URL(host).scheme:
|
||||
host = str(URL.build(scheme="http", host=host))
|
||||
user_input[CONF_HOST] = host
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except FlowItAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except FlowItConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info["mac_address"])
|
||||
self._abort_if_unique_id_configured(updates=user_input)
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.TEXT, autocomplete="username"
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
host = self._discovery_info[CONF_HOST]
|
||||
if not URL(host).scheme:
|
||||
host = str(URL.build(scheme="http", host=host))
|
||||
|
||||
data = {
|
||||
CONF_HOST: host,
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
}
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, data)
|
||||
except FlowItAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except FlowItConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info["mac_address"])
|
||||
self._abort_if_unique_id_configured(updates=data)
|
||||
return self.async_create_entry(
|
||||
title=info["title"],
|
||||
data=data,
|
||||
)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.TEXT, autocomplete="username"
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD, autocomplete="current-password"
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"name": self._discovery_info.get(
|
||||
"friendly_name",
|
||||
self._discovery_info[CONF_HOST].removesuffix(".local"),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
host = discovery_info.host
|
||||
hostname = discovery_info.hostname.rstrip(".")
|
||||
friendly_name = discovery_info.name.removesuffix("._tbk_vmc._tcp.local.")
|
||||
|
||||
self._discovery_info = {
|
||||
CONF_HOST: hostname,
|
||||
"friendly_name": friendly_name,
|
||||
}
|
||||
|
||||
self._async_abort_entries_match({CONF_HOST: host})
|
||||
self._async_abort_entries_match({CONF_HOST: hostname})
|
||||
self._async_abort_entries_match({CONF_HOST: f"http://{host}"})
|
||||
self._async_abort_entries_match({CONF_HOST: f"http://{hostname}"})
|
||||
|
||||
self.context.update({"title_placeholders": {"name": friendly_name}})
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Constants for the Flow-it integration."""
|
||||
|
||||
DOMAIN = "flow_it"
|
||||
|
||||
|
||||
DEFAULT_USERNAME = "api"
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Data update coordinator for the Flow-it integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from flow_it_api.client import FlowItVMCMachine
|
||||
from flow_it_api.exceptions import (
|
||||
FlowItAuthError,
|
||||
FlowItConnectionError,
|
||||
FlowItResponseError,
|
||||
)
|
||||
from flow_it_api.models import MachineData, MachineStatusResponse
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type FlowItConfigEntry = ConfigEntry[FlowItData]
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class FlowItCoordinatorData:
|
||||
"""Data fetched from the Flow-it VMC."""
|
||||
|
||||
state: MachineStatusResponse
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class FlowItData:
|
||||
"""Data for the Flow-it integration."""
|
||||
|
||||
vmc: FlowItVMCMachine
|
||||
coordinator: FlowItCoordinator
|
||||
|
||||
|
||||
class FlowItCoordinator(DataUpdateCoordinator[FlowItCoordinatorData]):
|
||||
"""Class to manage fetching Flow-it data."""
|
||||
|
||||
config_entry: FlowItConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: FlowItConfigEntry,
|
||||
vmc: FlowItVMCMachine,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=60),
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.vmc = vmc
|
||||
|
||||
async def _on_ws_data(data: MachineData) -> None:
|
||||
"""Handle data from WebSocket."""
|
||||
_LOGGER.debug("Received WebSocket update")
|
||||
if self.data:
|
||||
self.data.state.data = data
|
||||
self.async_set_updated_data(self.data)
|
||||
|
||||
self.vmc.register_websocket_callback(_on_ws_data)
|
||||
self.vmc.websocket.start()
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> FlowItCoordinatorData:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
await self.vmc.refresh_state()
|
||||
except FlowItAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_failed",
|
||||
) from err
|
||||
except (FlowItConnectionError, FlowItResponseError) as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
) from err
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.vmc.state is not None
|
||||
return FlowItCoordinatorData(state=self.vmc.state)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Base entity for Flow-it."""
|
||||
|
||||
from flow_it_api.client import FlowItVMCMachine
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FlowItCoordinator
|
||||
|
||||
|
||||
class FlowItVmcEntity(CoordinatorEntity[FlowItCoordinator]):
|
||||
"""Base entity for Flow-it."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FlowItCoordinator,
|
||||
vmc: FlowItVMCMachine,
|
||||
entity_description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self.vmc = vmc
|
||||
self._attr_unique_id = f"{coordinator.data.state.name}_{entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.data.state.name)},
|
||||
name=coordinator.data.state.name,
|
||||
manufacturer="FLOW-IT",
|
||||
model="VMC",
|
||||
sw_version=coordinator.data.state.data.alert.version,
|
||||
)
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Fan platform for Flow-it."""
|
||||
|
||||
from typing import Any, override
|
||||
|
||||
from flow_it_api.client import FlowItVMCMachine
|
||||
from flow_it_api.const import Speed
|
||||
from flow_it_api.exceptions import FlowItAuthError, FlowItCommandError, FlowItError
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
FanEntity,
|
||||
FanEntityDescription,
|
||||
FanEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FlowItConfigEntry, FlowItCoordinator
|
||||
from .entity import FlowItVmcEntity
|
||||
|
||||
ORDERED_NAMED_FAN_SPEEDS = [
|
||||
Speed.LEVEL_1,
|
||||
Speed.LEVEL_2,
|
||||
Speed.LEVEL_3,
|
||||
Speed.LEVEL_4,
|
||||
Speed.LEVEL_5,
|
||||
]
|
||||
|
||||
PRESET_MODES = [Speed.AUTO, Speed.BOOST]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: FlowItConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Flow-it fan."""
|
||||
data = config_entry.runtime_data
|
||||
async_add_entities([FlowItVmcFan(data.coordinator, data.vmc)])
|
||||
|
||||
|
||||
class FlowItVmcFan(FlowItVmcEntity, FanEntity):
|
||||
"""Flow-it fan entity."""
|
||||
|
||||
_attr_supported_features = (
|
||||
FanEntityFeature.SET_SPEED
|
||||
| FanEntityFeature.PRESET_MODE
|
||||
| FanEntityFeature.TURN_ON
|
||||
| FanEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
_attr_preset_modes = list(PRESET_MODES)
|
||||
_attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
|
||||
|
||||
def __init__(self, coordinator: FlowItCoordinator, vmc: FlowItVMCMachine) -> None:
|
||||
"""Initialize the fan."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
vmc,
|
||||
FanEntityDescription(
|
||||
key="fan",
|
||||
name=None,
|
||||
),
|
||||
)
|
||||
|
||||
@override
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if fan is on."""
|
||||
return self.coordinator.data.state.data.mode.speed != Speed.OFF
|
||||
|
||||
@override
|
||||
@property
|
||||
def percentage(self) -> int | None:
|
||||
"""Return the current speed percentage."""
|
||||
speed = self.coordinator.data.state.data.mode.speed
|
||||
if speed in ORDERED_NAMED_FAN_SPEEDS:
|
||||
return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, speed)
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
speed = self.coordinator.data.state.data.mode.speed
|
||||
if speed in PRESET_MODES:
|
||||
return speed
|
||||
return None
|
||||
|
||||
async def _async_send_command(
|
||||
self, speed: Speed, flow_in: bool, flow_out: bool
|
||||
) -> None:
|
||||
"""Send a command to the VMC and handle exceptions."""
|
||||
try:
|
||||
await self.vmc.send_command(speed, flow_in=flow_in, flow_out=flow_out)
|
||||
except FlowItAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_failed",
|
||||
) from err
|
||||
except (FlowItCommandError, FlowItError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
) from err
|
||||
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@override
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
if percentage == 0:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
|
||||
speed = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage)
|
||||
mode = self.coordinator.data.state.data.mode
|
||||
await self._async_send_command(
|
||||
speed,
|
||||
flow_in=mode.flowIn, # codespell:ignore flowin
|
||||
flow_out=mode.flowOut,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
mode = self.coordinator.data.state.data.mode
|
||||
await self._async_send_command(
|
||||
Speed(preset_mode),
|
||||
flow_in=mode.flowIn, # codespell:ignore flowin
|
||||
flow_out=mode.flowOut,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn on the fan."""
|
||||
mode = self.coordinator.data.state.data.mode
|
||||
if percentage is not None:
|
||||
await self.async_set_percentage(percentage)
|
||||
elif preset_mode is not None:
|
||||
await self.async_set_preset_mode(preset_mode)
|
||||
else:
|
||||
await self._async_send_command(
|
||||
Speed.LEVEL_1,
|
||||
flow_in=mode.flowIn, # codespell:ignore flowin
|
||||
flow_out=mode.flowOut,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the fan."""
|
||||
mode = self.coordinator.data.state.data.mode
|
||||
await self._async_send_command(
|
||||
Speed.OFF,
|
||||
flow_in=mode.flowIn, # codespell:ignore flowin
|
||||
flow_out=mode.flowOut,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "flow_it",
|
||||
"name": "Flow-it",
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@albertogeniola"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flow_it",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["flow-it-api==0.0.1.1"],
|
||||
"zeroconf": ["_tbk_vmc._tcp.local."]
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: This integration does not expose any action.
|
||||
appropriate-polling: done
|
||||
brands:
|
||||
status: done
|
||||
comment: logos and icons have been submitted for a PR on the dedicated repo.
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: This integration does not expose custom actions.
|
||||
docs-conditions:
|
||||
status: exempt
|
||||
comment: This integration does not expose custom conditions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
docs-triggers:
|
||||
status: exempt
|
||||
comment: This integration does not expose custom triggers.
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: This integration does not expose any action.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: todo
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Flow-it device.",
|
||||
"password": "The password is available on the physical device LCD display, by interacting with the menu, clicking on the 'settings' icon and then on the 'wifi icon'.",
|
||||
"username": "The default username is 'api'."
|
||||
},
|
||||
"title": "Configure Flow-it"
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::flow_it::config::step::user::data_description::password%]",
|
||||
"username": "[%key:component::flow_it::config::step::user::data_description::username%]"
|
||||
},
|
||||
"description": "Do you want to set up {name}?",
|
||||
"title": "Discovered Flow-it"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_failed": {
|
||||
"message": "Authentication failed when communicating with Flow-it VMC"
|
||||
},
|
||||
"command_failed": {
|
||||
"message": "Failed to send command to Flow-it VMC"
|
||||
},
|
||||
"update_failed": {
|
||||
"message": "Error communicating with API"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -242,6 +242,7 @@ FLOWS = {
|
||||
"flexit_bacnet",
|
||||
"flipr",
|
||||
"flo",
|
||||
"flow_it",
|
||||
"flume",
|
||||
"fluss",
|
||||
"flux_led",
|
||||
|
||||
@@ -2171,6 +2171,12 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"flow_it": {
|
||||
"name": "Flow-it",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"flume": {
|
||||
"name": "Flume",
|
||||
"integration_type": "hub",
|
||||
|
||||
Generated
+5
@@ -1011,6 +1011,11 @@ ZEROCONF = {
|
||||
"domain": "systemnexa2",
|
||||
},
|
||||
],
|
||||
"_tbk_vmc._tcp.local.": [
|
||||
{
|
||||
"domain": "flow_it",
|
||||
},
|
||||
],
|
||||
"_technove-stations._tcp.local.": [
|
||||
{
|
||||
"domain": "technove",
|
||||
|
||||
@@ -1917,6 +1917,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.flow_it.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.flux_led.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
Generated
+3
@@ -1029,6 +1029,9 @@ flexit_bacnet==2.2.3
|
||||
# homeassistant.components.flipr
|
||||
flipr-api==1.6.1
|
||||
|
||||
# homeassistant.components.flow_it
|
||||
flow-it-api==0.0.1.1
|
||||
|
||||
# homeassistant.components.fluss
|
||||
fluss-api==0.2.5
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for the Flow-it integration."""
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Common fixtures for the Flow-it tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from flow_it_api.models import MachineStatusResponse
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.flow_it.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_value_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.flow_it.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_flow_it() -> Generator[AsyncMock]:
|
||||
"""Mock FlowItVMCMachine for integration tests."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.flow_it.FlowItVMCMachine",
|
||||
autospec=True,
|
||||
) as mock,
|
||||
patch(
|
||||
"homeassistant.components.flow_it.config_flow.FlowItVMCMachine",
|
||||
new=mock,
|
||||
),
|
||||
):
|
||||
mock_vmc = mock.return_value
|
||||
|
||||
# Override methods with faulty signatures due to decorators
|
||||
mock_vmc.refresh_state = AsyncMock()
|
||||
mock_vmc.send_command = AsyncMock()
|
||||
|
||||
mock_vmc.get_info.return_value.hostname = "Flow-it Device"
|
||||
|
||||
json_data = load_json_value_fixture("machine_status.json", DOMAIN)
|
||||
json_data["name"] = "001122334455"
|
||||
mock_vmc.state = MachineStatusResponse(**json_data)
|
||||
|
||||
mock_vmc.register_websocket_callback = MagicMock()
|
||||
mock_vmc.websocket.start = MagicMock()
|
||||
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Return a mock config entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Flow-it Device",
|
||||
unique_id="001122334455",
|
||||
data={
|
||||
"host": "http://1.1.1.1",
|
||||
"username": "api",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"lastUpdate": 123456789,
|
||||
"chrono_id": "test",
|
||||
"status": true,
|
||||
"name": "001122334455",
|
||||
"data": {
|
||||
"event": "update",
|
||||
"sensors": {
|
||||
"Sin": { "pressure": 1.0, "temperature": 293.15, "humidity": 50.0 },
|
||||
"Sout": { "pressure": 1.0, "temperature": 293.15, "humidity": 50.0 },
|
||||
"Iin": { "pressure": 1.0, "temperature": 293.15, "humidity": 50.0 },
|
||||
"Iout": { "pressure": 1.0, "temperature": 293.15, "humidity": 50.0 }
|
||||
},
|
||||
"mode": {
|
||||
"speed": "2",
|
||||
"autoSpeed": "1",
|
||||
"flowIn": true,
|
||||
"flowOut": true,
|
||||
"bypassMode": "0",
|
||||
"iaq": 100,
|
||||
"temperatureIn": 293.15,
|
||||
"temperatureOut": 293.15,
|
||||
"humidityIn": 50.0,
|
||||
"humidityOut": 50.0,
|
||||
"pressureIn": 1.0,
|
||||
"pressureOut": 1.0,
|
||||
"bypassOn": false
|
||||
},
|
||||
"filter": {
|
||||
"hepa": { "status": 0, "changed": 0 },
|
||||
"g4": { "status": 0, "changed": 0 }
|
||||
},
|
||||
"alert": {
|
||||
"update_reboot": false,
|
||||
"worries": false,
|
||||
"ice": false,
|
||||
"condensation": false,
|
||||
"filterS": 0,
|
||||
"filterI": 0,
|
||||
"warmup": false,
|
||||
"service": false,
|
||||
"fault-code": "0",
|
||||
"net-fault-code": "0",
|
||||
"version": "1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Test the Flow-it config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from flow_it_api.exceptions import FlowItAuthError, FlowItConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.flow_it.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
USER_INPUT = {
|
||||
"host": "1.1.1.1",
|
||||
"username": "api",
|
||||
"password": "test-password",
|
||||
}
|
||||
|
||||
|
||||
async def test_user_flow(hass: HomeAssistant, mock_flow_it: AsyncMock) -> None:
|
||||
"""Test we get the form and create an entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Flow-it Device"
|
||||
assert result["data"] == {**USER_INPUT, "host": f"http://{USER_INPUT['host']}"}
|
||||
assert result["result"].unique_id == "001122334455"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(FlowItAuthError(), "invalid_auth"),
|
||||
(FlowItConnectionError(), "cannot_connect"),
|
||||
(Exception(), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_user_flow_exceptions(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock, exception: Exception, error: str
|
||||
) -> None:
|
||||
"""Test we handle exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_flow_it.return_value.refresh_state.side_effect = exception
|
||||
mock_flow_it.return_value.get_info.side_effect = exception
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_flow_it.return_value.refresh_state.side_effect = None
|
||||
mock_flow_it.return_value.get_info.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Flow-it Device"
|
||||
assert result["data"] == {**USER_INPUT, "host": f"http://{USER_INPUT['host']}"}
|
||||
assert result["result"].unique_id == "001122334455"
|
||||
|
||||
|
||||
async def test_zeroconf(hass: HomeAssistant, mock_flow_it: AsyncMock) -> None:
|
||||
"""Test zeroconf discovery."""
|
||||
discovery_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("1.1.1.1"),
|
||||
ip_addresses=[ip_address("1.1.1.1")],
|
||||
port=80,
|
||||
hostname="mock_hostname.local.",
|
||||
type="_tbk_vmc._tcp.local.",
|
||||
name="mock_name",
|
||||
properties={},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["description_placeholders"] == {"name": "mock_name"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"username": "api",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Flow-it Device"
|
||||
assert result["data"] == {
|
||||
"host": "http://mock_hostname.local",
|
||||
"username": "api",
|
||||
"password": "test-password",
|
||||
}
|
||||
assert result["result"].unique_id == "001122334455"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(FlowItAuthError(), "invalid_auth"),
|
||||
(FlowItConnectionError(), "cannot_connect"),
|
||||
(Exception(), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_zeroconf_exceptions(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock, exception: Exception, error: str
|
||||
) -> None:
|
||||
"""Test zeroconf exceptions."""
|
||||
discovery_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("1.1.1.1"),
|
||||
ip_addresses=[ip_address("1.1.1.1")],
|
||||
port=80,
|
||||
hostname="mock_hostname.local.",
|
||||
type="_tbk_vmc._tcp.local.",
|
||||
name="mock_name",
|
||||
properties={},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
mock_flow_it.return_value.refresh_state.side_effect = exception
|
||||
mock_flow_it.return_value.get_info.side_effect = exception
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"username": "api",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_flow_it.return_value.refresh_state.side_effect = None
|
||||
mock_flow_it.return_value.get_info.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"username": "api",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Flow-it Device"
|
||||
assert result["data"] == {
|
||||
"host": "http://mock_hostname.local",
|
||||
"username": "api",
|
||||
"password": "test-password",
|
||||
}
|
||||
assert result["result"].unique_id == "001122334455"
|
||||
|
||||
|
||||
async def test_user_already_configured(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test user already configured."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_already_configured(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test zeroconf already configured aborts."""
|
||||
discovery_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("1.1.1.1"),
|
||||
ip_addresses=[ip_address("1.1.1.1")],
|
||||
port=80,
|
||||
hostname="mock_hostname.local.",
|
||||
type="_tbk_vmc._tcp.local.",
|
||||
name="mock_name",
|
||||
properties={},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_user_flow_with_http(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock
|
||||
) -> None:
|
||||
"""Test form with http:// already in host."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{**USER_INPUT, "host": f"http://{USER_INPUT['host']}"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Flow-it Device"
|
||||
assert result["data"] == {**USER_INPUT, "host": f"http://{USER_INPUT['host']}"}
|
||||
assert result["result"].unique_id == "001122334455"
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Test Flow-it fan platform."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from flow_it_api.const import Speed
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_PERCENTAGE,
|
||||
ATTR_PRESET_MODE,
|
||||
DOMAIN as FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ENTITY_ID = "fan.001122334455"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "speed"),
|
||||
[
|
||||
(SERVICE_TURN_ON, Speed.LEVEL_1),
|
||||
(SERVICE_TURN_OFF, Speed.OFF),
|
||||
],
|
||||
)
|
||||
async def test_fan_turn_on_off(
|
||||
hass: HomeAssistant,
|
||||
mock_flow_it: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
speed: Speed,
|
||||
) -> None:
|
||||
"""Test turning on and off the fan."""
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_flow_it.return_value.send_command.assert_awaited_once_with(
|
||||
speed, flow_in=True, flow_out=True
|
||||
)
|
||||
|
||||
|
||||
async def test_fan_set_percentage(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test setting percentage."""
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = ENTITY_ID
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 60},
|
||||
blocking=True,
|
||||
)
|
||||
mock_flow_it.return_value.send_command.assert_awaited_once_with(
|
||||
Speed.LEVEL_3, flow_in=True, flow_out=True
|
||||
)
|
||||
|
||||
|
||||
async def test_fan_set_percentage_zero(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test setting percentage to 0 turns off fan."""
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = ENTITY_ID
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 0},
|
||||
blocking=True,
|
||||
)
|
||||
mock_flow_it.return_value.send_command.assert_awaited_once_with(
|
||||
Speed.OFF, flow_in=True, flow_out=True
|
||||
)
|
||||
|
||||
|
||||
async def test_fan_set_preset_mode(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test setting preset mode."""
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = ENTITY_ID
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "boost"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_flow_it.return_value.send_command.assert_awaited_once_with(
|
||||
Speed.BOOST, flow_in=True, flow_out=True
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Test Flow-it integration setup and unload."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from flow_it_api.exceptions import FlowItAuthError, FlowItConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_unload_entry(
|
||||
hass: HomeAssistant, mock_flow_it: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test setting up and unloading the integration."""
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_flow_it.return_value.refresh_state.assert_awaited()
|
||||
mock_flow_it.return_value.register_websocket_callback.assert_called_once()
|
||||
mock_flow_it.return_value.websocket.start.assert_called_once()
|
||||
|
||||
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
mock_flow_it.return_value.close.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_state"),
|
||||
[
|
||||
(FlowItAuthError(), ConfigEntryState.SETUP_ERROR),
|
||||
(FlowItConnectionError(), ConfigEntryState.SETUP_RETRY),
|
||||
],
|
||||
)
|
||||
async def test_setup_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_flow_it: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
expected_state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test setup handles exceptions correctly."""
|
||||
mock_flow_it.return_value.refresh_state.side_effect = exception
|
||||
mock_flow_it.return_value.get_info.side_effect = exception
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state == expected_state
|
||||
Reference in New Issue
Block a user