diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index b26c523ce40..3459da309ee 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -18,6 +18,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.orm.session import Session +from homeassistant.const import MAX_LENGTH_EVENT_TYPE from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util @@ -53,7 +54,7 @@ class Events(Base): # type: ignore } __tablename__ = TABLE_EVENTS event_id = Column(Integer, primary_key=True) - event_type = Column(String(64)) + event_type = Column(String(MAX_LENGTH_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) time_fired = Column(DATETIME_TYPE, index=True) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea86400d963..7d05a7c03f4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -22,6 +22,10 @@ ENTITY_MATCH_ALL = "all" # If no name is specified DEVICE_DEFAULT_NAME = "Unnamed Device" +# Max characters for an event_type (changing this requires a recorder +# database migration) +MAX_LENGTH_EVENT_TYPE = 64 + # Sun events SUN_EVENT_SUNSET = "sunset" SUN_EVENT_SUNRISE = "sunrise" diff --git a/homeassistant/core.py b/homeassistant/core.py index fdf2a093928..6ad722e0d18 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -58,12 +58,14 @@ from homeassistant.const import ( EVENT_TIMER_OUT_OF_SYNC, LENGTH_METERS, MATCH_ALL, + MAX_LENGTH_EVENT_TYPE, __version__, ) from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError, + MaxLengthExceeded, ServiceNotFound, Unauthorized, ) @@ -697,6 +699,9 @@ class EventBus: This method must be run in the event loop. """ + if len(event_type) > MAX_LENGTH_EVENT_TYPE: + raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_TYPE) + listeners = self._listeners.get(event_type, []) # EVENT_HOMEASSISTANT_CLOSE should go only to his listeners diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 375db789618..b40aa99520d 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -154,3 +154,20 @@ class ServiceNotFound(HomeAssistantError): def __str__(self) -> str: """Return string representation.""" return f"Unable to find service {self.domain}.{self.service}" + + +class MaxLengthExceeded(HomeAssistantError): + """Raised when a property value has exceeded the max character length.""" + + def __init__(self, value: str, property_name: str, max_length: int) -> None: + """Initialize error.""" + super().__init__( + self, + ( + f"Value {value} for property {property_name} has a max length of " + f"{max_length} characters" + ), + ) + self.value = value + self.property_name = property_name + self.max_length = max_length diff --git a/tests/test_core.py b/tests/test_core.py index 88b4e1d58f6..d3283c14b84 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -36,6 +36,7 @@ import homeassistant.core as ha from homeassistant.exceptions import ( InvalidEntityFormatError, InvalidStateError, + MaxLengthExceeded, ServiceNotFound, ) import homeassistant.util.dt as dt_util @@ -524,6 +525,21 @@ async def test_eventbus_coroutine_event_listener(hass): assert len(coroutine_calls) == 1 +async def test_eventbus_max_length_exceeded(hass): + """Test that an exception is raised when the max character length is exceeded.""" + + long_evt_name = ( + "this_event_exceeds_the_max_character_length_even_with_the_new_limit" + ) + + with pytest.raises(MaxLengthExceeded) as exc_info: + hass.bus.async_fire(long_evt_name) + + assert exc_info.value.property_name == "event_type" + assert exc_info.value.max_length == 64 + assert exc_info.value.value == long_evt_name + + def test_state_init(): """Test state.init.""" with pytest.raises(InvalidEntityFormatError):