diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 7ca64482763..7102b693e45 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -19,7 +19,7 @@ from .event import DISCOVERY_SCHEMAS as EVENT_SCHEMAS from .fan import DISCOVERY_SCHEMAS as FAN_SCHEMAS from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS -from .models import MatterDiscoverySchema, MatterEntityInfo +from .models import UNSET, MatterDiscoverySchema, MatterEntityInfo from .number import DISCOVERY_SCHEMAS as NUMBER_SCHEMAS from .select import DISCOVERY_SCHEMAS as SELECT_SCHEMAS from .sensor import DISCOVERY_SCHEMAS as SENSOR_SCHEMAS @@ -67,6 +67,8 @@ def async_discover_entities( if any(x in schema.required_attributes for x in discovered_attributes): continue + primary_attribute = schema.required_attributes[0] + # check vendor_id if ( schema.vendor_id is not None @@ -121,31 +123,6 @@ def async_discover_entities( ): continue - # check if value exists but is none/null - if not schema.allow_none_value and any( - endpoint.get_attribute_value(None, val_schema) in (None, NullValue) - for val_schema in schema.required_attributes - ): - continue - - # check for required value in (primary) attribute - primary_attribute = schema.required_attributes[0] - primary_value = endpoint.get_attribute_value(None, primary_attribute) - if schema.value_contains is not None and ( - isinstance(primary_value, list) - and schema.value_contains not in primary_value - ): - continue - - # check for value that may not be present - if schema.value_is_not is not None and ( - schema.value_is_not == primary_value - or ( - isinstance(primary_value, list) and schema.value_is_not in primary_value - ) - ): - continue - # check for required value in cluster featuremap if schema.featuremap_contains is not None and ( not bool( @@ -159,6 +136,61 @@ def async_discover_entities( ): continue + # BEGIN checks on actual attribute values + # these are the least likely to be used and least efficient, so they are checked last + + # check if PRIMARY value exists but is none/null + if not schema.allow_none_value and any( + endpoint.get_attribute_value(None, val_schema) in (None, NullValue) + for val_schema in schema.required_attributes + ): + continue + + # check for required value in PRIMARY attribute + primary_value = endpoint.get_attribute_value(None, primary_attribute) + if schema.value_contains is not UNSET and ( + isinstance(primary_value, list) + and schema.value_contains not in primary_value + ): + continue + + # check for value that may not be present in PRIMARY attribute + if schema.value_is_not is not UNSET and ( + schema.value_is_not == primary_value + or ( + isinstance(primary_value, list) and schema.value_is_not in primary_value + ) + ): + continue + + # check for value that may not be present in SECONDARY attribute + secondary_attribute = ( + schema.required_attributes[1] + if len(schema.required_attributes) > 1 + else None + ) + secondary_value = ( + endpoint.get_attribute_value(None, secondary_attribute) + if secondary_attribute + else None + ) + if schema.secondary_value_is_not is not UNSET and ( + (schema.secondary_value_is_not == secondary_value) + or ( + isinstance(secondary_value, list) + and schema.secondary_value_is_not in secondary_value + ) + ): + continue + + # check for required value in SECONDARY attribute + if schema.secondary_value_contains is not UNSET and ( + isinstance(secondary_value, list) + and schema.secondary_value_contains not in secondary_value + ): + continue + + # FINISH all validation checks # all checks passed, this value belongs to an entity attributes_to_watch = list(schema.required_attributes) diff --git a/homeassistant/components/matter/models.py b/homeassistant/components/matter/models.py index ea80d0eb903..4af7cc3c026 100644 --- a/homeassistant/components/matter/models.py +++ b/homeassistant/components/matter/models.py @@ -18,6 +18,14 @@ type SensorValueTypes = type[ ] +# A sentinel object to detect if a parameter is supplied or not. +class _UNSET_TYPE: + pass + + +UNSET = _UNSET_TYPE() + + class MatterDeviceInfo(TypedDict): """Dictionary with Matter Device info. @@ -111,16 +119,6 @@ class MatterDiscoverySchema: # are not discovered by other entities optional_attributes: tuple[type[ClusterAttributeDescriptor], ...] | None = None - # [optional] the primary attribute value must contain this value - # for example for the AcceptedCommandList - # NOTE: only works for list values - value_contains: Any | None = None - - # [optional] the primary attribute value must NOT have this value - # for example to filter out invalid values (such as empty string instead of null) - # in case of a list value, the list may not contain this value - value_is_not: Any | None = None - # [optional] the primary attribute's cluster featuremap must contain this value # for example for the DoorSensor on a DoorLock Cluster featuremap_contains: int | None = None @@ -131,3 +129,22 @@ class MatterDiscoverySchema: # [optional] the primary attribute value may not be null/None allow_none_value: bool = False + + # [optional] the primary attribute value must contain this value + # for example for the AcceptedCommandList + # NOTE: only works for list values + value_contains: Any = UNSET + + # [optional] the secondary (required) attribute value must contain this value + # for example for the AcceptedCommandList + # NOTE: only works for list values + secondary_value_contains: Any = UNSET + + # [optional] the primary attribute value must NOT have this value + # for example to filter out invalid values (such as empty string instead of null) + # in case of a list value, the list may not contain this value + value_is_not: Any = UNSET + + # [optional] the secondary (required) attribute value must NOT have this value + # for example to filter out empty lists in list sensor values + secondary_value_is_not: Any = UNSET diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index ab3e708d7a9..dd4f8314bef 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -217,6 +217,8 @@ DISCOVERY_SCHEMAS = [ clusters.ModeSelect.Attributes.CurrentMode, clusters.ModeSelect.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -229,6 +231,8 @@ DISCOVERY_SCHEMAS = [ clusters.OvenMode.Attributes.CurrentMode, clusters.OvenMode.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -241,6 +245,8 @@ DISCOVERY_SCHEMAS = [ clusters.LaundryWasherMode.Attributes.CurrentMode, clusters.LaundryWasherMode.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -253,6 +259,8 @@ DISCOVERY_SCHEMAS = [ clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.CurrentMode, clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -265,6 +273,8 @@ DISCOVERY_SCHEMAS = [ clusters.RvcCleanMode.Attributes.CurrentMode, clusters.RvcCleanMode.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -277,6 +287,8 @@ DISCOVERY_SCHEMAS = [ clusters.DishwasherMode.Attributes.CurrentMode, clusters.DishwasherMode.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -289,6 +301,8 @@ DISCOVERY_SCHEMAS = [ clusters.EnergyEvseMode.Attributes.CurrentMode, clusters.EnergyEvseMode.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -301,6 +315,8 @@ DISCOVERY_SCHEMAS = [ clusters.DeviceEnergyManagementMode.Attributes.CurrentMode, clusters.DeviceEnergyManagementMode.Attributes.SupportedModes, ), + # don't discover this entry if the supported modes list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -384,6 +400,8 @@ DISCOVERY_SCHEMAS = [ clusters.TemperatureControl.Attributes.SelectedTemperatureLevel, clusters.TemperatureControl.Attributes.SupportedTemperatureLevels, ), + # don't discover this entry if the supported levels list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -397,6 +415,8 @@ DISCOVERY_SCHEMAS = [ clusters.LaundryWasherControls.Attributes.SpinSpeedCurrent, clusters.LaundryWasherControls.Attributes.SpinSpeeds, ), + # don't discover this entry if the spinspeeds list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SELECT, @@ -412,5 +432,7 @@ DISCOVERY_SCHEMAS = [ clusters.LaundryWasherControls.Attributes.NumberOfRinses, clusters.LaundryWasherControls.Attributes.SupportedRinses, ), + # don't discover this entry if the supported rinses list is empty + secondary_value_is_not=[], ), ] diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index eaab91136c9..3503e112db5 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -809,6 +809,8 @@ DISCOVERY_SCHEMAS = [ clusters.OperationalState.Attributes.OperationalState, clusters.OperationalState.Attributes.OperationalStateList, ), + # don't discover this entry if the supported state list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -822,6 +824,8 @@ DISCOVERY_SCHEMAS = [ clusters.OperationalState.Attributes.CurrentPhase, clusters.OperationalState.Attributes.PhaseList, ), + # don't discover this entry if the supported state list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -835,6 +839,8 @@ DISCOVERY_SCHEMAS = [ clusters.RvcOperationalState.Attributes.CurrentPhase, clusters.RvcOperationalState.Attributes.PhaseList, ), + # don't discover this entry if the supported state list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -848,6 +854,8 @@ DISCOVERY_SCHEMAS = [ clusters.OvenCavityOperationalState.Attributes.CurrentPhase, clusters.OvenCavityOperationalState.Attributes.PhaseList, ), + # don't discover this entry if the supported state list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -877,6 +885,8 @@ DISCOVERY_SCHEMAS = [ clusters.RvcOperationalState.Attributes.OperationalStateList, ), allow_multi=True, # also used for vacuum entity + # don't discover this entry if the supported state list is empty + secondary_value_is_not=[], ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -891,5 +901,7 @@ DISCOVERY_SCHEMAS = [ clusters.OvenCavityOperationalState.Attributes.OperationalState, clusters.OvenCavityOperationalState.Attributes.OperationalStateList, ), + # don't discover this entry if the supported state list is empty + secondary_value_is_not=[], ), ] diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index e9aa169b4fd..d7ddf636ff9 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -1518,108 +1518,6 @@ 'state': 'previous', }) # --- -# name: test_selects[silabs_dishwasher][select.dishwasher_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': None, - 'entity_id': 'select.dishwasher_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Mode', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'mode', - 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-MatterDishwasherMode-89-1', - 'unit_of_measurement': None, - }) -# --- -# name: test_selects[silabs_dishwasher][select.dishwasher_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dishwasher Mode', - 'options': list([ - ]), - }), - 'context': , - 'entity_id': 'select.dishwasher_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_selects[silabs_laundrywasher][select.laundrywasher_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': None, - 'entity_id': 'select.laundrywasher_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Mode', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'mode', - 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-MatterLaundryWasherMode-81-1', - 'unit_of_measurement': None, - }) -# --- -# name: test_selects[silabs_laundrywasher][select.laundrywasher_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LaundryWasher Mode', - 'options': list([ - ]), - }), - 'context': , - 'entity_id': 'select.laundrywasher_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_selects[silabs_laundrywasher][select.laundrywasher_number_of_rinses-entry] EntityRegistryEntrySnapshot({ 'aliases': set({