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/faa_delays/binary_sensor.py
homeassistant/components/fastdotcom/* homeassistant/components/fastdotcom/*
homeassistant/components/ffmpeg/camera.py 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/fints/sensor.py
homeassistant/components/fireservicerota/__init__.py homeassistant/components/fireservicerota/__init__.py
homeassistant/components/fireservicerota/binary_sensor.py homeassistant/components/fireservicerota/binary_sensor.py

View file

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

View file

@ -3,10 +3,13 @@ from __future__ import annotations
from collections import defaultdict from collections import defaultdict
import logging import logging
from typing import Any
from fiblary3.client.v4.client import Client as FibaroClient, StateHandler from fiblary3.client.v4.client import Client as FibaroClient, StateHandler
from fiblary3.common.exceptions import HTTPException
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ARMED, ATTR_ARMED,
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
@ -17,16 +20,17 @@ from homeassistant.const import (
CONF_URL, CONF_URL,
CONF_USERNAME, CONF_USERNAME,
CONF_WHITE_VALUE, CONF_WHITE_VALUE,
EVENT_HOMEASSISTANT_STOP,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import convert, slugify from homeassistant.util import convert, slugify
from .const import CONF_IMPORT_PLUGINS, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_POWER_W = "current_power_w" ATTR_CURRENT_POWER_W = "current_power_w"
@ -37,8 +41,7 @@ CONF_DIMMING = "dimming"
CONF_GATEWAYS = "gateways" CONF_GATEWAYS = "gateways"
CONF_PLUGINS = "plugins" CONF_PLUGINS = "plugins"
CONF_RESET_COLOR = "reset_color" CONF_RESET_COLOR = "reset_color"
DOMAIN = "fibaro" FIBARO_CONTROLLER = "fibaro_controller"
FIBARO_CONTROLLERS = "fibaro_controllers"
FIBARO_DEVICES = "fibaro_devices" FIBARO_DEVICES = "fibaro_devices"
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
@ -102,11 +105,14 @@ GATEWAY_CONFIG = vol.Schema(
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])} {vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])}
) )
}, },
),
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
@ -116,21 +122,19 @@ class FibaroController:
def __init__(self, config): def __init__(self, config):
"""Initialize the Fibaro controller.""" """Initialize the Fibaro controller."""
self._client = FibaroClient( self._client = FibaroClient(
config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD]
) )
self._scene_map = None self._scene_map = None
# Whether to import devices from plugins # Whether to import devices from plugins
self._import_plugins = config[CONF_PLUGINS] self._import_plugins = config[CONF_IMPORT_PLUGINS]
self._device_config = config[CONF_DEVICE_CONFIG]
self._room_map = None # Mapping roomId to room object self._room_map = None # Mapping roomId to room object
self._device_map = None # Mapping deviceId to device object self._device_map = None # Mapping deviceId to device object
self.fibaro_devices = None # List of devices by type self.fibaro_devices = None # List of devices by type
self._callbacks = {} # Update value callbacks by deviceId self._callbacks = {} # Update value callbacks by deviceId
self._state_handler = None # Fiblary's StateHandler object 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.hub_serial = None # Unique serial number of the hub
self.name = None # The friendly name of the hub
def connect(self): def connect(self):
"""Start the communication with the Fibaro controller.""" """Start the communication with the Fibaro controller."""
@ -138,6 +142,7 @@ class FibaroController:
login = self._client.login.get() login = self._client.login.get()
info = self._client.info.get() info = self._client.info.get()
self.hub_serial = slugify(info.serialNumber) self.hub_serial = slugify(info.serialNumber)
self.name = slugify(info.hcName)
except AssertionError: except AssertionError:
_LOGGER.error("Can't connect to Fibaro HC. Please check URL") _LOGGER.error("Can't connect to Fibaro HC. Please check URL")
return False return False
@ -152,6 +157,23 @@ class FibaroController:
self._read_scenes() self._read_scenes()
return True 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): def enable_state_handler(self):
"""Start StateHandler thread for monitoring updates.""" """Start StateHandler thread for monitoring updates."""
self._state_handler = StateHandler(self._client, self._on_state_change) self._state_handler = StateHandler(self._client, self._on_state_change)
@ -299,16 +321,11 @@ class FibaroController:
device.ha_id = ( device.ha_id = (
f"{slugify(room_name)}_{slugify(device.name)}_{device.id}" f"{slugify(room_name)}_{slugify(device.name)}_{device.id}"
) )
if ( if device.enabled and (
device.enabled
and (
"isPlugin" not in device "isPlugin" not in device
or (not device.isPlugin or self._import_plugins) or (not device.isPlugin or self._import_plugins)
)
and device.ha_id not in self._excluded_devices
): ):
device.mapped_type = self._map_device_to_type(device) device.mapped_type = self._map_device_to_type(device)
device.device_config = self._device_config.get(device.ha_id, {})
else: else:
device.mapped_type = None device.mapped_type = None
if (dtype := device.mapped_type) is None: if (dtype := device.mapped_type) is None:
@ -357,40 +374,79 @@ class FibaroController:
pass pass
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
"""Set up the Fibaro Component.""" """Migrate configuration from configuration.yaml."""
if DOMAIN not in base_config:
return True
gateways = base_config[DOMAIN][CONF_GATEWAYS] gateways = base_config[DOMAIN][CONF_GATEWAYS]
hass.data[FIBARO_CONTROLLERS] = {} if gateways is None:
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)
return True return True
# 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 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): class FibaroDevice(Entity):
"""Representation of a Fibaro device entity.""" """Representation of a Fibaro device entity."""
@ -519,3 +575,11 @@ class FibaroDevice(Entity):
pass pass
return attr 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 __future__ import annotations
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DOMAIN, ENTITY_ID_FORMAT,
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import FIBARO_DEVICES, FibaroDevice from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
SENSOR_TYPES = { SENSOR_TYPES = {
"com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"],
@ -28,20 +28,18 @@ SENSOR_TYPES = {
} }
def setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, entry: ConfigEntry,
add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Perform the setup for Fibaro controller devices.""" """Perform the setup for Fibaro controller devices."""
if discovery_info is None: async_add_entities(
return
add_entities(
[ [
FibaroBinarySensor(device) 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, True,
) )
@ -54,9 +52,8 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._state = None self._state = None
super().__init__(fibaro_device) super().__init__(fibaro_device)
self.entity_id = f"{DOMAIN}.{self.ha_id}" self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None stype = None
devconf = fibaro_device.device_config
if fibaro_device.type in SENSOR_TYPES: if fibaro_device.type in SENSOR_TYPES:
stype = fibaro_device.type stype = fibaro_device.type
elif fibaro_device.baseType in SENSOR_TYPES: elif fibaro_device.baseType in SENSOR_TYPES:
@ -67,9 +64,6 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
else: else:
self._device_class = None self._device_class = None
self._icon = 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 @property
def icon(self): def icon(self):

View file

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

View file

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

View file

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

View file

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

View file

@ -4,25 +4,26 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.scene import Scene from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import FIBARO_DEVICES, FibaroDevice from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Perform the setup for Fibaro scenes.""" """Perform the setup for Fibaro scenes."""
if discovery_info is None:
return
async_add_entities( 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 contextlib import suppress
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
DOMAIN, ENTITY_ID_FORMAT,
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
@ -19,10 +20,10 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import convert from homeassistant.util import convert
from . import FIBARO_DEVICES, FibaroDevice from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
SENSOR_TYPES = { SENSOR_TYPES = {
"com.fibaro.temperatureSensor": [ "com.fibaro.temperatureSensor": [
@ -54,25 +55,21 @@ SENSOR_TYPES = {
} }
def setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, entry: ConfigEntry,
add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Fibaro controller devices.""" """Set up the Fibaro controller devices."""
if discovery_info is None:
return
entities: list[SensorEntity] = [] 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)) entities.append(FibaroSensor(device))
for device_type in ("cover", "light", "switch"): 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: if "energy" in device.interfaces:
entities.append(FibaroEnergySensor(device)) entities.append(FibaroEnergySensor(device))
add_entities(entities, True) async_add_entities(entities, True)
class FibaroSensor(FibaroDevice, SensorEntity): class FibaroSensor(FibaroDevice, SensorEntity):
@ -83,7 +80,7 @@ class FibaroSensor(FibaroDevice, SensorEntity):
self.current_value = None self.current_value = None
self.last_changed_time = None self.last_changed_time = None
super().__init__(fibaro_device) 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: if fibaro_device.type in SENSOR_TYPES:
self._unit = SENSOR_TYPES[fibaro_device.type][1] self._unit = SENSOR_TYPES[fibaro_device.type][1]
self._icon = SENSOR_TYPES[fibaro_device.type][2] self._icon = SENSOR_TYPES[fibaro_device.type][2]
@ -139,7 +136,7 @@ class FibaroEnergySensor(FibaroDevice, SensorEntity):
def __init__(self, fibaro_device): def __init__(self, fibaro_device):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(fibaro_device) 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_name = f"{fibaro_device.friendly_name} Energy"
self._attr_unique_id = f"{fibaro_device.unique_id_str}_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.""" """Support for Fibaro switches."""
from __future__ import annotations 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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import convert from homeassistant.util import convert
from . import FIBARO_DEVICES, FibaroDevice from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN
def setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, entry: ConfigEntry,
add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Fibaro switches.""" """Set up the Fibaro switches."""
if discovery_info is None: async_add_entities(
return [
FibaroSwitch(device)
add_entities( for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["switch"]
[FibaroSwitch(device) for device in hass.data[FIBARO_DEVICES]["switch"]], True ],
True,
) )
@ -32,7 +33,7 @@ class FibaroSwitch(FibaroDevice, SwitchEntity):
"""Initialize the Fibaro device.""" """Initialize the Fibaro device."""
self._state = False self._state = False
super().__init__(fibaro_device) 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): def turn_on(self, **kwargs):
"""Turn device on.""" """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", "evil_genius_labs",
"ezviz", "ezviz",
"faa_delays", "faa_delays",
"fibaro",
"filesize", "filesize",
"fireservicerota", "fireservicerota",
"fivem", "fivem",

View file

@ -433,6 +433,9 @@ faadelays==0.0.7
# homeassistant.components.feedreader # homeassistant.components.feedreader
feedparser==6.0.2 feedparser==6.0.2
# homeassistant.components.fibaro
fiblary3==0.1.8
# homeassistant.components.fivem # homeassistant.components.fivem
fivem-api==0.1.2 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,
}