-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Differentiate between device info types #95641
Changes from all commits
c1b57f2
fd33608
ab83a56
61ab966
592de14
409fbe7
4ed6c4c
2cfd1f1
5f33f97
1919d34
6708b35
9c3bb44
c56794c
646e221
35ab721
206dcee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,6 @@ | |
from homeassistant.exceptions import ( | ||
HomeAssistantError, | ||
PlatformNotReady, | ||
RequiredParameterMissing, | ||
) | ||
from homeassistant.generated import languages | ||
from homeassistant.setup import async_start_setup | ||
|
@@ -42,14 +41,13 @@ | |
service, | ||
translation, | ||
) | ||
from .device_registry import DeviceRegistry | ||
from .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider | ||
from .event import async_call_later, async_track_time_interval | ||
from .issue_registry import IssueSeverity, async_create_issue | ||
from .typing import UNDEFINED, ConfigType, DiscoveryInfoType | ||
|
||
if TYPE_CHECKING: | ||
from .entity import Entity | ||
from .entity import DeviceInfo, Entity | ||
|
||
|
||
SLOW_SETUP_WARNING = 10 | ||
|
@@ -61,6 +59,37 @@ | |
DATA_ENTITY_PLATFORM = "entity_platform" | ||
PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds | ||
|
||
DEVICE_INFO_TYPES = { | ||
# Device info is categorized by finding the first device info type which has all | ||
# the keys of the device info. The link device info type must be kept first | ||
# to make it preferred over primary. | ||
"link": { | ||
"connections", | ||
"identifiers", | ||
}, | ||
"primary": { | ||
"configuration_url", | ||
"connections", | ||
"entry_type", | ||
"hw_version", | ||
"identifiers", | ||
"manufacturer", | ||
"model", | ||
"name", | ||
"suggested_area", | ||
"sw_version", | ||
"via_device", | ||
}, | ||
"secondary": { | ||
"connections", | ||
"default_manufacturer", | ||
"default_model", | ||
"default_name", | ||
# Used by Fritz | ||
"via_device", | ||
}, | ||
} | ||
|
||
_LOGGER = getLogger(__name__) | ||
|
||
|
||
|
@@ -485,12 +514,9 @@ async def async_add_entities( | |
|
||
hass = self.hass | ||
|
||
device_registry = dev_reg.async_get(hass) | ||
entity_registry = ent_reg.async_get(hass) | ||
tasks = [ | ||
self._async_add_entity( | ||
entity, update_before_add, entity_registry, device_registry | ||
) | ||
self._async_add_entity(entity, update_before_add, entity_registry) | ||
for entity in new_entities | ||
] | ||
|
||
|
@@ -552,7 +578,6 @@ async def _async_add_entity( # noqa: C901 | |
entity: Entity, | ||
update_before_add: bool, | ||
entity_registry: EntityRegistry, | ||
device_registry: DeviceRegistry, | ||
) -> None: | ||
"""Add an entity to the platform.""" | ||
if entity is None: | ||
|
@@ -608,68 +633,10 @@ async def _async_add_entity( # noqa: C901 | |
entity.add_to_platform_abort() | ||
return | ||
|
||
device_info = entity.device_info | ||
device_id = None | ||
device = None | ||
|
||
if self.config_entry and device_info is not None: | ||
processed_dev_info: dict[str, str | None] = {} | ||
for key in ( | ||
"connections", | ||
"default_manufacturer", | ||
"default_model", | ||
"default_name", | ||
"entry_type", | ||
"identifiers", | ||
"manufacturer", | ||
"model", | ||
"name", | ||
"suggested_area", | ||
"sw_version", | ||
"hw_version", | ||
"via_device", | ||
): | ||
if key in device_info: | ||
processed_dev_info[key] = device_info[ | ||
key # type: ignore[literal-required] | ||
] | ||
|
||
if ( | ||
# device info that is purely meant for linking doesn't need default name | ||
any( | ||
key not in {"identifiers", "connections"} | ||
for key in (processed_dev_info) | ||
) | ||
and "default_name" not in processed_dev_info | ||
and not processed_dev_info.get("name") | ||
): | ||
processed_dev_info["name"] = self.config_entry.title | ||
|
||
if "configuration_url" in device_info: | ||
if device_info["configuration_url"] is None: | ||
processed_dev_info["configuration_url"] = None | ||
else: | ||
configuration_url = str(device_info["configuration_url"]) | ||
if urlparse(configuration_url).scheme in [ | ||
"http", | ||
"https", | ||
"homeassistant", | ||
]: | ||
processed_dev_info["configuration_url"] = configuration_url | ||
else: | ||
_LOGGER.warning( | ||
"Ignoring invalid device configuration_url '%s'", | ||
configuration_url, | ||
) | ||
|
||
try: | ||
device = device_registry.async_get_or_create( | ||
config_entry_id=self.config_entry.entry_id, | ||
**processed_dev_info, # type: ignore[arg-type] | ||
) | ||
device_id = device.id | ||
except RequiredParameterMissing: | ||
pass | ||
if self.config_entry and (device_info := entity.device_info): | ||
device = self._async_process_device_info(device_info) | ||
else: | ||
device = None | ||
|
||
# An entity may suggest the entity_id by setting entity_id itself | ||
suggested_entity_id: str | None = entity.entity_id | ||
|
@@ -704,7 +671,7 @@ async def _async_add_entity( # noqa: C901 | |
entity.unique_id, | ||
capabilities=entity.capability_attributes, | ||
config_entry=self.config_entry, | ||
device_id=device_id, | ||
device_id=device.id if device else None, | ||
disabled_by=disabled_by, | ||
entity_category=entity.entity_category, | ||
get_initial_options=entity.get_initial_entity_options, | ||
|
@@ -792,6 +759,62 @@ def remove_entity_cb() -> None: | |
|
||
await entity.add_to_platform_finish() | ||
|
||
@callback | ||
def _async_process_device_info( | ||
self, device_info: DeviceInfo | ||
) -> dev_reg.DeviceEntry | None: | ||
"""Process a device info.""" | ||
keys = set(device_info) | ||
|
||
# If no keys or not enough info to match up, abort | ||
if len(keys & {"connections", "identifiers"}) == 0: | ||
self.logger.error( | ||
"Ignoring device info without identifiers or connections: %s", | ||
device_info, | ||
) | ||
return None | ||
Comment on lines
+771
to
+775
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It didn't raise in the old code either, but is there a good reason to not raise here? Why do we go ahead and add an entity which has invalid device info? I'm not suggesting we change the behavior in this PR, but it's something to consider in a follow-up I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose not to change that behavior as it would be breaking. It would probably be a small breaking change as I was pleasantly surprised the low amount of bad device info this PR found. If we decide to do this, we should do it in a follow-up PR. |
||
|
||
device_info_type: str | None = None | ||
|
||
# Find the first device info type which has all keys in the device info | ||
for possible_type, allowed_keys in DEVICE_INFO_TYPES.items(): | ||
balloob marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if keys <= allowed_keys: | ||
device_info_type = possible_type | ||
break | ||
|
||
if device_info_type is None: | ||
self.logger.error( | ||
"Device info for %s needs to either describe a device, " | ||
"link to existing device or provide extra information.", | ||
device_info, | ||
) | ||
return None | ||
Comment on lines
+786
to
+791
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment here, should this raise instead? |
||
|
||
if (config_url := device_info.get("configuration_url")) is not None: | ||
if type(config_url) is not str or urlparse(config_url).scheme not in [ | ||
"http", | ||
"https", | ||
"homeassistant", | ||
]: | ||
self.logger.error( | ||
"Ignoring device info with invalid configuration_url '%s'", | ||
config_url, | ||
) | ||
return None | ||
Comment on lines
+799
to
+803
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
|
||
assert self.config_entry is not None | ||
|
||
if device_info_type == "primary" and not device_info.get("name"): | ||
device_info = { | ||
**device_info, # type: ignore[misc] | ||
"name": self.config_entry.title, | ||
} | ||
|
||
return dev_reg.async_get(self.hass).async_get_or_create( | ||
config_entry_id=self.config_entry.entry_id, | ||
**device_info, | ||
) | ||
|
||
async def async_reset(self) -> None: | ||
"""Remove all entities and reset data. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,4 +10,5 @@ def fritz_fixture() -> Mock: | |
with patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( | ||
"homeassistant.components.fritzbox.config_flow.Fritzhome" | ||
): | ||
fritz.return_value.get_prefixed_host.return_value = "http://1.2.3.4" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this test changed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test used to return a Mock for this function, which resulted in passing Mock to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have slightly changed the behavior here. In the past, an invalid configuration URL would merely result in removal of the invalid configuration URL. With the updated logic, we will actually mark the whole device info as invalid and not create a device. |
||
yield fritz |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -114,6 +114,7 @@ def create_mock_client() -> Mock: | |
mock_client.instances = [ | ||
{"friendly_name": "Test instance 1", "instance": 0, "running": True} | ||
] | ||
mock_client.remote_url = f"http://{TEST_HOST}:{TEST_PORT_UI}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this test changed? |
||
|
||
return mock_client | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ def api_fixture(get_sensors_response): | |
"""Define a fixture to return a mocked aiopurple API object.""" | ||
return Mock( | ||
async_check_api_key=AsyncMock(), | ||
get_map_url=Mock(return_value="http://example.com"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this test changed? |
||
sensors=Mock( | ||
async_get_nearby_sensors=AsyncMock( | ||
return_value=[ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't expect this to be set to indicate which network device is used to connect a device to the network. I guess it fits, but other integrations don't do it.