From d103103da3ff44f0506042396bcde89b4fe32774 Mon Sep 17 00:00:00 2001 From: Bartlomiej Kobus Date: Fri, 8 May 2026 07:53:09 +0000 Subject: [PATCH 1/6] blebox: add has_entity_name and translation keys to blebox entities for proper naming --- .../components/blebox/binary_sensor.py | 7 ++ homeassistant/components/blebox/button.py | 2 + homeassistant/components/blebox/climate.py | 1 + homeassistant/components/blebox/cover.py | 2 + homeassistant/components/blebox/entity.py | 5 +- homeassistant/components/blebox/light.py | 9 +- homeassistant/components/blebox/sensor.py | 31 ++++++- homeassistant/components/blebox/strings.json | 35 ++++++++ homeassistant/components/blebox/switch.py | 11 +++ tests/components/blebox/test_binary_sensor.py | 46 +++++++++- tests/components/blebox/test_button.py | 4 +- tests/components/blebox/test_climate.py | 6 +- tests/components/blebox/test_cover.py | 12 +-- tests/components/blebox/test_light.py | 81 ++++++++++++++++-- tests/components/blebox/test_sensor.py | 85 +++++++++++++++++-- tests/components/blebox/test_switch.py | 39 +++++++-- 16 files changed, 340 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/blebox/binary_sensor.py b/homeassistant/components/blebox/binary_sensor.py index b9032e6e70582f..f021510cd1b78e 100644 --- a/homeassistant/components/blebox/binary_sensor.py +++ b/homeassistant/components/blebox/binary_sensor.py @@ -16,6 +16,7 @@ BINARY_SENSOR_TYPES = ( BinarySensorEntityDescription( key="moisture", + translation_key="moisture", device_class=BinarySensorDeviceClass.MOISTURE, ), ) @@ -45,6 +46,12 @@ def __init__( """Initialize a BleBox binary sensor feature.""" super().__init__(feature) self.entity_description = description + if feature.name: + self._attr_name = feature.name + elif feature.index: + self._attr_translation_placeholders = {"index": f" {feature.index}"} + else: + self._attr_translation_placeholders = {"index": ""} @property def is_on(self) -> bool: diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py index 5fae765ec3a794..df9a80c3f7af82 100644 --- a/homeassistant/components/blebox/button.py +++ b/homeassistant/components/blebox/button.py @@ -26,6 +26,8 @@ async def async_setup_entry( class BleBoxButtonEntity(BleBoxEntity[blebox_uniapi.button.Button], ButtonEntity): """Representation of BleBox buttons.""" + _attr_name = None + def __init__(self, feature: blebox_uniapi.button.Button) -> None: """Initialize a BleBox button feature.""" super().__init__(feature) diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 2d1f6c5ae9e4bd..9c2f35cff1ec31 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -50,6 +50,7 @@ async def async_setup_entry( class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity): """Representation of a BleBox climate feature (saunaBox).""" + _attr_name = None _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 444713d5b17aaf..1047393fd879ec 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -56,6 +56,8 @@ async def async_setup_entry( class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity): """Representation of a BleBox cover feature.""" + _attr_name = None + def __init__(self, feature: blebox_uniapi.cover.Cover) -> None: """Initialize a BleBox cover feature.""" super().__init__(feature) diff --git a/homeassistant/components/blebox/entity.py b/homeassistant/components/blebox/entity.py index 14e87349a623e2..be87d5b5bf069f 100644 --- a/homeassistant/components/blebox/entity.py +++ b/homeassistant/components/blebox/entity.py @@ -16,10 +16,11 @@ class BleBoxEntity[_FeatureT: Feature](Entity): """Implements a common class for entities representing a BleBox feature.""" + _attr_has_entity_name = True + def __init__(self, feature: _FeatureT) -> None: """Initialize a BleBox entity.""" self._feature = feature - self._attr_name = feature.full_name self._attr_unique_id = feature.unique_id product = feature.product self._attr_device_info = DeviceInfo( @@ -36,4 +37,4 @@ async def async_update(self) -> None: try: await self._feature.async_update() except Error as ex: - _LOGGER.error("Updating '%s' failed: %s", self.name, ex) + _LOGGER.error("Updating '%s' failed: %s", self._feature.full_name, ex) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index a30f6f3ee84bb8..0e51f30fa16b49 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -66,6 +66,11 @@ def __init__(self, feature: blebox_uniapi.light.Light) -> None: super().__init__(feature) if feature.effect_list: self._attr_supported_features = LightEntityFeature.EFFECT + if feature.index is not None: + self._attr_translation_key = "channel" + self._attr_translation_placeholders = {"index": f" {feature.index + 1}"} + else: + self._attr_name = None @property def is_on(self) -> bool: @@ -205,7 +210,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: await self._feature.async_on(value) except ValueError as exc: raise ValueError( - f"Turning on '{self.name}' failed: Bad value {value}" + f"Turning on '{self._feature.full_name}' failed: Bad value {value}" ) from exc if effect is not None: @@ -214,7 +219,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: await self._feature.async_api_command("effect", effect_value) except ValueError as exc: raise ValueError( - f"Turning on with effect '{self.name}' failed: {effect} not in" + f"Turning on with effect '{self._feature.full_name}' failed: {effect} not in" " effect list." ) from exc diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 8570c4b29e188e..7ef652caaa0c71 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,5 +1,6 @@ """BleBox sensor entities.""" +from collections import Counter from datetime import datetime, timedelta import blebox_uniapi.sensor @@ -36,82 +37,98 @@ SENSOR_TYPES = ( SensorEntityDescription( key="pm1", + translation_key="pm1", device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), SensorEntityDescription( key="pm2_5", + translation_key="pm2_5", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), SensorEntityDescription( key="pm10", + translation_key="pm10", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), SensorEntityDescription( key="temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), SensorEntityDescription( key="powerConsumption", + translation_key="power_consumption", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, icon="mdi:lightning-bolt", ), SensorEntityDescription( key="humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="wind", + translation_key="wind_speed", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, ), SensorEntityDescription( key="illuminance", + translation_key="illuminance", device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=LIGHT_LUX, ), SensorEntityDescription( key="forwardActiveEnergy", + translation_key="forward_active_energy", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), SensorEntityDescription( key="reverseActiveEnergy", + translation_key="reverse_active_energy", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), SensorEntityDescription( key="reactivePower", + translation_key="reactive_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE, ), SensorEntityDescription( key="activePower", + translation_key="active_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="apparentPower", + translation_key="apparent_power", device_class=SensorDeviceClass.APPARENT_POWER, native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, ), SensorEntityDescription( key="voltage", + translation_key="voltage", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, ), SensorEntityDescription( key="current", + translation_key="current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, ), SensorEntityDescription( key="frequency", + translation_key="frequency", device_class=SensorDeviceClass.FREQUENCY, native_unit_of_measurement=UnitOfFrequency.HERTZ, ), @@ -124,9 +141,17 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up a BleBox entry.""" + features = config_entry.runtime_data.features.get("sensors", []) + counts = Counter(f.device_class for f in features) entities = [ - BleBoxSensorEntity(feature, description) - for feature in config_entry.runtime_data.features.get("sensors", []) + BleBoxSensorEntity( + feature, + description, + f" {feature.index}" + if counts[feature.device_class] > 1 and feature.index + else "", + ) + for feature in features for description in SENSOR_TYPES if description.key == feature.device_class ] @@ -140,10 +165,12 @@ def __init__( self, feature: blebox_uniapi.sensor.BaseSensor, description: SensorEntityDescription, + index: str = "", ) -> None: """Initialize a BleBox sensor feature.""" super().__init__(feature) self.entity_description = description + self._attr_translation_placeholders = {"index": index} @property def native_value(self): diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index a9193cd590ce54..9fa112f3ffcdc7 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -18,9 +18,44 @@ "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]" }, + "data_description": { + "host": "The IP address of your BleBox device.", + "password": "The password for your BleBox device.", + "port": "The port of your BleBox device.", + "username": "The username for your BleBox device." + }, "description": "Set up your BleBox to integrate with Home Assistant.", "title": "Set up your BleBox device" } } + }, + "entity": { + "binary_sensor": { + "moisture": { "name": "Moisture{index}" } + }, + "light": { + "channel": { "name": "Channel{index}" } + }, + "sensor": { + "active_power": { "name": "Active power{index}" }, + "apparent_power": { "name": "Apparent power{index}" }, + "current": { "name": "Current{index}" }, + "forward_active_energy": { "name": "Forward active energy{index}" }, + "frequency": { "name": "Frequency{index}" }, + "humidity": { "name": "Humidity{index}" }, + "illuminance": { "name": "Illuminance{index}" }, + "pm1": { "name": "PM 1{index}" }, + "pm10": { "name": "PM 10{index}" }, + "pm2_5": { "name": "PM 2.5{index}" }, + "power_consumption": { "name": "Energy (last 1 h){index}" }, + "reactive_power": { "name": "Reactive power{index}" }, + "reverse_active_energy": { "name": "Reverse active energy{index}" }, + "temperature": { "name": "Temperature{index}" }, + "voltage": { "name": "Voltage{index}" }, + "wind_speed": { "name": "Wind speed{index}" } + }, + "switch": { + "relay": { "name": "Relay{index}" } + } } } diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index c0f9d9a5e4b3a5..2a8f0dcb5e6fd3 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -33,6 +33,17 @@ class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity _attr_device_class = SwitchDeviceClass.SWITCH + def __init__(self, feature: blebox_uniapi.switch.Switch) -> None: + """Initialize a BleBox switch feature.""" + super().__init__(feature) + if feature.name: + self._attr_name = feature.name + elif feature.index: + self._attr_translation_key = "relay" + self._attr_translation_placeholders = {"index": f" {feature.index}"} + else: + self._attr_name = None + @property def is_on(self) -> bool | None: """Return whether switch is on.""" diff --git a/tests/components/blebox/test_binary_sensor.py b/tests/components/blebox/test_binary_sensor.py index 79da8b41ee4098..8a9d19a03fc99e 100644 --- a/tests/components/blebox/test_binary_sensor.py +++ b/tests/components/blebox/test_binary_sensor.py @@ -22,11 +22,13 @@ def airsensor_fixture() -> tuple[AsyncMock, str]: unique_id="BleBox-windRainSensor-ea68e74f4f49-0.rain", full_name="windRainSensor-0.rain", device_class="moisture", + index=None, ) + type(feature).name = PropertyMock(return_value=None) product = feature.product type(product).name = PropertyMock(return_value="My rain sensor") type(product).model = PropertyMock(return_value="rainSensor") - return feature, "binary_sensor.my_rain_sensor_windrainsensor_0_rain" + return feature, "binary_sensor.my_rain_sensor_moisture" async def test_init( @@ -38,7 +40,7 @@ async def test_init( assert entry.unique_id == "BleBox-windRainSensor-ea68e74f4f49-0.rain" state = hass.states.get(entity_id) - assert state.name == "My rain sensor windRainSensor-0.rain" + assert state.name == "My rain sensor Moisture" assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOISTURE assert state.state == STATE_ON @@ -46,3 +48,43 @@ async def test_init( device = device_registry.async_get(entry.device_id) assert device.name == "My rain sensor" + + +async def test_binary_sensor_with_index(hass: HomeAssistant) -> None: + """Test that a binary sensor with a non-zero index gets a numeric suffix.""" + feature = mock_feature( + "binary_sensors", + blebox_uniapi.binary_sensor.Rain, + unique_id="BleBox-windRainSensor-ea68e74f4f49-1.rain", + full_name="windRainSensor-1.rain", + device_class="moisture", + index=1, + ) + type(feature).name = PropertyMock(return_value=None) + product = feature.product + type(product).name = PropertyMock(return_value="My rain sensor") + type(product).model = PropertyMock(return_value="rainSensor") + + await async_setup_entity(hass, "binary_sensor.my_rain_sensor_moisture_1") + state = hass.states.get("binary_sensor.my_rain_sensor_moisture_1") + assert state.name == "My rain sensor Moisture 1" + + +async def test_binary_sensor_with_name(hass: HomeAssistant) -> None: + """Test that a binary sensor with a feature name uses it as the entity name.""" + feature = mock_feature( + "binary_sensors", + blebox_uniapi.binary_sensor.Rain, + unique_id="BleBox-windRainSensor-ea68e74f4f49-0.rain", + full_name="windRainSensor-0.rain", + device_class="moisture", + index=0, + ) + type(feature).name = PropertyMock(return_value="Front yard") + product = feature.product + type(product).name = PropertyMock(return_value="My rain sensor") + type(product).model = PropertyMock(return_value="rainSensor") + + await async_setup_entity(hass, "binary_sensor.my_rain_sensor_front_yard") + state = hass.states.get("binary_sensor.my_rain_sensor_front_yard") + assert state.name == "My rain sensor Front yard" diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py index d3aae3d61c8453..5e74f090121da5 100644 --- a/tests/components/blebox/test_button.py +++ b/tests/components/blebox/test_button.py @@ -38,7 +38,7 @@ def tv_lift_box_fixture(caplog: pytest.LogCaptureFixture): type(product).model = PropertyMock(return_value="tvLiftBox") type(product)._query_string = PropertyMock(return_value="open_or_stop") - return (feature, "button.my_tvliftbox_tvliftbox_open_or_stop") + return (feature, "button.my_tvliftbox") async def test_tvliftbox_init( @@ -53,7 +53,7 @@ async def test_tvliftbox_init( assert entry.unique_id == "BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop" - assert state.name == "My tvLiftBox tvLiftBox-open_or_stop" + assert state.name == "My tvLiftBox" @pytest.mark.parametrize("input", query_icon_matching) diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py index 5b56c2b903639c..aa13072b1debd4 100644 --- a/tests/components/blebox/test_climate.py +++ b/tests/components/blebox/test_climate.py @@ -52,7 +52,7 @@ def saunabox_fixture(): product = feature.product type(product).name = PropertyMock(return_value="My sauna") type(product).model = PropertyMock(return_value="saunaBox") - return (feature, "climate.my_sauna_saunabox_thermostat") + return (feature, "climate.my_sauna") @pytest.fixture(name="thermobox") @@ -75,7 +75,7 @@ def thermobox_fixture(): product = feature.product type(product).name = PropertyMock(return_value="My thermo") type(product).model = PropertyMock(return_value="thermoBox") - return (feature, "climate.my_thermo_thermobox_thermostat") + return (feature, "climate.my_thermo") async def test_init( @@ -88,7 +88,7 @@ async def test_init( assert entry.unique_id == "BleBox-saunaBox-1afe34db9437-thermostat" state = hass.states.get(entity_id) - assert state.name == "My sauna saunaBox-thermostat" + assert state.name == "My sauna" supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index ae169760180ad3..0188a7c994f530 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -56,7 +56,7 @@ def shutterbox_fixture(): product = feature.product type(product).name = PropertyMock(return_value="My shutter") type(product).model = PropertyMock(return_value="shutterBox") - return (feature, "cover.my_shutter_shutterbox_position") + return (feature, "cover.my_shutter") @pytest.fixture(name="gatebox") @@ -77,7 +77,7 @@ def gatebox_fixture(): product = feature.product type(product).name = PropertyMock(return_value="My gatebox") type(product).model = PropertyMock(return_value="gateBox") - return (feature, "cover.my_gatebox_gatebox_position") + return (feature, "cover.my_gatebox") @pytest.fixture(name="gatecontroller") @@ -98,7 +98,7 @@ def gate_fixture(): product = feature.product type(product).name = PropertyMock(return_value="My gate controller") type(product).model = PropertyMock(return_value="gateController") - return (feature, "cover.my_gate_controller_gatecontroller_position") + return (feature, "cover.my_gate_controller") async def test_init_gatecontroller( @@ -111,7 +111,7 @@ async def test_init_gatecontroller( assert entry.unique_id == "BleBox-gateController-2bee34e750b8-position" state = hass.states.get(entity_id) - assert state.name == "My gate controller gateController-position" + assert state.name == "My gate controller" assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.GATE supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] @@ -142,7 +142,7 @@ async def test_init_shutterbox( assert entry.unique_id == "BleBox-shutterBox-2bee34e750b8-position" state = hass.states.get(entity_id) - assert state.name == "My shutter shutterBox-position" + assert state.name == "My shutter" assert entry.original_device_class == CoverDeviceClass.SHUTTER supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] @@ -173,7 +173,7 @@ async def test_init_gatebox( assert entry.unique_id == "BleBox-gateBox-1afe34db9437-position" state = hass.states.get(entity_id) - assert state.name == "My gatebox gateBox-position" + assert state.name == "My gatebox" assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.DOOR supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 323cea552937e9..d16c59a01d6458 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -46,11 +46,12 @@ def dimmer_fixture(): supports_white=False, color_mode=blebox_uniapi.light.BleboxColorMode.MONO, effect_list=None, + index=None, ) product = feature.product type(product).name = PropertyMock(return_value="My dimmer") type(product).model = PropertyMock(return_value="dimmerBox") - return (feature, "light.my_dimmer_dimmerbox_brightness") + return (feature, "light.my_dimmer") async def test_dimmer_init( @@ -63,7 +64,7 @@ async def test_dimmer_init( assert entry.unique_id == "BleBox-dimmerBox-1afe34e750b8-brightness" state = hass.states.get(entity_id) - assert state.name == "My dimmer dimmerBox-brightness" + assert state.name == "My dimmer" color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] assert color_modes == [ColorMode.BRIGHTNESS] @@ -220,11 +221,12 @@ def wlightboxs_fixture(): supports_white=False, color_mode=blebox_uniapi.light.BleboxColorMode.MONO, effect_list=["NONE", "PL", "RELAX"], + index=None, ) product = feature.product type(product).name = PropertyMock(return_value="My wLightBoxS") type(product).model = PropertyMock(return_value="wLightBoxS") - return (feature, "light.my_wlightboxs_wlightboxs_color") + return (feature, "light.my_wlightboxs") async def test_wlightbox_s_init( @@ -237,7 +239,7 @@ async def test_wlightbox_s_init( assert entry.unique_id == "BleBox-wLightBoxS-1afe34e750b8-color" state = hass.states.get(entity_id) - assert state.name == "My wLightBoxS wLightBoxS-color" + assert state.name == "My wLightBoxS" color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] assert color_modes == [ColorMode.BRIGHTNESS] @@ -324,11 +326,12 @@ def wlightbox_fixture(): color_mode=blebox_uniapi.light.BleboxColorMode.RGBW, effect="NONE", effect_list=["NONE", "PL", "POLICE"], + index=None, ) product = feature.product type(product).name = PropertyMock(return_value="My wLightBox") type(product).model = PropertyMock(return_value="wLightBox") - return (feature, "light.my_wlightbox_wlightbox_color") + return (feature, "light.my_wlightbox") @pytest.fixture(name="wlightbox_ct") @@ -408,7 +411,7 @@ async def test_wlightbox_init( assert entry.unique_id == "BleBox-wLightBox-1afe34e750b8-color" state = hass.states.get(entity_id) - assert state.name == "My wLightBox wLightBox-color" + assert state.name == "My wLightBox" color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] assert color_modes == [ColorMode.RGBW] @@ -648,3 +651,69 @@ def turn_on(value): state = hass.states.get(entity_id) assert state.attributes[ATTR_EFFECT] == "POLICE" + + +@pytest.mark.parametrize( + ("index", "color_mode", "entity_id", "expected_name"), + [ + ( + 0, + blebox_uniapi.light.BleboxColorMode.MONO, + "light.my_wlightbox_channel_1", + "My wLightBox Channel 1", + ), + ( + 1, + blebox_uniapi.light.BleboxColorMode.MONO, + "light.my_wlightbox_channel_2", + "My wLightBox Channel 2", + ), + ( + 3, + blebox_uniapi.light.BleboxColorMode.MONO, + "light.my_wlightbox_channel_4", + "My wLightBox Channel 4", + ), + ( + 0, + blebox_uniapi.light.BleboxColorMode.CTx2, + "light.my_wlightbox_channel_1", + "My wLightBox Channel 1", + ), + ( + 1, + blebox_uniapi.light.BleboxColorMode.CTx2, + "light.my_wlightbox_channel_2", + "My wLightBox Channel 2", + ), + ], +) +async def test_multichannel_light_name( + hass: HomeAssistant, + index: int, + color_mode: blebox_uniapi.light.BleboxColorMode, + entity_id: str, + expected_name: str, +) -> None: + """Test that multi-channel lights get the correct channel name.""" + feature = mock_feature( + "lights", + blebox_uniapi.light.Light, + unique_id=f"BleBox-wLightBox-1afe34e750b8-brightness_{index}", + full_name=f"wLightBox-brightness_{index}", + device_class=None, + brightness=None, + is_on=None, + supports_color=False, + supports_white=False, + color_mode=color_mode, + effect_list=None, + index=index, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My wLightBox") + type(product).model = PropertyMock(return_value="wLightBox") + + await async_setup_entity(hass, entity_id) + state = hass.states.get(entity_id) + assert state.name == expected_name diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index 295321b557a5a7..f5861b61292fab 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -17,7 +17,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .conftest import async_setup_entity, mock_feature +from .conftest import ( + async_setup_entities, + async_setup_entity, + mock_feature, + mock_only_feature, + setup_product_mock, +) @pytest.fixture(name="airsensor") @@ -31,11 +37,12 @@ def airsensor_fixture(): device_class="pm1", unit="concentration_of_mp", native_value=None, + index=None, ) product = feature.product type(product).name = PropertyMock(return_value="My air sensor") type(product).model = PropertyMock(return_value="airSensor") - return (feature, "sensor.my_air_sensor_airsensor_0_air") + return (feature, "sensor.my_air_sensor_pm_1") @pytest.fixture(name="tempsensor") @@ -50,11 +57,12 @@ def tempsensor_fixture(): unit="celsius", current=None, native_value=None, + index=None, ) product = feature.product type(product).name = PropertyMock(return_value="My temperature sensor") type(product).model = PropertyMock(return_value="tempSensor") - return (feature, "sensor.my_temperature_sensor_tempsensor_0_temperature") + return (feature, "sensor.my_temperature_sensor_temperature") async def test_init( @@ -67,7 +75,7 @@ async def test_init( assert entry.unique_id == "BleBox-tempSensor-1afe34db9437-0.temperature" state = hass.states.get(entity_id) - assert state.name == "My temperature sensor tempSensor-0.temperature" + assert state.name == "My temperature sensor Temperature" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS @@ -122,7 +130,7 @@ async def test_airsensor_init( assert entry.unique_id == "BleBox-airSensor-1afe34db9437-0.air" state = hass.states.get(entity_id) - assert state.name == "My air sensor airSensor-0.air" + assert state.name == "My air sensor PM 1" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.PM1 assert state.state == STATE_UNKNOWN @@ -136,6 +144,73 @@ async def test_airsensor_init( assert device.sw_version == "1.23" +async def test_multi_sensor_single_has_no_channel_suffix( + hass: HomeAssistant, +) -> None: + """Test that a single sensor of its type shows no channel suffix.""" + feature = mock_feature( + "sensors", + blebox_uniapi.sensor.Temperature, + unique_id="BleBox-tempSensor-1afe34db9437-0.temperature", + full_name="tempSensor-0.temperature", + device_class="temperature", + unit="celsius", + current=None, + native_value=None, + sensor_id=0, + index=0, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My temp sensor") + type(product).model = PropertyMock(return_value="tempSensor") + + await async_setup_entity(hass, "sensor.my_temp_sensor_temperature") + state = hass.states.get("sensor.my_temp_sensor_temperature") + assert state.name == "My temp sensor Temperature" + + +async def test_multi_sensor_multiple_have_channel_suffix( + hass: HomeAssistant, +) -> None: + """Test that multiple sensors of the same type get a channel number suffix.""" + features = [ + mock_only_feature( + blebox_uniapi.sensor.Temperature, + unique_id=f"BleBox-multiSensor-aabbcc-temperature_{i}", + full_name=f"multiSensor-temperature_{i}", + device_class="temperature", + unit="celsius", + current=None, + native_value=None, + sensor_id=i, + index=i, + ) + for i in range(3) + ] + + product = setup_product_mock("sensors", features) + type(product).name = PropertyMock(return_value="My multi sensor") + type(product).model = PropertyMock(return_value="multiSensor") + type(product).brand = PropertyMock(return_value="BleBox") + type(product).firmware_version = PropertyMock(return_value="1.23") + type(product).unique_id = PropertyMock(return_value="aabbcc112233") + + for feature in features: + type(feature).product = PropertyMock(return_value=product) + feature.async_update = AsyncMock() + + entity_ids = [ + "sensor.my_multi_sensor_temperature", + "sensor.my_multi_sensor_temperature_1", + "sensor.my_multi_sensor_temperature_2", + ] + await async_setup_entities(hass, entity_ids) + + assert hass.states.get(entity_ids[0]).name == "My multi sensor Temperature" + assert hass.states.get(entity_ids[1]).name == "My multi sensor Temperature 1" + assert hass.states.get(entity_ids[2]).name == "My multi sensor Temperature 2" + + async def test_airsensor_update(airsensor, hass: HomeAssistant) -> None: """Test air quality sensor state after update.""" diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index 82c4db0d6cf252..484877f0a6c019 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -37,12 +37,14 @@ def switchbox_fixture(): full_name="switchBox-0.relay", device_class="relay", is_on=False, + index=0, ) + type(feature).name = PropertyMock(return_value=None) feature.async_update = AsyncMock() product = feature.product type(product).name = PropertyMock(return_value="My switch box") type(product).model = PropertyMock(return_value="switchBox") - return (feature, "switch.my_switch_box_switchbox_0_relay") + return (feature, "switch.my_switch_box") async def test_switchbox_init( @@ -57,7 +59,7 @@ async def test_switchbox_init( assert entry.unique_id == "BleBox-switchBox-1afe34e750b8-0.relay" state = hass.states.get(entity_id) - assert state.name == "My switch box switchBox-0.relay" + assert state.name == "My switch box" assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH @@ -160,13 +162,16 @@ def turn_off(): def relay_mock(relay_id=0): """Return a default switchBoxD switch entity mock.""" - return mock_only_feature( + feature = mock_only_feature( blebox_uniapi.switch.Switch, unique_id=f"BleBox-switchBoxD-1afe34e750b8-{relay_id}.relay", full_name=f"switchBoxD-{relay_id}.relay", device_class="relay", is_on=None, + index=relay_id, ) + type(feature).name = PropertyMock(return_value=None) + return feature @pytest.fixture(name="switchbox_d") @@ -190,7 +195,7 @@ def switchbox_d_fixture(): return ( features, - ["switch.my_relays_switchboxd_0_relay", "switch.my_relays_switchboxd_1_relay"], + ["switch.my_relays", "switch.my_relays_relay_1"], ) @@ -209,7 +214,7 @@ async def test_switchbox_d_init( assert entry.unique_id == "BleBox-switchBoxD-1afe34e750b8-0.relay" state = hass.states.get(entity_ids[0]) - assert state.name == "My relays switchBoxD-0.relay" + assert state.name == "My relays" assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH assert state.state == STATE_UNKNOWN @@ -225,7 +230,7 @@ async def test_switchbox_d_init( assert entry.unique_id == "BleBox-switchBoxD-1afe34e750b8-1.relay" state = hass.states.get(entity_ids[1]) - assert state.name == "My relays switchBoxD-1.relay" + assert state.name == "My relays Relay 1" assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH assert state.state == STATE_UNKNOWN @@ -389,6 +394,28 @@ def turn_off1(): assert hass.states.get(entity_ids[1]).state == STATE_OFF +async def test_switchbox_with_name(hass: HomeAssistant) -> None: + """Test that a switch with a feature name uses it as the entity name.""" + feature = mock_feature( + "switches", + blebox_uniapi.switch.Switch, + unique_id="BleBox-switchBoxD-1afe34e750b8-0.relay", + full_name="switchBoxD-0.relay", + device_class="relay", + is_on=False, + index=0, + ) + type(feature).name = PropertyMock(return_value="Garden lights") + feature.async_update = AsyncMock() + product = feature.product + type(product).name = PropertyMock(return_value="My switch box") + type(product).model = PropertyMock(return_value="switchBoxD") + + await async_setup_entity(hass, "switch.my_switch_box_garden_lights") + state = hass.states.get("switch.my_switch_box_garden_lights") + assert state.name == "My switch box Garden lights" + + ALL_SWITCH_FIXTURES = ["switchbox", "switchbox_d"] From 11a0d602a028632928c0da182bf6b188c7618cd5 Mon Sep 17 00:00:00 2001 From: Bartlomiej Kobus Date: Tue, 12 May 2026 10:57:08 +0000 Subject: [PATCH 2/6] blebox: move index spacing from placeholder values to translation strings --- .../components/blebox/binary_sensor.py | 2 +- homeassistant/components/blebox/light.py | 2 +- homeassistant/components/blebox/sensor.py | 2 +- homeassistant/components/blebox/strings.json | 38 +++++++++---------- homeassistant/components/blebox/switch.py | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/blebox/binary_sensor.py b/homeassistant/components/blebox/binary_sensor.py index f021510cd1b78e..c1917c549acd99 100644 --- a/homeassistant/components/blebox/binary_sensor.py +++ b/homeassistant/components/blebox/binary_sensor.py @@ -49,7 +49,7 @@ def __init__( if feature.name: self._attr_name = feature.name elif feature.index: - self._attr_translation_placeholders = {"index": f" {feature.index}"} + self._attr_translation_placeholders = {"index": f"{feature.index}"} else: self._attr_translation_placeholders = {"index": ""} diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 0e51f30fa16b49..45ce9aab72fb6d 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -68,7 +68,7 @@ def __init__(self, feature: blebox_uniapi.light.Light) -> None: self._attr_supported_features = LightEntityFeature.EFFECT if feature.index is not None: self._attr_translation_key = "channel" - self._attr_translation_placeholders = {"index": f" {feature.index + 1}"} + self._attr_translation_placeholders = {"index": f"{feature.index + 1}"} else: self._attr_name = None diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 7ef652caaa0c71..2d55a21ca3d3af 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -147,7 +147,7 @@ async def async_setup_entry( BleBoxSensorEntity( feature, description, - f" {feature.index}" + f"{feature.index}" if counts[feature.device_class] > 1 and feature.index else "", ) diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index 9fa112f3ffcdc7..b174cd689df243 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -31,31 +31,31 @@ }, "entity": { "binary_sensor": { - "moisture": { "name": "Moisture{index}" } + "moisture": { "name": "Moisture {index}" } }, "light": { - "channel": { "name": "Channel{index}" } + "channel": { "name": "Channel {index}" } }, "sensor": { - "active_power": { "name": "Active power{index}" }, - "apparent_power": { "name": "Apparent power{index}" }, - "current": { "name": "Current{index}" }, - "forward_active_energy": { "name": "Forward active energy{index}" }, - "frequency": { "name": "Frequency{index}" }, - "humidity": { "name": "Humidity{index}" }, - "illuminance": { "name": "Illuminance{index}" }, - "pm1": { "name": "PM 1{index}" }, - "pm10": { "name": "PM 10{index}" }, - "pm2_5": { "name": "PM 2.5{index}" }, - "power_consumption": { "name": "Energy (last 1 h){index}" }, - "reactive_power": { "name": "Reactive power{index}" }, - "reverse_active_energy": { "name": "Reverse active energy{index}" }, - "temperature": { "name": "Temperature{index}" }, - "voltage": { "name": "Voltage{index}" }, - "wind_speed": { "name": "Wind speed{index}" } + "active_power": { "name": "Active power {index}" }, + "apparent_power": { "name": "Apparent power {index}" }, + "current": { "name": "Current {index}" }, + "forward_active_energy": { "name": "Forward active energy {index}" }, + "frequency": { "name": "Frequency {index}" }, + "humidity": { "name": "Humidity {index}" }, + "illuminance": { "name": "Illuminance {index}" }, + "pm1": { "name": "PM 1 {index}" }, + "pm10": { "name": "PM 10 {index}" }, + "pm2_5": { "name": "PM 2.5 {index}" }, + "power_consumption": { "name": "Energy (last 1 h) {index}" }, + "reactive_power": { "name": "Reactive power {index}" }, + "reverse_active_energy": { "name": "Reverse active energy {index}" }, + "temperature": { "name": "Temperature {index}" }, + "voltage": { "name": "Voltage {index}" }, + "wind_speed": { "name": "Wind speed {index}" } }, "switch": { - "relay": { "name": "Relay{index}" } + "relay": { "name": "Relay {index}" } } } } diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index 2a8f0dcb5e6fd3..c3a017e652a5bd 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -40,7 +40,7 @@ def __init__(self, feature: blebox_uniapi.switch.Switch) -> None: self._attr_name = feature.name elif feature.index: self._attr_translation_key = "relay" - self._attr_translation_placeholders = {"index": f" {feature.index}"} + self._attr_translation_placeholders = {"index": f"{feature.index}"} else: self._attr_name = None From 4fc634e274db58fd893c25ab2e54e873161ca6ef Mon Sep 17 00:00:00 2001 From: Bartlomiej Kobus Date: Tue, 12 May 2026 11:20:40 +0000 Subject: [PATCH 3/6] Revert "blebox: move index spacing from placeholder values to translation strings" This reverts commit 850db311ede39cc9faccc4f83246292c7af7a3b7. --- .../components/blebox/binary_sensor.py | 2 +- homeassistant/components/blebox/light.py | 2 +- homeassistant/components/blebox/sensor.py | 2 +- homeassistant/components/blebox/strings.json | 38 +++++++++---------- homeassistant/components/blebox/switch.py | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/blebox/binary_sensor.py b/homeassistant/components/blebox/binary_sensor.py index c1917c549acd99..f021510cd1b78e 100644 --- a/homeassistant/components/blebox/binary_sensor.py +++ b/homeassistant/components/blebox/binary_sensor.py @@ -49,7 +49,7 @@ def __init__( if feature.name: self._attr_name = feature.name elif feature.index: - self._attr_translation_placeholders = {"index": f"{feature.index}"} + self._attr_translation_placeholders = {"index": f" {feature.index}"} else: self._attr_translation_placeholders = {"index": ""} diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 45ce9aab72fb6d..0e51f30fa16b49 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -68,7 +68,7 @@ def __init__(self, feature: blebox_uniapi.light.Light) -> None: self._attr_supported_features = LightEntityFeature.EFFECT if feature.index is not None: self._attr_translation_key = "channel" - self._attr_translation_placeholders = {"index": f"{feature.index + 1}"} + self._attr_translation_placeholders = {"index": f" {feature.index + 1}"} else: self._attr_name = None diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 2d55a21ca3d3af..7ef652caaa0c71 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -147,7 +147,7 @@ async def async_setup_entry( BleBoxSensorEntity( feature, description, - f"{feature.index}" + f" {feature.index}" if counts[feature.device_class] > 1 and feature.index else "", ) diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index b174cd689df243..9fa112f3ffcdc7 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -31,31 +31,31 @@ }, "entity": { "binary_sensor": { - "moisture": { "name": "Moisture {index}" } + "moisture": { "name": "Moisture{index}" } }, "light": { - "channel": { "name": "Channel {index}" } + "channel": { "name": "Channel{index}" } }, "sensor": { - "active_power": { "name": "Active power {index}" }, - "apparent_power": { "name": "Apparent power {index}" }, - "current": { "name": "Current {index}" }, - "forward_active_energy": { "name": "Forward active energy {index}" }, - "frequency": { "name": "Frequency {index}" }, - "humidity": { "name": "Humidity {index}" }, - "illuminance": { "name": "Illuminance {index}" }, - "pm1": { "name": "PM 1 {index}" }, - "pm10": { "name": "PM 10 {index}" }, - "pm2_5": { "name": "PM 2.5 {index}" }, - "power_consumption": { "name": "Energy (last 1 h) {index}" }, - "reactive_power": { "name": "Reactive power {index}" }, - "reverse_active_energy": { "name": "Reverse active energy {index}" }, - "temperature": { "name": "Temperature {index}" }, - "voltage": { "name": "Voltage {index}" }, - "wind_speed": { "name": "Wind speed {index}" } + "active_power": { "name": "Active power{index}" }, + "apparent_power": { "name": "Apparent power{index}" }, + "current": { "name": "Current{index}" }, + "forward_active_energy": { "name": "Forward active energy{index}" }, + "frequency": { "name": "Frequency{index}" }, + "humidity": { "name": "Humidity{index}" }, + "illuminance": { "name": "Illuminance{index}" }, + "pm1": { "name": "PM 1{index}" }, + "pm10": { "name": "PM 10{index}" }, + "pm2_5": { "name": "PM 2.5{index}" }, + "power_consumption": { "name": "Energy (last 1 h){index}" }, + "reactive_power": { "name": "Reactive power{index}" }, + "reverse_active_energy": { "name": "Reverse active energy{index}" }, + "temperature": { "name": "Temperature{index}" }, + "voltage": { "name": "Voltage{index}" }, + "wind_speed": { "name": "Wind speed{index}" } }, "switch": { - "relay": { "name": "Relay {index}" } + "relay": { "name": "Relay{index}" } } } } diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index c3a017e652a5bd..2a8f0dcb5e6fd3 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -40,7 +40,7 @@ def __init__(self, feature: blebox_uniapi.switch.Switch) -> None: self._attr_name = feature.name elif feature.index: self._attr_translation_key = "relay" - self._attr_translation_placeholders = {"index": f"{feature.index}"} + self._attr_translation_placeholders = {"index": f" {feature.index}"} else: self._attr_name = None From 1e9a620fe6fbfac2e30d6c5a6d1c8b8a22f87b63 Mon Sep 17 00:00:00 2001 From: Bartlomiej Kobus Date: Thu, 14 May 2026 05:58:30 +0000 Subject: [PATCH 4/6] blebox: use device class names and fix indexed entity naming --- .../components/blebox/binary_sensor.py | 5 -- homeassistant/components/blebox/light.py | 2 +- homeassistant/components/blebox/sensor.py | 76 ++++++++++--------- homeassistant/components/blebox/strings.json | 36 ++++----- homeassistant/components/blebox/switch.py | 7 +- tests/components/blebox/test_binary_sensor.py | 20 ----- tests/components/blebox/test_sensor.py | 64 ++++++++-------- tests/components/blebox/test_switch.py | 4 +- 8 files changed, 94 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/blebox/binary_sensor.py b/homeassistant/components/blebox/binary_sensor.py index f021510cd1b78e..21656d0f7580bd 100644 --- a/homeassistant/components/blebox/binary_sensor.py +++ b/homeassistant/components/blebox/binary_sensor.py @@ -16,7 +16,6 @@ BINARY_SENSOR_TYPES = ( BinarySensorEntityDescription( key="moisture", - translation_key="moisture", device_class=BinarySensorDeviceClass.MOISTURE, ), ) @@ -48,10 +47,6 @@ def __init__( self.entity_description = description if feature.name: self._attr_name = feature.name - elif feature.index: - self._attr_translation_placeholders = {"index": f" {feature.index}"} - else: - self._attr_translation_placeholders = {"index": ""} @property def is_on(self) -> bool: diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 0e51f30fa16b49..1e3126f8b787ec 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -68,7 +68,7 @@ def __init__(self, feature: blebox_uniapi.light.Light) -> None: self._attr_supported_features = LightEntityFeature.EFFECT if feature.index is not None: self._attr_translation_key = "channel" - self._attr_translation_placeholders = {"index": f" {feature.index + 1}"} + self._attr_translation_placeholders = {"index": str(feature.index + 1)} else: self._attr_name = None diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 7ef652caaa0c71..b1459f2398d03c 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,6 +1,7 @@ """BleBox sensor entities.""" from collections import Counter +from dataclasses import dataclass from datetime import datetime, timedelta import blebox_uniapi.sensor @@ -34,101 +35,104 @@ SCAN_INTERVAL = timedelta(seconds=5) +@dataclass(kw_only=True, frozen=True) +class BleBoxSensorEntityDescription(SensorEntityDescription): + """Describes a BleBox sensor entity.""" + + indexed_translation_key: str | None = None + + SENSOR_TYPES = ( - SensorEntityDescription( + BleBoxSensorEntityDescription( key="pm1", - translation_key="pm1", device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="pm2_5", - translation_key="pm2_5", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="pm10", - translation_key="pm10", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="temperature", - translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="powerConsumption", translation_key="power_consumption", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, icon="mdi:lightning-bolt", ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="humidity", - translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="wind", - translation_key="wind_speed", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="illuminance", - translation_key="illuminance", device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=LIGHT_LUX, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="forwardActiveEnergy", translation_key="forward_active_energy", + indexed_translation_key="forward_active_energy_n", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="reverseActiveEnergy", translation_key="reverse_active_energy", + indexed_translation_key="reverse_active_energy_n", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="reactivePower", - translation_key="reactive_power", - device_class=SensorDeviceClass.POWER, + indexed_translation_key="reactive_power_n", + device_class=SensorDeviceClass.REACTIVE_POWER, native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="activePower", translation_key="active_power", + indexed_translation_key="active_power_n", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="apparentPower", - translation_key="apparent_power", + indexed_translation_key="apparent_power_n", device_class=SensorDeviceClass.APPARENT_POWER, native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="voltage", - translation_key="voltage", + indexed_translation_key="voltage_n", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="current", - translation_key="current", + indexed_translation_key="current_n", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, ), - SensorEntityDescription( + BleBoxSensorEntityDescription( key="frequency", - translation_key="frequency", + indexed_translation_key="frequency_n", device_class=SensorDeviceClass.FREQUENCY, native_unit_of_measurement=UnitOfFrequency.HERTZ, ), @@ -147,9 +151,9 @@ async def async_setup_entry( BleBoxSensorEntity( feature, description, - f" {feature.index}" + feature.index if counts[feature.device_class] > 1 and feature.index - else "", + else None, ) for feature in features for description in SENSOR_TYPES @@ -164,13 +168,17 @@ class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEn def __init__( self, feature: blebox_uniapi.sensor.BaseSensor, - description: SensorEntityDescription, - index: str = "", + description: BleBoxSensorEntityDescription, + index: int | None = None, ) -> None: """Initialize a BleBox sensor feature.""" super().__init__(feature) self.entity_description = description - self._attr_translation_placeholders = {"index": index} + if feature.name: + self._attr_name = feature.name + elif index is not None and description.indexed_translation_key: + self._attr_translation_key = description.indexed_translation_key + self._attr_translation_placeholders = {"index": str(index)} @property def native_value(self): diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index 9fa112f3ffcdc7..7648e0928dad9c 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -30,32 +30,22 @@ } }, "entity": { - "binary_sensor": { - "moisture": { "name": "Moisture{index}" } - }, "light": { - "channel": { "name": "Channel{index}" } + "channel": { "name": "Channel {index}" } }, "sensor": { - "active_power": { "name": "Active power{index}" }, - "apparent_power": { "name": "Apparent power{index}" }, - "current": { "name": "Current{index}" }, - "forward_active_energy": { "name": "Forward active energy{index}" }, - "frequency": { "name": "Frequency{index}" }, - "humidity": { "name": "Humidity{index}" }, - "illuminance": { "name": "Illuminance{index}" }, - "pm1": { "name": "PM 1{index}" }, - "pm10": { "name": "PM 10{index}" }, - "pm2_5": { "name": "PM 2.5{index}" }, - "power_consumption": { "name": "Energy (last 1 h){index}" }, - "reactive_power": { "name": "Reactive power{index}" }, - "reverse_active_energy": { "name": "Reverse active energy{index}" }, - "temperature": { "name": "Temperature{index}" }, - "voltage": { "name": "Voltage{index}" }, - "wind_speed": { "name": "Wind speed{index}" } - }, - "switch": { - "relay": { "name": "Relay{index}" } + "active_power": { "name": "Active power" }, + "active_power_n": { "name": "Active power {index}" }, + "apparent_power_n": { "name": "Apparent power {index}" }, + "current_n": { "name": "Current {index}" }, + "forward_active_energy": { "name": "Forward active energy" }, + "forward_active_energy_n": { "name": "Forward active energy {index}" }, + "frequency_n": { "name": "Frequency {index}" }, + "power_consumption": { "name": "Energy (last 1 h)" }, + "reactive_power_n": { "name": "Reactive power {index}" }, + "reverse_active_energy": { "name": "Reverse active energy" }, + "reverse_active_energy_n": { "name": "Reverse active energy {index}" }, + "voltage_n": { "name": "Voltage {index}" } } } } diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index 2a8f0dcb5e6fd3..4d8f80a03189f8 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -33,16 +33,13 @@ class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity _attr_device_class = SwitchDeviceClass.SWITCH + _attr_name = None + def __init__(self, feature: blebox_uniapi.switch.Switch) -> None: """Initialize a BleBox switch feature.""" super().__init__(feature) if feature.name: self._attr_name = feature.name - elif feature.index: - self._attr_translation_key = "relay" - self._attr_translation_placeholders = {"index": f" {feature.index}"} - else: - self._attr_name = None @property def is_on(self) -> bool | None: diff --git a/tests/components/blebox/test_binary_sensor.py b/tests/components/blebox/test_binary_sensor.py index 8a9d19a03fc99e..65143f28b3704c 100644 --- a/tests/components/blebox/test_binary_sensor.py +++ b/tests/components/blebox/test_binary_sensor.py @@ -50,26 +50,6 @@ async def test_init( assert device.name == "My rain sensor" -async def test_binary_sensor_with_index(hass: HomeAssistant) -> None: - """Test that a binary sensor with a non-zero index gets a numeric suffix.""" - feature = mock_feature( - "binary_sensors", - blebox_uniapi.binary_sensor.Rain, - unique_id="BleBox-windRainSensor-ea68e74f4f49-1.rain", - full_name="windRainSensor-1.rain", - device_class="moisture", - index=1, - ) - type(feature).name = PropertyMock(return_value=None) - product = feature.product - type(product).name = PropertyMock(return_value="My rain sensor") - type(product).model = PropertyMock(return_value="rainSensor") - - await async_setup_entity(hass, "binary_sensor.my_rain_sensor_moisture_1") - state = hass.states.get("binary_sensor.my_rain_sensor_moisture_1") - assert state.name == "My rain sensor Moisture 1" - - async def test_binary_sensor_with_name(hass: HomeAssistant) -> None: """Test that a binary sensor with a feature name uses it as the entity name.""" feature = mock_feature( diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index f5861b61292fab..d2a875e43bc973 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -39,10 +39,11 @@ def airsensor_fixture(): native_value=None, index=None, ) + type(feature).name = PropertyMock(return_value=None) product = feature.product type(product).name = PropertyMock(return_value="My air sensor") type(product).model = PropertyMock(return_value="airSensor") - return (feature, "sensor.my_air_sensor_pm_1") + return (feature, "sensor.my_air_sensor_pm1") @pytest.fixture(name="tempsensor") @@ -59,6 +60,7 @@ def tempsensor_fixture(): native_value=None, index=None, ) + type(feature).name = PropertyMock(return_value=None) product = feature.product type(product).name = PropertyMock(return_value="My temperature sensor") type(product).model = PropertyMock(return_value="tempSensor") @@ -130,7 +132,7 @@ async def test_airsensor_init( assert entry.unique_id == "BleBox-airSensor-1afe34db9437-0.air" state = hass.states.get(entity_id) - assert state.name == "My air sensor PM 1" + assert state.name == "My air sensor PM1" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.PM1 assert state.state == STATE_UNKNOWN @@ -147,68 +149,70 @@ async def test_airsensor_init( async def test_multi_sensor_single_has_no_channel_suffix( hass: HomeAssistant, ) -> None: - """Test that a single sensor of its type shows no channel suffix.""" + """Test that a single indexed sensor shows no channel suffix.""" feature = mock_feature( "sensors", - blebox_uniapi.sensor.Temperature, - unique_id="BleBox-tempSensor-1afe34db9437-0.temperature", - full_name="tempSensor-0.temperature", - device_class="temperature", - unit="celsius", - current=None, + blebox_uniapi.sensor.GenericSensor, + unique_id="BleBox-smartMeter-aabbcc-voltage_0", + full_name="smartMeter-voltage_0", + device_class="voltage", + unit="volt", native_value=None, sensor_id=0, index=0, ) + type(feature).name = PropertyMock(return_value=None) product = feature.product - type(product).name = PropertyMock(return_value="My temp sensor") - type(product).model = PropertyMock(return_value="tempSensor") + type(product).name = PropertyMock(return_value="My smart meter") + type(product).model = PropertyMock(return_value="smartMeter") - await async_setup_entity(hass, "sensor.my_temp_sensor_temperature") - state = hass.states.get("sensor.my_temp_sensor_temperature") - assert state.name == "My temp sensor Temperature" + await async_setup_entity(hass, "sensor.my_smart_meter_voltage") + state = hass.states.get("sensor.my_smart_meter_voltage") + assert state.name == "My smart meter Voltage" async def test_multi_sensor_multiple_have_channel_suffix( hass: HomeAssistant, ) -> None: - """Test that multiple sensors of the same type get a channel number suffix.""" + """Test SmartMeter-like device: index=0 (summary) has no suffix, index=1-3 (phases) get phase number suffix.""" features = [ mock_only_feature( - blebox_uniapi.sensor.Temperature, - unique_id=f"BleBox-multiSensor-aabbcc-temperature_{i}", - full_name=f"multiSensor-temperature_{i}", - device_class="temperature", - unit="celsius", - current=None, + blebox_uniapi.sensor.GenericSensor, + unique_id=f"BleBox-smartMeter-aabbcc-voltage_{i}", + full_name=f"smartMeter-voltage_{i}", + device_class="voltage", + unit="volt", native_value=None, sensor_id=i, index=i, ) - for i in range(3) + for i in range(4) ] product = setup_product_mock("sensors", features) - type(product).name = PropertyMock(return_value="My multi sensor") - type(product).model = PropertyMock(return_value="multiSensor") + type(product).name = PropertyMock(return_value="My smart meter") + type(product).model = PropertyMock(return_value="smartMeter") type(product).brand = PropertyMock(return_value="BleBox") type(product).firmware_version = PropertyMock(return_value="1.23") type(product).unique_id = PropertyMock(return_value="aabbcc112233") for feature in features: type(feature).product = PropertyMock(return_value=product) + type(feature).name = PropertyMock(return_value=None) feature.async_update = AsyncMock() entity_ids = [ - "sensor.my_multi_sensor_temperature", - "sensor.my_multi_sensor_temperature_1", - "sensor.my_multi_sensor_temperature_2", + "sensor.my_smart_meter_voltage", + "sensor.my_smart_meter_voltage_1", + "sensor.my_smart_meter_voltage_2", + "sensor.my_smart_meter_voltage_3", ] await async_setup_entities(hass, entity_ids) - assert hass.states.get(entity_ids[0]).name == "My multi sensor Temperature" - assert hass.states.get(entity_ids[1]).name == "My multi sensor Temperature 1" - assert hass.states.get(entity_ids[2]).name == "My multi sensor Temperature 2" + assert hass.states.get(entity_ids[0]).name == "My smart meter Voltage" + assert hass.states.get(entity_ids[1]).name == "My smart meter Voltage 1" + assert hass.states.get(entity_ids[2]).name == "My smart meter Voltage 2" + assert hass.states.get(entity_ids[3]).name == "My smart meter Voltage 3" async def test_airsensor_update(airsensor, hass: HomeAssistant) -> None: diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index 484877f0a6c019..870719b8917680 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -195,7 +195,7 @@ def switchbox_d_fixture(): return ( features, - ["switch.my_relays", "switch.my_relays_relay_1"], + ["switch.my_relays", "switch.my_relays_2"], ) @@ -230,7 +230,7 @@ async def test_switchbox_d_init( assert entry.unique_id == "BleBox-switchBoxD-1afe34e750b8-1.relay" state = hass.states.get(entity_ids[1]) - assert state.name == "My relays Relay 1" + assert state.name == "My relays" assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH assert state.state == STATE_UNKNOWN From b5da46a953d657f33b0c43204945e8f68a0aaa14 Mon Sep 17 00:00:00 2001 From: Bartlomiej Kobus Date: Fri, 15 May 2026 08:36:30 +0000 Subject: [PATCH 5/6] feat: add indexed translation key for temperature sensor --- homeassistant/components/blebox/sensor.py | 1 + homeassistant/components/blebox/strings.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index b1459f2398d03c..5badc2cd653b32 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -60,6 +60,7 @@ class BleBoxSensorEntityDescription(SensorEntityDescription): ), BleBoxSensorEntityDescription( key="temperature", + indexed_translation_key="temperature_n", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index 7648e0928dad9c..868d26f24583e1 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -45,6 +45,7 @@ "reactive_power_n": { "name": "Reactive power {index}" }, "reverse_active_energy": { "name": "Reverse active energy" }, "reverse_active_energy_n": { "name": "Reverse active energy {index}" }, + "temperature_n": { "name": "Temperature {index}" }, "voltage_n": { "name": "Voltage {index}" } } } From daa537c99956fcf9d899144e9604f9d32b3aea22 Mon Sep 17 00:00:00 2001 From: Bartlomiej Kobus Date: Fri, 15 May 2026 09:00:20 +0000 Subject: [PATCH 6/6] fix: update wlightbox_ct fixture entity_id and add missing index=None --- tests/components/blebox/test_light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index d16c59a01d6458..36beb6f9e3cec3 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -352,11 +352,12 @@ def wlightbox_ct_fixture() -> tuple[MagicMock, str]: color_mode=blebox_uniapi.light.BleboxColorMode.CT, effect="NONE", effect_list=["NONE", "PL", "POLICE"], + index=None, ) product = feature.product type(product).name = PropertyMock(return_value="My wLightBox") type(product).model = PropertyMock(return_value="wLightBox") - return feature, "light.my_wlightbox_wlightbox_ct" + return feature, "light.my_wlightbox" @pytest.mark.parametrize("kelvin_requested", [1000, 2700, 3000, 4000, 5000, 6500, 8000])