mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Fix has_entity_name not always being set in ESPHome (#97055)
This commit is contained in:
parent
bf66dc7a91
commit
095146b163
@ -306,7 +306,6 @@ omit =
|
||||
homeassistant/components/escea/climate.py
|
||||
homeassistant/components/escea/discovery.py
|
||||
homeassistant/components/esphome/bluetooth/*
|
||||
homeassistant/components/esphome/entry_data.py
|
||||
homeassistant/components/esphome/manager.py
|
||||
homeassistant/components/etherscan/sensor.py
|
||||
homeassistant/components/eufy/*
|
||||
|
@ -59,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry_data = RuntimeEntryData(
|
||||
client=cli,
|
||||
entry_id=entry.entry_id,
|
||||
title=entry.title,
|
||||
store=domain_data.get_or_create_store(hass, entry),
|
||||
original_options=dict(entry.options),
|
||||
)
|
||||
|
@ -140,6 +140,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
"""Define a base esphome entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
@ -164,7 +165,6 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
if object_id := entity_info.object_id:
|
||||
# Use the object_id to suggest the entity_id
|
||||
self.entity_id = f"{domain}.{device_info.name}_{object_id}"
|
||||
self._attr_has_entity_name = bool(device_info.friendly_name)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
|
@ -86,6 +86,7 @@ class RuntimeEntryData:
|
||||
"""Store runtime data for esphome config entries."""
|
||||
|
||||
entry_id: str
|
||||
title: str
|
||||
client: APIClient
|
||||
store: ESPHomeStorage
|
||||
state: dict[type[EntityState], dict[int, EntityState]] = field(default_factory=dict)
|
||||
@ -127,14 +128,16 @@ class RuntimeEntryData:
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device."""
|
||||
return self.device_info.name if self.device_info else self.entry_id
|
||||
device_info = self.device_info
|
||||
return (device_info and device_info.name) or self.title
|
||||
|
||||
@property
|
||||
def friendly_name(self) -> str:
|
||||
"""Return the friendly name of the device."""
|
||||
if self.device_info and self.device_info.friendly_name:
|
||||
return self.device_info.friendly_name
|
||||
return self.name
|
||||
device_info = self.device_info
|
||||
return (device_info and device_info.friendly_name) or self.name.title().replace(
|
||||
"_", " "
|
||||
)
|
||||
|
||||
@property
|
||||
def signal_device_updated(self) -> str:
|
||||
@ -303,6 +306,7 @@ class RuntimeEntryData:
|
||||
current_state_by_type = self.state[state_type]
|
||||
current_state = current_state_by_type.get(key, _SENTINEL)
|
||||
subscription_key = (state_type, key)
|
||||
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||
if (
|
||||
current_state == state
|
||||
and subscription_key not in stale_state
|
||||
@ -314,19 +318,21 @@ class RuntimeEntryData:
|
||||
and (cast(SensorInfo, entity_info)).force_update
|
||||
)
|
||||
):
|
||||
if debug_enabled:
|
||||
_LOGGER.debug(
|
||||
"%s: ignoring duplicate update with key %s: %s",
|
||||
self.name,
|
||||
key,
|
||||
state,
|
||||
)
|
||||
return
|
||||
if debug_enabled:
|
||||
_LOGGER.debug(
|
||||
"%s: ignoring duplicate update with key %s: %s",
|
||||
"%s: dispatching update with key %s: %s",
|
||||
self.name,
|
||||
key,
|
||||
state,
|
||||
)
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"%s: dispatching update with key %s: %s",
|
||||
self.name,
|
||||
key,
|
||||
state,
|
||||
)
|
||||
stale_state.discard(subscription_key)
|
||||
current_state_by_type[key] = state
|
||||
if subscription := self.state_subscriptions.get(subscription_key):
|
||||
@ -367,8 +373,8 @@ class RuntimeEntryData:
|
||||
|
||||
async def async_save_to_store(self) -> None:
|
||||
"""Generate dynamic data to store and save it to the filesystem."""
|
||||
if self.device_info is None:
|
||||
raise ValueError("device_info is not set yet")
|
||||
if TYPE_CHECKING:
|
||||
assert self.device_info is not None
|
||||
store_data: StoreData = {
|
||||
"device_info": self.device_info.to_dict(),
|
||||
"services": [],
|
||||
@ -377,9 +383,10 @@ class RuntimeEntryData:
|
||||
for info_type, infos in self.info.items():
|
||||
comp_type = INFO_TO_COMPONENT_TYPE[info_type]
|
||||
store_data[comp_type] = [info.to_dict() for info in infos.values()] # type: ignore[literal-required]
|
||||
for service in self.services.values():
|
||||
store_data["services"].append(service.to_dict())
|
||||
|
||||
store_data["services"] = [
|
||||
service.to_dict() for service in self.services.values()
|
||||
]
|
||||
if store_data == self._storage_contents:
|
||||
return
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, NamedTuple
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
@ -395,9 +395,7 @@ class ESPHomeManager:
|
||||
)
|
||||
)
|
||||
|
||||
self.device_id = _async_setup_device_registry(
|
||||
hass, entry, entry_data.device_info
|
||||
)
|
||||
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
||||
entry_data.async_update_device_state(hass)
|
||||
|
||||
entity_infos, services = await cli.list_entities_services()
|
||||
@ -515,9 +513,12 @@ class ESPHomeManager:
|
||||
|
||||
@callback
|
||||
def _async_setup_device_registry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo
|
||||
hass: HomeAssistant, entry: ConfigEntry, entry_data: RuntimeEntryData
|
||||
) -> str:
|
||||
"""Set up device registry feature for a particular config entry."""
|
||||
device_info = entry_data.device_info
|
||||
if TYPE_CHECKING:
|
||||
assert device_info is not None
|
||||
sw_version = device_info.esphome_version
|
||||
if device_info.compilation_time:
|
||||
sw_version += f" ({device_info.compilation_time})"
|
||||
@ -544,7 +545,7 @@ def _async_setup_device_registry(
|
||||
config_entry_id=entry.entry_id,
|
||||
configuration_url=configuration_url,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)},
|
||||
name=device_info.friendly_name or device_info.name,
|
||||
name=entry_data.friendly_name,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
sw_version=sw_version,
|
||||
|
@ -211,13 +211,13 @@ async def _mock_generic_device_entry(
|
||||
|
||||
mock_device = MockESPHomeDevice(entry)
|
||||
|
||||
device_info = DeviceInfo(
|
||||
name="test",
|
||||
friendly_name="Test",
|
||||
mac_address="11:22:33:44:55:aa",
|
||||
esphome_version="1.0.0",
|
||||
**mock_device_info,
|
||||
)
|
||||
default_device_info = {
|
||||
"name": "test",
|
||||
"friendly_name": "Test",
|
||||
"esphome_version": "1.0.0",
|
||||
"mac_address": "11:22:33:44:55:aa",
|
||||
}
|
||||
device_info = DeviceInfo(**(default_device_info | mock_device_info))
|
||||
|
||||
async def _subscribe_states(callback: Callable[[EntityState], None]) -> None:
|
||||
"""Subscribe to state."""
|
||||
|
@ -184,3 +184,38 @@ async def test_deep_sleep_device(
|
||||
state = hass.states.get("binary_sensor.test_mybinary_sensor")
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_esphome_device_without_friendly_name(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
hass_storage: dict[str, Any],
|
||||
mock_esphome_device: Callable[
|
||||
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||
Awaitable[MockESPHomeDevice],
|
||||
],
|
||||
) -> None:
|
||||
"""Test a device without friendly_name set."""
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
BinarySensorState(key=1, state=True, missing_state=False),
|
||||
BinarySensorState(key=2, state=True, missing_state=False),
|
||||
]
|
||||
user_service = []
|
||||
await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
device_info={"friendly_name": None},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.test_mybinary_sensor")
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
@ -1,30 +1,45 @@
|
||||
"""Test ESPHome sensors."""
|
||||
from collections.abc import Awaitable, Callable
|
||||
import logging
|
||||
import math
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
EntityCategory as ESPHomeEntityCategory,
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
LastResetType,
|
||||
SensorInfo,
|
||||
SensorState,
|
||||
SensorStateClass as ESPHomeSensorStateClass,
|
||||
TextSensorInfo,
|
||||
TextSensorState,
|
||||
UserService,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass
|
||||
from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN
|
||||
from homeassistant.const import (
|
||||
ATTR_ICON,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
from .conftest import MockESPHomeDevice
|
||||
|
||||
|
||||
async def test_generic_numeric_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_generic_device_entry,
|
||||
mock_esphome_device: Callable[
|
||||
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||
Awaitable[MockESPHomeDevice],
|
||||
],
|
||||
) -> None:
|
||||
"""Test a generic sensor entity."""
|
||||
logging.getLogger("homeassistant.components.esphome").setLevel(logging.DEBUG)
|
||||
entity_info = [
|
||||
SensorInfo(
|
||||
object_id="mysensor",
|
||||
@ -35,7 +50,7 @@ async def test_generic_numeric_sensor(
|
||||
]
|
||||
states = [SensorState(key=1, state=50)]
|
||||
user_service = []
|
||||
await mock_generic_device_entry(
|
||||
mock_device = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
@ -45,6 +60,34 @@ async def test_generic_numeric_sensor(
|
||||
assert state is not None
|
||||
assert state.state == "50"
|
||||
|
||||
# Test updating state
|
||||
mock_device.set_state(SensorState(key=1, state=60))
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_mysensor")
|
||||
assert state is not None
|
||||
assert state.state == "60"
|
||||
|
||||
# Test sending the same state again
|
||||
mock_device.set_state(SensorState(key=1, state=60))
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_mysensor")
|
||||
assert state is not None
|
||||
assert state.state == "60"
|
||||
|
||||
# Test we can still update after the same state
|
||||
mock_device.set_state(SensorState(key=1, state=70))
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_mysensor")
|
||||
assert state is not None
|
||||
assert state.state == "70"
|
||||
|
||||
# Test invalid data from the underlying api does not crash us
|
||||
mock_device.set_state(SensorState(key=1, state=object()))
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_mysensor")
|
||||
assert state is not None
|
||||
assert state.state == "70"
|
||||
|
||||
|
||||
async def test_generic_numeric_sensor_with_entity_category_and_icon(
|
||||
hass: HomeAssistant,
|
||||
|
Loading…
x
Reference in New Issue
Block a user