mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add Big Ass Fans integration (#71498)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
0584e84c30
commit
51c6a68036
@ -93,6 +93,9 @@ omit =
|
|||||||
homeassistant/components/azure_devops/const.py
|
homeassistant/components/azure_devops/const.py
|
||||||
homeassistant/components/azure_devops/sensor.py
|
homeassistant/components/azure_devops/sensor.py
|
||||||
homeassistant/components/azure_service_bus/*
|
homeassistant/components/azure_service_bus/*
|
||||||
|
homeassistant/components/baf/__init__.py
|
||||||
|
homeassistant/components/baf/entity.py
|
||||||
|
homeassistant/components/baf/fan.py
|
||||||
homeassistant/components/baidu/tts.py
|
homeassistant/components/baidu/tts.py
|
||||||
homeassistant/components/balboa/__init__.py
|
homeassistant/components/balboa/__init__.py
|
||||||
homeassistant/components/beewi_smartclim/sensor.py
|
homeassistant/components/beewi_smartclim/sensor.py
|
||||||
|
@ -55,6 +55,7 @@ homeassistant.components.aseko_pool_live.*
|
|||||||
homeassistant.components.asuswrt.*
|
homeassistant.components.asuswrt.*
|
||||||
homeassistant.components.automation.*
|
homeassistant.components.automation.*
|
||||||
homeassistant.components.backup.*
|
homeassistant.components.backup.*
|
||||||
|
homeassistant.components.baf.*
|
||||||
homeassistant.components.binary_sensor.*
|
homeassistant.components.binary_sensor.*
|
||||||
homeassistant.components.bluetooth_tracker.*
|
homeassistant.components.bluetooth_tracker.*
|
||||||
homeassistant.components.bmw_connected_drive.*
|
homeassistant.components.bmw_connected_drive.*
|
||||||
|
@ -120,6 +120,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/azure_service_bus/ @hfurubotten
|
/homeassistant/components/azure_service_bus/ @hfurubotten
|
||||||
/homeassistant/components/backup/ @home-assistant/core
|
/homeassistant/components/backup/ @home-assistant/core
|
||||||
/tests/components/backup/ @home-assistant/core
|
/tests/components/backup/ @home-assistant/core
|
||||||
|
/homeassistant/components/baf/ @bdraco @jfroy
|
||||||
|
/tests/components/baf/ @bdraco @jfroy
|
||||||
/homeassistant/components/balboa/ @garbled1
|
/homeassistant/components/balboa/ @garbled1
|
||||||
/tests/components/balboa/ @garbled1
|
/tests/components/balboa/ @garbled1
|
||||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||||
|
46
homeassistant/components/baf/__init__.py
Normal file
46
homeassistant/components/baf/__init__.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""The Big Ass Fans integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aiobafi6 import Device, Service
|
||||||
|
from aiobafi6.discovery import PORT
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_IP_ADDRESS, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
|
from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT
|
||||||
|
from .models import BAFData
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.FAN]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Big Ass Fans from a config entry."""
|
||||||
|
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||||
|
|
||||||
|
service = Service(ip_addresses=[ip_address], uuid=entry.unique_id, port=PORT)
|
||||||
|
device = Device(service, query_interval_seconds=QUERY_INTERVAL)
|
||||||
|
run_future = device.async_run()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT)
|
||||||
|
except asyncio.TimeoutError as ex:
|
||||||
|
run_future.cancel()
|
||||||
|
raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_future)
|
||||||
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
data: BAFData = hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
data.run_future.cancel()
|
||||||
|
|
||||||
|
return unload_ok
|
120
homeassistant/components/baf/config_flow.py
Normal file
120
homeassistant/components/baf/config_flow.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"""Config flow for baf."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiobafi6 import Device, Service
|
||||||
|
from aiobafi6.discovery import PORT
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import zeroconf
|
||||||
|
from homeassistant.const import CONF_IP_ADDRESS
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.util.network import is_ipv6_address
|
||||||
|
|
||||||
|
from .const import DOMAIN, RUN_TIMEOUT
|
||||||
|
from .models import BAFDiscovery
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_try_connect(ip_address: str) -> Device:
|
||||||
|
"""Validate we can connect to a device."""
|
||||||
|
device = Device(Service(ip_addresses=[ip_address], port=PORT))
|
||||||
|
run_future = device.async_run()
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT)
|
||||||
|
except asyncio.TimeoutError as ex:
|
||||||
|
raise CannotConnect from ex
|
||||||
|
finally:
|
||||||
|
run_future.cancel()
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle BAF discovery config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the BAF config flow."""
|
||||||
|
self.discovery: BAFDiscovery | None = None
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle zeroconf discovery."""
|
||||||
|
properties = discovery_info.properties
|
||||||
|
ip_address = discovery_info.host
|
||||||
|
if is_ipv6_address(ip_address):
|
||||||
|
return self.async_abort(reason="ipv6_not_supported")
|
||||||
|
uuid = properties["uuid"]
|
||||||
|
model = properties["model"]
|
||||||
|
name = properties["name"]
|
||||||
|
await self.async_set_unique_id(uuid, raise_on_progress=False)
|
||||||
|
self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: ip_address})
|
||||||
|
self.discovery = BAFDiscovery(ip_address, name, uuid, model)
|
||||||
|
return await self.async_step_discovery_confirm()
|
||||||
|
|
||||||
|
async def async_step_discovery_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Confirm discovery."""
|
||||||
|
assert self.discovery is not None
|
||||||
|
discovery = self.discovery
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=discovery.name,
|
||||||
|
data={CONF_IP_ADDRESS: discovery.ip_address},
|
||||||
|
)
|
||||||
|
placeholders = {
|
||||||
|
"name": discovery.name,
|
||||||
|
"model": discovery.model,
|
||||||
|
"ip_address": discovery.ip_address,
|
||||||
|
}
|
||||||
|
self.context["title_placeholders"] = placeholders
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="discovery_confirm", description_placeholders=placeholders
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
ip_address = (user_input or {}).get(CONF_IP_ADDRESS, "")
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
device = await async_try_connect(ip_address)
|
||||||
|
except CannotConnect:
|
||||||
|
errors[CONF_IP_ADDRESS] = "cannot_connect"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Unknown exception during connection test to %s", ip_address
|
||||||
|
)
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(device.dns_sd_uuid)
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates={CONF_IP_ADDRESS: ip_address}
|
||||||
|
)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=device.name,
|
||||||
|
data={CONF_IP_ADDRESS: ip_address},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{vol.Required(CONF_IP_ADDRESS, default=ip_address): str}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(Exception):
|
||||||
|
"""Exception to raise when we cannot connect."""
|
19
homeassistant/components/baf/const.py
Normal file
19
homeassistant/components/baf/const.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
"""Constants for the Big Ass Fans integration."""
|
||||||
|
|
||||||
|
DOMAIN = "baf"
|
||||||
|
|
||||||
|
# Most properties are pushed, only the
|
||||||
|
# query every 5 minutes so we keep the RPM
|
||||||
|
# sensors up to date
|
||||||
|
QUERY_INTERVAL = 300
|
||||||
|
|
||||||
|
RUN_TIMEOUT = 20
|
||||||
|
|
||||||
|
PRESET_MODE_AUTO = "Auto"
|
||||||
|
|
||||||
|
SPEED_COUNT = 7
|
||||||
|
SPEED_RANGE = (1, SPEED_COUNT)
|
||||||
|
|
||||||
|
ONE_MIN_SECS = 60
|
||||||
|
ONE_DAY_SECS = 86400
|
||||||
|
HALF_DAY_SECS = 43200
|
48
homeassistant/components/baf/entity.py
Normal file
48
homeassistant/components/baf/entity.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""The baf integration entities."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aiobafi6 import Device
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||||
|
|
||||||
|
|
||||||
|
class BAFEntity(Entity):
|
||||||
|
"""Base class for baf entities."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, device: Device, name: str) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
self._device = device
|
||||||
|
self._attr_unique_id = format_mac(self._device.mac_address)
|
||||||
|
self._attr_name = name
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)},
|
||||||
|
name=self._device.name,
|
||||||
|
manufacturer="Big Ass Fans",
|
||||||
|
model=self._device.model,
|
||||||
|
sw_version=self._device.firmware_version,
|
||||||
|
)
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update attrs from device."""
|
||||||
|
self._attr_available = self._device.available
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_from_device(self, device: Device) -> None:
|
||||||
|
"""Process an update from the device."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Add data updated listener after this object has been initialized."""
|
||||||
|
self._device.add_callback(self._async_update_from_device)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Remove data updated listener after this object has been initialized."""
|
||||||
|
self._device.remove_callback(self._async_update_from_device)
|
97
homeassistant/components/baf/fan.py
Normal file
97
homeassistant/components/baf/fan.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
"""Support for Big Ass Fans fan."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiobafi6 import OffOnAuto
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.fan import (
|
||||||
|
DIRECTION_FORWARD,
|
||||||
|
DIRECTION_REVERSE,
|
||||||
|
FanEntity,
|
||||||
|
FanEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.util.percentage import (
|
||||||
|
percentage_to_ranged_value,
|
||||||
|
ranged_value_to_percentage,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE
|
||||||
|
from .entity import BAFEntity
|
||||||
|
from .models import BAFData
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: config_entries.ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up SenseME fans."""
|
||||||
|
data: BAFData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
if data.device.has_fan:
|
||||||
|
async_add_entities([BAFFan(data.device, data.device.name)])
|
||||||
|
|
||||||
|
|
||||||
|
class BAFFan(BAFEntity, FanEntity):
|
||||||
|
"""BAF ceiling fan component."""
|
||||||
|
|
||||||
|
_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION
|
||||||
|
_attr_preset_modes = [PRESET_MODE_AUTO]
|
||||||
|
_attr_speed_count = SPEED_COUNT
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update attrs from device."""
|
||||||
|
self._attr_is_on = self._device.fan_mode == OffOnAuto.ON
|
||||||
|
self._attr_current_direction = DIRECTION_FORWARD
|
||||||
|
if self._device.reverse_enable:
|
||||||
|
self._attr_current_direction = DIRECTION_REVERSE
|
||||||
|
if self._device.speed is not None:
|
||||||
|
self._attr_percentage = ranged_value_to_percentage(
|
||||||
|
SPEED_RANGE, self._device.speed
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._attr_percentage = None
|
||||||
|
auto = self._device.fan_mode == OffOnAuto.AUTO
|
||||||
|
self._attr_preset_mode = PRESET_MODE_AUTO if auto else None
|
||||||
|
super()._async_update_attrs()
|
||||||
|
|
||||||
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
|
"""Set the speed of the fan, as a percentage."""
|
||||||
|
device = self._device
|
||||||
|
if device.fan_mode != OffOnAuto.ON:
|
||||||
|
device.fan_mode = OffOnAuto.ON
|
||||||
|
device.speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
|
||||||
|
|
||||||
|
async def async_turn_on(
|
||||||
|
self,
|
||||||
|
percentage: int | None = None,
|
||||||
|
preset_mode: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Turn the fan on with a percentage or preset mode."""
|
||||||
|
if preset_mode is not None:
|
||||||
|
await self.async_set_preset_mode(preset_mode)
|
||||||
|
return
|
||||||
|
if percentage is None:
|
||||||
|
self._device.fan_mode = OffOnAuto.ON
|
||||||
|
return
|
||||||
|
await self.async_set_percentage(percentage)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the fan off."""
|
||||||
|
self._device.fan_mode = OffOnAuto.OFF
|
||||||
|
|
||||||
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
"""Set the preset mode of the fan."""
|
||||||
|
if preset_mode != PRESET_MODE_AUTO:
|
||||||
|
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||||
|
self._device.fan_mode = OffOnAuto.AUTO
|
||||||
|
|
||||||
|
async def async_set_direction(self, direction: str) -> None:
|
||||||
|
"""Set the direction of the fan."""
|
||||||
|
self._device.reverse_enable = direction == DIRECTION_REVERSE
|
13
homeassistant/components/baf/manifest.json
Normal file
13
homeassistant/components/baf/manifest.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"domain": "baf",
|
||||||
|
"name": "Big Ass Fans",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||||
|
"requirements": ["aiobafi6==0.3.0"],
|
||||||
|
"codeowners": ["@bdraco", "@jfroy"],
|
||||||
|
"iot_class": "local_push",
|
||||||
|
"zeroconf": [
|
||||||
|
{ "type": "_api._tcp.local.", "properties": { "model": "haiku*" } },
|
||||||
|
{ "type": "_api._tcp.local.", "properties": { "model": "i6*" } }
|
||||||
|
]
|
||||||
|
}
|
25
homeassistant/components/baf/models.py
Normal file
25
homeassistant/components/baf/models.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""The baf integration models."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from aiobafi6 import Device
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BAFData:
|
||||||
|
"""Data for the baf integration."""
|
||||||
|
|
||||||
|
device: Device
|
||||||
|
run_future: asyncio.Future
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BAFDiscovery:
|
||||||
|
"""A BAF Discovery."""
|
||||||
|
|
||||||
|
ip_address: str
|
||||||
|
name: str
|
||||||
|
uuid: str
|
||||||
|
model: str
|
23
homeassistant/components/baf/strings.json
Normal file
23
homeassistant/components/baf/strings.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "{name} - {model} ({ip_address})",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"discovery_confirm": {
|
||||||
|
"description": "Do you want to setup {name} - {model} ({ip_address})?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"ipv6_not_supported": "IPv6 is not supported.",
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
homeassistant/components/baf/translations/en.json
Normal file
23
homeassistant/components/baf/translations/en.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured",
|
||||||
|
"ipv6_not_supported": "IPv6 is not supported."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"flow_title": "{name} - {model} ({ip_address})",
|
||||||
|
"step": {
|
||||||
|
"discovery_confirm": {
|
||||||
|
"description": "Do you want to setup {name} - {model} ({ip_address})?"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"ip_address": "IP Address"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,7 @@ FLOWS = {
|
|||||||
"axis",
|
"axis",
|
||||||
"azure_devops",
|
"azure_devops",
|
||||||
"azure_event_hub",
|
"azure_event_hub",
|
||||||
|
"baf",
|
||||||
"balboa",
|
"balboa",
|
||||||
"blebox",
|
"blebox",
|
||||||
"blink",
|
"blink",
|
||||||
|
@ -42,6 +42,20 @@ ZEROCONF = {
|
|||||||
"domain": "apple_tv"
|
"domain": "apple_tv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"_api._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "baf",
|
||||||
|
"properties": {
|
||||||
|
"model": "haiku*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "baf",
|
||||||
|
"properties": {
|
||||||
|
"model": "i6*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"_api._udp.local.": [
|
"_api._udp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "guardian"
|
"domain": "guardian"
|
||||||
|
11
mypy.ini
11
mypy.ini
@ -368,6 +368,17 @@ no_implicit_optional = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.baf.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.binary_sensor.*]
|
[mypy-homeassistant.components.binary_sensor.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -121,6 +121,9 @@ aioasuswrt==1.4.0
|
|||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==1.3.5
|
aioazuredevops==1.3.5
|
||||||
|
|
||||||
|
# homeassistant.components.baf
|
||||||
|
aiobafi6==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.aws
|
# homeassistant.components.aws
|
||||||
aiobotocore==2.1.0
|
aiobotocore==2.1.0
|
||||||
|
|
||||||
|
@ -108,6 +108,9 @@ aioasuswrt==1.4.0
|
|||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==1.3.5
|
aioazuredevops==1.3.5
|
||||||
|
|
||||||
|
# homeassistant.components.baf
|
||||||
|
aiobafi6==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.aws
|
# homeassistant.components.aws
|
||||||
aiobotocore==2.1.0
|
aiobotocore==2.1.0
|
||||||
|
|
||||||
|
1
tests/components/baf/__init__.py
Normal file
1
tests/components/baf/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Big Ass Fans integration."""
|
158
tests/components/baf/test_config_flow.py
Normal file
158
tests/components/baf/test_config_flow.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
"""Test the baf config flow."""
|
||||||
|
import asyncio
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import zeroconf
|
||||||
|
from homeassistant.components.baf.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_IP_ADDRESS
|
||||||
|
from homeassistant.data_entry_flow import (
|
||||||
|
RESULT_TYPE_ABORT,
|
||||||
|
RESULT_TYPE_CREATE_ENTRY,
|
||||||
|
RESULT_TYPE_FORM,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_user(hass):
|
||||||
|
"""Test we get the user form."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch(
|
||||||
|
"homeassistant.components.baf.config_flow.Device.async_wait_available",
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.baf.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_IP_ADDRESS: "127.0.0.1"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result2["title"] == "127.0.0.1"
|
||||||
|
assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch(
|
||||||
|
"homeassistant.components.baf.config_flow.Device.async_wait_available",
|
||||||
|
side_effect=asyncio.TimeoutError,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_IP_ADDRESS: "127.0.0.1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_unknown_exception(hass):
|
||||||
|
"""Test we handle unknown exceptions."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch(
|
||||||
|
"homeassistant.components.baf.config_flow.Device.async_wait_available",
|
||||||
|
side_effect=Exception,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_IP_ADDRESS: "127.0.0.1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result2["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_discovery(hass):
|
||||||
|
"""Test we can setup from zeroconf discovery."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=zeroconf.ZeroconfServiceInfo(
|
||||||
|
host="127.0.0.1",
|
||||||
|
addresses=["127.0.0.1"],
|
||||||
|
hostname="mock_hostname",
|
||||||
|
name="testfan",
|
||||||
|
port=None,
|
||||||
|
properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"},
|
||||||
|
type="mock_type",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.baf.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "My Fan"
|
||||||
|
assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_updates_existing_ip(hass):
|
||||||
|
"""Test we can setup from zeroconf discovery."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_IP_ADDRESS: "127.0.0.2"}, unique_id="1234"
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=zeroconf.ZeroconfServiceInfo(
|
||||||
|
host="127.0.0.1",
|
||||||
|
addresses=["127.0.0.1"],
|
||||||
|
hostname="mock_hostname",
|
||||||
|
name="testfan",
|
||||||
|
port=None,
|
||||||
|
properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"},
|
||||||
|
type="mock_type",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_rejects_ipv6(hass):
|
||||||
|
"""Test zeroconf discovery rejects ipv6."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=zeroconf.ZeroconfServiceInfo(
|
||||||
|
host="fd00::b27c:63bb:cc85:4ea0",
|
||||||
|
addresses=["fd00::b27c:63bb:cc85:4ea0"],
|
||||||
|
hostname="mock_hostname",
|
||||||
|
name="testfan",
|
||||||
|
port=None,
|
||||||
|
properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"},
|
||||||
|
type="mock_type",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "ipv6_not_supported"
|
Loading…
x
Reference in New Issue
Block a user