Add Avea device info (#171624)

This commit is contained in:
Sören
2026-05-26 16:27:07 +02:00
committed by GitHub
parent fe071ff66b
commit 1f873927aa
3 changed files with 177 additions and 5 deletions
+44 -4
View File
@@ -1,5 +1,6 @@
"""Light platform for Avea."""
from collections.abc import Callable
from contextlib import suppress
import logging
from typing import Any
@@ -19,6 +20,7 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@@ -27,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import color as color_util
from . import AveaConfigEntry
from .const import DOMAIN, INTEGRATION_TITLE, UNKNOWN_NAME
from .const import DOMAIN, INTEGRATION_TITLE, MODEL, UNKNOWN_NAME
_LOGGER = logging.getLogger(__name__)
UPDATE_EXCEPTIONS = (BleakError, OSError, RuntimeError)
@@ -42,6 +44,13 @@ def _normalize_name(name: str | None) -> str | None:
return name
def _read_device_info_value(read: Callable[[], str | None]) -> str | None:
"""Read a device information value from an Avea bulb."""
with suppress(*UPDATE_EXCEPTIONS):
return _normalize_name(read())
return None
def _ha_brightness_to_avea(brightness: int) -> int:
"""Convert Home Assistant brightness to Avea brightness."""
return round((brightness / 255) * AVEA_MAX_BRIGHTNESS)
@@ -96,7 +105,8 @@ async def async_setup_entry(
) -> None:
"""Set up the Avea light platform."""
async_add_entities(
[AveaLight(entry.runtime_data, entry.title)], update_before_add=True
[AveaLight(entry.runtime_data, entry.data[CONF_ADDRESS])],
update_before_add=True,
)
@@ -180,14 +190,42 @@ class AveaLight(LightEntity):
"""Representation of an Avea."""
_attr_color_mode = ColorMode.HS
_attr_has_entity_name = True
_attr_name = None
_attr_supported_color_modes = {ColorMode.HS}
def __init__(self, light: avea.Bulb, entry_title: str) -> None:
def __init__(self, light: avea.Bulb, address: str) -> None:
"""Initialize an AveaLight."""
self._light = light
self._attr_name = entry_title
self._attr_unique_id = address
self._attr_brightness = light.brightness
self._last_brightness = 255
self._device_info_updated = False
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_BLUETOOTH, address)},
model=MODEL,
)
def _update_device_info(self) -> None:
"""Fetch device information from the Avea bulb."""
device_info = self._attr_device_info
assert device_info is not None
manufacturer = _read_device_info_value(self._light.get_manufacturer_name)
hardware_revision = _read_device_info_value(self._light.get_hardware_revision)
firmware_version = _read_device_info_value(self._light.get_fw_version)
serial_number = _read_device_info_value(self._light.get_serial_number)
if manufacturer:
device_info["manufacturer"] = manufacturer
if hardware_revision:
device_info["hw_version"] = hardware_revision
if firmware_version:
device_info["sw_version"] = firmware_version
if serial_number:
device_info["serial_number"] = serial_number
self._device_info_updated = True
def turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on."""
@@ -214,6 +252,8 @@ class AveaLight(LightEntity):
connected = self._light.connect()
try:
if not self._device_info_updated:
self._update_device_info()
brightness = self._light.get_brightness()
rgb_color = self._light.get_rgb()
finally:
+3
View File
@@ -5,6 +5,9 @@ from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
AVEA_FIRMWARE_VERSION = "2.4.6 (135)"
AVEA_SERIAL_NUMBER = "FFEEDDCCBBAA"
AVEA_DISCOVERY_INFO = BluetoothServiceInfoBleak(
name="Avea Bulb",
address="AA:BB:CC:DD:EE:FF",
+130 -1
View File
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, call, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.avea.const import UNKNOWN_NAME
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_HS_COLOR,
@@ -15,8 +16,9 @@ from homeassistant.components.light import (
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import AVEA_DISCOVERY_INFO
from . import AVEA_DISCOVERY_INFO, AVEA_FIRMWARE_VERSION, AVEA_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -26,10 +28,18 @@ def mock_bulb() -> MagicMock:
"""Return a mocked Avea bulb."""
bulb = MagicMock()
bulb.name = "Unknown"
bulb.fw_version = "Unknown"
bulb.hardware_revision = "Unknown"
bulb.manufacturer_name = "Unknown"
bulb.serial_number = "Unknown"
bulb.brightness = 0
bulb.connect.return_value = True
bulb.get_brightness.return_value = 0
bulb.get_fw_version.return_value = AVEA_FIRMWARE_VERSION
bulb.get_hardware_revision.return_value = "Elgato Avea"
bulb.get_manufacturer_name.return_value = "Elgato Systems GmbH"
bulb.get_rgb.return_value = (0, 0, 0)
bulb.get_serial_number.return_value = AVEA_SERIAL_NUMBER
return bulb
@@ -65,6 +75,125 @@ async def test_init_state(
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS]
async def test_device_info(
device_registry: dr.DeviceRegistry,
setup_integration: MagicMock,
) -> None:
"""Test the device info."""
bulb = setup_integration
device = device_registry.async_get_device(
connections={(dr.CONNECTION_BLUETOOTH, AVEA_DISCOVERY_INFO.address)},
)
assert device is not None
assert device.name == "Bedroom"
assert device.manufacturer == "Elgato Systems GmbH"
assert device.model == "Avea"
assert device.hw_version == "Elgato Avea"
assert device.sw_version == AVEA_FIRMWARE_VERSION
assert device.serial_number == AVEA_SERIAL_NUMBER
bulb.get_manufacturer_name.assert_called_once()
bulb.get_hardware_revision.assert_called_once()
bulb.get_fw_version.assert_called_once()
bulb.get_serial_number.assert_called_once()
async def test_device_info_populates_when_connect_fails(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
mock_bulb: MagicMock,
) -> None:
"""Test device info is populated when the shared connection fails."""
mock_bulb.connect.return_value = False
with (
patch(
"homeassistant.components.avea.async_ble_device_from_address",
return_value=AVEA_DISCOVERY_INFO.device,
),
patch("homeassistant.components.avea.avea.Bulb", return_value=mock_bulb),
):
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
mock_bulb.connect.assert_called_once()
mock_bulb.get_manufacturer_name.assert_called_once()
mock_bulb.get_hardware_revision.assert_called_once()
mock_bulb.get_fw_version.assert_called_once()
mock_bulb.get_serial_number.assert_called_once()
mock_bulb.disconnect.assert_not_called()
device = device_registry.async_get_device(
connections={(dr.CONNECTION_BLUETOOTH, AVEA_DISCOVERY_INFO.address)},
)
assert device is not None
assert device.manufacturer == "Elgato Systems GmbH"
assert device.model == "Avea"
assert device.hw_version == "Elgato Avea"
assert device.sw_version == AVEA_FIRMWARE_VERSION
assert device.serial_number == AVEA_SERIAL_NUMBER
async def test_device_info_ignores_unknown_values(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
mock_bulb: MagicMock,
) -> None:
"""Test unknown device info is not populated."""
mock_bulb.get_manufacturer_name.return_value = UNKNOWN_NAME
mock_bulb.get_hardware_revision.return_value = ""
mock_bulb.get_fw_version.return_value = UNKNOWN_NAME
mock_bulb.get_serial_number.return_value = ""
with (
patch(
"homeassistant.components.avea.async_ble_device_from_address",
return_value=AVEA_DISCOVERY_INFO.device,
),
patch("homeassistant.components.avea.avea.Bulb", return_value=mock_bulb),
):
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
device = device_registry.async_get_device(
connections={(dr.CONNECTION_BLUETOOTH, AVEA_DISCOVERY_INFO.address)},
)
assert device is not None
assert device.manufacturer is None
assert device.model == "Avea"
assert device.hw_version is None
assert device.sw_version is None
assert device.serial_number is None
async def test_device_info_is_read_once(
hass: HomeAssistant,
setup_integration: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test device info is read once."""
bulb = setup_integration
bulb.get_manufacturer_name.reset_mock()
bulb.get_hardware_revision.reset_mock()
bulb.get_fw_version.reset_mock()
bulb.get_serial_number.reset_mock()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
bulb.get_manufacturer_name.assert_not_called()
bulb.get_hardware_revision.assert_not_called()
bulb.get_fw_version.assert_not_called()
bulb.get_serial_number.assert_not_called()
async def test_turn_on_and_off(
hass: HomeAssistant,
setup_integration: MagicMock,