Replace fiblary3 with pyfibaro library (#83500)

* Replace fiblary3 with pyfibaro library

* Fix some missing replacements for pyfibaro library

* Remove debug code which was committed accidentially

* Use fibaro_parent_id in another place

* Fix some bugs

* Move more code to the library

* Move has_unit check to correct place
This commit is contained in:
rappenze
2023-02-11 22:13:12 +01:00
committed by GitHub
parent 86a93e9fce
commit e1340e6ed4
13 changed files with 234 additions and 292 deletions

View File

@ -6,12 +6,10 @@ from collections.abc import Mapping
import logging
from typing import Any
from fiblary3.client.v4.client import (
Client as FibaroClientV4,
StateHandler as StateHandlerV4,
)
from fiblary3.client.v5.client import StateHandler as StateHandlerV5
from fiblary3.common.exceptions import HTTPException
from pyfibaro.fibaro_client import FibaroClient
from pyfibaro.fibaro_device import DeviceModel
from pyfibaro.fibaro_scene import SceneModel
from requests.exceptions import HTTPError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@ -134,24 +132,11 @@ class FibaroController:
def __init__(
self, config: Mapping[str, Any], serial_number: str | None = None
) -> None:
"""Initialize the Fibaro controller.
"""Initialize the Fibaro controller."""
Version 4 is used for home center 2 (SN starts with HC2) and
home center lite (SN starts with HCL).
Version 5 is used for home center 3 (SN starts with HC3),
home center 3 lite (SN starts with HC3L) and yubii home (SN starts with YH).
Here the serial number is optional and we choose then the V4 client. You
should do that only when you use the FibaroController for login test as only
the login and info API's are equal throughout the different versions.
"""
# Only use V4 API as it works better even for HC3, after the library is fixed, we should
# add here support for the newer library version V5 again.
self._client = FibaroClientV4(
config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD]
)
# The FibaroClient uses the correct API version automatically
self._client = FibaroClient(config[CONF_URL])
self._client.set_authentication(config[CONF_USERNAME], config[CONF_PASSWORD])
self._scene_map = None
# Whether to import devices from plugins
@ -162,7 +147,6 @@ class FibaroController:
list
) # List of devices by entity platform
self._callbacks: dict[Any, Any] = {} # Update value callbacks by deviceId
self._state_handler = None # Fiblary's StateHandler object
self.hub_serial: str # Unique serial number of the hub
self.hub_name: str # The friendly name of the hub
self.hub_software_version: str
@ -173,21 +157,21 @@ class FibaroController:
def connect(self):
"""Start the communication with the Fibaro controller."""
try:
login = self._client.login.get()
info = self._client.info.get()
self.hub_serial = info.serialNumber
self.hub_name = info.hcName
self.hub_software_version = info.softVersion
connected = self._client.connect()
info = self._client.read_info()
self.hub_serial = info.serial_number
self.hub_name = info.hc_name
self.hub_software_version = info.current_version
except AssertionError:
_LOGGER.error("Can't connect to Fibaro HC. Please check URL")
return False
if login is None or login.status is False:
if connected 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._room_map = {room.fibaro_id: room for room in self._client.read_rooms()}
self._read_devices()
self._read_scenes()
return True
@ -201,8 +185,8 @@ class FibaroController:
connected = self.connect()
if not connected:
raise FibaroConnectFailed("Connect status is false")
except HTTPException as http_ex:
if http_ex.details == "Forbidden":
except HTTPError as http_ex:
if http_ex.response.status_code == 403:
raise FibaroAuthFailed from http_ex
raise FibaroConnectFailed from http_ex
@ -211,15 +195,11 @@ class FibaroController:
def enable_state_handler(self):
"""Start StateHandler thread for monitoring updates."""
if isinstance(self._client, FibaroClientV4):
self._state_handler = StateHandlerV4(self._client, self._on_state_change)
else:
self._state_handler = StateHandlerV5(self._client, self._on_state_change)
self._client.register_update_handler(self._on_state_change)
def disable_state_handler(self):
"""Stop StateHandler thread used for monitoring updates."""
self._state_handler.stop()
self._state_handler = None
self._client.unregister_update_handler()
def _on_state_change(self, state):
"""Handle change report received from the HomeCenter."""
@ -262,7 +242,7 @@ class FibaroController:
return [
device
for device in self._device_map.values()
if device.parentId == device_id
if device.parent_fibaro_id == device_id
]
def get_children2(self, device_id, endpoint_id):
@ -270,31 +250,28 @@ class FibaroController:
return [
device
for device in self._device_map.values()
if device.parentId == device_id
and (
"endPointId" not in device.properties
or device.properties.endPointId == endpoint_id
)
if device.parent_fibaro_id == device_id
and (not device.has_endpoint_id or device.endpoint_id == endpoint_id)
]
def get_siblings(self, device):
"""Get the siblings of a device."""
if "endPointId" in device.properties:
if device.has_endpoint_id:
return self.get_children2(
self._device_map[device.id].parentId,
self._device_map[device.id].properties.endPointId,
self._device_map[device.fibaro_id].parent_fibaro_id,
self._device_map[device.fibaro_id].endpoint_id,
)
return self.get_children(self._device_map[device.id].parentId)
return self.get_children(self._device_map[device.fibaro_id].parent_fibaro_id)
@staticmethod
def _map_device_to_platform(device: Any) -> Platform | None:
"""Map device to HA device type."""
# Use our lookup table to identify device type
platform: Platform | None = None
if "type" in device:
if device.type:
platform = FIBARO_TYPEMAP.get(device.type)
if platform is None and "baseType" in device:
platform = FIBARO_TYPEMAP.get(device.baseType)
if platform is None and device.base_type:
platform = FIBARO_TYPEMAP.get(device.base_type)
# We can also identify device type by its capabilities
if platform is None:
@ -306,8 +283,8 @@ class FibaroController:
platform = Platform.COVER
elif "secure" in device.actions:
platform = Platform.LOCK
elif "value" in device.properties:
if device.properties.value in ("true", "false"):
elif device.value.has_value:
if device.value.is_bool_value:
platform = Platform.BINARY_SENSOR
else:
platform = Platform.SENSOR
@ -317,31 +294,33 @@ class FibaroController:
platform = Platform.LIGHT
return platform
def _create_device_info(self, device: Any, devices: list) -> None:
def _create_device_info(
self, device: DeviceModel, devices: list[DeviceModel]
) -> None:
"""Create the device info. Unrooted entities are directly shown below the home center."""
# The home center is always id 1 (z-wave primary controller)
if "parentId" not in device or device.parentId <= 1:
if device.parent_fibaro_id <= 1:
return
master_entity: Any | None = None
if device.parentId == 1:
if device.parent_fibaro_id == 1:
master_entity = device
else:
for parent in devices:
if "id" in parent and parent.id == device.parentId:
if parent.fibaro_id == device.parent_fibaro_id:
master_entity = parent
if master_entity is None:
_LOGGER.error("Parent with id %s not found", device.parentId)
_LOGGER.error("Parent with id %s not found", device.parent_fibaro_id)
return
if "zwaveCompany" in master_entity.properties:
manufacturer = master_entity.properties.zwaveCompany
manufacturer = master_entity.properties.get("zwaveCompany")
else:
manufacturer = "Unknown"
self._device_infos[master_entity.id] = DeviceInfo(
identifiers={(DOMAIN, master_entity.id)},
self._device_infos[master_entity.fibaro_id] = DeviceInfo(
identifiers={(DOMAIN, master_entity.fibaro_id)},
manufacturer=manufacturer,
name=master_entity.name,
via_device=(DOMAIN, self.hub_serial),
@ -349,70 +328,65 @@ class FibaroController:
def get_device_info(self, device: Any) -> DeviceInfo:
"""Get the device info by fibaro device id."""
if device.id in self._device_infos:
return self._device_infos[device.id]
if "parentId" in device and device.parentId in self._device_infos:
return self._device_infos[device.parentId]
if device.fibaro_id in self._device_infos:
return self._device_infos[device.fibaro_id]
if device.parent_fibaro_id in self._device_infos:
return self._device_infos[device.parent_fibaro_id]
return DeviceInfo(identifiers={(DOMAIN, self.hub_serial)})
def _read_scenes(self):
scenes = self._client.scenes.list()
scenes = self._client.read_scenes()
self._scene_map = {}
for device in scenes:
if "name" not in device or "id" not in device:
continue
device.fibaro_controller = self
if "roomID" not in device or device.roomID == 0:
if device.room_id == 0:
room_name = "Unknown"
else:
room_name = self._room_map[device.roomID].name
room_name = self._room_map[device.room_id].name
device.room_name = room_name
device.friendly_name = f"{room_name} {device.name}"
device.ha_id = (
f"scene_{slugify(room_name)}_{slugify(device.name)}_{device.id}"
f"scene_{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
)
device.unique_id_str = f"{slugify(self.hub_serial)}.scene.{device.id}"
self._scene_map[device.id] = device
device.unique_id_str = (
f"{slugify(self.hub_serial)}.scene.{device.fibaro_id}"
)
self._scene_map[device.fibaro_id] = device
self.fibaro_devices[Platform.SCENE].append(device)
_LOGGER.debug("%s scene -> %s", device.ha_id, device)
def _read_devices(self):
"""Read and process the device list."""
devices = list(self._client.devices.list())
devices = self._client.read_devices()
self._device_map = {}
last_climate_parent = None
last_endpoint = None
for device in devices:
try:
if "name" not in device or "id" not in device:
continue
device.fibaro_controller = self
if "roomID" not in device or device.roomID == 0:
if device.room_id == 0:
room_name = "Unknown"
else:
room_name = self._room_map[device.roomID].name
room_name = self._room_map[device.room_id].name
device.room_name = room_name
device.friendly_name = f"{room_name} {device.name}"
device.ha_id = (
f"{slugify(room_name)}_{slugify(device.name)}_{device.id}"
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
)
if device.enabled and (
"isPlugin" not in device
or (not device.isPlugin or self._import_plugins)
):
if device.enabled and (not device.is_plugin or self._import_plugins):
device.mapped_platform = self._map_device_to_platform(device)
else:
device.mapped_platform = None
if (platform := device.mapped_platform) is None:
continue
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.id}"
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
self._create_device_info(device, devices)
self._device_map[device.id] = device
self._device_map[device.fibaro_id] = device
_LOGGER.debug(
"%s (%s, %s) -> %s %s",
device.ha_id,
device.type,
device.baseType,
device.base_type,
platform,
str(device),
)
@ -421,11 +395,11 @@ class FibaroController:
continue
# We group climate devices into groups with the same
# endPointID belonging to the same parent device.
if "endPointId" in device.properties:
if device.has_endpoint_id:
_LOGGER.debug(
"climate device: %s, endPointId: %s",
device.ha_id,
device.properties.endPointId,
device.endpoint_id,
)
else:
_LOGGER.debug("climate device: %s, no endPointId", device.ha_id)
@ -433,17 +407,13 @@ class FibaroController:
# otherwise add the first visible device in the group
# which is a hack, but solves a problem with FGT having
# hidden compatibility devices before the real device
if last_climate_parent != device.parentId or (
"endPointId" in device.properties
and last_endpoint != device.properties.endPointId
if last_climate_parent != device.parent_fibaro_id or (
device.has_endpoint_id and last_endpoint != device.endpoint_id
):
_LOGGER.debug("Handle separately")
self.fibaro_devices[platform].append(device)
last_climate_parent = device.parentId
if "endPointId" in device.properties:
last_endpoint = device.properties.endPointId
else:
last_endpoint = 0
last_climate_parent = device.parent_fibaro_id
last_endpoint = device.endpoint_id
else:
_LOGGER.debug("not handling separately")
except (KeyError, ValueError):
@ -548,21 +518,23 @@ class FibaroDevice(Entity):
_attr_should_poll = False
def __init__(self, fibaro_device):
def __init__(self, fibaro_device: DeviceModel | SceneModel) -> None:
"""Initialize the device."""
self.fibaro_device = fibaro_device
self.controller = fibaro_device.fibaro_controller
self.ha_id = fibaro_device.ha_id
self._attr_name = fibaro_device.friendly_name
self._attr_unique_id = fibaro_device.unique_id_str
self._attr_device_info = self.controller.get_device_info(fibaro_device)
if isinstance(fibaro_device, DeviceModel):
self._attr_device_info = self.controller.get_device_info(fibaro_device)
# propagate hidden attribute set in fibaro home center to HA
if "visible" in fibaro_device and fibaro_device.visible is False:
if not fibaro_device.visible:
self._attr_entity_registry_visible_default = False
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.controller.register(self.fibaro_device.id, self._update_callback)
self.controller.register(self.fibaro_device.fibaro_id, self._update_callback)
def _update_callback(self):
"""Update the state."""
@ -571,15 +543,15 @@ class FibaroDevice(Entity):
@property
def level(self):
"""Get the level of Fibaro device."""
if "value" in self.fibaro_device.properties:
return self.fibaro_device.properties.value
if self.fibaro_device.value.has_value:
return self.fibaro_device.value.int_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
if self.fibaro_device.value_2.has_value:
return self.fibaro_device.value_2.int_value()
return None
def dont_know_message(self, action):
@ -593,16 +565,16 @@ class FibaroDevice(Entity):
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
if self.fibaro_device.value.has_value:
self.fibaro_device.properties["value"] = level
if self.fibaro_device.has_brightness:
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
if self.fibaro_device.value_2.has_value:
self.fibaro_device.properties["value2"] = level
def call_turn_on(self):
"""Turn on the Fibaro device."""
@ -619,13 +591,13 @@ class FibaroDevice(Entity):
blue = int(max(0, min(255, blue)))
white = int(max(0, min(255, white)))
color_str = f"{red},{green},{blue},{white}"
self.fibaro_device.properties.color = color_str
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)
self.fibaro_device.execute_action(cmd, args)
_LOGGER.debug("-> %s.%s%s called", str(self.ha_id), str(cmd), str(args))
else:
self.dont_know_message(cmd)
@ -633,35 +605,18 @@ class FibaroDevice(Entity):
@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
return self.fibaro_device.value.bool_value(False)
@property
def extra_state_attributes(self):
"""Return the state attributes of the device."""
attr = {"fibaro_id": self.fibaro_device.id}
attr = {"fibaro_id": self.fibaro_device.fibaro_id}
try:
if "battery" in self.fibaro_device.interfaces:
attr[ATTR_BATTERY_LEVEL] = int(
self.fibaro_device.properties.batteryLevel
)
if "armed" in self.fibaro_device.properties:
armed = self.fibaro_device.properties.armed
if isinstance(armed, bool):
attr[ATTR_ARMED] = armed
elif isinstance(armed, str) and armed.lower() in ("true", "false"):
attr[ATTR_ARMED] = armed.lower() == "true"
else:
attr[ATTR_ARMED] = None
except (ValueError, KeyError):
pass
if isinstance(self.fibaro_device, DeviceModel):
if self.fibaro_device.has_battery_level:
attr[ATTR_BATTERY_LEVEL] = self.fibaro_device.battery_level
if self.fibaro_device.has_armed:
attr[ATTR_ARMED] = self.fibaro_device.armed
return attr

View File

@ -2,9 +2,10 @@
from __future__ import annotations
from collections.abc import Mapping
import json
from typing import Any, cast
from pyfibaro.fibaro_device import DeviceModel
from homeassistant.components.binary_sensor import (
ENTITY_ID_FORMAT,
BinarySensorDeviceClass,
@ -58,7 +59,7 @@ async def async_setup_entry(
class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
"""Representation of a Fibaro Binary Sensor."""
def __init__(self, fibaro_device: Any) -> None:
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the binary_sensor."""
super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@ -66,8 +67,8 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
self._fibaro_sensor_type = None
if fibaro_device.type in SENSOR_TYPES:
self._fibaro_sensor_type = fibaro_device.type
elif fibaro_device.baseType in SENSOR_TYPES:
self._fibaro_sensor_type = fibaro_device.baseType
elif fibaro_device.base_type in SENSOR_TYPES:
self._fibaro_sensor_type = fibaro_device.base_type
if self._fibaro_sensor_type:
self._attr_device_class = cast(
BinarySensorDeviceClass, SENSOR_TYPES[self._fibaro_sensor_type][2]
@ -105,9 +106,4 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
def _get_moving_values(self) -> Mapping[str, Any]:
"""Get the moving values of the accelerator sensor in a dict."""
value = self.fibaro_device.properties.value
if isinstance(value, str):
# HC2 returns dict as str
return json.loads(value)
# HC3 returns a real dict
return value
return self.fibaro_device.value.dict_value()

View File

@ -5,6 +5,8 @@ from contextlib import suppress
import logging
from typing import Any
from pyfibaro.fibaro_device import DeviceModel
from homeassistant.components.climate import (
ENTITY_ID_FORMAT,
PRESET_AWAY,
@ -124,7 +126,7 @@ async def async_setup_entry(
class FibaroThermostat(FibaroDevice, ClimateEntity):
"""Representation of a Fibaro Thermostat."""
def __init__(self, fibaro_device):
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the Fibaro device."""
super().__init__(fibaro_device)
self._temp_sensor_device: FibaroDevice | None = None
@ -141,26 +143,23 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
# doing so, so we prefer the hard evidence, if there is such.
if device.type == "com.fibaro.temperatureSensor":
self._temp_sensor_device = FibaroDevice(device)
tempunit = device.properties.unit
tempunit = device.unit
elif (
self._temp_sensor_device is None
and "unit" in device.properties
and (
"value" in device.properties
or "heatingThermostatSetpoint" in device.properties
)
and device.properties.unit in ("C", "F")
and device.has_unit
and (device.value.has_value or device.has_heating_thermostat_setpoint)
and device.unit in ("C", "F")
):
self._temp_sensor_device = FibaroDevice(device)
tempunit = device.properties.unit
tempunit = device.unit
if any(
action for action in TARGET_TEMP_ACTIONS if action in device.actions
):
self._target_temp_device = FibaroDevice(device)
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
if "unit" in device.properties:
tempunit = device.properties.unit
if device.has_unit:
tempunit = device.unit
if any(action for action in OP_MODE_ACTIONS if action in device.actions):
self._op_mode_device = FibaroDevice(device)
@ -176,12 +175,9 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
if self._fan_mode_device:
fan_modes = (
self._fan_mode_device.fibaro_device.properties.supportedModes.split(",")
)
fan_modes = self._fan_mode_device.fibaro_device.supported_modes
self._attr_fan_modes = []
for mode in fan_modes:
mode = int(mode)
if mode not in FANMODES:
_LOGGER.warning("%d unknown fan mode", mode)
continue
@ -190,21 +186,20 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
self._attr_hvac_modes = [HVACMode.AUTO] # default
if self._op_mode_device:
self._attr_preset_modes = []
self._attr_hvac_modes = []
prop = self._op_mode_device.fibaro_device.properties
if "supportedThermostatModes" in prop:
for mode in prop.supportedThermostatModes:
self._attr_hvac_modes: list[HVACMode] = []
device = self._op_mode_device.fibaro_device
if device.has_supported_thermostat_modes:
for mode in device.supported_thermostat_modes:
try:
self._attr_hvac_modes.append(HVACMode(mode.lower()))
except ValueError:
self._attr_preset_modes.append(mode)
else:
if "supportedOperatingModes" in prop:
op_modes = prop.supportedOperatingModes.split(",")
if device.has_supported_operating_modes:
op_modes = device.supported_operating_modes
else:
op_modes = prop.supportedModes.split(",")
op_modes = device.supported_modes
for mode in op_modes:
mode = int(mode)
if (
mode in OPMODES_HVAC
and (mode_ha := OPMODES_HVAC.get(mode))
@ -236,14 +231,14 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
siblings = self.fibaro_device.fibaro_controller.get_siblings(self.fibaro_device)
for device in siblings:
if device != self.fibaro_device:
self.controller.register(device.id, self._update_callback)
self.controller.register(device.fibaro_id, self._update_callback)
@property
def fan_mode(self) -> str | None:
"""Return the fan setting."""
if not self._fan_mode_device:
return None
mode = int(self._fan_mode_device.fibaro_device.properties.mode)
mode = self._fan_mode_device.fibaro_device.mode
return FANMODES[mode]
def set_fan_mode(self, fan_mode: str) -> None:
@ -258,14 +253,13 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
if not self._op_mode_device:
return HA_OPMODES_HVAC[HVACMode.AUTO]
prop = self._op_mode_device.fibaro_device.properties
device = self._op_mode_device.fibaro_device
if "operatingMode" in prop:
return int(prop.operatingMode)
if "thermostatMode" in prop:
return prop.thermostatMode
return int(prop.mode)
if device.has_operating_mode:
return device.operating_mode
if device.has_thermostat_mode:
return device.thermostat_mode
return device.mode
@property
def hvac_mode(self) -> HVACMode | str | None:
@ -288,9 +282,9 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
if "setOperatingMode" in self._op_mode_device.fibaro_device.actions:
self._op_mode_device.action("setOperatingMode", HA_OPMODES_HVAC[hvac_mode])
elif "setThermostatMode" in self._op_mode_device.fibaro_device.actions:
prop = self._op_mode_device.fibaro_device.properties
if "supportedThermostatModes" in prop:
for mode in prop.supportedThermostatModes:
device = self._op_mode_device.fibaro_device
if device.has_supported_thermostat_modes:
for mode in device.supported_thermostat_modes:
if mode.lower() == hvac_mode:
self._op_mode_device.action("setThermostatMode", mode)
break
@ -303,10 +297,10 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
if not self._op_mode_device:
return None
prop = self._op_mode_device.fibaro_device.properties
if "thermostatOperatingState" in prop:
device = self._op_mode_device.fibaro_device
if device.has_thermostat_operating_state:
with suppress(ValueError):
return HVACAction(prop.thermostatOperatingState.lower())
return HVACAction(device.thermostat_operating_state.lower())
return None
@ -319,15 +313,15 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
if not self._op_mode_device:
return None
if "thermostatMode" in self._op_mode_device.fibaro_device.properties:
mode = self._op_mode_device.fibaro_device.properties.thermostatMode
if self._op_mode_device.fibaro_device.has_thermostat_mode:
mode = self._op_mode_device.fibaro_device.thermostat_mode
if self.preset_modes is not None and mode in self.preset_modes:
return mode
return None
if "operatingMode" in self._op_mode_device.fibaro_device.properties:
mode = int(self._op_mode_device.fibaro_device.properties.operatingMode)
if self._op_mode_device.fibaro_device.has_operating_mode:
mode = self._op_mode_device.fibaro_device.operating_mode
else:
mode = int(self._op_mode_device.fibaro_device.properties.mode)
mode = self._op_mode_device.fibaro_device.mode
if mode not in OPMODES_PRESET:
return None
@ -352,9 +346,9 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
"""Return the current temperature."""
if self._temp_sensor_device:
device = self._temp_sensor_device.fibaro_device
if "heatingThermostatSetpoint" in device.properties:
return float(device.properties.heatingThermostatSetpoint)
return float(device.properties.value)
if device.has_heating_thermostat_setpoint:
return device.heating_thermostat_setpoint
return device.value.float_value()
return None
@property
@ -362,9 +356,9 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
"""Return the temperature we try to reach."""
if self._target_temp_device:
device = self._target_temp_device.fibaro_device
if "heatingThermostatSetpointFuture" in device.properties:
return float(device.properties.heatingThermostatSetpointFuture)
return float(device.properties.targetLevel)
if device.has_heating_thermostat_setpoint_future:
return device.heating_thermostat_setpoint_future
return device.target_level
return None
def set_temperature(self, **kwargs: Any) -> None:

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from typing import Any
from pyfibaro.fibaro_device import DeviceModel
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
@ -39,7 +41,7 @@ async def async_setup_entry(
class FibaroCover(FibaroDevice, CoverEntity):
"""Representation a Fibaro Cover."""
def __init__(self, fibaro_device):
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the Vera device."""
super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@ -67,9 +69,7 @@ class FibaroCover(FibaroDevice, CoverEntity):
"""Return if only open / close is supported."""
# Normally positionable devices report the position over value,
# so if it is missing we have a device which supports open / close only
if "value" not in self.fibaro_device.properties:
return True
return False
return not self.fibaro_device.value.has_value
@property
def current_cover_position(self) -> int | None:
@ -93,12 +93,10 @@ class FibaroCover(FibaroDevice, CoverEntity):
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self._is_open_close_only():
if (
"state" not in self.fibaro_device.properties
or self.fibaro_device.properties.state.lower() == "unknown"
):
state = self.fibaro_device.state
if not state.has_value or state.str_value.lower() == "unknown":
return None
return self.fibaro_device.properties.state.lower() == "closed"
return state.str_value.lower() == "closed"
if self.current_cover_position is None:
return None

View File

@ -6,6 +6,8 @@ from contextlib import suppress
from functools import partial
from typing import Any
from pyfibaro.fibaro_device import DeviceModel
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_RGB_COLOR,
@ -68,7 +70,7 @@ async def async_setup_entry(
class FibaroLight(FibaroDevice, LightEntity):
"""Representation of a Fibaro Light, including dimmable."""
def __init__(self, fibaro_device):
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the light."""
self._update_lock = asyncio.Lock()
@ -77,7 +79,7 @@ class FibaroLight(FibaroDevice, LightEntity):
or "colorComponents" in fibaro_device.properties
or "RGB" in fibaro_device.type
or "rgb" in fibaro_device.type
or "color" in fibaro_device.baseType
or "color" in fibaro_device.base_type
) and (
"setColor" in fibaro_device.actions
or "setColorComponents" in fibaro_device.actions
@ -88,7 +90,7 @@ class FibaroLight(FibaroDevice, LightEntity):
or "rgbw" in fibaro_device.type
)
supports_dimming = (
"levelChange" in fibaro_device.interfaces
fibaro_device.has_interface("levelChange")
and "setValue" in fibaro_device.actions
)
@ -153,17 +155,16 @@ class FibaroLight(FibaroDevice, LightEntity):
JSON for HC2 uses always string, HC3 uses int for integers.
"""
props = self.fibaro_device.properties
if self.current_binary_state:
return True
with suppress(ValueError, TypeError):
if "brightness" in props and int(props.brightness) != 0:
with suppress(TypeError):
if self.fibaro_device.brightness != 0:
return True
with suppress(ValueError, TypeError):
if "currentProgram" in props and int(props.currentProgram) != 0:
with suppress(TypeError):
if self.fibaro_device.current_program != 0:
return True
with suppress(ValueError, TypeError):
if "currentProgramID" in props and int(props.currentProgramID) != 0:
with suppress(TypeError):
if self.fibaro_device.current_program_id != 0:
return True
return False
@ -177,21 +178,19 @@ class FibaroLight(FibaroDevice, LightEntity):
"""Really update the state."""
# Brightness handling
if brightness_supported(self.supported_color_modes):
self._attr_brightness = scaleto255(int(self.fibaro_device.properties.value))
self._attr_brightness = scaleto255(self.fibaro_device.value.int_value())
# Color handling
if (
color_supported(self.supported_color_modes)
and "color" in self.fibaro_device.properties
and "," in self.fibaro_device.properties.color
and self.fibaro_device.color.has_color
):
# Fibaro communicates the color as an 'R, G, B, W' string
rgbw_s = self.fibaro_device.properties.color
if rgbw_s == "0,0,0,0" and "lastColorSet" in self.fibaro_device.properties:
rgbw_s = self.fibaro_device.properties.lastColorSet
rgbw_list = [int(i) for i in rgbw_s.split(",")][:4]
rgbw = self.fibaro_device.color.rgbw_color
if rgbw == (0, 0, 0, 0) and self.fibaro_device.last_color_set.has_color:
rgbw = self.fibaro_device.last_color_set.rgbw_color
if self._attr_color_mode == ColorMode.RGB:
self._attr_rgb_color = tuple(rgbw_list[:3])
self._attr_rgb_color = rgbw[:3]
else:
self._attr_rgbw_color = tuple(rgbw_list)
self._attr_rgbw_color = rgbw

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from fiblary3.client.v4.models import DeviceModel, SceneModel
from pyfibaro.fibaro_device import DeviceModel
from homeassistant.components.lock import ENTITY_ID_FORMAT, LockEntity
from homeassistant.config_entries import ConfigEntry
@ -35,7 +35,7 @@ async def async_setup_entry(
class FibaroLock(FibaroDevice, LockEntity):
"""Representation of a Fibaro Lock."""
def __init__(self, fibaro_device: DeviceModel | SceneModel) -> None:
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the Fibaro device."""
super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)

View File

@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/fibaro",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["fiblary3"],
"requirements": ["fiblary3==0.1.8"]
"loggers": ["pyfibaro"],
"requirements": ["pyfibaro==0.6.6"]
}

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from typing import Any
from pyfibaro.fibaro_scene import SceneModel
from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
@ -34,7 +36,7 @@ async def async_setup_entry(
class FibaroScene(FibaroDevice, Scene):
"""Representation of a Fibaro scene entity."""
def __init__(self, fibaro_device: Any) -> None:
def __init__(self, fibaro_device: SceneModel) -> None:
"""Initialize the Fibaro scene."""
super().__init__(fibaro_device)

View File

@ -2,7 +2,8 @@
from __future__ import annotations
from contextlib import suppress
from typing import Any
from pyfibaro.fibaro_device import DeviceModel
from homeassistant.components.sensor import (
ENTITY_ID_FORMAT,
@ -125,7 +126,9 @@ class FibaroSensor(FibaroDevice, SensorEntity):
"""Representation of a Fibaro Sensor."""
def __init__(
self, fibaro_device: Any, entity_description: SensorEntityDescription | None
self,
fibaro_device: DeviceModel,
entity_description: SensorEntityDescription | None,
) -> None:
"""Initialize the sensor."""
super().__init__(fibaro_device)
@ -138,20 +141,20 @@ class FibaroSensor(FibaroDevice, SensorEntity):
with suppress(KeyError, ValueError):
if not self.native_unit_of_measurement:
self._attr_native_unit_of_measurement = FIBARO_TO_HASS_UNIT.get(
fibaro_device.properties.unit, fibaro_device.properties.unit
fibaro_device.unit, fibaro_device.unit
)
def update(self) -> None:
"""Update the state."""
with suppress(KeyError, ValueError):
self._attr_native_value = float(self.fibaro_device.properties.value)
with suppress(TypeError):
self._attr_native_value = self.fibaro_device.value.float_value()
class FibaroAdditionalSensor(FibaroDevice, SensorEntity):
"""Representation of a Fibaro Additional Sensor."""
def __init__(
self, fibaro_device: Any, entity_description: SensorEntityDescription
self, fibaro_device: DeviceModel, entity_description: SensorEntityDescription
) -> None:
"""Initialize the sensor."""
super().__init__(fibaro_device)

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from typing import Any
from pyfibaro.fibaro_device import DeviceModel
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
@ -33,7 +35,7 @@ async def async_setup_entry(
class FibaroSwitch(FibaroDevice, SwitchEntity):
"""Representation of a Fibaro Switch."""
def __init__(self, fibaro_device: Any) -> None:
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the Fibaro device."""
super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)

View File

@ -703,9 +703,6 @@ fastdotcom==0.0.3
# homeassistant.components.feedreader
feedparser==6.0.10
# homeassistant.components.fibaro
fiblary3==0.1.8
# homeassistant.components.file
file-read-backwards==2.0.0
@ -1620,6 +1617,9 @@ pyevilgenius==2.0.0
# homeassistant.components.ezviz
pyezviz==0.2.0.9
# homeassistant.components.fibaro
pyfibaro==0.6.6
# homeassistant.components.fido
pyfido==2.1.1

View File

@ -537,9 +537,6 @@ faadelays==0.0.7
# homeassistant.components.feedreader
feedparser==6.0.10
# homeassistant.components.fibaro
fiblary3==0.1.8
# homeassistant.components.file
file-read-backwards==2.0.0
@ -1160,6 +1157,9 @@ pyevilgenius==2.0.0
# homeassistant.components.ezviz
pyezviz==0.2.0.9
# homeassistant.components.fibaro
pyfibaro==0.6.6
# homeassistant.components.fido
pyfido==2.1.1

View File

@ -1,8 +1,8 @@
"""Test the Fibaro config flow."""
from unittest.mock import Mock, patch
from fiblary3.common.exceptions import HTTPException
import pytest
from requests.exceptions import HTTPError
from homeassistant import config_entries
from homeassistant.components.fibaro import DOMAIN
@ -25,37 +25,31 @@ TEST_VERSION = "4.360"
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, softVersion=TEST_VERSION
)
array_mock = Mock()
array_mock.list.return_value = []
info_mock.return_value.serial_number = TEST_SERIALNUMBER
info_mock.return_value.hc_name = TEST_NAME
info_mock.return_value.current_version = TEST_VERSION
client_mock = Mock()
client_mock.base_url.return_value = TEST_URL
with patch(
"homeassistant.components.fibaro.FibaroClientV4.__init__",
"homeassistant.components.fibaro.FibaroClient.__init__",
return_value=None,
), patch(
"homeassistant.components.fibaro.FibaroClientV4.info",
"homeassistant.components.fibaro.FibaroClient.read_info",
info_mock,
create=True,
), patch(
"homeassistant.components.fibaro.FibaroClientV4.rooms",
array_mock,
create=True,
"homeassistant.components.fibaro.FibaroClient.read_rooms",
return_value=[],
), patch(
"homeassistant.components.fibaro.FibaroClientV4.devices",
array_mock,
create=True,
"homeassistant.components.fibaro.FibaroClient.read_devices",
return_value=[],
), patch(
"homeassistant.components.fibaro.FibaroClientV4.scenes",
array_mock,
create=True,
"homeassistant.components.fibaro.FibaroClient.read_scenes",
return_value=[],
), patch(
"homeassistant.components.fibaro.FibaroClientV4.client",
"homeassistant.components.fibaro.FibaroClient._rest_client",
client_mock,
create=True,
):
@ -72,10 +66,9 @@ async def test_config_flow_user_initiated_success(hass: HomeAssistant) -> None:
assert result["step_id"] == "user"
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.return_value = Mock(status=True)
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect",
return_value=True,
), patch(
"homeassistant.components.fibaro.async_setup_entry",
return_value=True,
@ -109,10 +102,9 @@ async def test_config_flow_user_initiated_connect_failure(hass: HomeAssistant) -
assert result["step_id"] == "user"
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.return_value = Mock(status=False)
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect",
return_value=False,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -139,9 +131,9 @@ async def test_config_flow_user_initiated_auth_failure(hass: HomeAssistant) -> N
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.side_effect = HTTPException(details="Forbidden")
login_mock.side_effect = HTTPError(response=Mock(status_code=403))
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect", login_mock, create=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -170,9 +162,9 @@ async def test_config_flow_user_initiated_unknown_failure_1(
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.side_effect = HTTPException(details="Any")
login_mock.side_effect = HTTPError(response=Mock(status_code=500))
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect", login_mock, create=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -200,26 +192,29 @@ async def test_config_flow_user_initiated_unknown_failure_2(
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,
},
)
login_mock = Mock()
login_mock.side_effect = Exception()
with patch(
"homeassistant.components.fibaro.FibaroClient.connect", 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"}
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_config_flow_import(hass: HomeAssistant) -> None:
"""Test for importing config from configuration.yaml."""
login_mock = Mock()
login_mock.get.return_value = Mock(status=True)
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect", return_value=True
), patch(
"homeassistant.components.fibaro.async_setup_entry",
return_value=True,
@ -271,10 +266,8 @@ async def test_reauth_success(hass: HomeAssistant) -> None:
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.return_value = Mock(status=True)
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect", return_value=True
), patch(
"homeassistant.components.fibaro.async_setup_entry",
return_value=True,
@ -315,9 +308,9 @@ async def test_reauth_connect_failure(hass: HomeAssistant) -> None:
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.return_value = Mock(status=False)
login_mock.side_effect = Exception()
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect", login_mock, create=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -356,9 +349,9 @@ async def test_reauth_auth_failure(hass: HomeAssistant) -> None:
assert result["errors"] == {}
login_mock = Mock()
login_mock.get.side_effect = HTTPException(details="Forbidden")
login_mock.side_effect = HTTPError(response=Mock(status_code=403))
with patch(
"homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True
"homeassistant.components.fibaro.FibaroClient.connect", login_mock, create=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],