From 2d306166d0e5a462f9efb9131cbbfca454c6cb63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 17:12:45 -0500 Subject: [PATCH 01/36] baf --- CODEOWNERS | 2 ++ homeassistant/components/baf/__init__.py | 30 +++++++++++++++++++++ homeassistant/components/baf/config_flow.py | 17 ++++++++++++ homeassistant/components/baf/const.py | 3 +++ homeassistant/components/baf/manifest.json | 13 +++++++++ homeassistant/components/baf/strings.json | 13 +++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 9 files changed, 85 insertions(+) create mode 100644 homeassistant/components/baf/__init__.py create mode 100644 homeassistant/components/baf/config_flow.py create mode 100644 homeassistant/components/baf/const.py create mode 100644 homeassistant/components/baf/manifest.json create mode 100644 homeassistant/components/baf/strings.json diff --git a/CODEOWNERS b/CODEOWNERS index b8d737cdfb7d16..c00a653e6f54e0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -116,6 +116,8 @@ build.json @home-assistant/supervisor /homeassistant/components/azure_service_bus/ @hfurubotten /homeassistant/components/backup/ @home-assistant/core /tests/components/backup/ @home-assistant/core +/homeassistant/components/baf/ @bdraco +/tests/components/baf/ @bdraco /homeassistant/components/balboa/ @garbled1 /tests/components/balboa/ @garbled1 /homeassistant/components/beewi_smartclim/ @alemuro diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py new file mode 100644 index 00000000000000..4867957d8f7c7e --- /dev/null +++ b/homeassistant/components/baf/__init__.py @@ -0,0 +1,30 @@ +"""The Big Ass Fans integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Big Ass Fans from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + + 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): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py new file mode 100644 index 00000000000000..0cc47484d24444 --- /dev/null +++ b/homeassistant/components/baf/config_flow.py @@ -0,0 +1,17 @@ +"""Config flow for Big Ass Fans.""" +import my_pypi_dependency + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + + +async def _async_has_devices(hass: HomeAssistant) -> bool: + """Return if there are devices that can be discovered.""" + # TODO Check if there are any devices that can be discovered in the network. + devices = await hass.async_add_executor_job(my_pypi_dependency.discover) + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow(DOMAIN, "Big Ass Fans", _async_has_devices) diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py new file mode 100644 index 00000000000000..8b78d091a17195 --- /dev/null +++ b/homeassistant/components/baf/const.py @@ -0,0 +1,3 @@ +"""Constants for the Big Ass Fans integration.""" + +DOMAIN = "baf" diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json new file mode 100644 index 00000000000000..2b717c397bf713 --- /dev/null +++ b/homeassistant/components/baf/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "baf", + "name": "Big Ass Fans", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/baf", + "requirements": ["aiobafi6==0.1.0"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json new file mode 100644 index 00000000000000..ad8f0f41ae7b29 --- /dev/null +++ b/homeassistant/components/baf/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 710d97f3c34979..d06bc4e19700d0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -41,6 +41,7 @@ "axis", "azure_devops", "azure_event_hub", + "baf", "balboa", "blebox", "blink", diff --git a/requirements_all.txt b/requirements_all.txt index 48f71c37f93845..e564d1ece29958 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,6 +124,9 @@ aioasuswrt==1.4.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 +# homeassistant.components.baf +aiobafi6==0.1.0 + # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8adb336e48890c..316019fac217ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -108,6 +108,9 @@ aioasuswrt==1.4.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 +# homeassistant.components.baf +aiobafi6==0.1.0 + # homeassistant.components.aws aiobotocore==2.1.0 From c478cb74df65fa1ad672b0e355789d3484b5c40d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 17:29:24 -0500 Subject: [PATCH 02/36] wip --- CODEOWNERS | 4 +-- homeassistant/components/baf/__init__.py | 32 ++++++++++++++++------ homeassistant/components/baf/const.py | 4 +++ homeassistant/components/baf/manifest.json | 12 ++++---- homeassistant/components/baf/models.py | 15 ++++++++++ homeassistant/generated/zeroconf.py | 14 ++++++++++ 6 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/baf/models.py diff --git a/CODEOWNERS b/CODEOWNERS index c00a653e6f54e0..45f890bfad3987 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -116,8 +116,8 @@ build.json @home-assistant/supervisor /homeassistant/components/azure_service_bus/ @hfurubotten /homeassistant/components/backup/ @home-assistant/core /tests/components/backup/ @home-assistant/core -/homeassistant/components/baf/ @bdraco -/tests/components/baf/ @bdraco +/homeassistant/components/baf/ @bdraco @jfroy +/tests/components/baf/ @bdraco @jfroy /homeassistant/components/balboa/ @garbled1 /tests/components/balboa/ @garbled1 /homeassistant/components/beewi_smartclim/ @alemuro diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 4867957d8f7c7e..6791b285a8b9a1 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -1,22 +1,37 @@ """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 Platform +from homeassistant.const import CONF_IP_ADDRESS, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT +from .models import BAFData -# TODO List the platforms that you want to support. -# For your initial PR, limit it to 1 platform. -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] +PLATFORMS: list[Platform] = [Platform.FAN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Big Ass Fans from a config entry.""" - # TODO Store an API object for your platforms to access - # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + ip_address = entry.data[CONF_IP_ADDRESS] + + service = Service(ip_addresses=[ip_address], port=PORT) + device = Device(service, query_interval_seconds=QUERY_INTERVAL) + run_task = device.run() + + try: + await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + except asyncio.TimeoutError as ex: + run_task.cancel() + raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex + hass.data[DOMAIN][entry.entry_id] = BAFData(device, run_task) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -25,6 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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): - hass.data[DOMAIN].pop(entry.entry_id) + data: BAFData = hass.data[DOMAIN].pop(entry.entry_id) + data.run_task.cancel() return unload_ok diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 8b78d091a17195..cb7297dca8d247 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -1,3 +1,7 @@ """Constants for the Big Ass Fans integration.""" DOMAIN = "baf" + +QUERY_INTERVAL = 60 + +RUN_TIMEOUT = 20 diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 2b717c397bf713..93b76840a60759 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -4,10 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", "requirements": ["aiobafi6==0.1.0"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], - "codeowners": ["@bdraco"], - "iot_class": "local_push" + "codeowners": ["@bdraco", "@jfroy"], + "iot_class": "local_push", + "zeroconf": [ + { "type": "_api._tcp.local.", "properties": { "model": "haiku*" } }, + { "type": "_api._tcp.local.", "properties": { "model": "i6*" } } + ] } diff --git a/homeassistant/components/baf/models.py b/homeassistant/components/baf/models.py new file mode 100644 index 00000000000000..d95b4a5b6a2294 --- /dev/null +++ b/homeassistant/components/baf/models.py @@ -0,0 +1,15 @@ +"""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_task: asyncio.Task diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index b93d7249211f47..50d3b5f101c941 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -42,6 +42,20 @@ "domain": "apple_tv" } ], + "_api._tcp.local.": [ + { + "domain": "baf", + "properties": { + "model": "haiku*" + } + }, + { + "domain": "baf", + "properties": { + "model": "i6*" + } + } + ], "_api._udp.local.": [ { "domain": "guardian" From d0edbc37ec9a284e2bd1794e1aef9d231404883a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 17:51:26 -0500 Subject: [PATCH 03/36] add discovery --- homeassistant/components/baf/config_flow.py | 75 +++++++++++-- homeassistant/components/baf/const.py | 5 + homeassistant/components/baf/entity.py | 48 ++++++++ homeassistant/components/baf/fan.py | 104 ++++++++++++++++++ homeassistant/components/baf/models.py | 10 ++ homeassistant/components/baf/strings.json | 14 ++- .../components/baf/translations/en.json | 19 ++++ 7 files changed, 261 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/baf/entity.py create mode 100644 homeassistant/components/baf/fan.py create mode 100644 homeassistant/components/baf/translations/en.json diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 0cc47484d24444..83f5ee9f0d61af 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -1,17 +1,72 @@ -"""Config flow for Big Ass Fans.""" -import my_pypi_dependency +"""Config flow for baf.""" +from __future__ import annotations -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_flow +from typing import Any + +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 .const import DOMAIN +from .models import BAFDiscovery + +API_SUFFIX = "._api._tcp.local." + + +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.""" + name = discovery_info.name + if name.endswith(API_SUFFIX): + name = name[: -len(API_SUFFIX)] + properties = discovery_info.properties + self.discovery = BAFDiscovery( + name, discovery_info.host, properties["uuid"], properties["model"] + ) + return await self.async_step_discovery_confirm() -async def _async_has_devices(hass: HomeAssistant) -> bool: - """Return if there are devices that can be discovered.""" - # TODO Check if there are any devices that can be discovered in the network. - devices = await hass.async_add_executor_job(my_pypi_dependency.discover) - return len(devices) > 0 + 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 await self._async_entry_for_discovered_device(discovery) + placeholders = { + "name": discovery.name, + "model": discovery.model, + "ip_address": discovery.ip_address, + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + async def _async_entry_for_discovered_device( + self, discovery: BAFDiscovery + ) -> FlowResult: + """Create a config entry for a device.""" + await self.async_set_unique_id(discovery.uuid, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=discovery.name, + data={CONF_IP_ADDRESS: discovery.ip_address}, + ) -config_entry_flow.register_discovery_flow(DOMAIN, "Big Ass Fans", _async_has_devices) + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + raise NotImplementedError diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index cb7297dca8d247..6166984356a810 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -5,3 +5,8 @@ QUERY_INTERVAL = 60 RUN_TIMEOUT = 20 + +PRESET_MODE_WHOOSH = "Whoosh" + +SPEED_COUNT = 7 +SPEED_RANGE = (1, SPEED_COUNT) diff --git a/homeassistant/components/baf/entity.py b/homeassistant/components/baf/entity.py new file mode 100644 index 00000000000000..865ebbe6176311 --- /dev/null +++ b/homeassistant/components/baf/entity.py @@ -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.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 = self._device.mac_address + self._attr_name = name + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac)}, + name=self._device.name, + manufacturer="Big Ass Fans", + model=self._device.model, + sw_version=self._device.fw_version, + suggested_area=self._device.room_name, + ) + 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) -> 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) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py new file mode 100644 index 00000000000000..4a328d9c508f2c --- /dev/null +++ b/homeassistant/components/baf/fan.py @@ -0,0 +1,104 @@ +"""Support for Big Ass Fans fan.""" +from __future__ import annotations + +import math +from typing import Any + +from aiobafi6 import Device, 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_WHOOSH, 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] + device = data.device + if device.has_fan: + async_add_entities([BAFFan(device)]) + + +class BAFFan(BAFEntity, FanEntity): + """BAF ceiling fan component.""" + + _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION + _attr_preset_modes = [PRESET_MODE_WHOOSH] + + def __init__(self, device: Device) -> None: + """Initialize the entity.""" + super().__init__(device, device.name) + self._attr_speed_count = SPEED_COUNT + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + device = self._device + self._attr_is_on = device.fan_mode != OffOnAuto.OFF + self._attr_current_direction = DIRECTION_FORWARD + if 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 + whoosh = self._device.whoosh_enable + self._attr_preset_mode = PRESET_MODE_WHOOSH if whoosh else None + super()._async_update_attrs() + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + self._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) + elif percentage is None: + self._device.fan_mode = OffOnAuto.ON + else: + 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_WHOOSH: + raise ValueError(f"Invalid preset mode: {preset_mode}") + self._device.whoosh_enable = True + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + if direction == DIRECTION_FORWARD: + self._device.reverse_enable = False + else: + self._device.reverse_enable = True diff --git a/homeassistant/components/baf/models.py b/homeassistant/components/baf/models.py index d95b4a5b6a2294..74461facdefb57 100644 --- a/homeassistant/components/baf/models.py +++ b/homeassistant/components/baf/models.py @@ -13,3 +13,13 @@ class BAFData: device: Device run_task: asyncio.Task + + +@dataclass +class BAFDiscovery: + """A BAF Discovery.""" + + name: str + ip_address: str + uuid: str + model: str diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index ad8f0f41ae7b29..1c6ea8f1d98fb1 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -1,13 +1,19 @@ { "config": { + "flow_title": "{name} - {model} ({ip_address})", "step": { - "confirm": { - "description": "[%key:common::config_flow::description::confirm_setup%]" + "user": {}, + "discovery_confirm": { + "description": "Do you want to setup {name} - {model} ({ip_address})?" } }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "error": { + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } } diff --git a/homeassistant/components/baf/translations/en.json b/homeassistant/components/baf/translations/en.json new file mode 100644 index 00000000000000..f5b2481038f04b --- /dev/null +++ b/homeassistant/components/baf/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Do you want to setup {name} - {model} ({ip_address})?" + }, + "user": {} + } + } +} \ No newline at end of file From 2a76682698a1edc00006f7fecea3aecbf55642e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 18:14:02 -0500 Subject: [PATCH 04/36] tweaks --- homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/baf/config_flow.py | 11 +++---- homeassistant/components/baf/const.py | 2 +- homeassistant/components/baf/entity.py | 13 +++++---- homeassistant/components/baf/fan.py | 32 +++++++++++---------- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 6791b285a8b9a1..56df3adb1072b7 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: run_task.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex - hass.data[DOMAIN][entry.entry_id] = BAFData(device, run_task) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_task) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 83f5ee9f0d61af..0aba7efa4d3633 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -31,9 +31,12 @@ async def async_step_zeroconf( if name.endswith(API_SUFFIX): name = name[: -len(API_SUFFIX)] properties = discovery_info.properties - self.discovery = BAFDiscovery( - name, discovery_info.host, properties["uuid"], properties["model"] - ) + ip_address = discovery_info.host + uuid = properties["uuid"] + model = properties["model"] + 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(name, ip_address, uuid, model) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( @@ -58,8 +61,6 @@ async def _async_entry_for_discovered_device( self, discovery: BAFDiscovery ) -> FlowResult: """Create a config entry for a device.""" - await self.async_set_unique_id(discovery.uuid, raise_on_progress=False) - self._abort_if_unique_id_configured() return self.async_create_entry( title=discovery.name, data={CONF_IP_ADDRESS: discovery.ip_address}, diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 6166984356a810..dea9aca6bd40eb 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -6,7 +6,7 @@ RUN_TIMEOUT = 20 -PRESET_MODE_WHOOSH = "Whoosh" +PRESET_MODE_AUTO = "Auto" SPEED_COUNT = 7 SPEED_RANGE = (1, SPEED_COUNT) diff --git a/homeassistant/components/baf/entity.py b/homeassistant/components/baf/entity.py index 865ebbe6176311..2fda532246c9b1 100644 --- a/homeassistant/components/baf/entity.py +++ b/homeassistant/components/baf/entity.py @@ -5,6 +5,7 @@ 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 @@ -16,15 +17,14 @@ class BAFEntity(Entity): def __init__(self, device: Device, name: str) -> None: """Initialize the entity.""" self._device = device - self._attr_unique_id = self._device.mac_address + 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)}, + 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.fw_version, - suggested_area=self._device.room_name, + sw_version=self._device.firmware_version, ) self._async_update_attrs() @@ -34,8 +34,11 @@ def _async_update_attrs(self) -> None: self._attr_available = self._device.available @callback - def _async_update_from_device(self) -> None: + def _async_update_from_device(self, device: Device) -> None: """Process an update from the device.""" + import pprint + + pprint.pprint(device.properties_dict) self._async_update_attrs() self.async_write_ha_state() diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 4a328d9c508f2c..3882d8c25eb003 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -20,7 +20,7 @@ ranged_value_to_percentage, ) -from .const import DOMAIN, PRESET_MODE_WHOOSH, SPEED_COUNT, SPEED_RANGE +from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE from .entity import BAFEntity from .models import BAFData @@ -41,7 +41,7 @@ class BAFFan(BAFEntity, FanEntity): """BAF ceiling fan component.""" _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION - _attr_preset_modes = [PRESET_MODE_WHOOSH] + _attr_preset_modes = [PRESET_MODE_AUTO] def __init__(self, device: Device) -> None: """Initialize the entity.""" @@ -52,7 +52,7 @@ def __init__(self, device: Device) -> None: def _async_update_attrs(self) -> None: """Update attrs from device.""" device = self._device - self._attr_is_on = device.fan_mode != OffOnAuto.OFF + self._attr_is_on = device.fan_mode != OffOnAuto.OFF.value self._attr_current_direction = DIRECTION_FORWARD if device.reverse_enable: self._attr_current_direction = DIRECTION_REVERSE @@ -62,15 +62,16 @@ def _async_update_attrs(self) -> None: ) else: self._attr_percentage = None - whoosh = self._device.whoosh_enable - self._attr_preset_mode = PRESET_MODE_WHOOSH if whoosh else None + auto = device.fan_mode == OffOnAuto.AUTO.value + 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.""" - self._device.speed = math.ceil( - percentage_to_ranged_value(SPEED_RANGE, percentage) - ) + device = self._device + if device.auto_comfort_enable: + device.auto_comfort_enable = False + device.speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) async def async_turn_on( self, @@ -88,17 +89,18 @@ async def async_turn_on( async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" - self._device.fan_mode = OffOnAuto.OFF + device = self._device + if device.fan_mode == OffOnAuto.ON: + device.fan_mode = OffOnAuto.OFF + elif device.auto_comfort_enable: + device.auto_comfort_enable = False async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if preset_mode != PRESET_MODE_WHOOSH: + if preset_mode != PRESET_MODE_AUTO: raise ValueError(f"Invalid preset mode: {preset_mode}") - self._device.whoosh_enable = True + self._device.auto_comfort_enable = True async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - if direction == DIRECTION_FORWARD: - self._device.reverse_enable = False - else: - self._device.reverse_enable = True + self._device.reverse_enable = direction == DIRECTION_REVERSE From 4775cf335c980ec25bb6512dd23340b13567eb8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 18:20:15 -0500 Subject: [PATCH 05/36] fixes --- homeassistant/components/baf/entity.py | 3 --- homeassistant/components/baf/fan.py | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/baf/entity.py b/homeassistant/components/baf/entity.py index 2fda532246c9b1..22054d0b16d01a 100644 --- a/homeassistant/components/baf/entity.py +++ b/homeassistant/components/baf/entity.py @@ -36,9 +36,6 @@ def _async_update_attrs(self) -> None: @callback def _async_update_from_device(self, device: Device) -> None: """Process an update from the device.""" - import pprint - - pprint.pprint(device.properties_dict) self._async_update_attrs() self.async_write_ha_state() diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 3882d8c25eb003..d3d04e60ad93bf 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -1,10 +1,11 @@ """Support for Big Ass Fans fan.""" from __future__ import annotations +from enum import IntEnum import math from typing import Any -from aiobafi6 import Device, OffOnAuto +from aiobafi6 import Device from homeassistant import config_entries from homeassistant.components.fan import ( @@ -25,6 +26,14 @@ from .models import BAFData +class OffOnAuto(IntEnum): + """Tri-state mode enum that matches the protocol buffer.""" + + OFF = 0 + ON = 1 + AUTO = 2 + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, @@ -52,7 +61,7 @@ def __init__(self, device: Device) -> None: def _async_update_attrs(self) -> None: """Update attrs from device.""" device = self._device - self._attr_is_on = device.fan_mode != OffOnAuto.OFF.value + self._attr_is_on = device.fan_mode != OffOnAuto.OFF self._attr_current_direction = DIRECTION_FORWARD if device.reverse_enable: self._attr_current_direction = DIRECTION_REVERSE @@ -62,7 +71,7 @@ def _async_update_attrs(self) -> None: ) else: self._attr_percentage = None - auto = device.fan_mode == OffOnAuto.AUTO.value + auto = device.fan_mode == OffOnAuto.AUTO self._attr_preset_mode = PRESET_MODE_AUTO if auto else None super()._async_update_attrs() From 012bc4802932b1f9bc1aeff75d2667f04e238a99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 18:27:44 -0500 Subject: [PATCH 06/36] tweak --- homeassistant/components/baf/fan.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index d3d04e60ad93bf..c1b0a9df1b3f5c 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -61,7 +61,7 @@ def __init__(self, device: Device) -> None: def _async_update_attrs(self) -> None: """Update attrs from device.""" device = self._device - self._attr_is_on = device.fan_mode != OffOnAuto.OFF + self._attr_is_on = device.fan_mode == OffOnAuto.ON self._attr_current_direction = DIRECTION_FORWARD if device.reverse_enable: self._attr_current_direction = DIRECTION_REVERSE @@ -98,17 +98,13 @@ async def async_turn_on( async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" - device = self._device - if device.fan_mode == OffOnAuto.ON: - device.fan_mode = OffOnAuto.OFF - elif device.auto_comfort_enable: - device.auto_comfort_enable = False + 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.auto_comfort_enable = True + self._device.fan_mode = OffOnAuto.AUTO async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" From 35387cc1d0afc3d4850ab8e79ab51b86fb3e1d0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 18:30:58 -0500 Subject: [PATCH 07/36] tweaks --- homeassistant/components/baf/fan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index c1b0a9df1b3f5c..ed9e4f78f327ec 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -78,8 +78,6 @@ def _async_update_attrs(self) -> None: async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" device = self._device - if device.auto_comfort_enable: - device.auto_comfort_enable = False device.speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) async def async_turn_on( From ca0162b7d19f39953f3c2e1cc5966883c723f6b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 18:31:51 -0500 Subject: [PATCH 08/36] tweaks --- homeassistant/components/baf/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 56df3adb1072b7..4823ce9aceb58e 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -26,6 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: run_task = device.run() try: + # Wait available doesn't mean the name is actually filled in yet + # so sometimes the device will show up without a name await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) except asyncio.TimeoutError as ex: run_task.cancel() From 28f86446a7737b130d351e79b5c836ec035f9afc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 10:14:21 -0500 Subject: [PATCH 09/36] add manual setup --- homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/baf/config_flow.py | 70 ++++++++++++++++----- homeassistant/components/baf/strings.json | 4 +- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 4823ce9aceb58e..059822283a26a2 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -21,7 +21,7 @@ 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], port=PORT) + service = Service(ip_addresses=[ip_address], uuid=entry.unique_id, port=PORT) device = Device(service, query_interval_seconds=QUERY_INTERVAL) run_task = device.run() diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 0aba7efa4d3633..d40aa94adc6db3 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -1,19 +1,37 @@ """Config flow for baf.""" from __future__ import annotations +import asyncio 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 .const import DOMAIN +from .const import DOMAIN, RUN_TIMEOUT from .models import BAFDiscovery API_SUFFIX = "._api._tcp.local." +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_task = device.run() + try: + await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + except asyncio.TimeoutError as ex: + raise CannotConnect from ex + finally: + run_task.cancel() + return device + + class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle BAF discovery config flow.""" @@ -27,13 +45,11 @@ async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - name = discovery_info.name - if name.endswith(API_SUFFIX): - name = name[: -len(API_SUFFIX)] properties = discovery_info.properties ip_address = discovery_info.host 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(name, ip_address, uuid, model) @@ -46,28 +62,50 @@ async def async_step_discovery_confirm( assert self.discovery is not None discovery = self.discovery if user_input is not None: - return await self._async_entry_for_discovered_device(discovery) + 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_entry_for_discovered_device( - self, discovery: BAFDiscovery - ) -> FlowResult: - """Create a config entry for a device.""" - return self.async_create_entry( - title=discovery.name, - data={CONF_IP_ADDRESS: discovery.ip_address}, - ) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle a flow initialized by the user.""" - raise NotImplementedError + """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" + 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.""" diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index 1c6ea8f1d98fb1..9e1393a1da9c37 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -8,11 +8,9 @@ } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { - "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } From fa4a880380495be1c45ba625326751ff9cb5dcc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 10:14:55 -0500 Subject: [PATCH 10/36] add manual setup --- homeassistant/components/baf/config_flow.py | 2 +- homeassistant/components/baf/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index d40aa94adc6db3..b03c29163ed991 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -52,7 +52,7 @@ async def async_step_zeroconf( 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(name, ip_address, uuid, model) + self.discovery = BAFDiscovery(ip_address, name, uuid, model) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( diff --git a/homeassistant/components/baf/models.py b/homeassistant/components/baf/models.py index 74461facdefb57..16b6f0467864ee 100644 --- a/homeassistant/components/baf/models.py +++ b/homeassistant/components/baf/models.py @@ -19,7 +19,7 @@ class BAFData: class BAFDiscovery: """A BAF Discovery.""" - name: str ip_address: str + name: str uuid: str model: str From 6c7975bdf6028a664461a1f1f3555e2498fe8676 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 10:22:06 -0500 Subject: [PATCH 11/36] strict typing --- .strict-typing | 1 + homeassistant/components/baf/strings.json | 6 +++++- homeassistant/components/baf/translations/en.json | 12 +++++++----- mypy.ini | 11 +++++++++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.strict-typing b/.strict-typing index 67efdfa7953b07..8c4de0525cbddb 100644 --- a/.strict-typing +++ b/.strict-typing @@ -54,6 +54,7 @@ homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* homeassistant.components.automation.* homeassistant.components.backup.* +homeassistant.components.baf.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index 9e1393a1da9c37..725faec8a89bef 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -2,7 +2,11 @@ "config": { "flow_title": "{name} - {model} ({ip_address})", "step": { - "user": {}, + "user": { + "data": { + "ip_address": "IP Address" + } + }, "discovery_confirm": { "description": "Do you want to setup {name} - {model} ({ip_address})?" } diff --git a/homeassistant/components/baf/translations/en.json b/homeassistant/components/baf/translations/en.json index f5b2481038f04b..0553140f292994 100644 --- a/homeassistant/components/baf/translations/en.json +++ b/homeassistant/components/baf/translations/en.json @@ -1,19 +1,21 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "cannot_connect": "Failed to connect" + "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect", - "invalid_host": "Invalid hostname or IP address" + "cannot_connect": "Failed to connect" }, "flow_title": "{name} - {model} ({ip_address})", "step": { "discovery_confirm": { "description": "Do you want to setup {name} - {model} ({ip_address})?" }, - "user": {} + "user": { + "data": { + "ip_address": "IP Address" + } + } } } } \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 81677b8d8ffa67..f1d7b1215667c0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -357,6 +357,17 @@ no_implicit_optional = true warn_return_any = 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.*] check_untyped_defs = true disallow_incomplete_defs = true From 0a2e1a0d13f4fda66320bd082cd9c876826d42ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 11:03:09 -0500 Subject: [PATCH 12/36] adjust --- homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/baf/switch.py | 94 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/baf/switch.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 059822283a26a2..8a4cd4c43ad85b 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData -PLATFORMS: list[Platform] = [Platform.FAN] +PLATFORMS: list[Platform] = [Platform.FAN, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py new file mode 100644 index 00000000000000..21093c04c19802 --- /dev/null +++ b/homeassistant/components/baf/switch.py @@ -0,0 +1,94 @@ +"""Support for Big Ass Fans switch.""" +from __future__ import annotations + +from typing import Any, cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + +BASE_PROPERTY_NAMES = { + "legacy_ir_remote_enable": "Legacy IR Remote", + "led_indicators_enable": "Led Indicators", +} + +BASE_SWITCHES = [ + SwitchEntityDescription(key=key, name=name) + for key, name in BASE_PROPERTY_NAMES.items() +] + +FAN_PROPERTY_NAMES = { + "auto_comfort_enable": "Auto Comfort", + "comfort_heat_assist_enable": "Auto Comfort Heat Assist", + "fan_beep_enable": "Beep", + "eco_enable": "Eco Mode", + "motion_sense_enable": "Motion Sense", + "return_to_auto_enable": "Return to Auto", + "whoosh_enable": "Whoosh", +} + +FAN_SWITCHES = [ + SwitchEntityDescription(key=key, name=name) + for key, name in FAN_PROPERTY_NAMES.items() +] + +LIGHT_PROPERTY_NAMES = { + "light_dim_to_warm_enable": "Dim to Warm", + "light_return_to_auto_enable": "Light Return to Auto", +} + +LIGHT_SWITCHES = [ + SwitchEntityDescription(key=key, name=name) + for key, name in LIGHT_PROPERTY_NAMES.items() +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF fan switches.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + descriptions: list[SwitchEntityDescription] = [] + descriptions.extend(BASE_SWITCHES) + if device.has_fan: + descriptions.extend(FAN_SWITCHES) + if device.has_light: + descriptions.extend(LIGHT_SWITCHES) + async_add_entities(BAFSwitch(device, description) for description in descriptions) + + +class BAFSwitch(BAFEntity, SwitchEntity): + """BAF switch component.""" + + entity_description: SwitchEntityDescription + + def __init__(self, device: Device, description: SwitchEntityDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + self._attr_is_on = cast( + bool, getattr(self._device, self.entity_description.key) + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + setattr(self._device, self.entity_description.key, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + setattr(self._device, self.entity_description.key, False) From b95bc2fbf7fbf768d7211561dedc32844599cbe5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 11:11:31 -0500 Subject: [PATCH 13/36] Add support for lights --- homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/baf/const.py | 9 ++ homeassistant/components/baf/fan.py | 11 +-- homeassistant/components/baf/light.py | 110 +++++++++++++++++++++++ 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/baf/light.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 8a4cd4c43ad85b..5759ccb0ba1962 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData -PLATFORMS: list[Platform] = [Platform.FAN, Platform.SWITCH] +PLATFORMS: list[Platform] = [Platform.FAN, Platform.LIGHT, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index dea9aca6bd40eb..6e9dd44af9c5f0 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -1,4 +1,5 @@ """Constants for the Big Ass Fans integration.""" +from enum import IntEnum DOMAIN = "baf" @@ -10,3 +11,11 @@ SPEED_COUNT = 7 SPEED_RANGE = (1, SPEED_COUNT) + + +class OffOnAuto(IntEnum): + """Tri-state mode enum that matches the protocol buffer.""" + + OFF = 0 + ON = 1 + AUTO = 2 diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index ed9e4f78f327ec..81e088ef14c0fa 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -1,7 +1,6 @@ """Support for Big Ass Fans fan.""" from __future__ import annotations -from enum import IntEnum import math from typing import Any @@ -21,19 +20,11 @@ ranged_value_to_percentage, ) -from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE +from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE, OffOnAuto from .entity import BAFEntity from .models import BAFData -class OffOnAuto(IntEnum): - """Tri-state mode enum that matches the protocol buffer.""" - - OFF = 0 - ON = 1 - AUTO = 2 - - async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, diff --git a/homeassistant/components/baf/light.py b/homeassistant/components/baf/light.py new file mode 100644 index 00000000000000..162db09623b298 --- /dev/null +++ b/homeassistant/components/baf/light.py @@ -0,0 +1,110 @@ +"""Support for Big Ass Fans lights.""" +from __future__ import annotations + +from typing import Any + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ColorMode, + LightEntity, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) + +from .const import DOMAIN, OffOnAuto +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 BAF lights.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + if not device.has_light: + return + if not device.has_fan: + async_add_entities([BAFStandaloneLight(device)]) + else: + async_add_entities([BAFFanLight(device)]) + + +class BAFLight(BAFEntity, LightEntity): + """Representation of a Big Ass Fans light.""" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + device = self._device + self._attr_is_on = device.light_mode == OffOnAuto.ON + if self._device.light_brightness_level is not None: + self._attr_brightness = int( + min(255, self._device.light_brightness_level * 16) + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: + # set the brightness, which will also turn on/off light + if brightness == 255: + brightness = 256 # this will end up as 16 which is max + self._device.light_brightness_level = int(brightness / 16) + else: + self._device.light_mode = OffOnAuto.ON + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + self._device.light_mode = OffOnAuto.OFF + + +class BAFFanLight(BAFLight): + """Representation of a Big Ass Fans light on a fan.""" + + def __init__(self, device: Device) -> None: + """Init a fan light.""" + super().__init__(device, device.name) + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + self._attr_color_mode = ColorMode.BRIGHTNESS + + +class BAFStandaloneLight(BAFLight): + """Representation of a Big Ass Fans light.""" + + def __init__(self, device: Device) -> None: + """Init a standalone light.""" + super().__init__(device, f"{device.name} Light") + self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_min_mireds = color_temperature_kelvin_to_mired( + device.light_warmest_color_temperature + ) + self._attr_max_mireds = color_temperature_kelvin_to_mired( + device.light_coolest_color_temperature + ) + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + super()._async_update_attrs() + self._attr_color_temp = color_temperature_kelvin_to_mired( + self._device.light_color_temperature + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: + self._device.light_color_temperature = color_temperature_mired_to_kelvin( + color_temp + ) + await super().async_turn_on(**kwargs) From 106630effc7518911912f005be009c8e18a235ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 11:35:54 -0500 Subject: [PATCH 14/36] Move auto comfort to the climate platform --- homeassistant/components/baf/__init__.py | 7 ++++++- homeassistant/components/baf/switch.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 5759ccb0ba1962..3e6d796928cfc7 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -14,7 +14,12 @@ from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData -PLATFORMS: list[Platform] = [Platform.FAN, Platform.LIGHT, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.CLIMATE, + Platform.FAN, + Platform.LIGHT, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index 21093c04c19802..573977cc4db33d 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -25,7 +25,6 @@ ] FAN_PROPERTY_NAMES = { - "auto_comfort_enable": "Auto Comfort", "comfort_heat_assist_enable": "Auto Comfort Heat Assist", "fan_beep_enable": "Beep", "eco_enable": "Eco Mode", From c0989314cc224f1c9bbab181106037bb994053d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 11:37:24 -0500 Subject: [PATCH 15/36] Move auto comfort to the climate platform --- homeassistant/components/baf/climate.py | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 homeassistant/components/baf/climate.py diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py new file mode 100644 index 00000000000000..16a2071b2e9bb9 --- /dev/null +++ b/homeassistant/components/baf/climate.py @@ -0,0 +1,61 @@ +"""Support for Big Ass Fans auto comfort.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +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 BAF fan switches.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + if not device.has_fan: + return + async_add_entities([BAFAutoComfort(device, f"{device.name} Auto Comfort")]) + + +class BAFAutoComfort(BAFEntity, ClimateEntity): + """BAF climate auto comfort.""" + + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_CELSIUS + _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY] + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + device = self._device + auto_on = device.auto_comfort_enable + self._attr_hvac_mode = HVACMode.FAN_ONLY if auto_on else HVACMode.OFF + self._attr_hvac_action = HVACAction.FAN if device.speed else HVACAction.OFF + self._attr_target_temperature = device.comfort_ideal_temperature + self._attr_current_temperature = device.temperature + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set the HVAC mode.""" + self._device.auto_comfort_enable = hvac_mode == HVACMode.FAN_ONLY + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set the target temperature.""" + temp = kwargs[ATTR_TEMPERATURE] + if not self._device.auto_comfort_enable: + self._device.auto_comfort_enable = True + self._device.comfort_ideal_temperature = temp From 2d8ff9eb06a51e658a34f0716af7d4c178e56e55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 11:55:04 -0500 Subject: [PATCH 16/36] Add sensor platform --- homeassistant/components/baf/__init__.py | 1 + homeassistant/components/baf/sensor.py | 96 ++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 homeassistant/components/baf/sensor.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 3e6d796928cfc7..adb5e61653f22e 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -18,6 +18,7 @@ Platform.CLIMATE, Platform.FAN, Platform.LIGHT, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py new file mode 100644 index 00000000000000..90c1d639d18dad --- /dev/null +++ b/homeassistant/components/baf/sensor.py @@ -0,0 +1,96 @@ +"""Support for Big Ass Fans sensors.""" +from __future__ import annotations + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + +BASE_SENSORS = ( + SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), +) + +DEFINED_ONLY_SENSORS = ( + SensorEntityDescription( + key="humidity", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), +) + +FAN_SENSORS = ( + SensorEntityDescription( + key="current_rpm", + name="Current RPM", + native_unit_of_measurement="RPM", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="target_rpm", + name="Target RPM", + native_unit_of_measurement="RPM", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="wifi_ssid", name="WiFi SSID", entity_registry_enabled_default=False + ), + SensorEntityDescription( + key="ip_address", name="IP Address", entity_registry_enabled_default=False + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF fan sensors.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + sensors_descriptions = list(BASE_SENSORS) + for description in DEFINED_ONLY_SENSORS: + if getattr(device, description.key): + sensors_descriptions.append(description) + if device.has_fan: + sensors_descriptions.extend(FAN_SENSORS) + async_add_entities( + BAFSensor(device, description) for description in sensors_descriptions + ) + + +class BAFSensor(BAFEntity, SensorEntity): + """BAF sensor.""" + + entity_description: SensorEntityDescription + + def __init__(self, device: Device, description: SensorEntityDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + self._attr_native_value = getattr(self._device, self.entity_description.key) From 7c4830a01df0e79b82069e9cbec7226f78812be0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 11:56:50 -0500 Subject: [PATCH 17/36] add workaround --- homeassistant/components/baf/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index adb5e61653f22e..b4dd7732e441db 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -39,6 +39,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: run_task.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex + # Temporary workaround until the upstream lib is fixed + # to ensure we get the name + for _ in range(50): + if not device.name: + await asyncio.sleep(0.1) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_task) hass.config_entries.async_setup_platforms(entry, PLATFORMS) From 8c959e5017414a5556f0d774666ba18d20d9c1ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 12:47:44 -0500 Subject: [PATCH 18/36] tweaks --- homeassistant/components/baf/climate.py | 3 +-- homeassistant/components/baf/const.py | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py index 16a2071b2e9bb9..a8541447324ab6 100644 --- a/homeassistant/components/baf/climate.py +++ b/homeassistant/components/baf/climate.py @@ -55,7 +55,6 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_temperature(self, **kwargs: Any) -> None: """Set the target temperature.""" - temp = kwargs[ATTR_TEMPERATURE] if not self._device.auto_comfort_enable: self._device.auto_comfort_enable = True - self._device.comfort_ideal_temperature = temp + self._device.comfort_ideal_temperature = kwargs[ATTR_TEMPERATURE] diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 6e9dd44af9c5f0..24cc3acf6d4169 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -3,7 +3,10 @@ DOMAIN = "baf" -QUERY_INTERVAL = 60 +# 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 From bfaba13756df52021a2f8ed6b0464c22c211bf8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 13:17:48 -0500 Subject: [PATCH 19/36] number platform --- homeassistant/components/baf/__init__.py | 1 + homeassistant/components/baf/const.py | 4 + homeassistant/components/baf/number.py | 114 +++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 homeassistant/components/baf/number.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index b4dd7732e441db..91b51fe87b078a 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -18,6 +18,7 @@ Platform.CLIMATE, Platform.FAN, Platform.LIGHT, + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 24cc3acf6d4169..564810adc1d7a3 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -15,6 +15,10 @@ SPEED_COUNT = 7 SPEED_RANGE = (1, SPEED_COUNT) +ONE_MIN_SECS = 60 +ONE_DAY_SECS = 86400 +HALF_DAY_SECS = 43200 + class OffOnAuto(IntEnum): """Tri-state mode enum that matches the protocol buffer.""" diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py new file mode 100644 index 00000000000000..090abec2d52931 --- /dev/null +++ b/homeassistant/components/baf/number.py @@ -0,0 +1,114 @@ +"""Support for Big Ass Fans number.""" +from __future__ import annotations + +from typing import cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, HALF_DAY_SECS, ONE_DAY_SECS, ONE_MIN_SECS, SPEED_RANGE +from .entity import BAFEntity +from .models import BAFData + +MODES = { + "return_to_auto_timeout": NumberMode.SLIDER, + "motion_sense_timeout": NumberMode.SLIDER, + "light_return_to_auto_timeout": NumberMode.SLIDER, + "light_auto_motion_timeout": NumberMode.SLIDER, +} + +FAN_NUMBER_DESCRIPTIONS = ( + NumberEntityDescription( + key="return_to_auto_timeout", + name="Return to Auto Timeout", + min_value=ONE_MIN_SECS, + max_value=HALF_DAY_SECS, + ), + NumberEntityDescription( + key="motion_sense_timeout", + name="Motion Sense Timeout", + min_value=ONE_MIN_SECS, + max_value=ONE_DAY_SECS, + ), + NumberEntityDescription( + key="comfort_min_speed", + name="Comfort Minimum Speed", + min_value=0, + max_value=SPEED_RANGE[1] - 1, + ), + NumberEntityDescription( + key="comfort_max_speed", + name="Comfort Maximum Speed", + min_value=1, + max_value=SPEED_RANGE[1], + ), + NumberEntityDescription( + key="comfort_heat_assist_speed", + name="Comfort Heat Assist Speed", + min_value=SPEED_RANGE[0], + max_value=SPEED_RANGE[1], + ), +) + +LIGHT_NUMBER_DESCRIPTIONS = ( + NumberEntityDescription( + key="light_return_to_auto_timeout", + name="Light Return to Auto Timeout", + min_value=ONE_MIN_SECS, + max_value=HALF_DAY_SECS, + ), + NumberEntityDescription( + key="light_auto_motion_timeout", + name="Light Motion Sense Timeout", + min_value=ONE_MIN_SECS, + max_value=ONE_DAY_SECS, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF numbers.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + descriptions: list[NumberEntityDescription] = [] + if device.has_fan: + descriptions.extend(FAN_NUMBER_DESCRIPTIONS) + if device.has_light: + descriptions.extend(LIGHT_NUMBER_DESCRIPTIONS) + async_add_entities(BAFNumber(device, description) for description in descriptions) + + +class BAFNumber(BAFEntity, NumberEntity): + """BAF number.""" + + entity_description: NumberEntityDescription + + def __init__(self, device: Device, description: NumberEntityDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + self._attr_mode = MODES.get(description.key, NumberMode.BOX) + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + self._attr_value = cast( + float, getattr(self._device, self.entity_description.key) + ) + + async def async_set_value(self, value: float) -> None: + """Set the value.""" + setattr(self._device, self.entity_description.key, int(value)) From 9796eacb254a102142576201328a35a512cb3a45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 13:19:44 -0500 Subject: [PATCH 20/36] tweak names --- homeassistant/components/baf/number.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 090abec2d52931..9e31281ae4ac35 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -40,19 +40,19 @@ ), NumberEntityDescription( key="comfort_min_speed", - name="Comfort Minimum Speed", + name="Auto Comfort Minimum Speed", min_value=0, max_value=SPEED_RANGE[1] - 1, ), NumberEntityDescription( key="comfort_max_speed", - name="Comfort Maximum Speed", + name="Auto Comfort Maximum Speed", min_value=1, max_value=SPEED_RANGE[1], ), NumberEntityDescription( key="comfort_heat_assist_speed", - name="Comfort Heat Assist Speed", + name="Auto Comfort Heat Assist Speed", min_value=SPEED_RANGE[0], max_value=SPEED_RANGE[1], ), From 375d2367dbe77e29a49c1c1a514280515c6d27af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 13:33:37 -0500 Subject: [PATCH 21/36] tweaks --- homeassistant/components/baf/const.py | 1 + homeassistant/components/baf/fan.py | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 564810adc1d7a3..ad3063a1629a8a 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -12,6 +12,7 @@ PRESET_MODE_AUTO = "Auto" +DEFAULT_PERCENTAGE = 40 SPEED_COUNT = 7 SPEED_RANGE = (1, SPEED_COUNT) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 81e088ef14c0fa..6d479e14aae591 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -20,7 +20,14 @@ ranged_value_to_percentage, ) -from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE, OffOnAuto +from .const import ( + DEFAULT_PERCENTAGE, + DOMAIN, + PRESET_MODE_AUTO, + SPEED_COUNT, + SPEED_RANGE, + OffOnAuto, +) from .entity import BAFEntity from .models import BAFData @@ -69,6 +76,8 @@ def _async_update_attrs(self) -> None: 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( @@ -80,10 +89,8 @@ async def async_turn_on( """Turn the fan on with a percentage or preset mode.""" if preset_mode is not None: await self.async_set_preset_mode(preset_mode) - elif percentage is None: - self._device.fan_mode = OffOnAuto.ON - else: - await self.async_set_percentage(percentage) + return + await self.async_set_percentage(percentage or DEFAULT_PERCENTAGE) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" From 46612d0d7d6df28a5284fc0efec315ce856f8964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 13:37:40 -0500 Subject: [PATCH 22/36] try to restore last speed --- homeassistant/components/baf/fan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 6d479e14aae591..5199021ca93c31 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -90,6 +90,9 @@ async def async_turn_on( if preset_mode is not None: await self.async_set_preset_mode(preset_mode) return + if percentage is None and self._device.speed: + self._device.fan_mode = OffOnAuto.ON + return await self.async_set_percentage(percentage or DEFAULT_PERCENTAGE) async def async_turn_off(self, **kwargs: Any) -> None: From 6effdb7695080e04fcbfe3ffe3ca8b55d6fb28b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 13:42:35 -0500 Subject: [PATCH 23/36] drop default --- homeassistant/components/baf/const.py | 1 - homeassistant/components/baf/fan.py | 13 +++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index ad3063a1629a8a..564810adc1d7a3 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -12,7 +12,6 @@ PRESET_MODE_AUTO = "Auto" -DEFAULT_PERCENTAGE = 40 SPEED_COUNT = 7 SPEED_RANGE = (1, SPEED_COUNT) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 5199021ca93c31..269980f09061a5 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -20,14 +20,7 @@ ranged_value_to_percentage, ) -from .const import ( - DEFAULT_PERCENTAGE, - DOMAIN, - PRESET_MODE_AUTO, - SPEED_COUNT, - SPEED_RANGE, - OffOnAuto, -) +from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE, OffOnAuto from .entity import BAFEntity from .models import BAFData @@ -90,10 +83,10 @@ async def async_turn_on( if preset_mode is not None: await self.async_set_preset_mode(preset_mode) return - if percentage is None and self._device.speed: + if percentage is None: self._device.fan_mode = OffOnAuto.ON return - await self.async_set_percentage(percentage or DEFAULT_PERCENTAGE) + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" From 769937f8c50f4031f335318e7c7e0b08f9c20b38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 13:57:50 -0500 Subject: [PATCH 24/36] add cover --- homeassistant/components/baf/config_flow.py | 7 + homeassistant/components/baf/strings.json | 3 +- .../components/baf/translations/en.json | 3 +- tests/components/baf/test_config_flow.py | 139 ++++++++++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 tests/components/baf/test_config_flow.py diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index b03c29163ed991..5ea8259c996015 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import logging from typing import Any from aiobafi6 import Device, Service @@ -17,6 +18,7 @@ from .models import BAFDiscovery API_SUFFIX = "._api._tcp.local." +_LOGGER = logging.getLogger(__name__) async def async_try_connect(ip_address: str) -> Device: @@ -88,6 +90,11 @@ async def async_step_user( 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( diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index 725faec8a89bef..081517b20ddd2a 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -15,7 +15,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/baf/translations/en.json b/homeassistant/components/baf/translations/en.json index 0553140f292994..2458d782419699 100644 --- a/homeassistant/components/baf/translations/en.json +++ b/homeassistant/components/baf/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" }, "flow_title": "{name} - {model} ({ip_address})", "step": { diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py new file mode 100644 index 00000000000000..b622b16c084277 --- /dev/null +++ b/tests/components/baf/test_config_flow.py @@ -0,0 +1,139 @@ +"""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.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.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.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" From 819255ddf7f38d9d5ea24fa89ce300d157e4eb2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 14:15:27 -0500 Subject: [PATCH 25/36] remove --- homeassistant/components/baf/__init__.py | 9 +- homeassistant/components/baf/climate.py | 60 ------------ homeassistant/components/baf/light.py | 110 ---------------------- homeassistant/components/baf/number.py | 114 ----------------------- homeassistant/components/baf/sensor.py | 96 ------------------- homeassistant/components/baf/switch.py | 93 ------------------ 6 files changed, 1 insertion(+), 481 deletions(-) delete mode 100644 homeassistant/components/baf/climate.py delete mode 100644 homeassistant/components/baf/light.py delete mode 100644 homeassistant/components/baf/number.py delete mode 100644 homeassistant/components/baf/sensor.py delete mode 100644 homeassistant/components/baf/switch.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 91b51fe87b078a..1b29188ea106b4 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -14,14 +14,7 @@ from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData -PLATFORMS: list[Platform] = [ - Platform.CLIMATE, - Platform.FAN, - Platform.LIGHT, - Platform.NUMBER, - Platform.SENSOR, - Platform.SWITCH, -] +PLATFORMS: list[Platform] = [Platform.FAN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py deleted file mode 100644 index a8541447324ab6..00000000000000 --- a/homeassistant/components/baf/climate.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Support for Big Ass Fans auto comfort.""" -from __future__ import annotations - -from typing import Any - -from homeassistant import config_entries -from homeassistant.components.climate import ( - ClimateEntity, - ClimateEntityFeature, - HVACAction, - HVACMode, -) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN -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 BAF fan switches.""" - data: BAFData = hass.data[DOMAIN][entry.entry_id] - device = data.device - if not device.has_fan: - return - async_add_entities([BAFAutoComfort(device, f"{device.name} Auto Comfort")]) - - -class BAFAutoComfort(BAFEntity, ClimateEntity): - """BAF climate auto comfort.""" - - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - _attr_temperature_unit = TEMP_CELSIUS - _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY] - - @callback - def _async_update_attrs(self) -> None: - """Update attrs from device.""" - device = self._device - auto_on = device.auto_comfort_enable - self._attr_hvac_mode = HVACMode.FAN_ONLY if auto_on else HVACMode.OFF - self._attr_hvac_action = HVACAction.FAN if device.speed else HVACAction.OFF - self._attr_target_temperature = device.comfort_ideal_temperature - self._attr_current_temperature = device.temperature - - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set the HVAC mode.""" - self._device.auto_comfort_enable = hvac_mode == HVACMode.FAN_ONLY - - async def async_set_temperature(self, **kwargs: Any) -> None: - """Set the target temperature.""" - if not self._device.auto_comfort_enable: - self._device.auto_comfort_enable = True - self._device.comfort_ideal_temperature = kwargs[ATTR_TEMPERATURE] diff --git a/homeassistant/components/baf/light.py b/homeassistant/components/baf/light.py deleted file mode 100644 index 162db09623b298..00000000000000 --- a/homeassistant/components/baf/light.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Support for Big Ass Fans lights.""" -from __future__ import annotations - -from typing import Any - -from aiobafi6 import Device - -from homeassistant import config_entries -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ColorMode, - LightEntity, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, - color_temperature_mired_to_kelvin, -) - -from .const import DOMAIN, OffOnAuto -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 BAF lights.""" - data: BAFData = hass.data[DOMAIN][entry.entry_id] - device = data.device - if not device.has_light: - return - if not device.has_fan: - async_add_entities([BAFStandaloneLight(device)]) - else: - async_add_entities([BAFFanLight(device)]) - - -class BAFLight(BAFEntity, LightEntity): - """Representation of a Big Ass Fans light.""" - - @callback - def _async_update_attrs(self) -> None: - """Update attrs from device.""" - device = self._device - self._attr_is_on = device.light_mode == OffOnAuto.ON - if self._device.light_brightness_level is not None: - self._attr_brightness = int( - min(255, self._device.light_brightness_level * 16) - ) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the light.""" - if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: - # set the brightness, which will also turn on/off light - if brightness == 255: - brightness = 256 # this will end up as 16 which is max - self._device.light_brightness_level = int(brightness / 16) - else: - self._device.light_mode = OffOnAuto.ON - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the light.""" - self._device.light_mode = OffOnAuto.OFF - - -class BAFFanLight(BAFLight): - """Representation of a Big Ass Fans light on a fan.""" - - def __init__(self, device: Device) -> None: - """Init a fan light.""" - super().__init__(device, device.name) - self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} - self._attr_color_mode = ColorMode.BRIGHTNESS - - -class BAFStandaloneLight(BAFLight): - """Representation of a Big Ass Fans light.""" - - def __init__(self, device: Device) -> None: - """Init a standalone light.""" - super().__init__(device, f"{device.name} Light") - self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} - self._attr_color_mode = ColorMode.COLOR_TEMP - self._attr_min_mireds = color_temperature_kelvin_to_mired( - device.light_warmest_color_temperature - ) - self._attr_max_mireds = color_temperature_kelvin_to_mired( - device.light_coolest_color_temperature - ) - - @callback - def _async_update_attrs(self) -> None: - """Update attrs from device.""" - super()._async_update_attrs() - self._attr_color_temp = color_temperature_kelvin_to_mired( - self._device.light_color_temperature - ) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the light.""" - if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: - self._device.light_color_temperature = color_temperature_mired_to_kelvin( - color_temp - ) - await super().async_turn_on(**kwargs) diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py deleted file mode 100644 index 9e31281ae4ac35..00000000000000 --- a/homeassistant/components/baf/number.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Support for Big Ass Fans number.""" -from __future__ import annotations - -from typing import cast - -from aiobafi6 import Device - -from homeassistant import config_entries -from homeassistant.components.number import ( - NumberEntity, - NumberEntityDescription, - NumberMode, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN, HALF_DAY_SECS, ONE_DAY_SECS, ONE_MIN_SECS, SPEED_RANGE -from .entity import BAFEntity -from .models import BAFData - -MODES = { - "return_to_auto_timeout": NumberMode.SLIDER, - "motion_sense_timeout": NumberMode.SLIDER, - "light_return_to_auto_timeout": NumberMode.SLIDER, - "light_auto_motion_timeout": NumberMode.SLIDER, -} - -FAN_NUMBER_DESCRIPTIONS = ( - NumberEntityDescription( - key="return_to_auto_timeout", - name="Return to Auto Timeout", - min_value=ONE_MIN_SECS, - max_value=HALF_DAY_SECS, - ), - NumberEntityDescription( - key="motion_sense_timeout", - name="Motion Sense Timeout", - min_value=ONE_MIN_SECS, - max_value=ONE_DAY_SECS, - ), - NumberEntityDescription( - key="comfort_min_speed", - name="Auto Comfort Minimum Speed", - min_value=0, - max_value=SPEED_RANGE[1] - 1, - ), - NumberEntityDescription( - key="comfort_max_speed", - name="Auto Comfort Maximum Speed", - min_value=1, - max_value=SPEED_RANGE[1], - ), - NumberEntityDescription( - key="comfort_heat_assist_speed", - name="Auto Comfort Heat Assist Speed", - min_value=SPEED_RANGE[0], - max_value=SPEED_RANGE[1], - ), -) - -LIGHT_NUMBER_DESCRIPTIONS = ( - NumberEntityDescription( - key="light_return_to_auto_timeout", - name="Light Return to Auto Timeout", - min_value=ONE_MIN_SECS, - max_value=HALF_DAY_SECS, - ), - NumberEntityDescription( - key="light_auto_motion_timeout", - name="Light Motion Sense Timeout", - min_value=ONE_MIN_SECS, - max_value=ONE_DAY_SECS, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up BAF numbers.""" - data: BAFData = hass.data[DOMAIN][entry.entry_id] - device = data.device - descriptions: list[NumberEntityDescription] = [] - if device.has_fan: - descriptions.extend(FAN_NUMBER_DESCRIPTIONS) - if device.has_light: - descriptions.extend(LIGHT_NUMBER_DESCRIPTIONS) - async_add_entities(BAFNumber(device, description) for description in descriptions) - - -class BAFNumber(BAFEntity, NumberEntity): - """BAF number.""" - - entity_description: NumberEntityDescription - - def __init__(self, device: Device, description: NumberEntityDescription) -> None: - """Initialize the entity.""" - self.entity_description = description - super().__init__(device, f"{device.name} {description.name}") - self._attr_unique_id = f"{self._device.mac_address}-{description.key}" - self._attr_mode = MODES.get(description.key, NumberMode.BOX) - - @callback - def _async_update_attrs(self) -> None: - """Update attrs from device.""" - self._attr_value = cast( - float, getattr(self._device, self.entity_description.key) - ) - - async def async_set_value(self, value: float) -> None: - """Set the value.""" - setattr(self._device, self.entity_description.key, int(value)) diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py deleted file mode 100644 index 90c1d639d18dad..00000000000000 --- a/homeassistant/components/baf/sensor.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Support for Big Ass Fans sensors.""" -from __future__ import annotations - -from aiobafi6 import Device - -from homeassistant import config_entries -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN -from .entity import BAFEntity -from .models import BAFData - -BASE_SENSORS = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), -) - -DEFINED_ONLY_SENSORS = ( - SensorEntityDescription( - key="humidity", - name="Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), -) - -FAN_SENSORS = ( - SensorEntityDescription( - key="current_rpm", - name="Current RPM", - native_unit_of_measurement="RPM", - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="target_rpm", - name="Target RPM", - native_unit_of_measurement="RPM", - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="wifi_ssid", name="WiFi SSID", entity_registry_enabled_default=False - ), - SensorEntityDescription( - key="ip_address", name="IP Address", entity_registry_enabled_default=False - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up BAF fan sensors.""" - data: BAFData = hass.data[DOMAIN][entry.entry_id] - device = data.device - sensors_descriptions = list(BASE_SENSORS) - for description in DEFINED_ONLY_SENSORS: - if getattr(device, description.key): - sensors_descriptions.append(description) - if device.has_fan: - sensors_descriptions.extend(FAN_SENSORS) - async_add_entities( - BAFSensor(device, description) for description in sensors_descriptions - ) - - -class BAFSensor(BAFEntity, SensorEntity): - """BAF sensor.""" - - entity_description: SensorEntityDescription - - def __init__(self, device: Device, description: SensorEntityDescription) -> None: - """Initialize the entity.""" - self.entity_description = description - super().__init__(device, f"{device.name} {description.name}") - self._attr_unique_id = f"{self._device.mac_address}-{description.key}" - - @callback - def _async_update_attrs(self) -> None: - """Update attrs from device.""" - self._attr_native_value = getattr(self._device, self.entity_description.key) diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py deleted file mode 100644 index 573977cc4db33d..00000000000000 --- a/homeassistant/components/baf/switch.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Support for Big Ass Fans switch.""" -from __future__ import annotations - -from typing import Any, cast - -from aiobafi6 import Device - -from homeassistant import config_entries -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN -from .entity import BAFEntity -from .models import BAFData - -BASE_PROPERTY_NAMES = { - "legacy_ir_remote_enable": "Legacy IR Remote", - "led_indicators_enable": "Led Indicators", -} - -BASE_SWITCHES = [ - SwitchEntityDescription(key=key, name=name) - for key, name in BASE_PROPERTY_NAMES.items() -] - -FAN_PROPERTY_NAMES = { - "comfort_heat_assist_enable": "Auto Comfort Heat Assist", - "fan_beep_enable": "Beep", - "eco_enable": "Eco Mode", - "motion_sense_enable": "Motion Sense", - "return_to_auto_enable": "Return to Auto", - "whoosh_enable": "Whoosh", -} - -FAN_SWITCHES = [ - SwitchEntityDescription(key=key, name=name) - for key, name in FAN_PROPERTY_NAMES.items() -] - -LIGHT_PROPERTY_NAMES = { - "light_dim_to_warm_enable": "Dim to Warm", - "light_return_to_auto_enable": "Light Return to Auto", -} - -LIGHT_SWITCHES = [ - SwitchEntityDescription(key=key, name=name) - for key, name in LIGHT_PROPERTY_NAMES.items() -] - - -async def async_setup_entry( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up BAF fan switches.""" - data: BAFData = hass.data[DOMAIN][entry.entry_id] - device = data.device - descriptions: list[SwitchEntityDescription] = [] - descriptions.extend(BASE_SWITCHES) - if device.has_fan: - descriptions.extend(FAN_SWITCHES) - if device.has_light: - descriptions.extend(LIGHT_SWITCHES) - async_add_entities(BAFSwitch(device, description) for description in descriptions) - - -class BAFSwitch(BAFEntity, SwitchEntity): - """BAF switch component.""" - - entity_description: SwitchEntityDescription - - def __init__(self, device: Device, description: SwitchEntityDescription) -> None: - """Initialize the entity.""" - self.entity_description = description - super().__init__(device, f"{device.name} {description.name}") - self._attr_unique_id = f"{self._device.mac_address}-{description.key}" - - @callback - def _async_update_attrs(self) -> None: - """Update attrs from device.""" - self._attr_is_on = cast( - bool, getattr(self._device, self.entity_description.key) - ) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the switch.""" - setattr(self._device, self.entity_description.key, True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the switch.""" - setattr(self._device, self.entity_description.key, False) From 9cd471a852738c94766263983e34a609e253806a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 14:20:34 -0500 Subject: [PATCH 26/36] add tests --- tests/components/baf/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/components/baf/__init__.py diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py new file mode 100644 index 00000000000000..e0432ed643a0fc --- /dev/null +++ b/tests/components/baf/__init__.py @@ -0,0 +1 @@ +"""Tests for the Big Ass Fans integration.""" From 63e027bd2919b79e22c1197f60d642ea213483aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 14:41:58 -0500 Subject: [PATCH 27/36] bump lib for upstream fixes, prune --- homeassistant/components/baf/config_flow.py | 1 - homeassistant/components/baf/const.py | 9 --------- homeassistant/components/baf/fan.py | 4 ++-- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 5ea8259c996015..2dcfce37144319 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -17,7 +17,6 @@ from .const import DOMAIN, RUN_TIMEOUT from .models import BAFDiscovery -API_SUFFIX = "._api._tcp.local." _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 564810adc1d7a3..9876d7ffec3c17 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -1,5 +1,4 @@ """Constants for the Big Ass Fans integration.""" -from enum import IntEnum DOMAIN = "baf" @@ -18,11 +17,3 @@ ONE_MIN_SECS = 60 ONE_DAY_SECS = 86400 HALF_DAY_SECS = 43200 - - -class OffOnAuto(IntEnum): - """Tri-state mode enum that matches the protocol buffer.""" - - OFF = 0 - ON = 1 - AUTO = 2 diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 269980f09061a5..91df1ed7bf6b5b 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -4,7 +4,7 @@ import math from typing import Any -from aiobafi6 import Device +from aiobafi6 import Device, OffOnAuto from homeassistant import config_entries from homeassistant.components.fan import ( @@ -20,7 +20,7 @@ ranged_value_to_percentage, ) -from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE, OffOnAuto +from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE from .entity import BAFEntity from .models import BAFData From 09e715adf17a300a7b676a5fe8f1d54666481f83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 14:45:45 -0500 Subject: [PATCH 28/36] preen --- homeassistant/components/baf/fan.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 91df1ed7bf6b5b..7ba3e224dbc4fe 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -32,9 +32,8 @@ async def async_setup_entry( ) -> None: """Set up SenseME fans.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] - device = data.device - if device.has_fan: - async_add_entities([BAFFan(device)]) + if data.device.has_fan: + async_add_entities([BAFFan(data.device)]) class BAFFan(BAFEntity, FanEntity): @@ -51,10 +50,9 @@ def __init__(self, device: Device) -> None: @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" - device = self._device - self._attr_is_on = device.fan_mode == OffOnAuto.ON + self._attr_is_on = self._device.fan_mode == OffOnAuto.ON self._attr_current_direction = DIRECTION_FORWARD - if device.reverse_enable: + 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( @@ -62,7 +60,7 @@ def _async_update_attrs(self) -> None: ) else: self._attr_percentage = None - auto = device.fan_mode == OffOnAuto.AUTO + auto = self._device.fan_mode == OffOnAuto.AUTO self._attr_preset_mode = PRESET_MODE_AUTO if auto else None super()._async_update_attrs() From 9e57686a2e9719d2bea2c927459a5314fa7e504f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 16:04:10 -0500 Subject: [PATCH 29/36] no cover for fan yet --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 353d2f07d06de1..d3f36491d0abe6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -95,6 +95,9 @@ omit = homeassistant/components/azure_devops/const.py homeassistant/components/azure_devops/sensor.py 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/balboa/__init__.py homeassistant/components/beewi_smartclim/sensor.py From dfa64d6d31af6c0cf2ae413dd0ac81763fbc3e45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 13:32:50 -0500 Subject: [PATCH 30/36] merge --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 93b76840a60759..9dfc35685e324a 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.1.0"], + "requirements": ["aiobafi6==0.3.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index e564d1ece29958..330d8979aedb3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -125,7 +125,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.1.0 +aiobafi6==0.3.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 316019fac217ca..1e75df5a1e6ec0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.1.0 +aiobafi6==0.3.0 # homeassistant.components.aws aiobotocore==2.1.0 From d66cf2e314a83263e8952b54be3f2defd0f8e1ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 13:34:53 -0500 Subject: [PATCH 31/36] remove workarounds --- homeassistant/components/baf/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 1b29188ea106b4..1a09cf68a69374 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -26,19 +26,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: run_task = device.run() try: - # Wait available doesn't mean the name is actually filled in yet - # so sometimes the device will show up without a name await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) except asyncio.TimeoutError as ex: run_task.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex - # Temporary workaround until the upstream lib is fixed - # to ensure we get the name - for _ in range(50): - if not device.name: - await asyncio.sleep(0.1) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_task) hass.config_entries.async_setup_platforms(entry, PLATFORMS) From 448d241fbefa72ee6ed8a64110f179a099bcdc53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 13:40:22 -0500 Subject: [PATCH 32/36] adapt --- homeassistant/components/baf/__init__.py | 8 ++++---- homeassistant/components/baf/config_flow.py | 4 ++-- homeassistant/components/baf/models.py | 2 +- tests/components/baf/test_config_flow.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 1a09cf68a69374..4327eb9ddb973c 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -23,15 +23,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: service = Service(ip_addresses=[ip_address], uuid=entry.unique_id, port=PORT) device = Device(service, query_interval_seconds=QUERY_INTERVAL) - run_task = device.run() + run_future = device.async_run() try: await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) except asyncio.TimeoutError as ex: - run_task.cancel() + run_future.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_task) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_future) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -41,6 +41,6 @@ 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_task.cancel() + data.run_future.cancel() return unload_ok diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 2dcfce37144319..3a1b0c29c980d0 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -23,13 +23,13 @@ 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_task = device.run() + 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_task.cancel() + run_future.cancel() return device diff --git a/homeassistant/components/baf/models.py b/homeassistant/components/baf/models.py index 16b6f0467864ee..de5c4a3498b576 100644 --- a/homeassistant/components/baf/models.py +++ b/homeassistant/components/baf/models.py @@ -12,7 +12,7 @@ class BAFData: """Data for the baf integration.""" device: Device - run_task: asyncio.Task + run_future: asyncio.Future @dataclass diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index b622b16c084277..da560e78f9a687 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form_user(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch("homeassistant.components.baf.config_flow.Device.run",), patch( + 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", @@ -48,7 +48,7 @@ async def test_form_cannot_connect(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.baf.config_flow.Device.run",), patch( + with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( "homeassistant.components.baf.config_flow.Device.async_wait_available", side_effect=asyncio.TimeoutError, ): @@ -67,7 +67,7 @@ async def test_form_unknown_exception(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.baf.config_flow.Device.run",), patch( + with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( "homeassistant.components.baf.config_flow.Device.async_wait_available", side_effect=Exception, ): From b499517cfdc31077cf8ecba5691baf4f1d94d7ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 16:24:03 -0500 Subject: [PATCH 33/36] Update homeassistant/components/baf/strings.json Co-authored-by: Paulus Schoutsen --- homeassistant/components/baf/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index 081517b20ddd2a..e9356a30d1ee35 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -4,7 +4,7 @@ "step": { "user": { "data": { - "ip_address": "IP Address" + "ip_address": "[%key:common::config_flow::data::ip%]" } }, "discovery_confirm": { From 65c17988785c6220567f87a668e6fc5b7660b70b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 16:25:46 -0500 Subject: [PATCH 34/36] fix copied code --- homeassistant/components/baf/fan.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 7ba3e224dbc4fe..360926363a57b6 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -4,7 +4,7 @@ import math from typing import Any -from aiobafi6 import Device, OffOnAuto +from aiobafi6 import OffOnAuto from homeassistant import config_entries from homeassistant.components.fan import ( @@ -33,7 +33,7 @@ async def async_setup_entry( """Set up SenseME fans.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] if data.device.has_fan: - async_add_entities([BAFFan(data.device)]) + async_add_entities([BAFFan(data.device, data.device.name)]) class BAFFan(BAFEntity, FanEntity): @@ -41,11 +41,7 @@ class BAFFan(BAFEntity, FanEntity): _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION _attr_preset_modes = [PRESET_MODE_AUTO] - - def __init__(self, device: Device) -> None: - """Initialize the entity.""" - super().__init__(device, device.name) - self._attr_speed_count = SPEED_COUNT + _attr_speed_count = SPEED_COUNT @callback def _async_update_attrs(self) -> None: From da45340e0a5d9b7f01bc3dc9cc3726c42e5b7e53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 16:28:38 -0500 Subject: [PATCH 35/36] Add ipv6 guard --- homeassistant/components/baf/config_flow.py | 3 +++ homeassistant/components/baf/strings.json | 1 + homeassistant/components/baf/translations/en.json | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 3a1b0c29c980d0..1c2873eb759110 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -13,6 +13,7 @@ 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 @@ -48,6 +49,8 @@ async def async_step_zeroconf( """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"] diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index e9356a30d1ee35..a26e3152326050 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -12,6 +12,7 @@ } }, "abort": { + "ipv6_not_supported": "IPv6 is not supported.", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { diff --git a/homeassistant/components/baf/translations/en.json b/homeassistant/components/baf/translations/en.json index 2458d782419699..4bb7256a69260f 100644 --- a/homeassistant/components/baf/translations/en.json +++ b/homeassistant/components/baf/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "ipv6_not_supported": "IPv6 is not supported." }, "error": { "cannot_connect": "Failed to connect", From dc017160f9e2485d8d73f444153ec5a87d79f154 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 16:30:14 -0500 Subject: [PATCH 36/36] cover --- tests/components/baf/test_config_flow.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index da560e78f9a687..687a871ed4c681 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -137,3 +137,22 @@ async def test_zeroconf_updates_existing_ip(hass): 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"