mirror of
https://github.com/home-assistant/core.git
synced 2026-10-08 15:29:17 -04:00
MQTT: Validate entity category for read-only platforms (#182382)
This commit is contained in:
@@ -14,6 +14,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_FORCE_UPDATE,
|
||||
CONF_NAME,
|
||||
CONF_PAYLOAD_OFF,
|
||||
@@ -37,6 +38,7 @@ from .const import CONF_OFF_DELAY, CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttValueTemplate, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
from .util import entity_category_without_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,7 +50,7 @@ DEFAULT_PAYLOAD_ON = "ON"
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
CONF_EXPIRE_AFTER = "expire_after"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(MQTT_ENTITY_COMMON_SCHEMA.schema).extend(
|
||||
{
|
||||
probatio.Optional(CONF_DEVICE_CLASS): probatio.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
probatio.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
@@ -57,8 +59,10 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
probatio.Optional(CONF_OFF_DELAY): cv.positive_int,
|
||||
probatio.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
probatio.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
# a binary sensor can not be added as a config entity
|
||||
probatio.Optional(CONF_ENTITY_CATEGORY): entity_category_without_config,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
)
|
||||
|
||||
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=probatio.REMOVE_EXTRA)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_FORCE_UPDATE,
|
||||
CONF_NAME,
|
||||
CONF_OPTIONS,
|
||||
@@ -54,7 +55,7 @@ from .const import (
|
||||
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
from .util import check_state_too_long
|
||||
from .util import check_state_too_long, entity_category_without_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,7 +75,7 @@ URL_DOCS_SUPPORTED_SENSOR_UOM = (
|
||||
"https://www.home-assistant.io/integrations/sensor/#device-class"
|
||||
)
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(MQTT_ENTITY_COMMON_SCHEMA.schema).extend(
|
||||
{
|
||||
probatio.Optional(CONF_DEVICE_CLASS): probatio.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
probatio.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
@@ -85,8 +86,10 @@ _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
|
||||
probatio.Optional(CONF_SUGGESTED_DISPLAY_PRECISION): cv.positive_int,
|
||||
probatio.Optional(CONF_STATE_CLASS): probatio.Any(STATE_CLASSES_SCHEMA, None),
|
||||
probatio.Optional(CONF_UNIT_OF_MEASUREMENT): probatio.Any(cv.string, None),
|
||||
# a sensor can not be added as a config entity
|
||||
probatio.Optional(CONF_ENTITY_CATEGORY): entity_category_without_config,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
)
|
||||
|
||||
|
||||
def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigType:
|
||||
|
||||
@@ -12,7 +12,12 @@ from typing import Any
|
||||
import probatio
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import MAX_LENGTH_STATE_STATE, STATE_UNKNOWN, Platform
|
||||
from homeassistant.const import (
|
||||
MAX_LENGTH_STATE_STATE,
|
||||
STATE_UNKNOWN,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
@@ -21,6 +26,7 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
|
||||
@@ -326,6 +332,21 @@ def valid_publish_topic(topic: Any) -> str:
|
||||
return validated_topic
|
||||
|
||||
|
||||
def entity_category_without_config(value: Any) -> EntityCategory:
|
||||
"""Validate the entity category of a platform not supporting `CONFIG`.
|
||||
|
||||
Entities of these platforms raise when they are added as config entities.
|
||||
"""
|
||||
entity_category: EntityCategory = ENTITY_CATEGORIES_SCHEMA(value)
|
||||
if entity_category is EntityCategory.CONFIG:
|
||||
_options = ", ".join(sorted(set(EntityCategory) - {EntityCategory.CONFIG}))
|
||||
raise probatio.Invalid(
|
||||
f"Entity category '{entity_category}' is not supported by this platform."
|
||||
f" Valid options are: {_options}"
|
||||
)
|
||||
return entity_category
|
||||
|
||||
|
||||
def valid_qos_schema(qos: Any) -> int:
|
||||
"""Validate that QOS value is valid."""
|
||||
validated_qos: int = _VALID_QOS_SCHEMA(qos)
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -1269,3 +1270,80 @@ async def test_value_template_fails(
|
||||
"TypeError: unsupported operand type(s) for *:"
|
||||
" 'NoneType' and 'int' rendering template" in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
{
|
||||
DOMAIN: {
|
||||
binary_sensor.DOMAIN: {
|
||||
"name": "test",
|
||||
"state_topic": "test-topic",
|
||||
"entity_category": "config",
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_unsupported_entity_category(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test `config` entity category is rejected for this read-only platform."""
|
||||
assert await mqtt_mock_entry()
|
||||
assert "Entity category 'config' is not supported by this platform" in caplog.text
|
||||
assert hass.states.get("binary_sensor.test") is None
|
||||
|
||||
|
||||
async def test_unsupported_entity_category_discovery(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a discovered `config` entity category is rejected."""
|
||||
await mqtt_mock_entry()
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/binary_sensor/bla/config",
|
||||
'{"name": "test", "state_topic": "test-topic",'
|
||||
' "unique_id": "veryunique", "entity_category": "config"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "Entity category 'config' is not supported by this platform" in caplog.text
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(binary_sensor.DOMAIN, DOMAIN, "veryunique")
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_unsupported_entity_category_discovery_update(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test an unsupported entity category is rejected on a discovery update."""
|
||||
await mqtt_mock_entry()
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/binary_sensor/bla/config",
|
||||
'{"name": "test", "state_topic": "test-topic", "unique_id": "veryunique"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
binary_sensor.DOMAIN, DOMAIN, "veryunique"
|
||||
)
|
||||
assert entity_id is not None
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/binary_sensor/bla/config",
|
||||
'{"name": "test", "state_topic": "test-topic",'
|
||||
' "unique_id": "veryunique", "entity_category": "config"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "Entity category 'config' is not supported by this platform" in caplog.text
|
||||
assert entity_registry.async_get(entity_id).entity_category is None
|
||||
|
||||
@@ -27,7 +27,11 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -2033,3 +2037,77 @@ async def test_value_incorrect_state_class_config(
|
||||
"The option `last_reset_value_template` cannot be used"
|
||||
" together with state class" in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
{
|
||||
DOMAIN: {
|
||||
sensor.DOMAIN: {
|
||||
"name": "test",
|
||||
"state_topic": "test-topic",
|
||||
"entity_category": "config",
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_unsupported_entity_category(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test `config` entity category is rejected for this read-only platform."""
|
||||
assert await mqtt_mock_entry()
|
||||
assert "Entity category 'config' is not supported by this platform" in caplog.text
|
||||
assert hass.states.get("sensor.test") is None
|
||||
|
||||
|
||||
async def test_unsupported_entity_category_discovery(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a discovered `config` entity category is rejected."""
|
||||
await mqtt_mock_entry()
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/bla/config",
|
||||
'{"name": "test", "state_topic": "test-topic",'
|
||||
' "unique_id": "veryunique", "entity_category": "config"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "Entity category 'config' is not supported by this platform" in caplog.text
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(sensor.DOMAIN, DOMAIN, "veryunique") is None
|
||||
)
|
||||
|
||||
|
||||
async def test_unsupported_entity_category_discovery_update(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test an unsupported entity category is rejected on a discovery update."""
|
||||
await mqtt_mock_entry()
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/bla/config",
|
||||
'{"name": "test", "state_topic": "test-topic", "unique_id": "veryunique"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
entity_id = entity_registry.async_get_entity_id(sensor.DOMAIN, DOMAIN, "veryunique")
|
||||
assert entity_id is not None
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/bla/config",
|
||||
'{"name": "test", "state_topic": "test-topic",'
|
||||
' "unique_id": "veryunique", "entity_category": "config"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "Entity category 'config' is not supported by this platform" in caplog.text
|
||||
assert entity_registry.async_get(entity_id).entity_category is None
|
||||
|
||||
Reference in New Issue
Block a user