diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 6bf86be7296..9479a64528c 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -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 diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 7954334689d..14f0a6a162c 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -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() diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 43bd99520e1..5f34e0d67dd 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -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: diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index 364cbcf39cb..e19c5c32e8a 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -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 diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 0913a18b405..577c6612552 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -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 diff --git a/homeassistant/components/fibaro/lock.py b/homeassistant/components/fibaro/lock.py index e8fc4ca7180..0fa1337e3e3 100644 --- a/homeassistant/components/fibaro/lock.py +++ b/homeassistant/components/fibaro/lock.py @@ -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) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 7a52ad4bf6e..f179cbc8a9c 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -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"] } diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index 045adce5764..0023b8e3fba 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -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) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index da1b66c6528..6bb8291bbbb 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -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) diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index 66aad4d673b..d5c6eebeee5 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index c91c7099e77..1c024d5b062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6735c90aac..167ba524d70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index 1e863edbd9a..bd7fa861e1b 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -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"],