diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index ece0c6a0d33fa3..0e5d896a11f94a 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.0.5"], + "requirements": ["androidtvremote2==0.0.7"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 6f290ccb293669..7b2321e172e458 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -80,9 +80,7 @@ NAME_TEMPLATE = "{} filter" ICON = "mdi:chart-line-variant" -FILTER_SCHEMA = vol.Schema( - {vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int)} -) +FILTER_SCHEMA = vol.Schema({vol.Optional(CONF_FILTER_PRECISION): vol.Coerce(int)}) FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend( { @@ -383,9 +381,9 @@ def __init__(self, state: _State) -> None: except ValueError: self.state = state.state - def set_precision(self, precision: int) -> None: + def set_precision(self, precision: int | None) -> None: """Set precision of Number based states.""" - if isinstance(self.state, Number): + if precision is not None and isinstance(self.state, Number): value = round(float(self.state), precision) self.state = int(value) if precision == 0 else value @@ -417,8 +415,8 @@ def __init__( self, name: str, window_size: int | timedelta, - precision: int, entity: str, + precision: int | None, ) -> None: """Initialize common attributes. @@ -467,6 +465,7 @@ def filter_state(self, new_state: _State) -> _State: filtered = self._filter_state(fstate) filtered.set_precision(self.filter_precision) + if self._store_raw: self.states.append(copy(FilterState(new_state))) else: @@ -485,8 +484,9 @@ class RangeFilter(Filter, SensorEntity): def __init__( self, + *, entity: str, - precision: int, + precision: int | None = None, lower_bound: float | None = None, upper_bound: float | None = None, ) -> None: @@ -495,7 +495,9 @@ def __init__( :param upper_bound: band upper bound :param lower_bound: band lower bound """ - super().__init__(FILTER_NAME_RANGE, DEFAULT_WINDOW_SIZE, precision, entity) + super().__init__( + FILTER_NAME_RANGE, DEFAULT_WINDOW_SIZE, precision=precision, entity=entity + ) self._lower_bound = lower_bound self._upper_bound = upper_bound self._stats_internal: Counter = Counter() @@ -539,13 +541,20 @@ class OutlierFilter(Filter, SensorEntity): """ def __init__( - self, window_size: int, precision: int, entity: str, radius: float + self, + *, + window_size: int, + entity: str, + radius: float, + precision: int | None = None, ) -> None: """Initialize Filter. :param radius: band radius """ - super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) + super().__init__( + FILTER_NAME_OUTLIER, window_size, precision=precision, entity=entity + ) self._radius = radius self._stats_internal: Counter = Counter() self._store_raw = True @@ -579,10 +588,17 @@ class LowPassFilter(Filter, SensorEntity): """BASIC Low Pass Filter.""" def __init__( - self, window_size: int, precision: int, entity: str, time_constant: int + self, + *, + window_size: int, + entity: str, + time_constant: int, + precision: int = DEFAULT_PRECISION, ) -> None: """Initialize Filter.""" - super().__init__(FILTER_NAME_LOWPASS, window_size, precision, entity) + super().__init__( + FILTER_NAME_LOWPASS, window_size, precision=precision, entity=entity + ) self._time_constant = time_constant def _filter_state(self, new_state: FilterState) -> FilterState: @@ -610,16 +626,19 @@ class TimeSMAFilter(Filter, SensorEntity): def __init__( self, + *, window_size: timedelta, - precision: int, entity: str, type: str, # pylint: disable=redefined-builtin + precision: int = DEFAULT_PRECISION, ) -> None: """Initialize Filter. :param type: type of algorithm used to connect discrete values """ - super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) + super().__init__( + FILTER_NAME_TIME_SMA, window_size, precision=precision, entity=entity + ) self._time_window = window_size self.last_leak: FilterState | None = None self.queue = deque[FilterState]() @@ -660,9 +679,13 @@ class ThrottleFilter(Filter, SensorEntity): One sample per window. """ - def __init__(self, window_size: int, precision: int, entity: str) -> None: + def __init__( + self, *, window_size: int, entity: str, precision: None = None + ) -> None: """Initialize Filter.""" - super().__init__(FILTER_NAME_THROTTLE, window_size, precision, entity) + super().__init__( + FILTER_NAME_THROTTLE, window_size, precision=precision, entity=entity + ) self._only_numbers = False def _filter_state(self, new_state: FilterState) -> FilterState: @@ -683,9 +706,13 @@ class TimeThrottleFilter(Filter, SensorEntity): One sample per time period. """ - def __init__(self, window_size: timedelta, precision: int, entity: str) -> None: + def __init__( + self, *, window_size: timedelta, entity: str, precision: int | None = None + ) -> None: """Initialize Filter.""" - super().__init__(FILTER_NAME_TIME_THROTTLE, window_size, precision, entity) + super().__init__( + FILTER_NAME_TIME_THROTTLE, window_size, precision=precision, entity=entity + ) self._time_window = window_size self._last_emitted_at: datetime | None = None self._only_numbers = False diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 8c5df8648e75ef..f4177e8c3004f9 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==4.1.3", "oauth2client==4.1.3"] + "requirements": ["gcal-sync==4.1.4", "oauth2client==4.1.3"] } diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 08815cae9fb2c2..7b437a4f8c485a 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -24,5 +24,5 @@ "documentation": "https://www.home-assistant.io/integrations/roomba", "iot_class": "local_push", "loggers": ["paho_mqtt", "roombapy"], - "requirements": ["roombapy==1.6.6"] + "requirements": ["roombapy==1.6.8"] } diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index c19c2c258bc065..8408b98730b99a 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -30,6 +30,7 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -153,10 +154,44 @@ async def async_setup_sensor( ): return + upper_query = query_str.upper() + if use_database_executor: + redacted_query = redact_credentials(query_str) + + issue_key = unique_id if unique_id else redacted_query + # If the query has a unique id and they fix it we can dismiss the issue + # but if it doesn't have a unique id they have to ignore it instead + + if "ENTITY_ID" in upper_query and "STATES_META" not in upper_query: + _LOGGER.error( + "The query `%s` contains the keyword `entity_id` but does not " + "reference the `states_meta` table. This will cause a full table " + "scan and database instability. Please check the documentation and use " + "`states_meta.entity_id` instead", + redacted_query, + ) + + ir.async_create_issue( + hass, + DOMAIN, + f"entity_id_query_does_full_table_scan_{issue_key}", + translation_key="entity_id_query_does_full_table_scan", + translation_placeholders={"query": redacted_query}, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + ) + raise ValueError( + "Query contains entity_id but does not reference states_meta" + ) + + ir.async_delete_issue( + hass, DOMAIN, f"entity_id_query_does_full_table_scan_{issue_key}" + ) + # MSSQL uses TOP and not LIMIT - if not ("LIMIT" in query_str.upper() or "SELECT TOP" in query_str.upper()): + if not ("LIMIT" in upper_query or "SELECT TOP" in upper_query): if "mssql" in db_url: - query_str = query_str.upper().replace("SELECT", "SELECT TOP 1") + query_str = upper_query.replace("SELECT", "SELECT TOP 1") else: query_str = query_str.replace(";", "") + " LIMIT 1;" diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 2a300f75b3e5d6..1e7aef4ffde062 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -53,5 +53,11 @@ "db_url_invalid": "[%key:component::sql::config::error::db_url_invalid%]", "query_invalid": "[%key:component::sql::config::error::query_invalid%]" } + }, + "issues": { + "entity_id_query_does_full_table_scan": { + "title": "SQL query does full table scan", + "description": "The query `{query}` contains the keyword `entity_id` but does not reference the `states_meta` table. This will cause a full table scan and database instability. Please check the documentation and use `states_meta.entity_id` instead." + } } } diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 5852136ca4564b..9fae6ca9f736e8 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/subaru", "iot_class": "cloud_polling", "loggers": ["stdiomask", "subarulink"], - "requirements": ["subarulink==0.7.5"] + "requirements": ["subarulink==0.7.6"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index eb4a9b7afe4306..1ffb8cfd946687 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -15,6 +15,12 @@ }, { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" + }, + { + "nt": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + }, + { + "nt": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" } ] } diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 2abe1398d7c672..b967954849c967 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.55.0"] + "requirements": ["zeroconf==0.56.0"] } diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index e312f398b5431e..b880a338a424fe 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -187,11 +187,16 @@ class SmartThingsAcceleration(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute updates on this cluster.""" + try: + attr_name = self._cluster.attributes[attrid].name + except KeyError: + attr_name = UNKNOWN + if attrid == self.value_attribute: self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, - self._cluster.attributes.get(attrid, [UNKNOWN])[0], + attr_name, value, ) return @@ -200,7 +205,7 @@ def attribute_updated(self, attrid, value): SIGNAL_ATTR_UPDATED, { ATTR_ATTRIBUTE_ID: attrid, - ATTR_ATTRIBUTE_NAME: self._cluster.attributes.get(attrid, [UNKNOWN])[0], + ATTR_ATTRIBUTE_NAME: attr_name, ATTR_VALUE: value, }, ) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bc5bf6a6d4b717..7bc482681ca8b2 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,7 +23,7 @@ "bellows==0.35.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.95", + "zha-quirks==0.0.96", "zigpy-deconz==0.20.0", "zigpy==0.54.0", "zigpy-xbee==0.17.0", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 3f26ec8fa78bbe..3a2097a1d30264 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -305,6 +305,12 @@ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2", }, + { + "nt": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + }, + { + "nt": "urn:schemas-upnp-org:device:InternetGatewayDevice:2", + }, ], "webostv": [ { diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a1a9ca9dd94b98..c1cae8fd9954e8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -50,7 +50,7 @@ ulid-transform==0.6.0 voluptuous-serialize==2.6.0 voluptuous==0.13.1 yarl==1.8.1 -zeroconf==0.55.0 +zeroconf==0.56.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index ad28a56161beef..fa38ae974c5cfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -333,7 +333,7 @@ amcrest==1.9.7 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.5 +androidtvremote2==0.0.7 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -757,7 +757,7 @@ gTTS==2.2.4 gassist-text==0.0.10 # homeassistant.components.google -gcal-sync==4.1.3 +gcal-sync==4.1.4 # homeassistant.components.geniushub geniushub-client==0.7.0 @@ -2258,7 +2258,7 @@ rocketchat-API==0.6.1 rokuecp==0.17.1 # homeassistant.components.roomba -roombapy==1.6.6 +roombapy==1.6.8 # homeassistant.components.roon roonapi==0.1.4 @@ -2428,7 +2428,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.7.5 +subarulink==0.7.6 # homeassistant.components.solarlog sunwatcher==0.2.1 @@ -2692,13 +2692,13 @@ zamg==0.2.2 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.55.0 +zeroconf==0.56.0 # homeassistant.components.zeversolar zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.95 +zha-quirks==0.0.96 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9e0ad5496176e..68ae56926585e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -308,7 +308,7 @@ ambiclimate==0.2.1 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.5 +androidtvremote2==0.0.7 # homeassistant.components.anthemav anthemav==1.4.1 @@ -579,7 +579,7 @@ gTTS==2.2.4 gassist-text==0.0.10 # homeassistant.components.google -gcal-sync==4.1.3 +gcal-sync==4.1.4 # homeassistant.components.geocaching geocachingapi==0.2.1 @@ -1612,7 +1612,7 @@ ring_doorbell==0.7.2 rokuecp==0.17.1 # homeassistant.components.roomba -roombapy==1.6.6 +roombapy==1.6.8 # homeassistant.components.roon roonapi==0.1.4 @@ -1743,7 +1743,7 @@ stookwijzer==1.3.0 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.7.5 +subarulink==0.7.6 # homeassistant.components.solarlog sunwatcher==0.2.1 @@ -1935,13 +1935,13 @@ youless-api==1.0.1 zamg==0.2.2 # homeassistant.components.zeroconf -zeroconf==0.55.0 +zeroconf==0.56.0 # homeassistant.components.zeversolar zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.95 +zha-quirks==0.0.96 # homeassistant.components.zha zigpy-deconz==0.20.0 diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index c794f7a6b9ac05..c976f87f50f503 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -63,6 +63,24 @@ } } +YAML_CONFIG_FULL_TABLE_SCAN = { + "sql": { + CONF_NAME: "Get entity_id", + CONF_QUERY: "SELECT entity_id from states", + CONF_COLUMN_NAME: "entity_id", + CONF_UNIQUE_ID: "entity_id_12345", + } +} + + +YAML_CONFIG_FULL_TABLE_SCAN_NO_UNIQUE_ID = { + "sql": { + CONF_NAME: "Get entity_id", + CONF_QUERY: "SELECT entity_id from states", + CONF_COLUMN_NAME: "entity_id", + } +} + YAML_CONFIG_BINARY = { "sql": { CONF_DB_URL: "sqlite://", diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 426dd9e196fb9c..811bb3f45bf8b0 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -11,14 +11,21 @@ from homeassistant.components.recorder import Recorder from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.components.sql.const import DOMAIN +from homeassistant.components.sql.const import CONF_QUERY, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import CONF_UNIQUE_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from homeassistant.util import dt -from . import YAML_CONFIG, YAML_CONFIG_BINARY, init_integration +from . import ( + YAML_CONFIG, + YAML_CONFIG_BINARY, + YAML_CONFIG_FULL_TABLE_SCAN, + YAML_CONFIG_FULL_TABLE_SCAN_NO_UNIQUE_ID, + init_integration, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -322,3 +329,48 @@ async def test_binary_data_from_yaml_setup( state = hass.states.get("sensor.get_binary_value") assert state.state == "0xd34324324230392032" assert state.attributes["test_attr"] == "0xd343aa" + + +async def test_issue_when_using_old_query( + recorder_mock: Recorder, hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we create an issue for an old query that will do a full table scan.""" + + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG_FULL_TABLE_SCAN) + await hass.async_block_till_done() + assert "Query contains entity_id but does not reference states_meta" in caplog.text + + assert not hass.states.async_all() + issue_registry = ir.async_get(hass) + + config = YAML_CONFIG_FULL_TABLE_SCAN["sql"] + + unique_id = config[CONF_UNIQUE_ID] + + issue = issue_registry.async_get_issue( + DOMAIN, f"entity_id_query_does_full_table_scan_{unique_id}" + ) + assert issue.translation_placeholders == {"query": config[CONF_QUERY]} + + +async def test_issue_when_using_old_query_without_unique_id( + recorder_mock: Recorder, hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we create an issue for an old query that will do a full table scan.""" + + assert await async_setup_component( + hass, DOMAIN, YAML_CONFIG_FULL_TABLE_SCAN_NO_UNIQUE_ID + ) + await hass.async_block_till_done() + assert "Query contains entity_id but does not reference states_meta" in caplog.text + + assert not hass.states.async_all() + issue_registry = ir.async_get(hass) + + config = YAML_CONFIG_FULL_TABLE_SCAN_NO_UNIQUE_ID["sql"] + query = config[CONF_QUERY] + + issue = issue_registry.async_get_issue( + DOMAIN, f"entity_id_query_does_full_table_scan_{query}" + ) + assert issue.translation_placeholders == {"query": query}