Add config flow to fibaro (#65203)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
rappenze 2022-03-26 20:50:50 +01:00 committed by GitHub
parent 00b53502fb
commit e844c2380a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 565 additions and 157 deletions

View File

@ -335,7 +335,16 @@ omit =
homeassistant/components/faa_delays/binary_sensor.py
homeassistant/components/fastdotcom/*
homeassistant/components/ffmpeg/camera.py
homeassistant/components/fibaro/*
homeassistant/components/fibaro/__init__.py
homeassistant/components/fibaro/binary_sensor.py
homeassistant/components/fibaro/climate.py
homeassistant/components/fibaro/cover.py
homeassistant/components/fibaro/light.py
homeassistant/components/fibaro/lock.py
homeassistant/components/fibaro/scene.py
homeassistant/components/fibaro/sensor.py
homeassistant/components/fibaro/switch.py
homeassistant/components/filesize/sensor.py
homeassistant/components/fints/sensor.py
homeassistant/components/fireservicerota/__init__.py
homeassistant/components/fireservicerota/binary_sensor.py

View File

@ -306,6 +306,8 @@ tests/components/faa_delays/* @ntilley905
homeassistant/components/fan/* @home-assistant/core
tests/components/fan/* @home-assistant/core
homeassistant/components/fastdotcom/* @rohankapoorcom
homeassistant/components/fibaro/* @rappenze
tests/components/fibaro/* @rappenze
homeassistant/components/file/* @fabaff
tests/components/file/* @fabaff
homeassistant/components/filesize/* @gjohansson-ST

View File

@ -3,10 +3,13 @@ from __future__ import annotations
from collections import defaultdict
import logging
from typing import Any
from fiblary3.client.v4.client import Client as FibaroClient, StateHandler
from fiblary3.common.exceptions import HTTPException
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_ARMED,
ATTR_BATTERY_LEVEL,
@ -17,16 +20,17 @@ from homeassistant.const import (
CONF_URL,
CONF_USERNAME,
CONF_WHITE_VALUE,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import convert, slugify
from .const import CONF_IMPORT_PLUGINS, DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_POWER_W = "current_power_w"
@ -37,8 +41,7 @@ CONF_DIMMING = "dimming"
CONF_GATEWAYS = "gateways"
CONF_PLUGINS = "plugins"
CONF_RESET_COLOR = "reset_color"
DOMAIN = "fibaro"
FIBARO_CONTROLLERS = "fibaro_controllers"
FIBARO_CONTROLLER = "fibaro_controller"
FIBARO_DEVICES = "fibaro_devices"
PLATFORMS = [
Platform.BINARY_SENSOR,
@ -102,11 +105,14 @@ GATEWAY_CONFIG = vol.Schema(
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])}
)
},
vol.All(
cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])}
)
},
),
extra=vol.ALLOW_EXTRA,
)
@ -116,21 +122,19 @@ class FibaroController:
def __init__(self, config):
"""Initialize the Fibaro controller."""
self._client = FibaroClient(
config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD]
)
self._scene_map = None
# Whether to import devices from plugins
self._import_plugins = config[CONF_PLUGINS]
self._device_config = config[CONF_DEVICE_CONFIG]
self._import_plugins = config[CONF_IMPORT_PLUGINS]
self._room_map = None # Mapping roomId to room object
self._device_map = None # Mapping deviceId to device object
self.fibaro_devices = None # List of devices by type
self._callbacks = {} # Update value callbacks by deviceId
self._state_handler = None # Fiblary's StateHandler object
self._excluded_devices = config[CONF_EXCLUDE]
self.hub_serial = None # Unique serial number of the hub
self.name = None # The friendly name of the hub
def connect(self):
"""Start the communication with the Fibaro controller."""
@ -138,6 +142,7 @@ class FibaroController:
login = self._client.login.get()
info = self._client.info.get()
self.hub_serial = slugify(info.serialNumber)
self.name = slugify(info.hcName)
except AssertionError:
_LOGGER.error("Can't connect to Fibaro HC. Please check URL")
return False
@ -152,6 +157,23 @@ class FibaroController:
self._read_scenes()
return True
def connect_with_error_handling(self) -> None:
"""Translate connect errors to easily differentiate auth and connect failures.
When there is a better error handling in the used library this can be improved.
"""
try:
connected = self.connect()
if not connected:
raise FibaroConnectFailed("Connect status is false")
except HTTPException as http_ex:
if http_ex.details == "Forbidden":
raise FibaroAuthFailed from http_ex
raise FibaroConnectFailed from http_ex
except Exception as ex:
raise FibaroConnectFailed from ex
def enable_state_handler(self):
"""Start StateHandler thread for monitoring updates."""
self._state_handler = StateHandler(self._client, self._on_state_change)
@ -299,16 +321,11 @@ class FibaroController:
device.ha_id = (
f"{slugify(room_name)}_{slugify(device.name)}_{device.id}"
)
if (
device.enabled
and (
"isPlugin" not in device
or (not device.isPlugin or self._import_plugins)
)
and device.ha_id not in self._excluded_devices
if device.enabled and (
"isPlugin" not in device
or (not device.isPlugin or self._import_plugins)
):
device.mapped_type = self._map_device_to_type(device)
device.device_config = self._device_config.get(device.ha_id, {})
else:
device.mapped_type = None
if (dtype := device.mapped_type) is None:
@ -357,39 +374,78 @@ class FibaroController:
pass
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
"""Set up the Fibaro Component."""
async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
"""Migrate configuration from configuration.yaml."""
if DOMAIN not in base_config:
return True
gateways = base_config[DOMAIN][CONF_GATEWAYS]
hass.data[FIBARO_CONTROLLERS] = {}
def stop_fibaro(event):
"""Stop Fibaro Thread."""
_LOGGER.info("Shutting down Fibaro connection")
for controller in hass.data[FIBARO_CONTROLLERS].values():
controller.disable_state_handler()
hass.data[FIBARO_DEVICES] = {}
for platform in PLATFORMS:
hass.data[FIBARO_DEVICES][platform] = []
for gateway in gateways:
controller = FibaroController(gateway)
if controller.connect():
hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller
for platform in PLATFORMS:
hass.data[FIBARO_DEVICES][platform].extend(
controller.fibaro_devices[platform]
)
if hass.data[FIBARO_CONTROLLERS]:
for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, base_config)
for controller in hass.data[FIBARO_CONTROLLERS].values():
controller.enable_state_handler()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro)
if gateways is None:
return True
return False
# check if already configured
if hass.config_entries.async_entries(DOMAIN):
return True
for gateway in gateways:
# prepare new config based on configuration.yaml
conf = {
CONF_URL: gateway[CONF_URL],
CONF_USERNAME: gateway[CONF_USERNAME],
CONF_PASSWORD: gateway[CONF_PASSWORD],
CONF_IMPORT_PLUGINS: gateway[CONF_PLUGINS],
}
# import into config flow based configuration
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
return True
def _init_controller(data: dict[str, Any]) -> FibaroController:
"""Validate the user input allows us to connect to fibaro."""
controller = FibaroController(data)
controller.connect_with_error_handling()
return controller
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Fibaro Component."""
try:
controller = await hass.async_add_executor_job(_init_controller, entry.data)
except FibaroConnectFailed as connect_ex:
raise ConfigEntryNotReady(
f"Could not connect to controller at {entry.data[CONF_URL]}"
) from connect_ex
except FibaroAuthFailed:
return False
data: dict[str, Any] = {}
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data
data[FIBARO_CONTROLLER] = controller
devices = data[FIBARO_DEVICES] = {}
for platform in PLATFORMS:
devices[platform] = [*controller.fibaro_devices[platform]]
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
controller.enable_state_handler()
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
_LOGGER.info("Shutting down Fibaro connection")
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN][entry.entry_id][FIBARO_CONTROLLER].disable_state_handler()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class FibaroDevice(Entity):
@ -519,3 +575,11 @@ class FibaroDevice(Entity):
pass
return attr
class FibaroConnectFailed(HomeAssistantError):
"""Error to indicate we cannot connect to fibaro home center."""
class FibaroAuthFailed(HomeAssistantError):
"""Error to indicate that authentication failed on fibaro home center."""

View File

@ -2,16 +2,16 @@
from __future__ import annotations
from homeassistant.components.binary_sensor import (
DOMAIN,
ENTITY_ID_FORMAT,
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
SENSOR_TYPES = {
"com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"],
@ -28,20 +28,18 @@ SENSOR_TYPES = {
}
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Perform the setup for Fibaro controller devices."""
if discovery_info is None:
return
add_entities(
async_add_entities(
[
FibaroBinarySensor(device)
for device in hass.data[FIBARO_DEVICES]["binary_sensor"]
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][
"binary_sensor"
]
],
True,
)
@ -54,9 +52,8 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
"""Initialize the binary_sensor."""
self._state = None
super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}"
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None
devconf = fibaro_device.device_config
if fibaro_device.type in SENSOR_TYPES:
stype = fibaro_device.type
elif fibaro_device.baseType in SENSOR_TYPES:
@ -67,9 +64,6 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
else:
self._device_class = None
self._icon = None
# device_config overrides:
self._device_class = devconf.get(CONF_DEVICE_CLASS, self._device_class)
self._icon = devconf.get(CONF_ICON, self._icon)
@property
def icon(self):

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity
from homeassistant.components.climate.const import (
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
@ -17,12 +17,13 @@ from homeassistant.components.climate.const import (
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
PRESET_RESUME = "resume"
PRESET_MOIST = "moist"
@ -98,18 +99,17 @@ HA_OPMODES_HVAC = {
}
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Perform the setup for Fibaro controller devices."""
if discovery_info is None:
return
add_entities(
[FibaroThermostat(device) for device in hass.data[FIBARO_DEVICES]["climate"]],
async_add_entities(
[
FibaroThermostat(device)
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["climate"]
],
True,
)
@ -125,7 +125,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
self._op_mode_device = None
self._fan_mode_device = None
self._support_flags = 0
self.entity_id = f"climate.{self.ha_id}"
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
self._hvac_support = []
self._preset_support = []
self._fan_support = []

View File

@ -0,0 +1,81 @@
"""Config flow for Fibaro integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.typing import ConfigType
from . import FibaroAuthFailed, FibaroConnectFailed, FibaroController
from .const import CONF_IMPORT_PLUGINS, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_IMPORT_PLUGINS, default=False): bool,
}
)
def _connect_to_fibaro(data: dict[str, Any]) -> FibaroController:
"""Validate the user input allows us to connect to fibaro."""
controller = FibaroController(data)
controller.connect_with_error_handling()
return controller
async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
controller = await hass.async_add_executor_job(_connect_to_fibaro, data)
_LOGGER.debug(
"Successfully connected to fibaro home center %s with name %s",
controller.hub_serial,
controller.name,
)
return {"serial_number": controller.hub_serial, "name": controller.name}
class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fibaro."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await _validate_input(self.hass, user_input)
except FibaroConnectFailed:
errors["base"] = "cannot_connect"
except FibaroAuthFailed:
errors["base"] = "invalid_auth"
else:
await self.async_set_unique_id(info["serial_number"])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["name"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_import(self, import_config: ConfigType | None) -> FlowResult:
"""Import a config entry."""
return await self.async_step_user(import_config)

View File

@ -0,0 +1,4 @@
"""Constants for the Fibaro integration."""
DOMAIN = "fibaro"
CONF_IMPORT_PLUGINS = "import_plugins"

View File

@ -4,28 +4,29 @@ from __future__ import annotations
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
DOMAIN,
ENTITY_ID_FORMAT,
CoverEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Fibaro covers."""
if discovery_info is None:
return
add_entities(
[FibaroCover(device) for device in hass.data[FIBARO_DEVICES]["cover"]], True
async_add_entities(
[
FibaroCover(device)
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["cover"]
],
True,
)
@ -35,7 +36,7 @@ class FibaroCover(FibaroDevice, CoverEntity):
def __init__(self, fibaro_device):
"""Initialize the Vera device."""
super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}"
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@staticmethod
def bound(position):

View File

@ -8,19 +8,19 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_HS_COLOR,
ATTR_WHITE_VALUE,
DOMAIN,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_WHITE_VALUE,
LightEntity,
)
from homeassistant.const import CONF_WHITE_VALUE
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.color as color_util
from . import CONF_COLOR, CONF_DIMMING, CONF_RESET_COLOR, FIBARO_DEVICES, FibaroDevice
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
def scaleto255(value):
@ -40,18 +40,18 @@ def scaleto100(value):
return max(0, min(100, ((value * 100.0) / 255.0)))
async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Perform the setup for Fibaro controller devices."""
if discovery_info is None:
return
async_add_entities(
[FibaroLight(device) for device in hass.data[FIBARO_DEVICES]["light"]], True
[
FibaroLight(device)
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["light"]
],
True,
)
@ -67,8 +67,7 @@ class FibaroLight(FibaroDevice, LightEntity):
self._update_lock = asyncio.Lock()
self._white = 0
devconf = fibaro_device.device_config
self._reset_color = devconf.get(CONF_RESET_COLOR, False)
self._reset_color = False
supports_color = (
"color" in fibaro_device.properties
or "colorComponents" in fibaro_device.properties
@ -91,15 +90,15 @@ class FibaroLight(FibaroDevice, LightEntity):
)
# Configuration can override default capability detection
if devconf.get(CONF_DIMMING, supports_dimming):
if supports_dimming:
self._supported_flags |= SUPPORT_BRIGHTNESS
if devconf.get(CONF_COLOR, supports_color):
if supports_color:
self._supported_flags |= SUPPORT_COLOR
if devconf.get(CONF_WHITE_VALUE, supports_white_v):
if supports_white_v:
self._supported_flags |= SUPPORT_WHITE_VALUE
super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}"
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@property
def brightness(self):

View File

@ -1,26 +1,27 @@
"""Support for Fibaro locks."""
from __future__ import annotations
from homeassistant.components.lock import DOMAIN, LockEntity
from homeassistant.components.lock import ENTITY_ID_FORMAT, LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Fibaro locks."""
if discovery_info is None:
return
add_entities(
[FibaroLock(device) for device in hass.data[FIBARO_DEVICES]["lock"]], True
async_add_entities(
[
FibaroLock(device)
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["lock"]
],
True,
)
@ -31,7 +32,7 @@ class FibaroLock(FibaroDevice, LockEntity):
"""Initialize the Fibaro device."""
self._state = False
super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}"
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
def lock(self, **kwargs):
"""Lock the device."""

View File

@ -3,7 +3,8 @@
"name": "Fibaro",
"documentation": "https://www.home-assistant.io/integrations/fibaro",
"requirements": ["fiblary3==0.1.8"],
"codeowners": [],
"codeowners": ["@rappenze"],
"iot_class": "local_push",
"config_flow": true,
"loggers": ["fiblary3"]
}

View File

@ -4,25 +4,26 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Perform the setup for Fibaro scenes."""
if discovery_info is None:
return
async_add_entities(
[FibaroScene(scene) for scene in hass.data[FIBARO_DEVICES]["scene"]], True
[
FibaroScene(scene)
for scene in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["scene"]
],
True,
)

View File

@ -4,11 +4,12 @@ from __future__ import annotations
from contextlib import suppress
from homeassistant.components.sensor import (
DOMAIN,
ENTITY_ID_FORMAT,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
ENERGY_KILO_WATT_HOUR,
@ -19,10 +20,10 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import convert
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
SENSOR_TYPES = {
"com.fibaro.temperatureSensor": [
@ -54,25 +55,21 @@ SENSOR_TYPES = {
}
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Fibaro controller devices."""
if discovery_info is None:
return
entities: list[SensorEntity] = []
for device in hass.data[FIBARO_DEVICES]["sensor"]:
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["sensor"]:
entities.append(FibaroSensor(device))
for device_type in ("cover", "light", "switch"):
for device in hass.data[FIBARO_DEVICES][device_type]:
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][device_type]:
if "energy" in device.interfaces:
entities.append(FibaroEnergySensor(device))
add_entities(entities, True)
async_add_entities(entities, True)
class FibaroSensor(FibaroDevice, SensorEntity):
@ -83,7 +80,7 @@ class FibaroSensor(FibaroDevice, SensorEntity):
self.current_value = None
self.last_changed_time = None
super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}"
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
if fibaro_device.type in SENSOR_TYPES:
self._unit = SENSOR_TYPES[fibaro_device.type][1]
self._icon = SENSOR_TYPES[fibaro_device.type][2]
@ -139,7 +136,7 @@ class FibaroEnergySensor(FibaroDevice, SensorEntity):
def __init__(self, fibaro_device):
"""Initialize the sensor."""
super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}_energy"
self.entity_id = ENTITY_ID_FORMAT.format(f"{self.ha_id}_energy")
self._attr_name = f"{fibaro_device.friendly_name} Energy"
self._attr_unique_id = f"{fibaro_device.unique_id_str}_energy"

View File

@ -0,0 +1,22 @@
{
"config": {
"step": {
"user": {
"data": {
"url": "URL in the format http://HOST/api/",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"import_plugins": "Import entities from fibaro plugins?"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -1,27 +1,28 @@
"""Support for Fibaro switches."""
from __future__ import annotations
from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import convert
from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Fibaro switches."""
if discovery_info is None:
return
add_entities(
[FibaroSwitch(device) for device in hass.data[FIBARO_DEVICES]["switch"]], True
async_add_entities(
[
FibaroSwitch(device)
for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["switch"]
],
True,
)
@ -32,7 +33,7 @@ class FibaroSwitch(FibaroDevice, SwitchEntity):
"""Initialize the Fibaro device."""
self._state = False
super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}"
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
def turn_on(self, **kwargs):
"""Turn device on."""

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"url": "URL in the format http://HOST/api/",
"import_plugins": "Import entities from fibaro plugins?",
"password": "Password",
"username": "Username"
}
}
}
}
}

View File

@ -97,6 +97,7 @@ FLOWS = {
"evil_genius_labs",
"ezviz",
"faa_delays",
"fibaro",
"filesize",
"fireservicerota",
"fivem",

View File

@ -433,6 +433,9 @@ faadelays==0.0.7
# homeassistant.components.feedreader
feedparser==6.0.2
# homeassistant.components.fibaro
fiblary3==0.1.8
# homeassistant.components.fivem
fivem-api==0.1.2

View File

@ -0,0 +1 @@
"""Tests for the Fibaro integration."""

View File

@ -0,0 +1,204 @@
"""Test the Fibaro config flow."""
from unittest.mock import Mock, patch
from fiblary3.common.exceptions import HTTPException
import pytest
from homeassistant import config_entries
from homeassistant.components.fibaro import DOMAIN
from homeassistant.components.fibaro.const import CONF_IMPORT_PLUGINS
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
TEST_SERIALNUMBER = "HC2-111111"
TEST_NAME = "my_fibaro_home_center"
TEST_URL = "http://192.168.1.1/api/"
TEST_USERNAME = "user"
TEST_PASSWORD = "password"
@pytest.fixture(name="fibaro_client", autouse=True)
def fibaro_client_fixture():
"""Mock common methods and attributes of fibaro client."""
info_mock = Mock()
info_mock.get.return_value = Mock(serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME)
array_mock = Mock()
array_mock.list.return_value = []
with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch(
"fiblary3.client.v4.client.Client.info",
info_mock,
create=True,
), patch("fiblary3.client.v4.client.Client.rooms", array_mock, create=True,), patch(
"fiblary3.client.v4.client.Client.devices",
array_mock,
create=True,
), patch(
"fiblary3.client.v4.client.Client.scenes",
array_mock,
create=True,
):
yield
async def test_config_flow_user_initiated_success(hass):
"""Successful flow manually initialized by the user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.return_value = Mock(status=True)
with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_IMPORT_PLUGINS: False,
}
async def test_config_flow_user_initiated_connect_failure(hass):
"""Connect failure in flow manually initialized by the user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.return_value = Mock(status=False)
with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_config_flow_user_initiated_auth_failure(hass):
"""Authentication failure in flow manually initialized by the user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.side_effect = HTTPException(details="Forbidden")
with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "invalid_auth"}
async def test_config_flow_user_initiated_unknown_failure_1(hass):
"""Unknown failure in flow manually initialized by the user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.side_effect = HTTPException(details="Any")
with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_config_flow_user_initiated_unknown_failure_2(hass):
"""Unknown failure in flow manually initialized by the user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_config_flow_import(hass):
"""Test for importing config from configuration.yaml."""
login_mock = Mock()
login_mock.get.return_value = Mock(status=True)
with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_IMPORT_PLUGINS: False,
},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_URL: TEST_URL,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_IMPORT_PLUGINS: False,
}