mirror of
https://github.com/home-assistant/core.git
synced 2025-06-03 12:47:06 +00:00

* 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
444 lines
17 KiB
Python
444 lines
17 KiB
Python
"""
|
|
Support for the Fibaro devices.
|
|
|
|
For more details about this platform, please refer to the documentation.
|
|
https://home-assistant.io/components/fibaro/
|
|
"""
|
|
|
|
import logging
|
|
from collections import defaultdict
|
|
from typing import Optional
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import (
|
|
ATTR_ARMED, ATTR_BATTERY_LEVEL, CONF_DEVICE_CLASS, CONF_EXCLUDE,
|
|
CONF_ICON, CONF_PASSWORD, CONF_URL, CONF_USERNAME,
|
|
CONF_WHITE_VALUE, EVENT_HOMEASSISTANT_STOP)
|
|
from homeassistant.helpers import discovery
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.util import convert, slugify
|
|
|
|
REQUIREMENTS = ['fiblary3==0.1.7']
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
DOMAIN = 'fibaro'
|
|
FIBARO_DEVICES = 'fibaro_devices'
|
|
FIBARO_CONTROLLERS = 'fibaro_controllers'
|
|
ATTR_CURRENT_POWER_W = "current_power_w"
|
|
ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh"
|
|
CONF_PLUGINS = "plugins"
|
|
CONF_GATEWAYS = 'gateways'
|
|
CONF_DIMMING = "dimming"
|
|
CONF_COLOR = "color"
|
|
CONF_RESET_COLOR = "reset_color"
|
|
CONF_DEVICE_CONFIG = "device_config"
|
|
|
|
FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light',
|
|
'scene', 'sensor', 'switch']
|
|
|
|
FIBARO_TYPEMAP = {
|
|
'com.fibaro.multilevelSensor': "sensor",
|
|
'com.fibaro.binarySwitch': 'switch',
|
|
'com.fibaro.multilevelSwitch': 'switch',
|
|
'com.fibaro.FGD212': 'light',
|
|
'com.fibaro.FGR': 'cover',
|
|
'com.fibaro.doorSensor': 'binary_sensor',
|
|
'com.fibaro.doorWindowSensor': 'binary_sensor',
|
|
'com.fibaro.FGMS001': 'binary_sensor',
|
|
'com.fibaro.heatDetector': 'binary_sensor',
|
|
'com.fibaro.lifeDangerSensor': 'binary_sensor',
|
|
'com.fibaro.smokeSensor': 'binary_sensor',
|
|
'com.fibaro.remoteSwitch': 'switch',
|
|
'com.fibaro.sensor': 'sensor',
|
|
'com.fibaro.colorController': 'light',
|
|
'com.fibaro.securitySensor': 'binary_sensor'
|
|
}
|
|
|
|
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
|
|
vol.Optional(CONF_DIMMING): cv.boolean,
|
|
vol.Optional(CONF_COLOR): cv.boolean,
|
|
vol.Optional(CONF_WHITE_VALUE): cv.boolean,
|
|
vol.Optional(CONF_RESET_COLOR): cv.boolean,
|
|
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
|
vol.Optional(CONF_ICON): 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({
|
|
DOMAIN: vol.Schema({
|
|
vol.Required(CONF_GATEWAYS):
|
|
vol.All(cv.ensure_list, [GATEWAY_CONFIG])
|
|
})
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
class FibaroController():
|
|
"""Initiate Fibaro Controller Class."""
|
|
|
|
def __init__(self, config):
|
|
"""Initialize the Fibaro controller."""
|
|
from fiblary3.client.v4.client import Client as FibaroClient
|
|
|
|
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._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
|
|
|
|
def connect(self):
|
|
"""Start the communication with the Fibaro controller."""
|
|
try:
|
|
login = self._client.login.get()
|
|
info = self._client.info.get()
|
|
self.hub_serial = slugify(info.serialNumber)
|
|
except AssertionError:
|
|
_LOGGER.error("Can't connect to Fibaro HC. "
|
|
"Please check URL.")
|
|
return False
|
|
if login is None or login.status is False:
|
|
_LOGGER.error("Invalid login for Fibaro HC. "
|
|
"Please check username and password.")
|
|
return False
|
|
|
|
self._room_map = {room.id: room for room in self._client.rooms.list()}
|
|
self._read_devices()
|
|
self._read_scenes()
|
|
return True
|
|
|
|
def enable_state_handler(self):
|
|
"""Start StateHandler thread for monitoring updates."""
|
|
from fiblary3.client.v4.client import StateHandler
|
|
self._state_handler = StateHandler(self._client, self._on_state_change)
|
|
|
|
def disable_state_handler(self):
|
|
"""Stop StateHandler thread used for monitoring updates."""
|
|
self._state_handler.stop()
|
|
self._state_handler = None
|
|
|
|
def _on_state_change(self, state):
|
|
"""Handle change report received from the HomeCenter."""
|
|
callback_set = set()
|
|
for change in state.get('changes', []):
|
|
try:
|
|
dev_id = change.pop('id')
|
|
if dev_id not in self._device_map.keys():
|
|
continue
|
|
device = self._device_map[dev_id]
|
|
for property_name, value in change.items():
|
|
if property_name == 'log':
|
|
if value and value != "transfer OK":
|
|
_LOGGER.debug("LOG %s: %s",
|
|
device.friendly_name, value)
|
|
continue
|
|
if property_name == 'logTemp':
|
|
continue
|
|
if property_name in device.properties:
|
|
device.properties[property_name] = \
|
|
value
|
|
_LOGGER.debug("<- %s.%s = %s", device.ha_id,
|
|
property_name, str(value))
|
|
else:
|
|
_LOGGER.warning("%s.%s not found", device.ha_id,
|
|
property_name)
|
|
if dev_id in self._callbacks:
|
|
callback_set.add(dev_id)
|
|
except (ValueError, KeyError):
|
|
pass
|
|
for item in callback_set:
|
|
self._callbacks[item]()
|
|
|
|
def register(self, device_id, callback):
|
|
"""Register device with a callback for updates."""
|
|
self._callbacks[device_id] = callback
|
|
|
|
@staticmethod
|
|
def _map_device_to_type(device):
|
|
"""Map device to HA device type."""
|
|
# Use our lookup table to identify device type
|
|
device_type = None
|
|
if 'type' in device:
|
|
device_type = FIBARO_TYPEMAP.get(device.type)
|
|
if device_type is None and 'baseType' in device:
|
|
device_type = FIBARO_TYPEMAP.get(device.baseType)
|
|
|
|
# We can also identify device type by its capabilities
|
|
if device_type is None:
|
|
if 'setBrightness' in device.actions:
|
|
device_type = 'light'
|
|
elif 'turnOn' in device.actions:
|
|
device_type = 'switch'
|
|
elif 'open' in device.actions:
|
|
device_type = 'cover'
|
|
elif 'value' in device.properties:
|
|
if device.properties.value in ('true', 'false'):
|
|
device_type = 'binary_sensor'
|
|
else:
|
|
device_type = 'sensor'
|
|
|
|
# Switches that control lights should show up as lights
|
|
if device_type == 'switch' and \
|
|
device.properties.get('isLight', 'false') == 'true':
|
|
device_type = 'light'
|
|
return device_type
|
|
|
|
def _read_scenes(self):
|
|
scenes = self._client.scenes.list()
|
|
self._scene_map = {}
|
|
for device in scenes:
|
|
if not device.visible:
|
|
continue
|
|
device.fibaro_controller = self
|
|
if device.roomID == 0:
|
|
room_name = 'Unknown'
|
|
else:
|
|
room_name = self._room_map[device.roomID].name
|
|
device.room_name = room_name
|
|
device.friendly_name = '{} {}'.format(room_name, device.name)
|
|
device.ha_id = '{}_{}_{}'.format(
|
|
slugify(room_name), slugify(device.name), device.id)
|
|
device.unique_id_str = "{}.{}".format(
|
|
self.hub_serial, device.id)
|
|
self._scene_map[device.id] = device
|
|
self.fibaro_devices['scene'].append(device)
|
|
|
|
def _read_devices(self):
|
|
"""Read and process the device list."""
|
|
devices = self._client.devices.list()
|
|
self._device_map = {}
|
|
self.fibaro_devices = defaultdict(list)
|
|
for device in devices:
|
|
try:
|
|
device.fibaro_controller = self
|
|
if device.roomID == 0:
|
|
room_name = 'Unknown'
|
|
else:
|
|
room_name = self._room_map[device.roomID].name
|
|
device.room_name = room_name
|
|
device.friendly_name = room_name + ' ' + device.name
|
|
device.ha_id = '{}_{}_{}'.format(
|
|
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:
|
|
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 device.mapped_type:
|
|
device.unique_id_str = "{}.{}".format(
|
|
self.hub_serial, device.id)
|
|
self._device_map[device.id] = device
|
|
self.fibaro_devices[device.mapped_type].append(device)
|
|
_LOGGER.debug("%s (%s, %s) -> %s. Prop: %s Actions: %s",
|
|
device.ha_id, device.type,
|
|
device.baseType, device.mapped_type,
|
|
str(device.properties), str(device.actions))
|
|
except (KeyError, ValueError):
|
|
pass
|
|
|
|
|
|
def setup(hass, base_config):
|
|
"""Set up the Fibaro Component."""
|
|
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 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:
|
|
discovery.load_platform(hass, component, 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 False
|
|
|
|
|
|
class FibaroDevice(Entity):
|
|
"""Representation of a Fibaro device entity."""
|
|
|
|
def __init__(self, fibaro_device):
|
|
"""Initialize the device."""
|
|
self.fibaro_device = fibaro_device
|
|
self.controller = fibaro_device.fibaro_controller
|
|
self._name = fibaro_device.friendly_name
|
|
self.ha_id = fibaro_device.ha_id
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Call when entity is added to hass."""
|
|
self.controller.register(self.fibaro_device.id, self._update_callback)
|
|
|
|
def _update_callback(self):
|
|
"""Update the state."""
|
|
self.schedule_update_ha_state(True)
|
|
|
|
@property
|
|
def level(self):
|
|
"""Get the level of Fibaro device."""
|
|
if 'value' in self.fibaro_device.properties:
|
|
return self.fibaro_device.properties.value
|
|
return None
|
|
|
|
@property
|
|
def level2(self):
|
|
"""Get the tilt level of Fibaro device."""
|
|
if 'value2' in self.fibaro_device.properties:
|
|
return self.fibaro_device.properties.value2
|
|
return None
|
|
|
|
def dont_know_message(self, action):
|
|
"""Make a warning in case we don't know how to perform an action."""
|
|
_LOGGER.warning("Not sure how to setValue: %s "
|
|
"(available actions: %s)", str(self.ha_id),
|
|
str(self.fibaro_device.actions))
|
|
|
|
def set_level(self, level):
|
|
"""Set the level of Fibaro device."""
|
|
self.action("setValue", level)
|
|
if 'value' in self.fibaro_device.properties:
|
|
self.fibaro_device.properties.value = level
|
|
if 'brightness' in self.fibaro_device.properties:
|
|
self.fibaro_device.properties.brightness = level
|
|
|
|
def set_level2(self, level):
|
|
"""Set the level2 of Fibaro device."""
|
|
self.action("setValue2", level)
|
|
if 'value2' in self.fibaro_device.properties:
|
|
self.fibaro_device.properties.value2 = level
|
|
|
|
def call_turn_on(self):
|
|
"""Turn on the Fibaro device."""
|
|
self.action("turnOn")
|
|
|
|
def call_turn_off(self):
|
|
"""Turn off the Fibaro device."""
|
|
self.action("turnOff")
|
|
|
|
def call_set_color(self, red, green, blue, white):
|
|
"""Set the color of Fibaro device."""
|
|
red = int(max(0, min(255, red)))
|
|
green = int(max(0, min(255, green)))
|
|
blue = int(max(0, min(255, blue)))
|
|
white = int(max(0, min(255, white)))
|
|
color_str = "{},{},{},{}".format(red, green, blue, white)
|
|
self.fibaro_device.properties.color = color_str
|
|
self.action("setColor", str(red), str(green),
|
|
str(blue), str(white))
|
|
|
|
def action(self, cmd, *args):
|
|
"""Perform an action on the Fibaro HC."""
|
|
if cmd in self.fibaro_device.actions:
|
|
getattr(self.fibaro_device, cmd)(*args)
|
|
_LOGGER.debug("-> %s.%s%s called", str(self.ha_id),
|
|
str(cmd), str(args))
|
|
else:
|
|
self.dont_know_message(cmd)
|
|
|
|
@property
|
|
def hidden(self) -> bool:
|
|
"""Return True if the entity should be hidden from UIs."""
|
|
return self.fibaro_device.visible is False
|
|
|
|
@property
|
|
def current_power_w(self):
|
|
"""Return the current power usage in W."""
|
|
if 'power' in self.fibaro_device.properties:
|
|
power = self.fibaro_device.properties.power
|
|
if power:
|
|
return convert(power, float, 0.0)
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def current_binary_state(self):
|
|
"""Return the current binary state."""
|
|
if self.fibaro_device.properties.value == 'false':
|
|
return False
|
|
if self.fibaro_device.properties.value == 'true' or \
|
|
int(self.fibaro_device.properties.value) > 0:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique ID."""
|
|
return self.fibaro_device.unique_id_str
|
|
|
|
@property
|
|
def name(self) -> Optional[str]:
|
|
"""Return the name of the device."""
|
|
return self._name
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Get polling requirement from fibaro device."""
|
|
return False
|
|
|
|
def update(self):
|
|
"""Call to update state."""
|
|
pass
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes of the device."""
|
|
attr = {}
|
|
|
|
try:
|
|
if 'battery' in self.fibaro_device.interfaces:
|
|
attr[ATTR_BATTERY_LEVEL] = \
|
|
int(self.fibaro_device.properties.batteryLevel)
|
|
if 'fibaroAlarmArm' in self.fibaro_device.interfaces:
|
|
attr[ATTR_ARMED] = bool(self.fibaro_device.properties.armed)
|
|
if 'power' in self.fibaro_device.interfaces:
|
|
attr[ATTR_CURRENT_POWER_W] = convert(
|
|
self.fibaro_device.properties.power, float, 0.0)
|
|
if 'energy' in self.fibaro_device.interfaces:
|
|
attr[ATTR_CURRENT_ENERGY_KWH] = convert(
|
|
self.fibaro_device.properties.energy, float, 0.0)
|
|
except (ValueError, KeyError):
|
|
pass
|
|
|
|
attr['fibaro_id'] = self.fibaro_device.id
|
|
return attr
|