diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index bd7c3a09ec0..3b5eece1a14 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -29,8 +29,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -156,16 +157,21 @@ class FibaroController: ) # 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 = None # Unique serial number of the hub - self.name = None # The friendly name of the hub + 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 + self.hub_api_url: str = config[CONF_URL] + # Device infos by fibaro device id + self._device_infos: dict[int, DeviceInfo] = {} 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) - self.name = slugify(info.hcName) + self.hub_serial = info.serialNumber + self.hub_name = info.hcName + self.hub_software_version = info.softVersion except AssertionError: _LOGGER.error("Can't connect to Fibaro HC. Please check URL") return False @@ -305,6 +311,44 @@ class FibaroController: platform = Platform.LIGHT return platform + def _create_device_info(self, device: Any, devices: list) -> 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: + return + + master_entity: Any | None = None + if device.parentId == 1: + master_entity = device + else: + for parent in devices: + if "id" in parent and parent.id == device.parentId: + master_entity = parent + if master_entity is None: + _LOGGER.error("Parent with id %s not found", device.parentId) + return + + if "zwaveCompany" in master_entity.properties: + manufacturer = master_entity.properties.zwaveCompany + else: + manufacturer = "Unknown" + + self._device_infos[master_entity.id] = DeviceInfo( + identifiers={(DOMAIN, master_entity.id)}, + manufacturer=manufacturer, + name=master_entity.name, + via_device=(DOMAIN, self.hub_serial), + ) + + 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] + return DeviceInfo(identifiers={(DOMAIN, self.hub_serial)}) + def _read_scenes(self): scenes = self._client.scenes.list() self._scene_map = {} @@ -321,14 +365,14 @@ class FibaroController: device.ha_id = ( f"scene_{slugify(room_name)}_{slugify(device.name)}_{device.id}" ) - device.unique_id_str = f"{self.hub_serial}.scene.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.scene.{device.id}" self._scene_map[device.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 = self._client.devices.list() + devices = list(self._client.devices.list()) self._device_map = {} last_climate_parent = None last_endpoint = None @@ -355,7 +399,8 @@ class FibaroController: device.mapped_platform = None if (platform := device.mapped_platform) is None: continue - device.unique_id_str = f"{self.hub_serial}.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.{device.id}" + self._create_device_info(device, devices) self._device_map[device.id] = device _LOGGER.debug( "%s (%s, %s) -> %s %s", @@ -462,6 +507,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for platform in PLATFORMS: devices[platform] = [*controller.fibaro_devices[platform]] + # register the hub device info separately as the hub has sometimes no entities + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, controller.hub_serial)}, + manufacturer="Fibaro", + name=controller.hub_name, + model=controller.hub_serial, + sw_version=controller.hub_software_version, + configuration_url=controller.hub_api_url.removesuffix("/api/"), + ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) controller.enable_state_handler() @@ -490,6 +547,7 @@ class FibaroDevice(Entity): 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) # propagate hidden attribute set in fibaro home center to HA if "visible" in fibaro_device and fibaro_device.visible is False: self._attr_entity_registry_visible_default = False diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index b0ea05e49e1..fd53bd5b94f 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from slugify import slugify import voluptuous as vol from homeassistant import config_entries @@ -44,9 +45,12 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str _LOGGER.debug( "Successfully connected to fibaro home center %s with name %s", controller.hub_serial, - controller.name, + controller.hub_name, ) - return {"serial_number": controller.hub_serial, "name": controller.name} + return { + "serial_number": slugify(controller.hub_serial), + "name": controller.hub_name, + } class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index e4e8b19d308..045adce5764 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -7,6 +7,7 @@ from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice @@ -33,6 +34,15 @@ async def async_setup_entry( class FibaroScene(FibaroDevice, Scene): """Representation of a Fibaro scene entity.""" + def __init__(self, fibaro_device: Any) -> None: + """Initialize the Fibaro scene.""" + super().__init__(fibaro_device) + + # All scenes are shown on hub device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.controller.hub_serial)} + ) + def activate(self, **kwargs: Any) -> None: """Activate the scene.""" self.fibaro_device.start() diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index f056f484a58..14f28257588 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -14,17 +14,23 @@ TEST_NAME = "my_fibaro_home_center" TEST_URL = "http://192.168.1.1/api/" TEST_USERNAME = "user" TEST_PASSWORD = "password" +TEST_VERSION = "4.360" @pytest.fixture(name="fibaro_client", autouse=True) def fibaro_client_fixture(): """Mock common methods and attributes of fibaro client.""" info_mock = Mock() - info_mock.get.return_value = Mock(serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME) + info_mock.get.return_value = Mock( + serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME, softVersion=TEST_VERSION + ) array_mock = Mock() array_mock.list.return_value = [] + client_mock = Mock() + client_mock.base_url.return_value = TEST_URL + with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( "fiblary3.client.v4.client.Client.info", info_mock, @@ -37,6 +43,10 @@ def fibaro_client_fixture(): "fiblary3.client.v4.client.Client.scenes", array_mock, create=True, + ), patch( + "fiblary3.client.v4.client.Client.client", + client_mock, + create=True, ): yield