Support for multiple Fibaro gateways (#19705)

* Preparing for transition to config flow

Added multiple gateway support
Reworked parameter flow to platforms to enable multiple controllers
Breaking change to config, now a list of gateways is expected instead of a single config

* Updated coveragerc

Added new location of fibaro component

* Fixes based on code review and extended logging

Addressed issues raised by code review
Added extended debug logging to get better reports from users if the device type mapping is not perfect

* Changhes based on code review

Changes to how configuration is read and schemas
Fix to device type mapping logic

* simplified reading config

* oops

oops

* grr

grr

* change based on code review

* changes based on code review

changes based on code review
This commit is contained in:
pbalogh77 2019-01-12 00:29:54 +01:00 committed by Paulus Schoutsen
parent d820efc4e3
commit 7dac7b9e5e
8 changed files with 78 additions and 58 deletions

View file

@ -127,7 +127,7 @@ omit =
homeassistant/components/eufy.py homeassistant/components/eufy.py
homeassistant/components/*/eufy.py homeassistant/components/*/eufy.py
homeassistant/components/fibaro.py homeassistant/components/fibaro/__init__.py
homeassistant/components/*/fibaro.py homeassistant/components/*/fibaro.py
homeassistant/components/gc100.py homeassistant/components/gc100.py

View file

@ -9,7 +9,7 @@ import logging
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT) BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_DEVICES, FibaroDevice)
from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON) from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON)
DEPENDENCIES = ['fibaro'] DEPENDENCIES = ['fibaro']
@ -33,17 +33,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
add_entities( add_entities(
[FibaroBinarySensor(device, hass.data[FIBARO_CONTROLLER]) [FibaroBinarySensor(device)
for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True) for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True)
class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
"""Representation of a Fibaro Binary Sensor.""" """Representation of a Fibaro Binary Sensor."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._state = None self._state = None
super().__init__(fibaro_device, controller) super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None stype = None
devconf = fibaro_device.device_config devconf = fibaro_device.device_config

View file

@ -9,7 +9,7 @@ import logging
from homeassistant.components.cover import ( from homeassistant.components.cover import (
CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION) CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_DEVICES, FibaroDevice)
DEPENDENCIES = ['fibaro'] DEPENDENCIES = ['fibaro']
@ -22,16 +22,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
add_entities( add_entities(
[FibaroCover(device, hass.data[FIBARO_CONTROLLER]) for [FibaroCover(device) for
device in hass.data[FIBARO_DEVICES]['cover']], True) device in hass.data[FIBARO_DEVICES]['cover']], True)
class FibaroCover(FibaroDevice, CoverDevice): class FibaroCover(FibaroDevice, CoverDevice):
"""Representation a Fibaro Cover.""" """Representation a Fibaro Cover."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the Vera device.""" """Initialize the Vera device."""
super().__init__(fibaro_device, controller) super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@staticmethod @staticmethod

View file

@ -11,8 +11,8 @@ from typing import Optional
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ARMED, ATTR_BATTERY_LEVEL, CONF_DEVICE_CLASS, ATTR_ARMED, ATTR_BATTERY_LEVEL, CONF_DEVICE_CLASS, CONF_EXCLUDE,
CONF_EXCLUDE, CONF_ICON, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_ICON, CONF_PASSWORD, CONF_URL, CONF_USERNAME,
CONF_WHITE_VALUE, EVENT_HOMEASSISTANT_STOP) CONF_WHITE_VALUE, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -24,10 +24,11 @@ REQUIREMENTS = ['fiblary3==0.1.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'fibaro' DOMAIN = 'fibaro'
FIBARO_DEVICES = 'fibaro_devices' FIBARO_DEVICES = 'fibaro_devices'
FIBARO_CONTROLLER = 'fibaro_controller' FIBARO_CONTROLLERS = 'fibaro_controllers'
ATTR_CURRENT_POWER_W = "current_power_w" ATTR_CURRENT_POWER_W = "current_power_w"
ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh"
CONF_PLUGINS = "plugins" CONF_PLUGINS = "plugins"
CONF_GATEWAYS = 'gateways'
CONF_DIMMING = "dimming" CONF_DIMMING = "dimming"
CONF_COLOR = "color" CONF_COLOR = "color"
CONF_RESET_COLOR = "reset_color" CONF_RESET_COLOR = "reset_color"
@ -65,15 +66,20 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
FIBARO_ID_LIST_SCHEMA = vol.Schema([cv.string]) FIBARO_ID_LIST_SCHEMA = vol.Schema([cv.string])
GATEWAY_CONFIG = vol.Schema({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_URL): cv.url,
vol.Optional(CONF_PLUGINS, default=False): cv.boolean,
vol.Optional(CONF_EXCLUDE, default=[]): FIBARO_ID_LIST_SCHEMA,
vol.Optional(CONF_DEVICE_CONFIG, default={}):
vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY})
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_GATEWAYS):
vol.Required(CONF_USERNAME): cv.string, vol.All(cv.ensure_list, [GATEWAY_CONFIG])
vol.Required(CONF_URL): cv.url,
vol.Optional(CONF_PLUGINS, default=False): cv.boolean,
vol.Optional(CONF_EXCLUDE, default=[]): FIBARO_ID_LIST_SCHEMA,
vol.Optional(CONF_DEVICE_CONFIG, default={}):
vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY})
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -81,20 +87,23 @@ CONFIG_SCHEMA = vol.Schema({
class FibaroController(): class FibaroController():
"""Initiate Fibaro Controller Class.""" """Initiate Fibaro Controller Class."""
def __init__(self, username, password, url, import_plugins, config): def __init__(self, config):
"""Initialize the Fibaro controller.""" """Initialize the Fibaro controller."""
from fiblary3.client.v4.client import Client as FibaroClient from fiblary3.client.v4.client import Client as FibaroClient
self._client = FibaroClient(url, username, password)
self._client = FibaroClient(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 = import_plugins self._import_plugins = config[CONF_PLUGINS]
self._device_config = config[CONF_DEVICE_CONFIG] 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.get(CONF_EXCLUDE, []) 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
def connect(self): def connect(self):
@ -167,12 +176,11 @@ class FibaroController():
def _map_device_to_type(device): def _map_device_to_type(device):
"""Map device to HA device type.""" """Map device to HA device type."""
# Use our lookup table to identify device type # Use our lookup table to identify device type
device_type = None
if 'type' in device: if 'type' in device:
device_type = FIBARO_TYPEMAP.get(device.type) device_type = FIBARO_TYPEMAP.get(device.type)
elif 'baseType' in device: if device_type is None and 'baseType' in device:
device_type = FIBARO_TYPEMAP.get(device.baseType) device_type = FIBARO_TYPEMAP.get(device.baseType)
else:
device_type = None
# We can also identify device type by its capabilities # We can also identify device type by its capabilities
if device_type is None: if device_type is None:
@ -200,6 +208,7 @@ class FibaroController():
for device in scenes: for device in scenes:
if not device.visible: if not device.visible:
continue continue
device.fibaro_controller = self
if device.roomID == 0: if device.roomID == 0:
room_name = 'Unknown' room_name = 'Unknown'
else: else:
@ -220,6 +229,7 @@ class FibaroController():
self.fibaro_devices = defaultdict(list) self.fibaro_devices = defaultdict(list)
for device in devices: for device in devices:
try: try:
device.fibaro_controller = self
if device.roomID == 0: if device.roomID == 0:
room_name = 'Unknown' room_name = 'Unknown'
else: else:
@ -242,33 +252,43 @@ class FibaroController():
self.hub_serial, device.id) self.hub_serial, device.id)
self._device_map[device.id] = device self._device_map[device.id] = device
self.fibaro_devices[device.mapped_type].append(device) self.fibaro_devices[device.mapped_type].append(device)
else: _LOGGER.debug("%s (%s, %s) -> %s. Prop: %s Actions: %s",
_LOGGER.debug("%s (%s, %s) not used", device.ha_id, device.type,
device.ha_id, device.type, device.baseType, device.mapped_type,
device.baseType) str(device.properties), str(device.actions))
except (KeyError, ValueError): except (KeyError, ValueError):
pass pass
def setup(hass, config): def setup(hass, base_config):
"""Set up the Fibaro Component.""" """Set up the Fibaro Component."""
hass.data[FIBARO_CONTROLLER] = controller = \ gateways = base_config[DOMAIN][CONF_GATEWAYS]
FibaroController(config[DOMAIN][CONF_USERNAME], hass.data[FIBARO_CONTROLLERS] = {}
config[DOMAIN][CONF_PASSWORD],
config[DOMAIN][CONF_URL],
config[DOMAIN][CONF_PLUGINS],
config[DOMAIN])
def stop_fibaro(event): def stop_fibaro(event):
"""Stop Fibaro Thread.""" """Stop Fibaro Thread."""
_LOGGER.info("Shutting down Fibaro connection") _LOGGER.info("Shutting down Fibaro connection")
hass.data[FIBARO_CONTROLLER].disable_state_handler() for controller in hass.data[FIBARO_CONTROLLERS].values():
controller.disable_state_handler()
if controller.connect(): hass.data[FIBARO_DEVICES] = {}
hass.data[FIBARO_DEVICES] = controller.fibaro_devices for component in FIBARO_COMPONENTS:
hass.data[FIBARO_DEVICES][component] = []
for gateway in gateways:
controller = FibaroController(gateway)
if controller.connect():
hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller
for component in FIBARO_COMPONENTS:
hass.data[FIBARO_DEVICES][component].extend(
controller.fibaro_devices[component])
if hass.data[FIBARO_CONTROLLERS]:
for component in FIBARO_COMPONENTS: for component in FIBARO_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {},
controller.enable_state_handler() base_config)
for controller in hass.data[FIBARO_CONTROLLERS].values():
controller.enable_state_handler()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro)
return True return True
@ -278,10 +298,10 @@ def setup(hass, config):
class FibaroDevice(Entity): class FibaroDevice(Entity):
"""Representation of a Fibaro device entity.""" """Representation of a Fibaro device entity."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the device.""" """Initialize the device."""
self.fibaro_device = fibaro_device self.fibaro_device = fibaro_device
self.controller = controller self.controller = fibaro_device.fibaro_controller
self._name = fibaro_device.friendly_name self._name = fibaro_device.friendly_name
self.ha_id = fibaro_device.ha_id self.ha_id = fibaro_device.ha_id

View file

@ -12,7 +12,7 @@ from functools import partial
from homeassistant.const import ( from homeassistant.const import (
CONF_WHITE_VALUE) CONF_WHITE_VALUE)
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice, FIBARO_DEVICES, FibaroDevice,
CONF_DIMMING, CONF_COLOR, CONF_RESET_COLOR) CONF_DIMMING, CONF_COLOR, CONF_RESET_COLOR)
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT,
@ -50,14 +50,14 @@ async def async_setup_platform(hass,
return return
async_add_entities( async_add_entities(
[FibaroLight(device, hass.data[FIBARO_CONTROLLER]) [FibaroLight(device)
for device in hass.data[FIBARO_DEVICES]['light']], True) for device in hass.data[FIBARO_DEVICES]['light']], True)
class FibaroLight(FibaroDevice, Light): class FibaroLight(FibaroDevice, Light):
"""Representation of a Fibaro Light, including dimmable.""" """Representation of a Fibaro Light, including dimmable."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the light.""" """Initialize the light."""
self._brightness = None self._brightness = None
self._color = (0, 0) self._color = (0, 0)
@ -81,7 +81,7 @@ class FibaroLight(FibaroDevice, Light):
if devconf.get(CONF_WHITE_VALUE, supports_white_v): if devconf.get(CONF_WHITE_VALUE, supports_white_v):
self._supported_flags |= SUPPORT_WHITE_VALUE self._supported_flags |= SUPPORT_WHITE_VALUE
super().__init__(fibaro_device, controller) super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@property @property

View file

@ -9,7 +9,7 @@ import logging
from homeassistant.components.scene import ( from homeassistant.components.scene import (
Scene) Scene)
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_DEVICES, FibaroDevice)
DEPENDENCIES = ['fibaro'] DEPENDENCIES = ['fibaro']
@ -23,7 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities,
return return
async_add_entities( async_add_entities(
[FibaroScene(scene, hass.data[FIBARO_CONTROLLER]) [FibaroScene(scene)
for scene in hass.data[FIBARO_DEVICES]['scene']], True) for scene in hass.data[FIBARO_DEVICES]['scene']], True)

View file

@ -12,7 +12,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.sensor import ENTITY_ID_FORMAT
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_DEVICES, FibaroDevice)
SENSOR_TYPES = { SENSOR_TYPES = {
'com.fibaro.temperatureSensor': 'com.fibaro.temperatureSensor':
@ -37,18 +37,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
add_entities( add_entities(
[FibaroSensor(device, hass.data[FIBARO_CONTROLLER]) [FibaroSensor(device)
for device in hass.data[FIBARO_DEVICES]['sensor']], True) for device in hass.data[FIBARO_DEVICES]['sensor']], True)
class FibaroSensor(FibaroDevice, Entity): class FibaroSensor(FibaroDevice, Entity):
"""Representation of a Fibaro Sensor.""" """Representation of a Fibaro Sensor."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the sensor.""" """Initialize the sensor."""
self.current_value = None self.current_value = None
self.last_changed_time = None self.last_changed_time = None
super().__init__(fibaro_device, controller) super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(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]

View file

@ -9,7 +9,7 @@ import logging
from homeassistant.util import convert from homeassistant.util import convert
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_DEVICES, FibaroDevice)
DEPENDENCIES = ['fibaro'] DEPENDENCIES = ['fibaro']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,17 +21,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
add_entities( add_entities(
[FibaroSwitch(device, hass.data[FIBARO_CONTROLLER]) for [FibaroSwitch(device) for
device in hass.data[FIBARO_DEVICES]['switch']], True) device in hass.data[FIBARO_DEVICES]['switch']], True)
class FibaroSwitch(FibaroDevice, SwitchDevice): class FibaroSwitch(FibaroDevice, SwitchDevice):
"""Representation of a Fibaro Switch.""" """Representation of a Fibaro Switch."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the Fibaro device.""" """Initialize the Fibaro device."""
self._state = False self._state = False
super().__init__(fibaro_device, controller) super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
def turn_on(self, **kwargs): def turn_on(self, **kwargs):