From 46e681f4fc6877607c9cacca501c5b42795fdeef Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 5 Jul 2024 00:42:29 -0700 Subject: [PATCH 001/116] Improve redaction for stream error messages (#120867) --- homeassistant/components/stream/worker.py | 17 +++++++++++++---- tests/components/stream/test_worker.py | 7 +++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 4fd9b27d02f..64eac965241 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -48,6 +48,14 @@ class StreamWorkerError(Exception): """An exception thrown while processing a stream.""" +def redact_av_error_string(err: av.AVError) -> str: + """Return an error string with credentials redacted from the url.""" + parts = [str(err.type), err.strerror] + if err.filename is not None: + parts.append(redact_credentials(err.filename)) + return ", ".join(parts) + + class StreamEndedError(StreamWorkerError): """Raised when the stream is complete, exposed for facilitating testing.""" @@ -517,8 +525,7 @@ def stream_worker( container = av.open(source, options=pyav_options, timeout=SOURCE_TIMEOUT) except av.AVError as err: raise StreamWorkerError( - f"Error opening stream ({err.type}, {err.strerror})" - f" {redact_credentials(str(source))}" + f"Error opening stream ({redact_av_error_string(err)})" ) from err try: video_stream = container.streams.video[0] @@ -593,7 +600,7 @@ def stream_worker( except av.AVError as ex: container.close() raise StreamWorkerError( - f"Error demuxing stream while finding first packet: {ex!s}" + f"Error demuxing stream while finding first packet ({redact_av_error_string(ex)})" ) from ex muxer = StreamMuxer( @@ -618,7 +625,9 @@ def stream_worker( except StopIteration as ex: raise StreamEndedError("Stream ended; no additional packets") from ex except av.AVError as ex: - raise StreamWorkerError(f"Error demuxing stream: {ex!s}") from ex + raise StreamWorkerError( + f"Error demuxing stream ({redact_av_error_string(ex)})" + ) from ex muxer.mux_packet(packet) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 2cb90c5ee9a..7226adc7d7e 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -772,12 +772,15 @@ async def test_worker_log( with patch("av.open") as av_open: # pylint: disable-next=c-extension-no-member - av_open.side_effect = av.error.InvalidDataError(-2, "error") + av_open.side_effect = av.error.InvalidDataError( + code=-2, message="Invalid data", filename=stream_url + ) with pytest.raises(StreamWorkerError) as err: run_worker(hass, stream, stream_url) await hass.async_block_till_done() assert ( - str(err.value) == f"Error opening stream (ERRORTYPE_-2, error) {redacted_url}" + str(err.value) + == f"Error opening stream (ERRORTYPE_-2, Invalid data, {redacted_url})" ) assert stream_url not in caplog.text From 59cf01e2525e67949c7cbfb311c8edcb6b4eb09f Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:36:31 -0300 Subject: [PATCH 002/116] Add device class translations in Random (#120890) --- homeassistant/components/random/strings.json | 90 ++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index 0faad1d8093..98072a21fe1 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -44,5 +44,95 @@ "title": "[%key:component::random::config::step::sensor::title%]" } } + }, + + "selector": { + "binary_sensor_device_class": { + "options": { + "battery": "[%key:component::binary_sensor::entity_component::battery::name%]", + "battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]", + "carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]", + "connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]", + "cold": "[%key:component::binary_sensor::entity_component::cold::name%]", + "door": "[%key:component::binary_sensor::entity_component::door::name%]", + "garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]", + "gas": "[%key:component::binary_sensor::entity_component::gas::name%]", + "heat": "[%key:component::binary_sensor::entity_component::heat::name%]", + "light": "[%key:component::binary_sensor::entity_component::light::name%]", + "lock": "[%key:component::binary_sensor::entity_component::lock::name%]", + "moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]", + "motion": "[%key:component::binary_sensor::entity_component::motion::name%]", + "moving": "[%key:component::binary_sensor::entity_component::moving::name%]", + "occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]", + "opening": "[%key:component::binary_sensor::entity_component::opening::name%]", + "plug": "[%key:component::binary_sensor::entity_component::plug::name%]", + "power": "[%key:component::binary_sensor::entity_component::power::name%]", + "presence": "[%key:component::binary_sensor::entity_component::presence::name%]", + "problem": "[%key:component::binary_sensor::entity_component::problem::name%]", + "running": "[%key:component::binary_sensor::entity_component::running::name%]", + "safety": "[%key:component::binary_sensor::entity_component::safety::name%]", + "smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]", + "sound": "[%key:component::binary_sensor::entity_component::sound::name%]", + "tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]", + "update": "[%key:component::binary_sensor::entity_component::update::name%]", + "vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]", + "window": "[%key:component::binary_sensor::entity_component::window::name%]" + } + }, + "sensor_device_class": { + "options": { + "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", + "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", + "battery": "[%key:component::sensor::entity_component::battery::name%]", + "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", + "current": "[%key:component::sensor::entity_component::current::name%]", + "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", + "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", + "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", + "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", + "frequency": "[%key:component::sensor::entity_component::frequency::name%]", + "gas": "[%key:component::sensor::entity_component::gas::name%]", + "humidity": "[%key:component::sensor::entity_component::humidity::name%]", + "illuminance": "[%key:component::sensor::entity_component::illuminance::name%]", + "irradiance": "[%key:component::sensor::entity_component::irradiance::name%]", + "moisture": "[%key:component::sensor::entity_component::moisture::name%]", + "monetary": "[%key:component::sensor::entity_component::monetary::name%]", + "nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]", + "nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]", + "nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]", + "ozone": "[%key:component::sensor::entity_component::ozone::name%]", + "ph": "[%key:component::sensor::entity_component::ph::name%]", + "pm1": "[%key:component::sensor::entity_component::pm1::name%]", + "pm10": "[%key:component::sensor::entity_component::pm10::name%]", + "pm25": "[%key:component::sensor::entity_component::pm25::name%]", + "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", + "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", + "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", + "pressure": "[%key:component::sensor::entity_component::pressure::name%]", + "reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]", + "signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]", + "sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]", + "speed": "[%key:component::sensor::entity_component::speed::name%]", + "sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]", + "temperature": "[%key:component::sensor::entity_component::temperature::name%]", + "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", + "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "voltage": "[%key:component::sensor::entity_component::voltage::name%]", + "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", + "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", + "water": "[%key:component::sensor::entity_component::water::name%]", + "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" + } + } } } From ba1cf84ea5e272f44a89d5bf74ec9f567a7907e3 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 4 Jul 2024 09:20:55 +0200 Subject: [PATCH 003/116] Fix locking/unlocking transition state in Matter lock platform (#121099) --- homeassistant/components/matter/lock.py | 30 ++++++++++++++--------- tests/components/matter/test_door_lock.py | 10 +++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index 5456554a535..1cc85fa897e 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -90,6 +90,9 @@ class MatterLock(MatterEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock with pin if needed.""" + # optimistically signal locking to state machine + self._attr_is_locking = True + self.async_write_ha_state() code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( @@ -98,6 +101,9 @@ class MatterLock(MatterEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock with pin if needed.""" + # optimistically signal unlocking to state machine + self._attr_is_unlocking = True + self.async_write_ha_state() code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None if self.supports_unbolt: @@ -114,6 +120,9 @@ class MatterLock(MatterEntity, LockEntity): async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" + # optimistically signal unlocking to state machine + self._attr_is_unlocking = True + self.async_write_ha_state() code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( @@ -135,26 +144,23 @@ class MatterLock(MatterEntity, LockEntity): clusters.DoorLock.Attributes.LockState ) + # always reset the optimisically (un)locking state on state update + self._attr_is_locking = False + self._attr_is_unlocking = False + LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id) if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked: self._attr_is_locked = True - self._attr_is_locking = False - self._attr_is_unlocking = False - elif lock_state is clusters.DoorLock.Enums.DlLockState.kUnlocked: + elif lock_state in ( + clusters.DoorLock.Enums.DlLockState.kUnlocked, + clusters.DoorLock.Enums.DlLockState.kUnlatched, + clusters.DoorLock.Enums.DlLockState.kNotFullyLocked, + ): self._attr_is_locked = False - self._attr_is_locking = False - self._attr_is_unlocking = False - elif lock_state is clusters.DoorLock.Enums.DlLockState.kNotFullyLocked: - if self.is_locked is True: - self._attr_is_unlocking = True - elif self.is_locked is False: - self._attr_is_locking = True else: # According to the matter docs a null state can happen during device startup. self._attr_is_locked = None - self._attr_is_locking = None - self._attr_is_unlocking = None if self.supports_door_position_sensor: door_state = self.get_matter_attribute_value( diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index a0664612aba..84f0e58a647 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -8,13 +8,11 @@ import pytest from homeassistant.components.lock import ( STATE_LOCKED, - STATE_LOCKING, STATE_OPEN, STATE_UNLOCKED, - STATE_UNLOCKING, LockEntityFeature, ) -from homeassistant.const import ATTR_CODE, STATE_UNKNOWN +from homeassistant.const import ATTR_CODE, STATE_LOCKING, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.entity_registry as er @@ -68,14 +66,14 @@ async def test_lock( state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_LOCKED + assert state.state == STATE_LOCKING set_node_attribute(door_lock, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_UNLOCKING + assert state.state == STATE_UNLOCKED set_node_attribute(door_lock, 1, 257, 0, 2) await trigger_subscription_callback(hass, matter_client) @@ -89,7 +87,7 @@ async def test_lock( state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_LOCKING + assert state.state == STATE_UNLOCKED set_node_attribute(door_lock, 1, 257, 0, None) await trigger_subscription_callback(hass, matter_client) From fc4af481795ddcd38a11bbbd0af57b548af70030 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 3 Jul 2024 22:19:59 +0200 Subject: [PATCH 004/116] Fix HmIP-ESI GAS sensor DeviceClass (#121106) should be SensorDeviceClass:GAS instead of SensorDeviceClass:VOLUME to be supported in the Energy Dashboard --- homeassistant/components/homematicip_cloud/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 6cdff6caef3..6bf128a1663 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -516,7 +516,7 @@ SENSORS_ESI = { HmipEsiSensorEntityDescription( key=ESI_TYPE_CURRENT_GAS_VOLUME, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, - device_class=SensorDeviceClass.VOLUME, + device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda device: device.functional_channel.gasVolume, exists_fn=lambda channel: channel.gasVolume is not None, From 930cd0dc50627c150013443ea0e0f9d10ba7c44b Mon Sep 17 00:00:00 2001 From: Pavel Skuratovich Date: Thu, 4 Jul 2024 22:54:39 +0300 Subject: [PATCH 005/116] Starline: Fix "Error updating SLNet token" message in Log (#121122) Fixes https://github.com/home-assistant/core/issues/116715 --- homeassistant/components/starline/account.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 6122ccbb3c2..4b1425ae7d9 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -9,7 +9,7 @@ from typing import Any from starline import StarlineApi, StarlineDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -65,9 +65,9 @@ class StarlineAccount: ) self._api.set_slnet_token(slnet_token) self._api.set_user_id(user_id) - self._hass.config_entries.async_update_entry( - self._config_entry, - data={ + self._hass.add_job( + self._save_slnet_token, + { **self._config_entry.data, DATA_SLNET_TOKEN: slnet_token, DATA_EXPIRES: slnet_token_expires, @@ -77,6 +77,13 @@ class StarlineAccount: except Exception as err: # noqa: BLE001 _LOGGER.error("Error updating SLNet token: %s", err) + @callback + def _save_slnet_token(self, data) -> None: + self._hass.config_entries.async_update_entry( + self._config_entry, + data=data, + ) + def _update_data(self): """Update StarLine data.""" self._check_slnet_token(self._update_interval) From 15933bb16f6e3e480450e9bc2142febc311dc750 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 23:51:05 -0500 Subject: [PATCH 006/116] Bump inkbird-ble to 0.5.8 (#121134) --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index fb74d1c565a..c1922004317 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -28,5 +28,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.5.7"] + "requirements": ["inkbird-ble==0.5.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5fd773147dc..93b518d4d85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.7 +inkbird-ble==0.5.8 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5c4945da95..ff50501d3f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.7 +inkbird-ble==0.5.8 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From 21815e16213bd175c6cbf05089ce7cf7ac0a68b5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 4 Jul 2024 01:18:28 +0200 Subject: [PATCH 007/116] Fix broken pathlib import in august integration (#121135) --- homeassistant/components/august/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index eec794896f6..53aa3cdffd8 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations +from pathlib import Path from typing import cast from aiohttp import ClientResponseError -from path import Path from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from yalexs.manager.gateway import Config as YaleXSConfig From 0acd1dc5d1bbc29ca6eff50553cfff092f521f40 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 5 Jul 2024 09:12:47 +0200 Subject: [PATCH 008/116] Bump velbusaio to 2024.7.5 (#121156) * Bump velbusaio to 2024.7.4 * bump to 2024.7.5 to remove print functions --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index f778533cad8..4e9478ae575 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2024.5.1"], + "requirements": ["velbus-aio==2024.7.5"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 93b518d4d85..db3d9de6e07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2830,7 +2830,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.5.1 +velbus-aio==2024.7.5 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff50501d3f4..9722c2d4d77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2204,7 +2204,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.5.1 +velbus-aio==2024.7.5 # homeassistant.components.venstar venstarcolortouch==0.19 From ee276aff44fb1dd984490baf1e19a62648a62b3f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 5 Jul 2024 09:40:43 +0200 Subject: [PATCH 009/116] Fix `pulse counter frequency` sensors for Shelly Plus Uni (#121178) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/sensor.py | 29 +++++++++++++++-------- tests/components/shelly/conftest.py | 4 +++- tests/components/shelly/test_sensor.py | 26 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 743c7c7ff01..5a6f03fd90c 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -960,14 +960,18 @@ RPC_SENSORS: Final = { name="Analog input", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - removal_condition=lambda config, _status, key: (config[key]["enable"] is False), + removal_condition=lambda config, _, key: ( + config[key]["type"] != "analog" or config[key]["enable"] is False + ), ), "analoginput_xpercent": RpcSensorDescription( key="input", sub_key="xpercent", name="Analog value", removal_condition=lambda config, status, key: ( - config[key]["enable"] is False or status[key].get("xpercent") is None + config[key]["type"] != "analog" + or config[key]["enable"] is False + or status[key].get("xpercent") is None ), ), "pulse_counter": RpcSensorDescription( @@ -977,7 +981,9 @@ RPC_SENSORS: Final = { native_unit_of_measurement="pulse", state_class=SensorStateClass.TOTAL, value=lambda status, _: status["total"], - removal_condition=lambda config, _status, key: (config[key]["enable"] is False), + removal_condition=lambda config, _status, key: ( + config[key]["type"] != "count" or config[key]["enable"] is False + ), ), "counter_value": RpcSensorDescription( key="input", @@ -985,26 +991,29 @@ RPC_SENSORS: Final = { name="Counter value", value=lambda status, _: status["xtotal"], removal_condition=lambda config, status, key: ( - config[key]["enable"] is False + config[key]["type"] != "count" + or config[key]["enable"] is False or status[key]["counts"].get("xtotal") is None ), ), "counter_frequency": RpcSensorDescription( key="input", - sub_key="counts", + sub_key="freq", name="Pulse counter frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, - value=lambda status, _: status["freq"], - removal_condition=lambda config, status, key: (config[key]["enable"] is False), + removal_condition=lambda config, _, key: ( + config[key]["type"] != "count" or config[key]["enable"] is False + ), ), "counter_frequency_value": RpcSensorDescription( key="input", - sub_key="counts", + sub_key="xfreq", name="Pulse counter frequency value", - value=lambda status, _: status["xfreq"], removal_condition=lambda config, status, key: ( - config[key]["enable"] is False or status[key]["counts"].get("xfreq") is None + config[key]["type"] != "count" + or config[key]["enable"] is False + or status[key].get("xfreq") is None ), ), } diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index a16cc62fbae..14c06d3f86a 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -228,7 +228,9 @@ MOCK_STATUS_RPC = { "input:1": {"id": 1, "percent": 89, "xpercent": 8.9}, "input:2": { "id": 2, - "counts": {"total": 56174, "xtotal": 561.74, "freq": 208.00, "xfreq": 6.11}, + "counts": {"total": 56174, "xtotal": 561.74}, + "freq": 208.00, + "xfreq": 6.11, }, "light:0": {"output": True, "brightness": 53.0}, "light:1": {"output": True, "brightness": 53.0}, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 513bcd875e2..c62a1f6f6ca 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -828,3 +828,29 @@ async def test_rpc_pulse_counter_frequency_sensors( entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == "123456789ABC-input:2-counter_frequency_value" + + +async def test_rpc_disabled_xfreq( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC input with the xfreq sensor disabled.""" + status = deepcopy(mock_rpc_device.status) + status["input:2"] = { + "id": 2, + "counts": {"total": 56174, "xtotal": 561.74}, + "freq": 208.00, + } + monkeypatch.setattr(mock_rpc_device, "status", status) + + await init_integration(hass, 2) + + entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency_value" + + state = hass.states.get(entity_id) + assert not state + + entry = entity_registry.async_get(entity_id) + assert not entry From f403afb012eee0904955e15b2b440dbec9494fd8 Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Thu, 4 Jul 2024 20:51:57 +0200 Subject: [PATCH 010/116] Bump youless library version 2.1.2 (#121181) --- homeassistant/components/youless/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/youless/snapshots/test_sensor.ambr | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 9a81de38388..1ccc8cda0ff 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/youless", "iot_class": "local_polling", "loggers": ["youless_api"], - "requirements": ["youless-api==2.1.0"] + "requirements": ["youless-api==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index db3d9de6e07..1e289ddbc52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2945,7 +2945,7 @@ yeelightsunflower==0.0.10 yolink-api==0.4.4 # homeassistant.components.youless -youless-api==2.1.0 +youless-api==2.1.2 # homeassistant.components.youtube youtubeaio==1.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9722c2d4d77..1bfef00a2c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2301,7 +2301,7 @@ yeelight==0.7.14 yolink-api==0.4.4 # homeassistant.components.youless -youless-api==2.1.0 +youless-api==2.1.2 # homeassistant.components.youtube youtubeaio==1.1.5 diff --git a/tests/components/youless/snapshots/test_sensor.ambr b/tests/components/youless/snapshots/test_sensor.ambr index 22e480c390e..bcfd0139e5c 100644 --- a/tests/components/youless/snapshots/test_sensor.ambr +++ b/tests/components/youless/snapshots/test_sensor.ambr @@ -47,7 +47,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_delivery_low-entry] @@ -98,7 +98,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0.029', }) # --- # name: test_sensors[sensor.energy_high-entry] @@ -405,7 +405,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1234.564', + 'state': '1624.264', }) # --- # name: test_sensors[sensor.phase_1_current-entry] @@ -967,6 +967,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1234.564', }) # --- From b2f23c1a5e996862450d098da1d8f203fe7a3299 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:31:58 +0100 Subject: [PATCH 011/116] Bump python-kasa to 0.7.0.3 (#121183) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 1270bb3469b..3786a2565c2 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -297,5 +297,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.2"] + "requirements": ["python-kasa[speedups]==0.7.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e289ddbc52..07c579c3013 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.2 +python-kasa[speedups]==0.7.0.3 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bfef00a2c8..f326ad62d94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.2 +python-kasa[speedups]==0.7.0.3 # homeassistant.components.matter python-matter-server==6.2.2 From d2b695e7b5229d49c636003c6d8201358e812bdb Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 5 Jul 2024 09:41:41 +0200 Subject: [PATCH 012/116] Fix Matter light discovery schema for DimmerSwitch (#121185) --- homeassistant/components/matter/light.py | 2 ++ homeassistant/components/matter/switch.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 749d82fd661..9ff6f45177e 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -446,6 +446,8 @@ DISCOVERY_SCHEMAS = [ device_types.DimmablePlugInUnit, device_types.ExtendedColorLight, device_types.OnOffLight, + device_types.DimmerSwitch, + device_types.ColorDimmerSwitch, ), ), # Additional schema to match (HS Color) lights with incorrect/missing device type diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index efa78446fc5..2fb325b8808 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -114,6 +114,7 @@ DISCOVERY_SCHEMAS = [ device_types.ColorTemperatureLight, device_types.DimmableLight, device_types.ExtendedColorLight, + device_types.DimmerSwitch, device_types.ColorDimmerSwitch, device_types.OnOffLight, device_types.AirPurifier, From 0b970f9a853dc2361683ece247b5442c6306c1b6 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 5 Jul 2024 09:41:57 +0200 Subject: [PATCH 013/116] Listen for attribute changes of OnOff cluster in appliances (#121198) --- homeassistant/components/matter/climate.py | 1 + homeassistant/components/matter/fan.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 192cb6b3bb4..713aadf5620 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -350,6 +350,7 @@ DISCOVERY_SCHEMAS = [ clusters.Thermostat.Attributes.TemperatureSetpointHold, clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, + clusters.OnOff.Attributes.OnOff, ), device_type=(device_types.Thermostat, device_types.RoomAirConditioner), ), diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 86f03dc7a03..8cbd24977e3 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -313,6 +313,7 @@ DISCOVERY_SCHEMAS = [ clusters.FanControl.Attributes.RockSetting, clusters.FanControl.Attributes.WindSetting, clusters.FanControl.Attributes.AirflowDirection, + clusters.OnOff.Attributes.OnOff, ), ), ] From 1bb4d62a3e4726111c0a5b29f6ede33d37a52153 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 4 Jul 2024 12:05:22 -0400 Subject: [PATCH 014/116] Bump anova-wifi to 0.15.0 (#121222) --- homeassistant/components/anova/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json index d75a791a6f5..7e605edc217 100644 --- a/homeassistant/components/anova/manifest.json +++ b/homeassistant/components/anova/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/anova", "iot_class": "cloud_push", "loggers": ["anova_wifi"], - "requirements": ["anova-wifi==0.14.0"] + "requirements": ["anova-wifi==0.15.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 07c579c3013..40cca4909fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ androidtvremote2==0.1.1 anel-pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anova -anova-wifi==0.14.0 +anova-wifi==0.15.0 # homeassistant.components.anthemav anthemav==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f326ad62d94..9d2ac6894c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ androidtv[async]==0.0.73 androidtvremote2==0.1.1 # homeassistant.components.anova -anova-wifi==0.14.0 +anova-wifi==0.15.0 # homeassistant.components.anthemav anthemav==1.4.1 From ac668dce7db317af4cb3d302cc3651f7f0af8fac Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:41:21 +0200 Subject: [PATCH 015/116] Fix work area sensor in Husqvarna Automower (#121228) --- .../components/husqvarna_automower/sensor.py | 11 +++++-- .../husqvarna_automower/strings.json | 3 +- .../snapshots/test_sensor.ambr | 2 ++ .../husqvarna_automower/test_sensor.py | 32 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 146ef17a6e4..2c8d369ea3a 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -184,6 +184,8 @@ RESTRICTED_REASONS: list = [ RestrictedReasons.WEEK_SCHEDULE.lower(), ] +STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active" + @callback def _get_work_area_names(data: MowerAttributes) -> list[str]: @@ -191,16 +193,21 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]: if TYPE_CHECKING: # Sensor does not get created if it is None assert data.work_areas is not None - return [data.work_areas[work_area_id].name for work_area_id in data.work_areas] + work_area_list = [ + data.work_areas[work_area_id].name for work_area_id in data.work_areas + ] + work_area_list.append(STATE_NO_WORK_AREA_ACTIVE) + return work_area_list @callback def _get_current_work_area_name(data: MowerAttributes) -> str: """Return the name of the current work area.""" + if data.mower.work_area_id is None: + return STATE_NO_WORK_AREA_ACTIVE if TYPE_CHECKING: # Sensor does not get created if values are None assert data.work_areas is not None - assert data.mower.work_area_id is not None return data.work_areas[data.mower.work_area_id].name diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 6cb1c17421a..be17cc25e32 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -252,7 +252,8 @@ "work_area": { "name": "Work area", "state": { - "my_lawn": "My lawn" + "my_lawn": "My lawn", + "no_work_area_active": "No work area active" } } }, diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 0b0d76620d3..935303e48fb 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -1059,6 +1059,7 @@ 'Front lawn', 'Back lawn', 'my_lawn', + 'no_work_area_active', ]), }), 'config_entry_id': , @@ -1097,6 +1098,7 @@ 'Front lawn', 'Back lawn', 'my_lawn', + 'no_work_area_active', ]), }), 'context': , diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 8f30a3dcb04..314bcaaa00c 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -87,6 +87,38 @@ async def test_next_start_sensor( assert state.state == STATE_UNKNOWN +async def test_work_area_sensor( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the work area sensor.""" + await setup_integration(hass, mock_config_entry) + state = hass.states.get("sensor.test_mower_1_work_area") + assert state is not None + assert state.state == "Front lawn" + + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + values[TEST_MOWER_ID].mower.work_area_id = None + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_work_area") + assert state.state == "no_work_area_active" + + values[TEST_MOWER_ID].mower.work_area_id = 0 + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_work_area") + assert state.state == "my_lawn" + + @pytest.mark.parametrize( ("sensor_to_test"), [ From b906daa4939254b9db394794d838da14b5577218 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:45:20 +0200 Subject: [PATCH 016/116] Revert Homematic IP Cloud unique ID changes (#121231) --- .../homematicip_cloud/generic_entity.py | 11 +- .../components/homematicip_cloud/sensor.py | 288 +++++++++++------- .../fixtures/homematicip_cloud.json | 135 ++++++++ .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_sensor.py | 36 +++ 5 files changed, 351 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 5cd48515ad7..163f3eec75e 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -216,14 +216,13 @@ class HomematicipGenericEntity(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" - suffix = "" - if self._post is not None: - suffix = f"_{self._post}" - + unique_id = f"{self.__class__.__name__}_{self._device.id}" if self._is_multi_channel: - return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}" + unique_id = ( + f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}" + ) - return f"{self.__class__.__name__}_{self._device.id}{suffix}" + return unique_id @property def icon(self) -> str | None: diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 6bf128a1663..1f76c6cce1f 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass from typing import Any from homematicip.aio.device import ( @@ -36,7 +35,6 @@ from homematicip.base.functionalChannels import FunctionalChannel from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, - SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -163,19 +161,28 @@ async def async_setup_entry( for ch in get_channels_from_device( device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL ): - if ch.connectedEnergySensorType not in SENSORS_ESI: - continue + if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_IEC: + if ch.currentPowerConsumption is not None: + entities.append(HmipEsiIecPowerConsumption(hap, device)) + if ch.energyCounterOneType != ESI_TYPE_UNKNOWN: + entities.append(HmipEsiIecEnergyCounterHighTariff(hap, device)) + if ch.energyCounterTwoType != ESI_TYPE_UNKNOWN: + entities.append(HmipEsiIecEnergyCounterLowTariff(hap, device)) + if ch.energyCounterThreeType != ESI_TYPE_UNKNOWN: + entities.append( + HmipEsiIecEnergyCounterInputSingleTariff(hap, device) + ) - new_entities = [ - HmipEsiSensorEntity(hap, device, ch.index, description) - for description in SENSORS_ESI[ch.connectedEnergySensorType] - ] + if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_GAS: + if ch.currentGasFlow is not None: + entities.append(HmipEsiGasCurrentGasFlow(hap, device)) + if ch.gasVolume is not None: + entities.append(HmipEsiGasGasVolume(hap, device)) - entities.extend( - entity - for entity in new_entities - if entity.entity_description.exists_fn(ch) - ) + if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_LED: + if ch.currentPowerConsumption is not None: + entities.append(HmipEsiLedCurrentPowerConsumption(hap, device)) + entities.append(HmipEsiLedEnergyCounterHighTariff(hap, device)) async_add_entities(entities) @@ -434,132 +441,185 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE return self._device.temperatureExternalDelta -@dataclass(kw_only=True, frozen=True) -class HmipEsiSensorEntityDescription(SensorEntityDescription): - """SensorEntityDescription for HmIP Sensors.""" - - value_fn: Callable[[AsyncEnergySensorsInterface], StateType] - exists_fn: Callable[[FunctionalChannel], bool] - type_fn: Callable[[AsyncEnergySensorsInterface], str] - - -SENSORS_ESI = { - ESI_CONNECTED_SENSOR_TYPE_IEC: [ - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: device.functional_channel.currentPowerConsumption, - exists_fn=lambda channel: channel.currentPowerConsumption is not None, - type_fn=lambda device: "CurrentPowerConsumption", - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterOne, - exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN, - type_fn=lambda device: device.functional_channel.energyCounterOneType, - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterTwo, - exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN, - type_fn=lambda device: device.functional_channel.energyCounterTwoType, - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterThree, - exists_fn=lambda channel: channel.energyCounterThreeType - != ESI_TYPE_UNKNOWN, - type_fn=lambda device: device.functional_channel.energyCounterThreeType, - ), - ], - ESI_CONNECTED_SENSOR_TYPE_LED: [ - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: device.functional_channel.currentPowerConsumption, - exists_fn=lambda channel: channel.currentPowerConsumption is not None, - type_fn=lambda device: "CurrentPowerConsumption", - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterOne, - exists_fn=lambda channel: channel.energyCounterOne is not None, - type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, - ), - ], - ESI_CONNECTED_SENSOR_TYPE_GAS: [ - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_GAS_FLOW, - native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - device_class=SensorDeviceClass.VOLUME_FLOW_RATE, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: device.functional_channel.currentGasFlow, - exists_fn=lambda channel: channel.currentGasFlow is not None, - type_fn=lambda device: "CurrentGasFlow", - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_GAS_VOLUME, - native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.gasVolume, - exists_fn=lambda channel: channel.gasVolume is not None, - type_fn=lambda device: "GasVolume", - ), - ], -} - - class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity): """EntityDescription for HmIP-ESI Sensors.""" - entity_description: HmipEsiSensorEntityDescription - def __init__( self, hap: HomematicipHAP, device: HomematicipGenericEntity, - channel_index: int, - entity_description: HmipEsiSensorEntityDescription, + key: str, + value_fn: Callable[[FunctionalChannel], StateType], + type_fn: Callable[[FunctionalChannel], str], ) -> None: """Initialize Sensor Entity.""" super().__init__( hap=hap, device=device, - channel=channel_index, - post=entity_description.key, + channel=1, + post=key, is_multi_channel=False, ) - self.entity_description = entity_description + + self._value_fn = value_fn + self._type_fn = type_fn @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the esi sensor.""" state_attr = super().extra_state_attributes - state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self) + state_attr[ATTR_ESI_TYPE] = self._type_fn(self.functional_channel) return state_attr @property def native_value(self) -> str | None: """Return the state of the sensor.""" - return str(self.entity_description.value_fn(self)) + return str(self._value_fn(self.functional_channel)) + + +class HmipEsiIecPowerConsumption(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC currentPowerConsumption sensor.""" + + _attr_device_class = SensorDeviceClass.POWER + _attr_native_unit_of_measurement = UnitOfPower.WATT + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="CurrentPowerConsumption", + value_fn=lambda channel: channel.currentPowerConsumption, + type_fn=lambda channel: "CurrentPowerConsumption", + ) + + +class HmipEsiIecEnergyCounterHighTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC energyCounterOne sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + value_fn=lambda channel: channel.energyCounterOne, + type_fn=lambda channel: channel.energyCounterOneType, + ) + + +class HmipEsiIecEnergyCounterLowTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC energyCounterTwo sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF, + value_fn=lambda channel: channel.energyCounterTwo, + type_fn=lambda channel: channel.energyCounterTwoType, + ) + + +class HmipEsiIecEnergyCounterInputSingleTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC energyCounterThree sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF, + value_fn=lambda channel: channel.energyCounterThree, + type_fn=lambda channel: channel.energyCounterThreeType, + ) + + +class HmipEsiGasCurrentGasFlow(HmipEsiSensorEntity): + """Representation of the Hmip-ESI Gas currentGasFlow sensor.""" + + _attr_device_class = SensorDeviceClass.VOLUME_FLOW_RATE + _attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="CurrentGasFlow", + value_fn=lambda channel: channel.currentGasFlow, + type_fn=lambda channel: "CurrentGasFlow", + ) + + +class HmipEsiGasGasVolume(HmipEsiSensorEntity): + """Representation of the Hmip-ESI Gas gasVolume sensor.""" + + _attr_device_class = SensorDeviceClass.GAS + _attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="GasVolume", + value_fn=lambda channel: channel.gasVolume, + type_fn=lambda channel: "GasVolume", + ) + + +class HmipEsiLedCurrentPowerConsumption(HmipEsiSensorEntity): + """Representation of the Hmip-ESI LED currentPowerConsumption sensor.""" + + _attr_device_class = SensorDeviceClass.POWER + _attr_native_unit_of_measurement = UnitOfPower.WATT + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="CurrentPowerConsumption", + value_fn=lambda channel: channel.currentPowerConsumption, + type_fn=lambda channel: "CurrentPowerConsumption", + ) + + +class HmipEsiLedEnergyCounterHighTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI LED energyCounterOne sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + value_fn=lambda channel: channel.energyCounterOne, + type_fn=lambda channel: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + ) class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index eba2c803b1f..e67ffd78467 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -7757,6 +7757,141 @@ "serializedGlobalTradeItemNumber": "3014F711000000000ESIIEC2", "type": "ENERGY_SENSORS_INTERFACE", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000ESIIE3": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F7110000000000ESIIE3", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000031"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -94, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": true, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": "ENERGY_SENSOR", + "connectedEnergySensorType": "ES_LED", + "currentGasFlow": null, + "currentPowerConsumption": 189.15, + "deviceId": "3014F7110000000000ESIIE3", + "energyCounterOne": 23825.748, + "energyCounterOneType": "UNKNOWN", + "energyCounterThree": null, + "energyCounterThreeType": "UNKNOWN", + "energyCounterTwo": null, + "energyCounterTwoType": "UNKNOWN", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": null, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000057"], + "impulsesPerKWH": 1000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": true, + "IOptionalFeatureCurrentGasFlow": false, + "IOptionalFeatureCurrentPowerConsumption": true, + "IOptionalFeatureEnergyCounterOne": true, + "IOptionalFeatureEnergyCounterThree": false, + "IOptionalFeatureEnergyCounterTwo": false, + "IOptionalFeatureGasVolume": false, + "IOptionalFeatureGasVolumePerImpulse": false, + "IOptionalFeatureImpulsesPerKWH": true + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000ESIIE3", + "label": "esi_led", + "lastStatusUpdate": 1702420986697, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": {}, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000ESIIE3", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" } }, "groups": { diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 348171b3187..074a30e94b2 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -26,7 +26,7 @@ async def test_hmip_load_all_supported_devices( test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 290 + assert len(mock_hap.hmip_device_by_entity_id) == 293 async def test_hmip_remove_device( diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 6951b750b2f..2b62c46fd72 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -634,3 +634,39 @@ async def test_hmip_esi_gas_gas_volume( ) assert ha_state.state == "1019.26" + + +async def test_hmip_esi_led_current_power_consumption( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC currentPowerConsumption Sensor.""" + entity_id = "sensor.esi_led_currentPowerConsumption" + entity_name = "esi_led CurrentPowerConsumption" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_led"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "189.15" + + +async def test_hmip_esi_led_energy_counter_usage_high_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_USAGE_HIGH_TARIFF.""" + entity_id = "sensor.esi_led_energy_counter_usage_high_tariff" + entity_name = "esi_led ENERGY_COUNTER_USAGE_HIGH_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_led"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "23825.748" From eda450838e4a1d8e8c8da2fc1264c5397861ed96 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 4 Jul 2024 21:03:33 +0200 Subject: [PATCH 017/116] Bump deebot-client to 8.1.1 (#121241) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 9568bf2c3ac..6ca9b9e3edc 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 40cca4909fb..681e7aba41e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.1.0 +deebot-client==8.1.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d2ac6894c3..322b5d0d7c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.1.0 +deebot-client==8.1.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 651439ea0602e9cdf5a7a5d6beec7aad9ba0937f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 5 Jul 2024 00:19:24 +0300 Subject: [PATCH 018/116] Fix WebOS TV media player status when OFF after IDLE (#121251) --- homeassistant/components/webostv/media_player.py | 3 ++- tests/components/webostv/test_media_player.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 6aef47515db..099b5a73784 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -239,7 +239,8 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): self._attr_assumed_state = True if ( - self._client.media_state is not None + self._client.is_on + and self._client.media_state is not None and self._client.media_state.get("foregroundAppInfo") is not None ): self._attr_assumed_state = False diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 6c4aeb5e984..f0d17057b33 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -832,3 +832,7 @@ async def test_update_media_state(hass: HomeAssistant, client, monkeypatch) -> N monkeypatch.setattr(client, "media_state", data) await client.mock_state_update() assert hass.states.get(ENTITY_ID).state == MediaPlayerState.IDLE + + monkeypatch.setattr(client, "is_on", False) + await client.mock_state_update() + assert hass.states.get(ENTITY_ID).state == STATE_OFF From a54223661425ddb04be408eb14c93bf8aa5bead7 Mon Sep 17 00:00:00 2001 From: Jordi Date: Fri, 5 Jul 2024 00:05:35 +0200 Subject: [PATCH 019/116] Bump aioaquacell to 0.1.8 (#121253) --- homeassistant/components/aquacell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aquacell/manifest.json b/homeassistant/components/aquacell/manifest.json index 1f43fa214d3..559bdf345bb 100644 --- a/homeassistant/components/aquacell/manifest.json +++ b/homeassistant/components/aquacell/manifest.json @@ -8,5 +8,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["aioaquacell"], - "requirements": ["aioaquacell==0.1.7"] + "requirements": ["aioaquacell==0.1.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 681e7aba41e..935f9571602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -195,7 +195,7 @@ aioambient==2024.01.0 aioapcaccess==0.4.2 # homeassistant.components.aquacell -aioaquacell==0.1.7 +aioaquacell==0.1.8 # homeassistant.components.aseko_pool_live aioaseko==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 322b5d0d7c5..b86bd8b9548 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ aioambient==2024.01.0 aioapcaccess==0.4.2 # homeassistant.components.aquacell -aioaquacell==0.1.7 +aioaquacell==0.1.8 # homeassistant.components.aseko_pool_live aioaseko==0.1.1 From f4e362c5d0f54d5933c69356a535c9304041fda7 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 5 Jul 2024 01:27:56 +0300 Subject: [PATCH 020/116] Bump aiowebostv to 0.4.2 (#121258) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index bcafb82a4b0..679bad9b9f5 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["aiowebostv"], "quality_scale": "platinum", - "requirements": ["aiowebostv==0.4.1"], + "requirements": ["aiowebostv==0.4.2"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index 935f9571602..029077a0f7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.1 +aiowebostv==0.4.2 # homeassistant.components.withings aiowithings==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b86bd8b9548..946052ec8de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.1 +aiowebostv==0.4.2 # homeassistant.components.withings aiowithings==3.0.2 From b015611a2ab5eeca58ed633818be0912987b395d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 5 Jul 2024 10:02:52 +0200 Subject: [PATCH 021/116] Bump python-holidays to 0.52 (#121283) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index cb67039f374..075285bbdb9 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.51", "babel==2.15.0"] + "requirements": ["holidays==0.52", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 1148f46e2d1..ad609954a57 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.51"] + "requirements": ["holidays==0.52"] } diff --git a/requirements_all.txt b/requirements_all.txt index 029077a0f7f..21d20d675c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.51 +holidays==0.52 # homeassistant.components.frontend home-assistant-frontend==20240703.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 946052ec8de..72c223cbac1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.51 +holidays==0.52 # homeassistant.components.frontend home-assistant-frontend==20240703.0 From 994d6f552cbca60d3436bc19f2c9e0899d3d6c1a Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:19:04 +0100 Subject: [PATCH 022/116] Fix tplink light effect behaviour when activating a scene (#121288) --- homeassistant/components/tplink/light.py | 7 +- tests/components/tplink/__init__.py | 51 +++++++++++--- tests/components/tplink/test_light.py | 88 +++++++++++++++++++++++- 3 files changed, 135 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index a736a0ba1e1..22e7c523d1a 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -382,7 +382,12 @@ class TPLinkLightEffectEntity(TPLinkLightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness, transition = self._async_extract_brightness_transition(**kwargs) - if ATTR_EFFECT in kwargs: + if ( + (effect := kwargs.get(ATTR_EFFECT)) + # Effect is unlikely to be LIGHT_EFFECTS_OFF but check for it anyway + and effect not in {LightEffect.LIGHT_EFFECTS_OFF, EFFECT_OFF} + and effect in self._effect_module.effect_list + ): await self._effect_module.set_effect( kwargs[ATTR_EFFECT], brightness=brightness, transition=transition ) diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index d12858017cc..b554cf07015 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -210,7 +210,8 @@ def _mocked_device( if modules: device.modules = { - module_name: MODULE_TO_MOCK_GEN[module_name]() for module_name in modules + module_name: MODULE_TO_MOCK_GEN[module_name](device) + for module_name in modules } if features: @@ -298,7 +299,7 @@ def _mocked_feature( return feature -def _mocked_light_module() -> Light: +def _mocked_light_module(device) -> Light: light = MagicMock(spec=Light, name="Mocked light module") light.update = AsyncMock() light.brightness = 50 @@ -314,26 +315,58 @@ def _mocked_light_module() -> Light: light.hsv = (10, 30, 5) light.valid_temperature_range = ColorTempRange(min=4000, max=9000) light.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - light.set_state = AsyncMock() - light.set_brightness = AsyncMock() - light.set_hsv = AsyncMock() - light.set_color_temp = AsyncMock() + + async def _set_state(state, *_, **__): + light.state = state + + light.set_state = AsyncMock(wraps=_set_state) + + async def _set_brightness(brightness, *_, **__): + light.state.brightness = brightness + light.state.light_on = brightness > 0 + + light.set_brightness = AsyncMock(wraps=_set_brightness) + + async def _set_hsv(h, s, v, *_, **__): + light.state.hue = h + light.state.saturation = s + light.state.brightness = v + light.state.light_on = True + + light.set_hsv = AsyncMock(wraps=_set_hsv) + + async def _set_color_temp(temp, *_, **__): + light.state.color_temp = temp + light.state.light_on = True + + light.set_color_temp = AsyncMock(wraps=_set_color_temp) light.protocol = _mock_protocol() return light -def _mocked_light_effect_module() -> LightEffect: +def _mocked_light_effect_module(device) -> LightEffect: effect = MagicMock(spec=LightEffect, name="Mocked light effect") effect.has_effects = True effect.has_custom_effects = True effect.effect = "Effect1" effect.effect_list = ["Off", "Effect1", "Effect2"] - effect.set_effect = AsyncMock() + + async def _set_effect(effect_name, *_, **__): + assert ( + effect_name in effect.effect_list + ), f"set_effect '{effect_name}' not in {effect.effect_list}" + assert device.modules[ + Module.Light + ], "Need a light module to test set_effect method" + device.modules[Module.Light].state.light_on = True + effect.effect = effect_name + + effect.set_effect = AsyncMock(wraps=_set_effect) effect.set_custom_effect = AsyncMock() return effect -def _mocked_fan_module() -> Fan: +def _mocked_fan_module(effect) -> Fan: fan = MagicMock(auto_spec=Fan, name="Mocked fan") fan.fan_speed_level = 0 fan.set_fan_speed_level = AsyncMock() diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 6fce04ec454..bb814d1f5d3 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, PropertyMock +from freezegun.api import FrozenDateTimeFactory from kasa import ( AuthenticationError, DeviceType, @@ -36,7 +37,13 @@ from homeassistant.components.light import ( ) from homeassistant.components.tplink.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -920,3 +927,82 @@ async def test_light_child( assert child_entity assert child_entity.unique_id == f"{DEVICE_ID}0{light_id}" assert child_entity.device_id == entity.device_id + + +async def test_scene_effect_light( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test activating a scene works with effects. + + i.e. doesn't try to set the effect to 'off' + """ + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light_effect = device.modules[Module.LightEffect] + light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF + + with _patch_discovery(device=device), _patch_connect(device=device): + assert await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + assert await async_setup_component(hass, "scene", {}) + await hass.async_block_till_done() + + entity_id = "light.my_light" + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + freezer.tick(5) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state is STATE_ON + assert state.attributes["effect"] is EFFECT_OFF + + await hass.services.async_call( + "scene", + "create", + {"scene_id": "effect_off_scene", "snapshot_entities": [entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + scene_state = hass.states.get("scene.effect_off_scene") + assert scene_state.state is STATE_UNKNOWN + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + freezer.tick(5) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + + await hass.services.async_call( + "scene", + "turn_on", + { + "entity_id": "scene.effect_off_scene", + }, + blocking=True, + ) + await hass.async_block_till_done() + scene_state = hass.states.get("scene.effect_off_scene") + assert scene_state.state is not STATE_UNKNOWN + + freezer.tick(5) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state is STATE_ON + assert state.attributes["effect"] is EFFECT_OFF From dfccd4abf93d949bc1acac999367b08bed7307fb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Jul 2024 11:21:36 +0200 Subject: [PATCH 023/116] Bump version to 2024.7.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d5c64823890..33087b0bfc1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 777ec8bb6a4..138d1dd80b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0" +version = "2024.7.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e3958d4adb64c6bc3500db7433f4ea868f709ef1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 5 Jul 2024 14:35:21 +0200 Subject: [PATCH 024/116] Update frontend to 20240705.0 (#121295) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 525ba507121..186f725c643 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240703.0"] + "requirements": ["home-assistant-frontend==20240705.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6058a781e2a..41d3af2ad47 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240703.0 +home-assistant-frontend==20240705.0 home-assistant-intents==2024.7.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 21d20d675c3..f1d77644104 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.52 # homeassistant.components.frontend -home-assistant-frontend==20240703.0 +home-assistant-frontend==20240705.0 # homeassistant.components.conversation home-assistant-intents==2024.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c223cbac1..90e59778751 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.52 # homeassistant.components.frontend -home-assistant-frontend==20240703.0 +home-assistant-frontend==20240705.0 # homeassistant.components.conversation home-assistant-intents==2024.7.3 From 440d83d754709fada7c7e08da2466c1f753d75c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 07:50:53 -0500 Subject: [PATCH 025/116] Remove legacy foreign key constraint from sqlite states table (#120779) --- .../components/recorder/migration.py | 76 ++++++++++++++-- tests/components/recorder/test_migrate.py | 89 ++++++++++++++++++- .../components/recorder/test_v32_migration.py | 9 +- 3 files changed, 162 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 561b446f493..cf003f72af4 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -24,7 +24,7 @@ from sqlalchemy.exc import ( SQLAlchemyError, ) from sqlalchemy.orm.session import Session -from sqlalchemy.schema import AddConstraint, DropConstraint +from sqlalchemy.schema import AddConstraint, CreateTable, DropConstraint from sqlalchemy.sql.expression import true from sqlalchemy.sql.lambdas import StatementLambdaElement @@ -1738,14 +1738,15 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool: # Only drop the index if there are no more event_ids in the states table # ex all NULL assert instance.engine is not None, "engine should never be None" - if instance.dialect_name != SupportedDialect.SQLITE: + if instance.dialect_name == SupportedDialect.SQLITE: # SQLite does not support dropping foreign key constraints - # so we can't drop the index at this time but we can avoid - # looking for legacy rows during purge + # so we have to rebuild the table + rebuild_sqlite_table(session_maker, instance.engine, States) + else: _drop_foreign_key_constraints( session_maker, instance.engine, TABLE_STATES, ["event_id"] ) - _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) + _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) instance.use_legacy_events_index = False return True @@ -1894,3 +1895,68 @@ def _mark_migration_done( migration_id=migration.migration_id, version=migration.migration_version ) ) + + +def rebuild_sqlite_table( + session_maker: Callable[[], Session], engine: Engine, table: type[Base] +) -> None: + """Rebuild an SQLite table. + + This must only be called after all migrations are complete + and the database is in a consistent state. + + If the table is not migrated to the current schema this + will likely fail. + """ + table_table = cast(Table, table.__table__) + orig_name = table_table.name + temp_name = f"{table_table.name}_temp_{int(time())}" + + _LOGGER.warning( + "Rebuilding SQLite table %s; This will take a while; Please be patient!", + orig_name, + ) + + try: + # 12 step SQLite table rebuild + # https://www.sqlite.org/lang_altertable.html + with session_scope(session=session_maker()) as session: + # Step 1 - Disable foreign keys + session.connection().execute(text("PRAGMA foreign_keys=OFF")) + # Step 2 - create a transaction + with session_scope(session=session_maker()) as session: + # Step 3 - we know all the indexes, triggers, and views associated with table X + new_sql = str(CreateTable(table_table).compile(engine)).strip("\n") + ";" + source_sql = f"CREATE TABLE {orig_name}" + replacement_sql = f"CREATE TABLE {temp_name}" + assert source_sql in new_sql, f"{source_sql} should be in new_sql" + new_sql = new_sql.replace(source_sql, replacement_sql) + # Step 4 - Create temp table + session.execute(text(new_sql)) + column_names = ",".join([column.name for column in table_table.columns]) + # Step 5 - Transfer content + sql = f"INSERT INTO {temp_name} SELECT {column_names} FROM {orig_name};" # noqa: S608 + session.execute(text(sql)) + # Step 6 - Drop the original table + session.execute(text(f"DROP TABLE {orig_name}")) + # Step 7 - Rename the temp table + session.execute(text(f"ALTER TABLE {temp_name} RENAME TO {orig_name}")) + # Step 8 - Recreate indexes + for index in table_table.indexes: + index.create(session.connection()) + # Step 9 - Recreate views (there are none) + # Step 10 - Check foreign keys + session.execute(text("PRAGMA foreign_key_check")) + # Step 11 - Commit transaction + session.commit() + except SQLAlchemyError: + _LOGGER.exception("Error recreating SQLite table %s", table_table.name) + # Swallow the exception since we do not want to ever raise + # an integrity error as it would cause the database + # to be discarded and recreated from scratch + else: + _LOGGER.warning("Rebuilding SQLite table %s finished", orig_name) + finally: + with session_scope(session=session_maker()) as session: + # Step 12 - Re-enable foreign keys + session.connection().execute(text("PRAGMA foreign_keys=ON")) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index a21f4771616..cb8e402f65a 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -16,7 +16,7 @@ from sqlalchemy.exc import ( ProgrammingError, SQLAlchemyError, ) -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component @@ -24,6 +24,7 @@ from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, + Events, RecorderRuns, States, ) @@ -633,3 +634,89 @@ def test_raise_if_exception_missing_empty_cause_str() -> None: with pytest.raises(ProgrammingError): migration.raise_if_exception_missing_str(programming_exc, ["not present"]) + + +def test_rebuild_sqlite_states_table(recorder_db_url: str) -> None: + """Test that we can rebuild the states table in SQLite.""" + if not recorder_db_url.startswith("sqlite://"): + # This test is specific for SQLite + return + + engine = create_engine(recorder_db_url) + session_maker = scoped_session(sessionmaker(bind=engine, future=True)) + with session_scope(session=session_maker()) as session: + db_schema.Base.metadata.create_all(engine) + with session_scope(session=session_maker()) as session: + session.add(States(state="on")) + session.commit() + + migration.rebuild_sqlite_table(session_maker, engine, States) + + with session_scope(session=session_maker()) as session: + assert session.query(States).count() == 1 + assert session.query(States).first().state == "on" + + engine.dispose() + + +def test_rebuild_sqlite_states_table_missing_fails( + recorder_db_url: str, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling missing states table when attempting rebuild.""" + if not recorder_db_url.startswith("sqlite://"): + # This test is specific for SQLite + return + + engine = create_engine(recorder_db_url) + session_maker = scoped_session(sessionmaker(bind=engine, future=True)) + with session_scope(session=session_maker()) as session: + db_schema.Base.metadata.create_all(engine) + + with session_scope(session=session_maker()) as session: + session.add(Events(event_type="state_changed", event_data="{}")) + session.connection().execute(text("DROP TABLE states")) + session.commit() + + migration.rebuild_sqlite_table(session_maker, engine, States) + assert "Error recreating SQLite table states" in caplog.text + caplog.clear() + + # Now rebuild the events table to make sure the database did not + # get corrupted + migration.rebuild_sqlite_table(session_maker, engine, Events) + + with session_scope(session=session_maker()) as session: + assert session.query(Events).count() == 1 + assert session.query(Events).first().event_type == "state_changed" + assert session.query(Events).first().event_data == "{}" + + engine.dispose() + + +def test_rebuild_sqlite_states_table_extra_columns( + recorder_db_url: str, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling extra columns when rebuilding the states table.""" + if not recorder_db_url.startswith("sqlite://"): + # This test is specific for SQLite + return + + engine = create_engine(recorder_db_url) + session_maker = scoped_session(sessionmaker(bind=engine, future=True)) + with session_scope(session=session_maker()) as session: + db_schema.Base.metadata.create_all(engine) + with session_scope(session=session_maker()) as session: + session.add(States(state="on")) + session.commit() + session.connection().execute( + text("ALTER TABLE states ADD COLUMN extra_column TEXT") + ) + + migration.rebuild_sqlite_table(session_maker, engine, States) + assert "Error recreating SQLite table states" not in caplog.text + + with session_scope(session=session_maker()) as session: + assert session.query(States).count() == 1 + assert session.query(States).first().state == "on" + + engine.dispose() diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index a07c63b3376..e3398fbf0e3 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -211,10 +211,9 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - ) states_index_names = {index["name"] for index in states_indexes} - # sqlite does not support dropping foreign keys so the - # ix_states_event_id index is not dropped in this case - # but use_legacy_events_index is still False - assert "ix_states_event_id" in states_index_names + # sqlite does not support dropping foreign keys so we had to + # create a new table and copy the data over + assert "ix_states_event_id" not in states_index_names assert recorder.get_instance(hass).use_legacy_events_index is False @@ -342,8 +341,6 @@ async def test_migrate_can_resume_entity_id_post_migration( await hass.async_stop() await hass.async_block_till_done() - assert "ix_states_entity_id_last_updated_ts" in states_index_names - async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( From c54717707e0b92a5aa55651a05dbf801944be1f8 Mon Sep 17 00:00:00 2001 From: "Robert C. Maehl" Date: Sat, 6 Jul 2024 05:12:38 -0400 Subject: [PATCH 026/116] Direct Users to App-Specific Passwords for iCloud integration to prevent MFA spam (#120945) Co-authored-by: Franck Nijhof --- homeassistant/components/icloud/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index 96db11d4656..22c711e919a 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -6,7 +6,7 @@ "description": "Enter your credentials", "data": { "username": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", + "password": "App-specific password", "with_family": "With family" } }, @@ -14,7 +14,7 @@ "title": "[%key:common::config_flow::title::reauth%]", "description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.", "data": { - "password": "[%key:common::config_flow::data::password%]" + "password": "App-specific password" } }, "trusted_device": { From a06af7ee9394fd39c256a1b540657ce316be0367 Mon Sep 17 00:00:00 2001 From: Alan Date: Sat, 6 Jul 2024 10:41:18 +0100 Subject: [PATCH 027/116] LLM to handle int attributes (#121037) --- homeassistant/helpers/llm.py | 2 +- tests/helpers/test_llm.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index ba307a785ac..506cadbf168 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -483,7 +483,7 @@ def _get_exposed_entities( if attributes := { attr_name: str(attr_value) - if isinstance(attr_value, (Enum, Decimal)) + if isinstance(attr_value, (Enum, Decimal, int)) else attr_value for attr_name, attr_value in state.attributes.items() if attr_name in interesting_attributes diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index ad18aa53071..81fa573852e 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -408,7 +408,7 @@ async def test_assist_api_prompt( hass.states.async_set( entry1.entity_id, "on", - {"friendly_name": "Kitchen", "temperature": Decimal("0.9")}, + {"friendly_name": "Kitchen", "temperature": Decimal("0.9"), "humidity": 65}, ) hass.states.async_set(entry2.entity_id, "on", {"friendly_name": "Living Room"}) @@ -517,9 +517,7 @@ async def test_assist_api_prompt( entry1.entity_id: { "names": "Kitchen", "state": "on", - "attributes": { - "temperature": "0.9", - }, + "attributes": {"temperature": "0.9", "humidity": "65"}, }, entry2.entity_id: { "areas": "Test Area, Alternative name", From 1133c41fa8e88df02bec6bf3e5df319e907bff91 Mon Sep 17 00:00:00 2001 From: Rasmus Lundsgaard Date: Sat, 6 Jul 2024 19:20:14 +0200 Subject: [PATCH 028/116] Fix empty list in kodi media_player (#121250) Co-authored-by: Franck Nijhof --- homeassistant/components/kodi/media_player.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 290b3b1e566..46dee891e3a 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -641,12 +641,10 @@ class KodiEntity(MediaPlayerEntity): if self.state == MediaPlayerState.OFF: return state_attr - hdr_type = ( - self._item.get("streamdetails", {}).get("video", [{}])[0].get("hdrtype") - ) - if hdr_type == "": - state_attr["dynamic_range"] = "sdr" - else: + state_attr["dynamic_range"] = "sdr" + if (video_details := self._item.get("streamdetails", {}).get("video")) and ( + hdr_type := video_details[0].get("hdrtype") + ): state_attr["dynamic_range"] = hdr_type return state_attr From 803d9c5a8e80db67c1571cb10665da65012f598d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Teme=C5=A1inko?= Date: Fri, 5 Jul 2024 19:12:09 +0200 Subject: [PATCH 029/116] Fix ombi configuration validation (#121314) --- homeassistant/components/ombi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index a4cbe39f3e0..d63f72592f8 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -73,7 +73,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, } ), - cv.has_at_least_one_key("auth"), + cv.has_at_least_one_key(CONF_API_KEY, CONF_PASSWORD), ) }, extra=vol.ALLOW_EXTRA, From 711bdaf37367b49b22b9d7d7e9a2ef192e22dbdf Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Sat, 6 Jul 2024 02:39:24 -0400 Subject: [PATCH 030/116] Bump anova-wifi to 0.17.0 (#121337) * Bump anova-wifi to 0.16.0 * Bump to .17 --- homeassistant/components/anova/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json index 7e605edc217..7e032f0e361 100644 --- a/homeassistant/components/anova/manifest.json +++ b/homeassistant/components/anova/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/anova", "iot_class": "cloud_push", "loggers": ["anova_wifi"], - "requirements": ["anova-wifi==0.15.0"] + "requirements": ["anova-wifi==0.17.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f1d77644104..ecbc48d6d8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ androidtvremote2==0.1.1 anel-pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anova -anova-wifi==0.15.0 +anova-wifi==0.17.0 # homeassistant.components.anthemav anthemav==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90e59778751..fa92d55f013 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ androidtv[async]==0.0.73 androidtvremote2==0.1.1 # homeassistant.components.anova -anova-wifi==0.15.0 +anova-wifi==0.17.0 # homeassistant.components.anthemav anthemav==1.4.1 From 1d7bddf449766f3c108c0131f7802b214be3f845 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 6 Jul 2024 18:54:09 +1000 Subject: [PATCH 031/116] Fix initial Wall Connector values in Tessie (#121353) --- homeassistant/components/tessie/entity.py | 2 +- tests/components/tessie/snapshots/test_sensor.ambr | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index d2a59f205fc..4c077ce19db 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -42,6 +42,7 @@ class TessieBaseEntity( self.key = key self._attr_translation_key = key super().__init__(coordinator) + self._async_update_attrs() @property def _value(self) -> Any: @@ -132,7 +133,6 @@ class TessieEnergyEntity(TessieBaseEntity): self._attr_device_info = data.device super().__init__(coordinator, key) - self._async_update_attrs() class TessieWallConnectorEntity(TessieBaseEntity): diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index afe229feba0..0a5ff4603aa 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -2120,7 +2120,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.wall_connector_power_2-entry] @@ -2177,7 +2177,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.wall_connector_state-entry] @@ -2249,7 +2249,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_state_2-entry] @@ -2321,7 +2321,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_vehicle-entry] From a5f4c25a2cbeba113f97ce3d5607e1740d4c0bf1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 6 Jul 2024 19:09:00 +0200 Subject: [PATCH 032/116] Bump psutil to 6.0.0 (#121385) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/systemmonitor/conftest.py | 10 +++++----- tests/components/systemmonitor/test_util.py | 12 +++++------- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 5e1ef6c02de..236f25bb1ed 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "iot_class": "local_push", "loggers": ["psutil"], - "requirements": ["psutil-home-assistant==0.0.1", "psutil==5.9.8"] + "requirements": ["psutil-home-assistant==0.0.1", "psutil==6.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ecbc48d6d8a..7d7dda06b0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1598,7 +1598,7 @@ proxmoxer==2.0.1 psutil-home-assistant==0.0.1 # homeassistant.components.systemmonitor -psutil==5.9.8 +psutil==6.0.0 # homeassistant.components.pulseaudio_loopback pulsectl==23.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa92d55f013..a0c20bfe3ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1275,7 +1275,7 @@ prometheus-client==0.17.1 psutil-home-assistant==0.0.1 # homeassistant.components.systemmonitor -psutil==5.9.8 +psutil==6.0.0 # homeassistant.components.androidtv pure-python-adb[async]==0.3.0.dev0 diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index e16debdf263..25611481433 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -174,11 +174,11 @@ def mock_psutil(mock_process: list[MockProcess]) -> Generator: "cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)] } mock_psutil.disk_partitions.return_value = [ - sdiskpart("test", "/", "ext4", "", 1, 1), - sdiskpart("test2", "/media/share", "ext4", "", 1, 1), - sdiskpart("test3", "/incorrect", "", "", 1, 1), - sdiskpart("hosts", "/etc/hosts", "bind", "", 1, 1), - sdiskpart("proc", "/proc/run", "proc", "", 1, 1), + sdiskpart("test", "/", "ext4", ""), + sdiskpart("test2", "/media/share", "ext4", ""), + sdiskpart("test3", "/incorrect", "", ""), + sdiskpart("hosts", "/etc/hosts", "bind", ""), + sdiskpart("proc", "/proc/run", "proc", ""), ] mock_psutil.boot_time.return_value = 1708786800.0 mock_psutil.NoSuchProcess = NoSuchProcess diff --git a/tests/components/systemmonitor/test_util.py b/tests/components/systemmonitor/test_util.py index b35c7b2e96c..582707f3574 100644 --- a/tests/components/systemmonitor/test_util.py +++ b/tests/components/systemmonitor/test_util.py @@ -50,21 +50,19 @@ async def test_disk_util( """Test the disk failures.""" mock_psutil.psutil.disk_partitions.return_value = [ - sdiskpart("test", "/", "ext4", "", 1, 1), # Should be ok - sdiskpart("test2", "/media/share", "ext4", "", 1, 1), # Should be ok - sdiskpart("test3", "/incorrect", "", "", 1, 1), # Should be skipped as no type + sdiskpart("test", "/", "ext4", ""), # Should be ok + sdiskpart("test2", "/media/share", "ext4", ""), # Should be ok + sdiskpart("test3", "/incorrect", "", ""), # Should be skipped as no type sdiskpart( - "proc", "/proc/run", "proc", "", 1, 1 + "proc", "/proc/run", "proc", "" ), # Should be skipped as in skipped disk types sdiskpart( "test4", "/tmpfs/", # noqa: S108 "tmpfs", "", - 1, - 1, ), # Should be skipped as in skipped disk types - sdiskpart("test5", "E:", "cd", "cdrom", 1, 1), # Should be skipped as cdrom + sdiskpart("test5", "E:", "cd", "cdrom"), # Should be skipped as cdrom ] mock_config_entry.add_to_hass(hass) From 8017386c73fffa58ef384c9a072917a7c71650ce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 6 Jul 2024 19:32:27 +0200 Subject: [PATCH 033/116] Fix unnecessary logging of turn on/off feature flags in Climate (#121387) --- homeassistant/components/climate/__init__.py | 8 +++ tests/components/climate/test_init.py | 62 ++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index bc81ce6e241..98500d97eb7 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -377,6 +377,14 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): # Return if integration has migrated already return + supported_features = self.supported_features + if supported_features & ( + ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_ON + ): + # The entity supports both turn_on and turn_off, the backwards compatibility + # checks are not needed + return + supported_features = self.supported_features if not supported_features & ClimateEntityFeature.TURN_OFF and ( type(self).async_turn_off is not ClimateEntity.async_turn_off diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index a459b991203..4756c265aea 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -709,6 +709,68 @@ async def test_no_warning_integration_has_migrated( ) +async def test_no_warning_integration_implement_feature_flags( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None +) -> None: + """Test no warning when integration uses the correct feature flags.""" + + class MockClimateEntityTest(MockClimateEntity): + """Mock Climate device.""" + + _attr_supported_features = ( + ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test climate platform via config entry.""" + async_add_entities( + [MockClimateEntityTest(name="test", entity_id="climate.test")] + ) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + with patch.object( + MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" + ): + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("climate.test") + assert state is not None + + assert "does not set ClimateEntityFeature" not in caplog.text + assert "implements HVACMode(s):" not in caplog.text + + async def test_turn_on_off_toggle(hass: HomeAssistant) -> None: """Test turn_on/turn_off/toggle methods.""" From 37621e77aecdac8fb7772365711ecfd3eb85060f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 6 Jul 2024 21:18:02 +0200 Subject: [PATCH 034/116] Fix timezone issue in smhi weather (#121389) --- homeassistant/components/smhi/weather.py | 4 ++- .../smhi/snapshots/test_weather.ambr | 36 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 3d5642a2784..aac4c5d24be 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -218,7 +218,9 @@ class SmhiWeather(WeatherEntity): data.append( { - ATTR_FORECAST_TIME: forecast.valid_time.isoformat(), + ATTR_FORECAST_TIME: forecast.valid_time.replace( + tzinfo=dt_util.UTC + ).isoformat(), ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max, ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min, ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation, diff --git a/tests/components/smhi/snapshots/test_weather.ambr b/tests/components/smhi/snapshots/test_weather.ambr index 0d2f6b3b3bf..d825e22d470 100644 --- a/tests/components/smhi/snapshots/test_weather.ambr +++ b/tests/components/smhi/snapshots/test_weather.ambr @@ -6,7 +6,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'clear-night', - 'datetime': '2023-08-08T00:00:00', + 'datetime': '2023-08-08T00:00:00+00:00', 'humidity': 100, 'precipitation': 0.0, 'pressure': 992.0, @@ -19,7 +19,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'clear-night', - 'datetime': '2023-08-08T01:00:00', + 'datetime': '2023-08-08T01:00:00+00:00', 'humidity': 100, 'precipitation': 0.0, 'pressure': 992.0, @@ -32,7 +32,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'clear-night', - 'datetime': '2023-08-08T02:00:00', + 'datetime': '2023-08-08T02:00:00+00:00', 'humidity': 97, 'precipitation': 0.0, 'pressure': 992.0, @@ -45,7 +45,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'sunny', - 'datetime': '2023-08-08T03:00:00', + 'datetime': '2023-08-08T03:00:00+00:00', 'humidity': 96, 'precipitation': 0.0, 'pressure': 991.0, @@ -223,7 +223,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'cloudy', - 'datetime': '2023-08-07T12:00:00', + 'datetime': '2023-08-07T12:00:00+00:00', 'humidity': 96, 'precipitation': 0.0, 'pressure': 991.0, @@ -236,7 +236,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'rainy', - 'datetime': '2023-08-08T12:00:00', + 'datetime': '2023-08-08T12:00:00+00:00', 'humidity': 97, 'precipitation': 10.6, 'pressure': 984.0, @@ -249,7 +249,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'rainy', - 'datetime': '2023-08-09T12:00:00', + 'datetime': '2023-08-09T12:00:00+00:00', 'humidity': 95, 'precipitation': 6.3, 'pressure': 1001.0, @@ -262,7 +262,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'cloudy', - 'datetime': '2023-08-10T12:00:00', + 'datetime': '2023-08-10T12:00:00+00:00', 'humidity': 75, 'precipitation': 4.8, 'pressure': 1011.0, @@ -275,7 +275,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'cloudy', - 'datetime': '2023-08-11T12:00:00', + 'datetime': '2023-08-11T12:00:00+00:00', 'humidity': 69, 'precipitation': 0.6, 'pressure': 1015.0, @@ -288,7 +288,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'cloudy', - 'datetime': '2023-08-12T12:00:00', + 'datetime': '2023-08-12T12:00:00+00:00', 'humidity': 82, 'precipitation': 0.0, 'pressure': 1014.0, @@ -301,7 +301,7 @@ dict({ 'cloud_coverage': 75, 'condition': 'partlycloudy', - 'datetime': '2023-08-13T12:00:00', + 'datetime': '2023-08-13T12:00:00+00:00', 'humidity': 59, 'precipitation': 0.0, 'pressure': 1013.0, @@ -314,7 +314,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'partlycloudy', - 'datetime': '2023-08-14T12:00:00', + 'datetime': '2023-08-14T12:00:00+00:00', 'humidity': 56, 'precipitation': 0.0, 'pressure': 1015.0, @@ -327,7 +327,7 @@ dict({ 'cloud_coverage': 88, 'condition': 'partlycloudy', - 'datetime': '2023-08-15T12:00:00', + 'datetime': '2023-08-15T12:00:00+00:00', 'humidity': 64, 'precipitation': 3.6, 'pressure': 1014.0, @@ -340,7 +340,7 @@ dict({ 'cloud_coverage': 75, 'condition': 'partlycloudy', - 'datetime': '2023-08-16T12:00:00', + 'datetime': '2023-08-16T12:00:00+00:00', 'humidity': 61, 'precipitation': 2.4, 'pressure': 1014.0, @@ -358,7 +358,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'cloudy', - 'datetime': '2023-08-07T12:00:00', + 'datetime': '2023-08-07T12:00:00+00:00', 'humidity': 96, 'precipitation': 0.0, 'pressure': 991.0, @@ -373,7 +373,7 @@ dict({ 'cloud_coverage': 75, 'condition': 'partlycloudy', - 'datetime': '2023-08-13T12:00:00', + 'datetime': '2023-08-13T12:00:00+00:00', 'humidity': 59, 'precipitation': 0.0, 'pressure': 1013.0, @@ -388,7 +388,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'fog', - 'datetime': '2023-08-07T09:00:00', + 'datetime': '2023-08-07T09:00:00+00:00', 'humidity': 100, 'precipitation': 0.0, 'pressure': 992.0, @@ -403,7 +403,7 @@ dict({ 'cloud_coverage': 100, 'condition': 'cloudy', - 'datetime': '2023-08-07T15:00:00', + 'datetime': '2023-08-07T15:00:00+00:00', 'humidity': 89, 'precipitation': 0.0, 'pressure': 991.0, From 780f7254c1aac9fc8696f3cbf8a8daf28f42ecd3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 6 Jul 2024 23:36:53 +0200 Subject: [PATCH 035/116] Fix feature flag in climate (#121398) --- homeassistant/components/climate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 98500d97eb7..2c891779c37 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -379,7 +379,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): supported_features = self.supported_features if supported_features & ( - ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_ON + ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF ): # The entity supports both turn_on and turn_off, the backwards compatibility # checks are not needed From a72cc3c248bef12dcb482f2220e169679f811872 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 7 Jul 2024 15:11:28 +0200 Subject: [PATCH 036/116] Allow current empty feeds to be configured in Feedreader (#121421) --- .../components/feedreader/config_flow.py | 14 ------ .../components/feedreader/strings.json | 7 +-- .../components/feedreader/test_config_flow.py | 50 +++---------------- 3 files changed, 7 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/feedreader/config_flow.py b/homeassistant/components/feedreader/config_flow.py index 6fa153b8177..d367432ff8c 100644 --- a/homeassistant/components/feedreader/config_flow.py +++ b/homeassistant/components/feedreader/config_flow.py @@ -107,13 +107,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN): return self.abort_on_import_error(user_input[CONF_URL], "url_error") return self.show_user_form(user_input, {"base": "url_error"}) - if not feed.entries: - if self.context["source"] == SOURCE_IMPORT: - return self.abort_on_import_error( - user_input[CONF_URL], "no_feed_entries" - ) - return self.show_user_form(user_input, {"base": "no_feed_entries"}) - feed_title = feed["feed"]["title"] return self.async_create_entry( @@ -161,13 +154,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN): step_id="reconfigure_confirm", errors={"base": "url_error"}, ) - if not feed.entries: - return self.show_user_form( - user_input=user_input, - description_placeholders={"name": self._config_entry.title}, - step_id="reconfigure_confirm", - errors={"base": "no_feed_entries"}, - ) self.hass.config_entries.async_update_entry(self._config_entry, data=user_input) return self.async_abort(reason="reconfigure_successful") diff --git a/homeassistant/components/feedreader/strings.json b/homeassistant/components/feedreader/strings.json index 31881b4112a..da66333fa5b 100644 --- a/homeassistant/components/feedreader/strings.json +++ b/homeassistant/components/feedreader/strings.json @@ -18,8 +18,7 @@ "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { - "url_error": "The URL could not be opened.", - "no_feed_entries": "The URL seems not to serve any feed entries." + "url_error": "The URL could not be opened." } }, "options": { @@ -38,10 +37,6 @@ "import_yaml_error_url_error": { "title": "The Feedreader YAML configuration import failed", "description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessable for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually." - }, - "import_yaml_error_no_feed_entries": { - "title": "[%key:component::feedreader::issues::import_yaml_error_url_error::title%]", - "description": "Configuring the Feedreader using YAML is being removed but when trying to import the YAML configuration for `{url}` no feed entries were found.\n\nPlease verify that url serves any feed entries and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually." } } } diff --git a/tests/components/feedreader/test_config_flow.py b/tests/components/feedreader/test_config_flow.py index 48c341492e0..669ca665f6b 100644 --- a/tests/components/feedreader/test_config_flow.py +++ b/tests/components/feedreader/test_config_flow.py @@ -83,16 +83,6 @@ async def test_user_errors( assert result["step_id"] == "user" assert result["errors"] == {"base": "url_error"} - # no feed entries returned - feedparser.side_effect = None - feedparser.return_value = None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_URL: URL} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "no_feed_entries"} - # success feedparser.side_effect = None feedparser.return_value = feed_one_event @@ -141,40 +131,25 @@ async def test_import( assert issue_registry.async_get_issue(HA_DOMAIN, "deprecated_yaml_feedreader") -@pytest.mark.parametrize( - ("side_effect", "return_value", "expected_issue_id"), - [ - ( - urllib.error.URLError("Test"), - None, - "import_yaml_error_feedreader_url_error_http_some_rss_local_rss_feed_xml", - ), - ( - None, - None, - "import_yaml_error_feedreader_no_feed_entries_http_some_rss_local_rss_feed_xml", - ), - ], -) async def test_import_errors( hass: HomeAssistant, issue_registry: ir.IssueRegistry, feedparser, setup_entry, feed_one_event, - side_effect, - return_value, - expected_issue_id, ) -> None: """Test starting an import flow which results in an URL error.""" config_entries = hass.config_entries.async_entries(DOMAIN) assert not config_entries # raise URLError - feedparser.side_effect = side_effect - feedparser.return_value = return_value + feedparser.side_effect = urllib.error.URLError("Test") + feedparser.return_value = None assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_URLS: [URL]}}) - assert issue_registry.async_get_issue(DOMAIN, expected_issue_id) + assert issue_registry.async_get_issue( + DOMAIN, + "import_yaml_error_feedreader_url_error_http_some_rss_local_rss_feed_xml", + ) async def test_reconfigure(hass: HomeAssistant, feedparser) -> None: @@ -248,19 +223,6 @@ async def test_reconfigure_errors( assert result["step_id"] == "reconfigure_confirm" assert result["errors"] == {"base": "url_error"} - # no feed entries returned - feedparser.side_effect = None - feedparser.return_value = None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_URL: "http://other.rss.local/rss_feed.xml", - }, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reconfigure_confirm" - assert result["errors"] == {"base": "no_feed_entries"} - # success feedparser.side_effect = None feedparser.return_value = feed_one_event From 8825c50671e9cb5d62ef4b02266a654a78213a2c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Jul 2024 14:54:29 +0200 Subject: [PATCH 037/116] Fix MPD config flow (#121431) --- homeassistant/components/mpd/manifest.json | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index e03005fb95a..a361152670a 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -2,6 +2,7 @@ "domain": "mpd", "name": "Music Player Daemon (MPD)", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mpd", "iot_class": "local_polling", "loggers": ["mpd"], diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 463a38feb9f..0020dc91ccd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -352,6 +352,7 @@ FLOWS = { "motionblinds_ble", "motioneye", "motionmount", + "mpd", "mqtt", "mullvad", "mutesync", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0ad8ac09c9e..a3db08e57c2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3814,7 +3814,7 @@ "mpd": { "name": "Music Player Daemon (MPD)", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "mqtt": { From cadd8521aeed818374e0214e8d7a593e46bbf8e6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Jul 2024 15:01:58 +0200 Subject: [PATCH 038/116] Sort mealie mealplans (#121433) --- homeassistant/components/mealie/calendar.py | 3 ++- .../mealie/snapshots/test_calendar.ambr | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index fb628754f06..1b1c14c2ca0 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -60,7 +60,8 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): mealplans = self.coordinator.data[self._entry_type] if not mealplans: return None - return _get_event_from_mealplan(mealplans[0]) + sorted_mealplans = sorted(mealplans, key=lambda x: x.mealplan_date) + return _get_event_from_mealplan(sorted_mealplans[0]) async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index c3b26e1e9e2..e5a0a697157 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -252,12 +252,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'all_day': True, - 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", - 'end_time': '2024-01-23 00:00:00', + 'description': 'Dineren met de boys', + 'end_time': '2024-01-22 00:00:00', 'friendly_name': 'Mealie Dinner', 'location': '', - 'message': 'Zoete aardappel curry traybake', - 'start_time': '2024-01-22 00:00:00', + 'message': 'Aquavite', + 'start_time': '2024-01-21 00:00:00', }), 'context': , 'entity_id': 'calendar.mealie_dinner', @@ -304,12 +304,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'all_day': True, - 'description': 'Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...', - 'end_time': '2024-01-24 00:00:00', + 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', + 'end_time': '2024-01-23 00:00:00', 'friendly_name': 'Mealie Lunch', 'location': '', - 'message': 'Receta de pollo al curry en 10 minutos (con vídeo incluido)', - 'start_time': '2024-01-23 00:00:00', + 'message': 'All-American Beef Stew Recipe', + 'start_time': '2024-01-22 00:00:00', }), 'context': , 'entity_id': 'calendar.mealie_lunch', From ec105e526508fb719700320dfd66db91f0f69cd1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Jul 2024 15:03:32 +0200 Subject: [PATCH 039/116] Fix Mealie URL field (#121434) --- homeassistant/components/mealie/strings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 0d67bb89759..7a89fb85128 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -3,8 +3,11 @@ "step": { "user": { "data": { - "host": "[%key:common::config_flow::data::host%]", + "host": "[%key:common::config_flow::data::url%]", "api_token": "[%key:common::config_flow::data::api_token%]" + }, + "data_description": { + "host": "The URL of your Mealie instance." } } }, From ab94422c18ae4b6da0bb7c0960d3a0b7d24b48fa Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 7 Jul 2024 17:38:17 +0200 Subject: [PATCH 040/116] Bump pymodbus to 3.6.9 (#121445) Bump pymodbus 3.6.9. --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 5635adc9392..292a2ee86a8 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "platinum", - "requirements": ["pymodbus==3.6.8"] + "requirements": ["pymodbus==3.6.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7d7dda06b0a..fdb00d10e4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2004,7 +2004,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.8 +pymodbus==3.6.9 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0c20bfe3ec..3f89f1a2182 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1576,7 +1576,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.8 +pymodbus==3.6.9 # homeassistant.components.monoprice pymonoprice==0.4 From 9512f9eec3e9fb7447d2766f7d843e7bd8f37921 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Jul 2024 12:02:47 -0400 Subject: [PATCH 041/116] Bump jaraco.abode to 5.2.1 (#121446) Bump dependency on jaraco.abode to 5.2.1. Closes #121300 --- homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index de1000319f1..225edea40ca 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_push", "loggers": ["jaraco.abode", "lomond"], - "requirements": ["jaraco.abode==5.1.2"] + "requirements": ["jaraco.abode==5.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index fdb00d10e4d..93b0853f977 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1179,7 +1179,7 @@ isal==1.6.1 ismartgate==5.0.1 # homeassistant.components.abode -jaraco.abode==5.1.2 +jaraco.abode==5.2.1 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f89f1a2182..518f9bef6ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -966,7 +966,7 @@ isal==1.6.1 ismartgate==5.0.1 # homeassistant.components.abode -jaraco.abode==5.1.2 +jaraco.abode==5.2.1 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 From 0a1b46c52fc81e8d575b5068157cd40cf290523e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Jul 2024 13:41:31 -0700 Subject: [PATCH 042/116] Bump yalexs to 6.4.2 (#121467) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/august/fixtures/get_lock.low_keypad_battery.json | 2 +- tests/components/august/test_sensor.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index f898ce64ce6..83d0e985b8a 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==6.4.2", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 93b0853f977..5ac963d62a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2933,7 +2933,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.1 +yalexs==6.4.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 518f9bef6ae..0196de3f0cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2292,7 +2292,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.1 +yalexs==6.4.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/fixtures/get_lock.low_keypad_battery.json b/tests/components/august/fixtures/get_lock.low_keypad_battery.json index 08bdfaa76ed..43b5513a527 100644 --- a/tests/components/august/fixtures/get_lock.low_keypad_battery.json +++ b/tests/components/august/fixtures/get_lock.low_keypad_battery.json @@ -36,7 +36,7 @@ "currentFirmwareVersion": "2.27.0", "battery": {}, "batteryLevel": "Low", - "batteryRaw": 170 + "batteryRaw": 128 }, "OfflineKeys": { "created": [], diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index 0227ee64ef1..67223e9dff0 100644 --- a/tests/components/august/test_sensor.py +++ b/tests/components/august/test_sensor.py @@ -88,7 +88,7 @@ async def test_create_lock_with_linked_keypad( assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_device_battery" state = hass.states.get("sensor.front_door_lock_keypad_battery") - assert state.state == "60" + assert state.state == "62" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery") assert entry From 21309eeb5d14098a34a28317ef32dbc9c55d76ed Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 13:35:34 +0200 Subject: [PATCH 043/116] Bump xiaomi-ble to 0.30.1 (#120743) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 1e0a09015ee..f901b9b412e 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.30.0"] + "requirements": ["xiaomi-ble==0.30.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5ac963d62a4..a458f4bd0e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2906,7 +2906,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.30.0 +xiaomi-ble==0.30.1 # homeassistant.components.knx xknx==2.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0196de3f0cb..69b08293542 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2268,7 +2268,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.30.0 +xiaomi-ble==0.30.1 # homeassistant.components.knx xknx==2.12.2 From 6bf9ec69f31bc2751ba877d94d472643a033cf22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ovidiu=20D=2E=20Ni=C8=9Ban?= Date: Mon, 8 Jul 2024 00:13:43 +0300 Subject: [PATCH 044/116] Bump xiaomi-ble to 0.30.2 (#121471) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index f901b9b412e..21e9bc45bb8 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.30.1"] + "requirements": ["xiaomi-ble==0.30.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index a458f4bd0e4..76ce0add19c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2906,7 +2906,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.30.1 +xiaomi-ble==0.30.2 # homeassistant.components.knx xknx==2.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69b08293542..1635796ddf8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2268,7 +2268,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.30.1 +xiaomi-ble==0.30.2 # homeassistant.components.knx xknx==2.12.2 From 51a6bb1c22ce3aa59518de20bb070449db909447 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Jul 2024 11:00:21 +0200 Subject: [PATCH 045/116] Include hass device ID in mobile app get_config webhook (#121496) --- homeassistant/components/mobile_app/webhook.py | 5 +++++ tests/components/mobile_app/test_webhook.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index e93b4c5ea99..125e4d27247 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -721,10 +721,15 @@ async def webhook_get_config( """Handle a get config webhook.""" hass_config = hass.config.as_dict() + device: dr.DeviceEntry = hass.data[DOMAIN][DATA_DEVICES][ + config_entry.data[CONF_WEBHOOK_ID] + ] + resp = { "latitude": hass_config["latitude"], "longitude": hass_config["longitude"], "elevation": hass_config["elevation"], + "hass_device_id": device.id, "unit_system": hass_config["unit_system"], "location_name": hass_config["location_name"], "time_zone": hass_config["time_zone"], diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index ca5c9936409..77798c57f10 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -10,7 +10,7 @@ from nacl.secret import SecretBox import pytest from homeassistant.components.camera import CameraEntityFeature -from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN +from homeassistant.components.mobile_app.const import CONF_SECRET, DATA_DEVICES, DOMAIN from homeassistant.components.tag import EVENT_TAG_SCANNED from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( @@ -243,6 +243,7 @@ async def test_webhook_handle_get_config( """Test that we can get config properly.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = f"/api/webhook/{webhook_id}" + device: dr.DeviceEntry = hass.data[DOMAIN][DATA_DEVICES][webhook_id] # Create two entities for sensor in ( @@ -280,6 +281,7 @@ async def test_webhook_handle_get_config( "latitude": hass_config["latitude"], "longitude": hass_config["longitude"], "elevation": hass_config["elevation"], + "hass_device_id": device.id, "unit_system": hass_config["unit_system"], "location_name": hass_config["location_name"], "time_zone": hass_config["time_zone"], From 4f2c3df518d7c2f2943aa363f01faabb8bff867d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jul 2024 07:58:18 -0700 Subject: [PATCH 046/116] Fix person tracking in unifiprotect (#121528) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 +- homeassistant/components/unifiprotect/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index c4e1aa87df2..75156308b1a 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -284,7 +284,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Tracking: person", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, - ufp_required_field="is_ptz", + ufp_required_field="feature_flags.is_ptz", ufp_value="is_person_tracking_enabled", ufp_perm=PermRequired.NO_WRITE, ), diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index ca56a602209..50372d47ea8 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -319,7 +319,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( name="Tracking: person", icon="mdi:walk", entity_category=EntityCategory.CONFIG, - ufp_required_field="is_ptz", + ufp_required_field="feature_flags.is_ptz", ufp_value="is_person_tracking_enabled", ufp_set_method="set_person_track", ufp_perm=PermRequired.WRITE, From e0b01ee94ea9105fe3a38601f8ac8ff5d7d5838f Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 8 Jul 2024 21:38:09 +0200 Subject: [PATCH 047/116] Remove homematic state_class from GAS_POWER sensor (#121533) --- homeassistant/components/homematic/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index eebcad95446..b33a725db0f 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -156,7 +156,6 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { key="GAS_POWER", native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.MEASUREMENT, ), "GAS_ENERGY_COUNTER": SensorEntityDescription( key="GAS_ENERGY_COUNTER", From 138b68ecc02f12abc41fadc2bf45360a8da31d11 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Jul 2024 08:30:10 +0200 Subject: [PATCH 048/116] Update vehicle to 2.2.2 (#121556) --- homeassistant/components/rdw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index f44dc7e0f12..7af3e861347 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["vehicle==2.2.1"] + "requirements": ["vehicle==2.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76ce0add19c..2006d9f37bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2827,7 +2827,7 @@ vacuum-map-parser-roborock==0.1.2 vallox-websocket-api==5.3.0 # homeassistant.components.rdw -vehicle==2.2.1 +vehicle==2.2.2 # homeassistant.components.velbus velbus-aio==2024.7.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1635796ddf8..6479e1958f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2201,7 +2201,7 @@ vacuum-map-parser-roborock==0.1.2 vallox-websocket-api==5.3.0 # homeassistant.components.rdw -vehicle==2.2.1 +vehicle==2.2.2 # homeassistant.components.velbus velbus-aio==2024.7.5 From 50802f84f0895bbb53d8f528585f23f45cb1ccd6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Jul 2024 08:29:57 +0200 Subject: [PATCH 049/116] Update tailscale to 0.6.1 (#121557) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index 14f4206f44f..24f485fcdbd 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["tailscale==0.6.0"] + "requirements": ["tailscale==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2006d9f37bc..97ccc3b2a4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2683,7 +2683,7 @@ systembridgeconnector==4.0.3 systembridgemodels==4.0.4 # homeassistant.components.tailscale -tailscale==0.6.0 +tailscale==0.6.1 # homeassistant.components.tank_utility tank-utility==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6479e1958f3..ecb39324da3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2096,7 +2096,7 @@ systembridgeconnector==4.0.3 systembridgemodels==4.0.4 # homeassistant.components.tailscale -tailscale==0.6.0 +tailscale==0.6.1 # homeassistant.components.tellduslive tellduslive==0.10.11 From 5a04a886cfcd1b7f7bcc33cd79b0519fd3f1879f Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Tue, 9 Jul 2024 02:32:34 -0400 Subject: [PATCH 050/116] Fix upb config flow connect (#121571) --- homeassistant/components/upb/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 1db0b0b6fe3..40f49e57c60 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -39,12 +39,13 @@ async def _validate_input(data): url = _make_url_from_data(data) upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file_path}) + + upb.connect(_connected_callback) + if not upb.config_ok: _LOGGER.error("Missing or invalid UPB file: %s", file_path) raise InvalidUpbFile - upb.connect(_connected_callback) - with suppress(TimeoutError): async with asyncio.timeout(VALIDATE_TIMEOUT): await connected_event.wait() From 73d1973625a2fdd5c96c9b359beae3d1e7a5210c Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:52:04 +0200 Subject: [PATCH 051/116] Bump pyenphase to 1.20.6 (#121583) bump pyenphase to 1.20.6 --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index b3c117556bf..f5d2778fc9d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.20.3"], + "requirements": ["pyenphase==1.20.6"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 97ccc3b2a4a..b3bb75aac31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1827,7 +1827,7 @@ pyeiscp==0.0.7 pyemoncms==0.0.7 # homeassistant.components.enphase_envoy -pyenphase==1.20.3 +pyenphase==1.20.6 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecb39324da3..f6f634d091b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1435,7 +1435,7 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.enphase_envoy -pyenphase==1.20.3 +pyenphase==1.20.6 # homeassistant.components.everlights pyeverlights==0.1.0 From 37c09dbdb6f8c102d4b65b53fbbe74268b899c75 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 9 Jul 2024 21:10:15 +0200 Subject: [PATCH 052/116] Allow ambilight when we have connection (philips_js) (#121620) --- homeassistant/components/philips_js/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 8e500592704..1d63b2062e6 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -385,6 +385,6 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity): """Return true if entity is available.""" if not super().available: return False - if not self.coordinator.api.on: + if not self._tv.on: return False - return self.coordinator.api.powerstate == "On" + return True From a4c5dee082c6d30a84edbf5ab1f7ed1f155d4221 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 10 Jul 2024 09:29:22 +0200 Subject: [PATCH 053/116] Update frontend to 20240710.0 (#121651) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 186f725c643..57de177da9c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240705.0"] + "requirements": ["home-assistant-frontend==20240710.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 41d3af2ad47..6b43f288762 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240705.0 +home-assistant-frontend==20240710.0 home-assistant-intents==2024.7.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b3bb75aac31..38f8b6a44cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.52 # homeassistant.components.frontend -home-assistant-frontend==20240705.0 +home-assistant-frontend==20240710.0 # homeassistant.components.conversation home-assistant-intents==2024.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6f634d091b..eb46f1e9c40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.52 # homeassistant.components.frontend -home-assistant-frontend==20240705.0 +home-assistant-frontend==20240710.0 # homeassistant.components.conversation home-assistant-intents==2024.7.3 From fd0c26cd567a5904c646251a3129bfa09720667d Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 10 Jul 2024 11:11:54 +0200 Subject: [PATCH 054/116] Small fix in velbus cover for the assumed states (#121656) --- homeassistant/components/velbus/cover.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 823d682d339..8b9d927f3d7 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -66,12 +66,16 @@ class VelbusCover(VelbusEntity, CoverEntity): @property def is_opening(self) -> bool: """Return if the cover is opening.""" - return self._channel.is_opening() + if opening := self._channel.is_opening(): + self._assumed_closed = False + return opening @property def is_closing(self) -> bool: """Return if the cover is closing.""" - return self._channel.is_closing() + if closing := self._channel.is_closing(): + self._assumed_closed = True + return closing @property def current_cover_position(self) -> int | None: @@ -89,13 +93,11 @@ class VelbusCover(VelbusEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._channel.open() - self._assumed_closed = False @api_call async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._channel.close() - self._assumed_closed = True @api_call async def async_stop_cover(self, **kwargs: Any) -> None: From ec0910e3da20ec1ad813b2e89eb8966d95f11d8f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 10 Jul 2024 09:47:15 +0200 Subject: [PATCH 055/116] Block icloud3 custom integration from breaking the recorder (#121658) --- homeassistant/loader.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 9afad610420..a654904d69b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -102,6 +102,13 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = { "mydolphin_plus": BlockedIntegration( AwesomeVersion("1.0.13"), "crashes Home Assistant" ), + # Added in 2024.7.2 because of + # https://github.com/gcobb321/icloud3/issues/349 + # Note: Current version 3.0.5.2, the fixed version is a guesstimate, + # as no solution is available at time of writing. + "icloud3": BlockedIntegration( + AwesomeVersion("3.0.5.3"), "prevents recorder from working" + ), } DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey( From ac3eecc879b537c25974c37fae5ca76c54e8913f Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 10 Jul 2024 02:21:38 -0700 Subject: [PATCH 056/116] Handle errors in Fully Kiosk camera (#121659) --- homeassistant/components/fully_kiosk/camera.py | 11 +++++++++-- tests/components/fully_kiosk/test_camera.py | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fully_kiosk/camera.py b/homeassistant/components/fully_kiosk/camera.py index 99419271c26..d55875e094f 100644 --- a/homeassistant/components/fully_kiosk/camera.py +++ b/homeassistant/components/fully_kiosk/camera.py @@ -2,9 +2,12 @@ from __future__ import annotations +from fullykiosk import FullyKioskError + from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -36,8 +39,12 @@ class FullyCameraEntity(FullyKioskEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" - image_bytes: bytes = await self.coordinator.fully.getCamshot() - return image_bytes + try: + image_bytes: bytes = await self.coordinator.fully.getCamshot() + except FullyKioskError as err: + raise HomeAssistantError(err) from err + else: + return image_bytes async def async_turn_on(self) -> None: """Turn on camera.""" diff --git a/tests/components/fully_kiosk/test_camera.py b/tests/components/fully_kiosk/test_camera.py index 4e48749eebb..a2e7067ff1b 100644 --- a/tests/components/fully_kiosk/test_camera.py +++ b/tests/components/fully_kiosk/test_camera.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from fullykiosk import FullyKioskError import pytest from homeassistant.components.camera import async_get_image @@ -41,6 +42,12 @@ async def test_camera( assert mock_fully_kiosk.getCamshot.call_count == 1 assert image.content == b"image_bytes" + fully_kiosk_error = FullyKioskError("error", "status") + mock_fully_kiosk.getCamshot.side_effect = fully_kiosk_error + with pytest.raises(HomeAssistantError) as error: + await async_get_image(hass, entity_camera) + assert error.value.args[0] == fully_kiosk_error + mock_fully_kiosk.getSettings.return_value = {"motionDetection": False} await hass.services.async_call( "camera", From 9c83af37894ad43c6f8590d6f1eedc3afe631463 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 10 Jul 2024 10:24:44 +0200 Subject: [PATCH 057/116] Block places <=2.7.0 custom integration from breaking the recorder (#121662) --- homeassistant/loader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index a654904d69b..f889f8fcb6e 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -109,6 +109,11 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = { "icloud3": BlockedIntegration( AwesomeVersion("3.0.5.3"), "prevents recorder from working" ), + # Added in 2024.7.2 because of + # https://github.com/custom-components/places/issues/289 + "places": BlockedIntegration( + AwesomeVersion("2.7.1"), "prevents recorder from working" + ), } DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey( From 2151086b0a14de80ced8add03543a5c83ca22daa Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 10 Jul 2024 10:32:42 +0200 Subject: [PATCH 058/116] Fix state for Matter Locks (including optional door sensor) (#121665) --- .../components/matter/binary_sensor.py | 16 ++++ homeassistant/components/matter/lock.py | 73 ++++++++++++------- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index b71c35c9cce..a6d68682e9d 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -145,4 +145,20 @@ DISCOVERY_SCHEMAS = [ required_attributes=(clusters.BooleanState.Attributes.StateValue,), device_type=(device_types.RainSensor,), ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="LockDoorStateSensor", + device_class=BinarySensorDeviceClass.DOOR, + # pylint: disable=unnecessary-lambda + measurement_to_ha=lambda x: { + clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True, + clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True, + clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True, + clusters.DoorLock.Enums.DoorStateEnum.kDoorClosed: False, + }.get(x), + ), + entity_class=MatterBinarySensor, + required_attributes=(clusters.DoorLock.Attributes.DoorState,), + ), ] diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index 1cc85fa897e..66b5040184c 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from typing import Any from chip.clusters import Objects as clusters @@ -38,6 +39,7 @@ class MatterLock(MatterEntity, LockEntity): """Representation of a Matter lock.""" features: int | None = None + _optimistic_timer: asyncio.TimerHandle | None = None @property def code_format(self) -> str | None: @@ -90,9 +92,15 @@ class MatterLock(MatterEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock with pin if needed.""" - # optimistically signal locking to state machine - self._attr_is_locking = True - self.async_write_ha_state() + if not self._attr_is_locked: + # optimistically signal locking to state machine + self._attr_is_locking = True + self.async_write_ha_state() + # the lock should acknowledge the command with an attribute update + # but bad things may happen, so guard against it with a timer. + self._optimistic_timer = self.hass.loop.call_later( + 5, self._reset_optimistic_state + ) code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( @@ -101,9 +109,15 @@ class MatterLock(MatterEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock with pin if needed.""" - # optimistically signal unlocking to state machine - self._attr_is_unlocking = True - self.async_write_ha_state() + if self._attr_is_locked: + # optimistically signal unlocking to state machine + self._attr_is_unlocking = True + self.async_write_ha_state() + # the lock should acknowledge the command with an attribute update + # but bad things may happen, so guard against it with a timer. + self._optimistic_timer = self.hass.loop.call_later( + 5, self._reset_optimistic_state + ) code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None if self.supports_unbolt: @@ -120,9 +134,14 @@ class MatterLock(MatterEntity, LockEntity): async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" - # optimistically signal unlocking to state machine - self._attr_is_unlocking = True + # optimistically signal opening to state machine + self._attr_is_opening = True self.async_write_ha_state() + # the lock should acknowledge the command with an attribute update + # but bad things may happen, so guard against it with a timer. + self._optimistic_timer = self.hass.loop.call_later( + 5, self._reset_optimistic_state + ) code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( @@ -145,38 +164,38 @@ class MatterLock(MatterEntity, LockEntity): ) # always reset the optimisically (un)locking state on state update - self._attr_is_locking = False - self._attr_is_unlocking = False + self._reset_optimistic_state(write_state=False) LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id) + if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched: + self._attr_is_locked = False + self._attr_is_open = True if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked: self._attr_is_locked = True + self._attr_is_open = False elif lock_state in ( clusters.DoorLock.Enums.DlLockState.kUnlocked, - clusters.DoorLock.Enums.DlLockState.kUnlatched, clusters.DoorLock.Enums.DlLockState.kNotFullyLocked, ): self._attr_is_locked = False + self._attr_is_open = False else: - # According to the matter docs a null state can happen during device startup. + # Treat any other state as unknown. + # NOTE: A null state can happen during device startup. self._attr_is_locked = None + self._attr_is_open = None - if self.supports_door_position_sensor: - door_state = self.get_matter_attribute_value( - clusters.DoorLock.Attributes.DoorState - ) - - assert door_state is not None - - LOGGER.debug("Door state: %s for %s", door_state, self.entity_id) - - self._attr_is_jammed = ( - door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed - ) - self._attr_is_open = ( - door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen - ) + @callback + def _reset_optimistic_state(self, write_state: bool = True) -> None: + if self._optimistic_timer and not self._optimistic_timer.cancelled(): + self._optimistic_timer.cancel() + self._optimistic_timer = None + self._attr_is_locking = False + self._attr_is_unlocking = False + self._attr_is_opening = False + if write_state: + self.async_write_ha_state() DISCOVERY_SCHEMAS = [ From 05ce3d35b3f2eef7ba30398ee59ceb6f27f635e7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 10 Jul 2024 11:20:26 +0200 Subject: [PATCH 059/116] Matter lock state follow-up (#121669) --- homeassistant/components/matter/lock.py | 6 +++--- tests/components/matter/fixtures/nodes/door-lock.json | 2 +- tests/components/matter/test_door_lock.py | 11 ++++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index 66b5040184c..ae01faa3bc7 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -99,7 +99,7 @@ class MatterLock(MatterEntity, LockEntity): # the lock should acknowledge the command with an attribute update # but bad things may happen, so guard against it with a timer. self._optimistic_timer = self.hass.loop.call_later( - 5, self._reset_optimistic_state + 30, self._reset_optimistic_state ) code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None @@ -116,7 +116,7 @@ class MatterLock(MatterEntity, LockEntity): # the lock should acknowledge the command with an attribute update # but bad things may happen, so guard against it with a timer. self._optimistic_timer = self.hass.loop.call_later( - 5, self._reset_optimistic_state + 30, self._reset_optimistic_state ) code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None @@ -140,7 +140,7 @@ class MatterLock(MatterEntity, LockEntity): # the lock should acknowledge the command with an attribute update # but bad things may happen, so guard against it with a timer. self._optimistic_timer = self.hass.loop.call_later( - 5, self._reset_optimistic_state + 30 if self._attr_is_locked else 5, self._reset_optimistic_state ) code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None diff --git a/tests/components/matter/fixtures/nodes/door-lock.json b/tests/components/matter/fixtures/nodes/door-lock.json index 8a3f0fd68dd..b6231e04af4 100644 --- a/tests/components/matter/fixtures/nodes/door-lock.json +++ b/tests/components/matter/fixtures/nodes/door-lock.json @@ -469,7 +469,7 @@ "1/47/65531": [ 0, 1, 2, 14, 15, 16, 19, 65528, 65529, 65530, 65531, 65532, 65533 ], - "1/257/0": 1, + "1/257/0": 0, "1/257/1": 0, "1/257/2": true, "1/257/3": 1, diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 84f0e58a647..461cc1b7f3d 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -8,11 +8,10 @@ import pytest from homeassistant.components.lock import ( STATE_LOCKED, - STATE_OPEN, STATE_UNLOCKED, LockEntityFeature, ) -from homeassistant.const import ATTR_CODE, STATE_LOCKING, STATE_UNKNOWN +from homeassistant.const import ATTR_CODE, STATE_LOCKING, STATE_OPENING, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.entity_registry as er @@ -64,6 +63,7 @@ async def test_lock( ) matter_client.send_device_command.reset_mock() + await hass.async_block_till_done() state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_LOCKING @@ -208,9 +208,14 @@ async def test_lock_with_unbolt( timed_request_timeout_ms=1000, ) + await hass.async_block_till_done() + state = hass.states.get("lock.mock_door_lock_lock") + assert state + assert state.state == STATE_OPENING + set_node_attribute(door_lock_with_unbolt, 1, 257, 3, 0) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_OPEN + assert state.state == STATE_LOCKED From 38a44676ebc626752b9844325ccc11d42da9c5bf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 10 Jul 2024 11:30:16 +0200 Subject: [PATCH 060/116] Block variable <=3.4.4 custom integration from breaking the recorder (#121670) --- homeassistant/loader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f889f8fcb6e..9acc1682602 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -114,6 +114,11 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = { "places": BlockedIntegration( AwesomeVersion("2.7.1"), "prevents recorder from working" ), + # Added in 2024.7.2 because of + # https://github.com/enkama/hass-variables/issues/120 + "variable": BlockedIntegration( + AwesomeVersion("3.4.4"), "prevents recorder from working" + ), } DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey( From 71370758a882d1123c7fdb541186baadcd6e078f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 10 Jul 2024 12:06:02 +0200 Subject: [PATCH 061/116] Bump version to 2024.7.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33087b0bfc1..8587f9e6137 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 138d1dd80b8..82c29948e3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.1" +version = "2024.7.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ef7d68bfd6dcbdf6d797a73eeb65f0bc03828153 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 19 Jul 2024 12:26:40 +0200 Subject: [PATCH 062/116] Fix reauth error and exception in ista EcoTrend integration (#121482) --- .../components/ista_ecotrend/coordinator.py | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/ista_ecotrend/coordinator.py b/homeassistant/components/ista_ecotrend/coordinator.py index 8d55574f0a1..0f14cd06fe3 100644 --- a/homeassistant/components/ista_ecotrend/coordinator.py +++ b/homeassistant/components/ista_ecotrend/coordinator.py @@ -8,6 +8,7 @@ from typing import Any from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -21,6 +22,8 @@ _LOGGER = logging.getLogger(__name__) class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Ista EcoTrend data update coordinator.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, ista: PyEcotrendIsta) -> None: """Initialize ista EcoTrend data update coordinator.""" super().__init__( @@ -35,11 +38,14 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self): """Fetch ista EcoTrend data.""" - if not self.details: - self.details = await self.async_get_details() - try: + await self.hass.async_add_executor_job(self.ista.login) + + if not self.details: + self.details = await self.async_get_details() + return await self.hass.async_add_executor_job(self.get_consumption_data) + except ServerError as e: raise UpdateFailed( "Unable to connect and retrieve data from ista EcoTrend, try again later" @@ -48,7 +54,9 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_exception", - translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 + translation_placeholders={ + CONF_EMAIL: self.config_entry.data[CONF_EMAIL] + }, ) from e def get_consumption_data(self) -> dict[str, Any]: @@ -61,26 +69,16 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def async_get_details(self) -> dict[str, Any]: """Retrieve details of consumption units.""" - try: - result = await self.hass.async_add_executor_job( - self.ista.get_consumption_unit_details + + result = await self.hass.async_add_executor_job( + self.ista.get_consumption_unit_details + ) + + return { + consumption_unit: next( + details + for details in result["consumptionUnits"] + if details["id"] == consumption_unit ) - except ServerError as e: - raise UpdateFailed( - "Unable to connect and retrieve data from ista EcoTrend, try again later" - ) from e - except (LoginError, KeycloakError) as e: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, - translation_key="authentication_exception", - translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 - ) from e - else: - return { - consumption_unit: next( - details - for details in result["consumptionUnits"] - if details["id"] == consumption_unit - ) - for consumption_unit in self.ista.get_uuids() - } + for consumption_unit in self.ista.get_uuids() + } From 37f37f728783f192ac61f24e2cf1fca5d3e046d6 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:54:02 +0200 Subject: [PATCH 063/116] Retain Jellyfin config flow input on connection issue (#121618) --- homeassistant/components/jellyfin/config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py index 4798a07b9cd..baecbcfb941 100644 --- a/homeassistant/components/jellyfin/config_flow.py +++ b/homeassistant/components/jellyfin/config_flow.py @@ -97,7 +97,11 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input + ), + errors=errors, ) async def async_step_reauth( From ec8e6398048882601f443a303b98b496d2033d8d Mon Sep 17 00:00:00 2001 From: Tomek Porozynski <36776636+ontaptom@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:48:08 +0200 Subject: [PATCH 064/116] Update Supla async_set_cover_position to use "REVEAL_PARTIALLY" (#121663) --- homeassistant/components/supla/cover.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 4cdee04b149..37b64c375eb 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -71,7 +71,9 @@ class SuplaCoverEntity(SuplaEntity, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - await self.async_action("REVEAL", percentage=kwargs.get(ATTR_POSITION)) + await self.async_action( + "REVEAL_PARTIALLY", percentage=kwargs.get(ATTR_POSITION) + ) @property def is_closed(self) -> bool | None: From 10cdf64f90a199ab1d7a1cf3e30cbfc804977915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mind=C3=AAllo=20de=20Andrade?= Date: Wed, 10 Jul 2024 15:16:36 -0300 Subject: [PATCH 065/116] Bump sunweg 3.0.2 (#121684) --- homeassistant/components/sunweg/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sunweg/manifest.json b/homeassistant/components/sunweg/manifest.json index bcf1ad9dae2..998d3610735 100644 --- a/homeassistant/components/sunweg/manifest.json +++ b/homeassistant/components/sunweg/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sunweg/", "iot_class": "cloud_polling", "loggers": ["sunweg"], - "requirements": ["sunweg==3.0.1"] + "requirements": ["sunweg==3.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 38f8b6a44cb..0a6550c8c43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2662,7 +2662,7 @@ stringcase==1.2.0 subarulink==0.7.11 # homeassistant.components.sunweg -sunweg==3.0.1 +sunweg==3.0.2 # homeassistant.components.surepetcare surepy==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb46f1e9c40..9f354c8b646 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2081,7 +2081,7 @@ stringcase==1.2.0 subarulink==0.7.11 # homeassistant.components.sunweg -sunweg==3.0.1 +sunweg==3.0.2 # homeassistant.components.surepetcare surepy==0.9.0 From ad5cbf0da67ea4c33ec7aa39aa7feecffdd6205c Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:46:39 +0200 Subject: [PATCH 066/116] Allow enigma2 devices to use different source bouquets (#121686) --- homeassistant/components/enigma2/__init__.py | 6 +++++- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enigma2/__init__.py b/homeassistant/components/enigma2/__init__.py index 4e4f8bdb687..de8283a5533 100644 --- a/homeassistant/components/enigma2/__init__.py +++ b/homeassistant/components/enigma2/__init__.py @@ -16,6 +16,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .const import CONF_SOURCE_BOUQUET + type Enigma2ConfigEntry = ConfigEntry[OpenWebIfDevice] PLATFORMS = [Platform.MEDIA_PLAYER] @@ -35,7 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> b hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url ) - entry.runtime_data = OpenWebIfDevice(session) + entry.runtime_data = OpenWebIfDevice( + session, source_bouquet=entry.options.get(CONF_SOURCE_BOUQUET) + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index ef08314e541..538cfb56388 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.2.4"] + "requirements": ["openwebifpy==4.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0a6550c8c43..3be48c03ae9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1492,7 +1492,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.2.4 +openwebifpy==4.2.5 # homeassistant.components.luci openwrt-luci-rpc==1.1.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f354c8b646..d9c49acb59b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1207,7 +1207,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.enigma2 -openwebifpy==4.2.4 +openwebifpy==4.2.5 # homeassistant.components.opower opower==0.4.7 From 269fb235279bc681d12bc06c798834c036cc19cb Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:10:47 +0100 Subject: [PATCH 067/116] Fix tplink bug changing color temp on bulbs with light effects (#121696) --- homeassistant/components/tplink/light.py | 4 ++-- tests/components/tplink/test_light.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 22e7c523d1a..9b7dd499c97 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -392,11 +392,11 @@ class TPLinkLightEffectEntity(TPLinkLightEntity): kwargs[ATTR_EFFECT], brightness=brightness, transition=transition ) elif ATTR_COLOR_TEMP_KELVIN in kwargs: - if self.effect: + if self.effect and self.effect != EFFECT_OFF: # If there is an effect in progress # we have to clear the effect # before we can set a color temp - await self._light_module.set_hsv(0, 0, brightness) + await self._effect_module.set_effect(LightEffect.LIGHT_EFFECTS_OFF) await self._async_set_color_temp( kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition ) diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index bb814d1f5d3..590274b8405 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -533,16 +533,16 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: assert state.attributes[ATTR_EFFECT_LIST] == ["Off", "Effect1", "Effect2"] # Ensure setting color temp when an effect - # is in progress calls set_hsv to clear the effect + # is in progress calls set_effect to clear the effect await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 4000}, blocking=True, ) - light.set_hsv.assert_called_once_with(0, 0, None) + light_effect.set_effect.assert_called_once_with(LightEffect.LIGHT_EFFECTS_OFF) light.set_color_temp.assert_called_once_with(4000, brightness=None, transition=None) - light.set_hsv.reset_mock() + light_effect.set_effect.reset_mock() light.set_color_temp.reset_mock() await hass.services.async_call( From 98df46f3ea9324476ba132fc903ed88247b084b7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 10 Jul 2024 21:53:11 +0200 Subject: [PATCH 068/116] Bump knocki to 0.3.0 (#121704) --- homeassistant/components/knocki/config_flow.py | 4 +++- homeassistant/components/knocki/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knocki/test_config_flow.py | 8 ++++++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/knocki/config_flow.py b/homeassistant/components/knocki/config_flow.py index 724c65f83df..654dd4a4d1f 100644 --- a/homeassistant/components/knocki/config_flow.py +++ b/homeassistant/components/knocki/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from knocki import KnockiClient, KnockiConnectionError +from knocki import KnockiClient, KnockiConnectionError, KnockiInvalidAuthError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -45,6 +45,8 @@ class KnockiConfigFlow(ConfigFlow, domain=DOMAIN): raise except KnockiConnectionError: errors["base"] = "cannot_connect" + except KnockiInvalidAuthError: + errors["base"] = "invalid_auth" except Exception: # noqa: BLE001 LOGGER.exception("Error logging into the Knocki API") errors["base"] = "unknown" diff --git a/homeassistant/components/knocki/manifest.json b/homeassistant/components/knocki/manifest.json index e78e9856d62..cad3e156c6c 100644 --- a/homeassistant/components/knocki/manifest.json +++ b/homeassistant/components/knocki/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["knocki"], - "requirements": ["knocki==0.2.0"] + "requirements": ["knocki==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3be48c03ae9..0919effd0bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,7 +1203,7 @@ kegtron-ble==0.4.0 kiwiki-client==0.1.1 # homeassistant.components.knocki -knocki==0.2.0 +knocki==0.3.0 # homeassistant.components.knx knx-frontend==2024.1.20.105944 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9c49acb59b..51bc970af1f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -981,7 +981,7 @@ justnimbus==0.7.4 kegtron-ble==0.4.0 # homeassistant.components.knocki -knocki==0.2.0 +knocki==0.3.0 # homeassistant.components.knx knx-frontend==2024.1.20.105944 diff --git a/tests/components/knocki/test_config_flow.py b/tests/components/knocki/test_config_flow.py index baf43c3ad30..188175035da 100644 --- a/tests/components/knocki/test_config_flow.py +++ b/tests/components/knocki/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from knocki import KnockiConnectionError +from knocki import KnockiConnectionError, KnockiInvalidAuthError import pytest from homeassistant.components.knocki.const import DOMAIN @@ -72,7 +72,11 @@ async def test_duplcate_entry( @pytest.mark.parametrize(("field"), ["login", "link"]) @pytest.mark.parametrize( ("exception", "error"), - [(KnockiConnectionError, "cannot_connect"), (Exception, "unknown")], + [ + (KnockiConnectionError, "cannot_connect"), + (KnockiInvalidAuthError, "invalid_auth"), + (Exception, "unknown"), + ], ) async def test_exceptions( hass: HomeAssistant, From 372649069e861f2cea781ad3099e974ae20d8e9a Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 10 Jul 2024 23:08:25 +0200 Subject: [PATCH 069/116] Bump pyloadapi to v1.3.2 (#121709) --- .../components/pyload/coordinator.py | 2 +- homeassistant/components/pyload/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/pyload/test_sensor.py | 22 +++++++++++++++++++ 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index c55ca4c1630..7eadefcd260 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -30,7 +30,7 @@ class PyLoadData: speed: float download: bool reconnect: bool - captcha: bool + captcha: bool | None = None free_space: int diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index fe1888478f8..788cdd1eb05 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["pyloadapi"], "quality_scale": "platinum", - "requirements": ["PyLoadAPI==1.2.0"] + "requirements": ["PyLoadAPI==1.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0919effd0bc..ec8dd306c4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -60,7 +60,7 @@ PyFlume==0.6.5 PyFronius==0.7.3 # homeassistant.components.pyload -PyLoadAPI==1.2.0 +PyLoadAPI==1.3.2 # homeassistant.components.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51bc970af1f..4873cddfc04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ PyFlume==0.6.5 PyFronius==0.7.3 # homeassistant.components.pyload -PyLoadAPI==1.2.0 +PyLoadAPI==1.3.2 # homeassistant.components.met_eireann PyMetEireann==2021.8.0 diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index a44c9c8bf91..3e18faca12b 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -157,3 +157,25 @@ async def test_deprecated_yaml( assert issue_registry.async_get_issue( domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}" ) + + +async def test_pyload_pre_0_5_0( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test setup of the pyload sensor platform.""" + mock_pyloadapi.get_status.return_value = { + "pause": False, + "active": 1, + "queue": 6, + "total": 37, + "speed": 5405963.0, + "download": True, + "reconnect": False, + } + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED From 4ab180f01644ab88ac5d1e1cbf9102043e40a338 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Jul 2024 14:06:58 -0700 Subject: [PATCH 070/116] Fix update happening too early in unifiprotect (#121714) --- homeassistant/components/unifiprotect/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 7eceb861955..97325135de5 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -189,7 +189,6 @@ class BaseProtectEntity(Entity): self._async_get_ufp_enabled = description.get_ufp_enabled self._async_set_device_info() - self._async_update_device_from_protect(device) self._state_getters = tuple( partial(attrgetter(attr), self) for attr in self._state_attrs ) @@ -264,6 +263,7 @@ class BaseProtectEntity(Entity): self.async_on_remove( self.data.async_subscribe(self.device.mac, self._async_updated_event) ) + self._async_update_device_from_protect(self.device) class ProtectDeviceEntity(BaseProtectEntity): From ebe7bc06866f6605c8e491ddb8ff130c7e98ddcc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 10 Jul 2024 23:22:03 +0200 Subject: [PATCH 071/116] Bump knocki to 0.3.1 (#121717) --- homeassistant/components/knocki/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knocki/manifest.json b/homeassistant/components/knocki/manifest.json index cad3e156c6c..f35827b8213 100644 --- a/homeassistant/components/knocki/manifest.json +++ b/homeassistant/components/knocki/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["knocki"], - "requirements": ["knocki==0.3.0"] + "requirements": ["knocki==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ec8dd306c4a..bfa675fed76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,7 +1203,7 @@ kegtron-ble==0.4.0 kiwiki-client==0.1.1 # homeassistant.components.knocki -knocki==0.3.0 +knocki==0.3.1 # homeassistant.components.knx knx-frontend==2024.1.20.105944 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4873cddfc04..09b458b53e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -981,7 +981,7 @@ justnimbus==0.7.4 kegtron-ble==0.4.0 # homeassistant.components.knocki -knocki==0.3.0 +knocki==0.3.1 # homeassistant.components.knx knx-frontend==2024.1.20.105944 From 85952421425ff84c8893c9e988fc7ecaa0c54247 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 10 Jul 2024 23:53:11 +0200 Subject: [PATCH 072/116] Fix bad access to UniFi runtime_data when not assigned (#121725) * Fix bad access to runtime_data when not assigned * Fix review comment * Clean up if statements --- homeassistant/components/unifi/config_flow.py | 13 +++---- tests/components/unifi/test_config_flow.py | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e93b59b0673..b5ad1ea2ff0 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -164,13 +164,12 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN): config_entry = self.reauth_config_entry abort_reason = "reauth_successful" - if ( - config_entry is not None - and config_entry.state is not ConfigEntryState.NOT_LOADED - ): - hub = config_entry.runtime_data - - if hub and hub.available: + if config_entry: + if ( + config_entry.state is ConfigEntryState.LOADED + and (hub := config_entry.runtime_data) + and hub.available + ): return self.async_abort(reason="already_configured") return self.async_update_reload_and_abort( diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 7b37437cd1d..9ae3af19b46 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,5 +1,6 @@ """Test UniFi Network config flow.""" +from collections.abc import Callable import socket from unittest.mock import PropertyMock, patch @@ -338,6 +339,44 @@ async def test_reauth_flow_update_configuration( assert config_entry.data[CONF_PASSWORD] == "new_pass" +async def test_reauth_flow_update_configuration_on_not_loaded_entry( + hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] +) -> None: + """Verify reauth flow can update hub configuration on a not loaded entry.""" + with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.RequestError): + config_entry = await config_entry_factory() + + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": config_entry.unique_id, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "new_name", + CONF_PASSWORD: "new_pass", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data[CONF_HOST] == "1.2.3.4" + assert config_entry.data[CONF_USERNAME] == "new_name" + assert config_entry.data[CONF_PASSWORD] == "new_pass" + + @pytest.mark.parametrize("client_payload", [CLIENTS]) @pytest.mark.parametrize("device_payload", [DEVICES]) @pytest.mark.parametrize("wlan_payload", [WLANS]) From 3d8afe7cb87a8e8f1ca91220155f68e4245f2ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 11 Jul 2024 08:07:18 +0100 Subject: [PATCH 073/116] Update Idasen Desk library to 2.6.2 (#121729) --- homeassistant/components/idasen_desk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/idasen_desk/manifest.json b/homeassistant/components/idasen_desk/manifest.json index a09d155b5b0..17a5f519274 100644 --- a/homeassistant/components/idasen_desk/manifest.json +++ b/homeassistant/components/idasen_desk/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/idasen_desk", "iot_class": "local_push", "quality_scale": "silver", - "requirements": ["idasen-ha==2.6.1"] + "requirements": ["idasen-ha==2.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index bfa675fed76..e9aec3fd756 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1137,7 +1137,7 @@ ical==8.1.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==2.6.1 +idasen-ha==2.6.2 # homeassistant.components.network ifaddr==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09b458b53e3..f02183553a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -933,7 +933,7 @@ ical==8.1.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==2.6.1 +idasen-ha==2.6.2 # homeassistant.components.network ifaddr==0.2.0 From 68841b3d8a46afe85396adc5e40d84fdb1d82274 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 11 Jul 2024 01:14:11 -0700 Subject: [PATCH 074/116] Bump opower to 0.5.2 to fix 403 forbidden errors for users with multiple accounts (#121762) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index d419fdcb043..28c2e8ba2a8 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.7"] + "requirements": ["opower==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e9aec3fd756..28b1f2f67b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1501,7 +1501,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.7 +opower==0.5.2 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f02183553a6..ef01832108b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1210,7 +1210,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.5 # homeassistant.components.opower -opower==0.4.7 +opower==0.5.2 # homeassistant.components.oralb oralb-ble==0.17.6 From 3b8e736fe39e0a720fa694c1b9ea00e60ae46d1c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 11 Jul 2024 08:23:10 -0700 Subject: [PATCH 075/116] Pin mashumaro version >= 3.13.1 for python 3.12.4 compatibility. (#121782) Pin mashumaro version for python 3.12.4 compatibility. --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b43f288762..fcf79258c25 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -136,6 +136,9 @@ backoff>=2.0 # v2 has breaking changes (#99218). pydantic==1.10.17 +# Required for Python 3.12.4 compatibility (#119223). +mashumaro>=3.13.1 + # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 434b4d0071f..3c593a2bdf7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -157,6 +157,9 @@ backoff>=2.0 # v2 has breaking changes (#99218). pydantic==1.10.17 +# Required for Python 3.12.4 compatibility (#119223). +mashumaro>=3.13.1 + # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 From 6aaaba6419949b4706283c22de17beb6221c2274 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:22:10 +0200 Subject: [PATCH 076/116] Bump pytedee_async to 0.2.20 (#121783) --- homeassistant/components/tedee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index 24df4cff95c..4f071267a25 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["pytedee_async"], "quality_scale": "platinum", - "requirements": ["pytedee-async==0.2.17"] + "requirements": ["pytedee-async==0.2.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index 28b1f2f67b6..1d0d96bbbd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2209,7 +2209,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.17 +pytedee-async==0.2.20 # homeassistant.components.tfiac pytfiac==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef01832108b..5a831e5cd59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1742,7 +1742,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.17 +pytedee-async==0.2.20 # homeassistant.components.motionmount python-MotionMount==2.0.0 From 63b14d14c17e5a1c195f72f30ad0c772ab6500bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Jul 2024 10:18:45 -0500 Subject: [PATCH 077/116] Add some missing tplink ouis (#121785) --- homeassistant/components/tplink/manifest.json | 8 ++++++-- homeassistant/generated/dhcp.py | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 3786a2565c2..337e05726ac 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -181,7 +181,7 @@ "macaddress": "1C61B4*" }, { - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "5CE931*" }, { @@ -189,9 +189,13 @@ "macaddress": "3C52A1*" }, { - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "5C628B*" }, + { + "hostname": "l[59]*", + "macaddress": "14EBB6*" + }, { "hostname": "tp*", "macaddress": "5C628B*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index e898f64d128..f6df799d01e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -827,7 +827,7 @@ DHCP: Final[list[dict[str, str | bool]]] = [ }, { "domain": "tplink", - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "5CE931*", }, { @@ -837,9 +837,14 @@ DHCP: Final[list[dict[str, str | bool]]] = [ }, { "domain": "tplink", - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "5C628B*", }, + { + "domain": "tplink", + "hostname": "l[59]*", + "macaddress": "14EBB6*", + }, { "domain": "tplink", "hostname": "tp*", From 1e6c96c6eb19a121e5a8012059ea413f9ef50bc8 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 11 Jul 2024 17:14:22 -0400 Subject: [PATCH 078/116] Use async_connect in newly bumped 0.5.8 UPB library (#121789) --- homeassistant/components/upb/__init__.py | 2 +- homeassistant/components/upb/config_flow.py | 2 +- homeassistant/components/upb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/upb/test_config_flow.py | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index f2db6ff1b3c..2e5a69393d4 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b file = config_entry.data[CONF_FILE_PATH] upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file}) - upb.connect() + await upb.async_connect() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb} diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 40f49e57c60..fec93a51202 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -40,7 +40,7 @@ async def _validate_input(data): upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file_path}) - upb.connect(_connected_callback) + await upb.async_connect(_connected_callback) if not upb.config_ok: _LOGGER.error("Missing or invalid UPB file: %s", file_path) diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index b208edbc0e5..6b49c859771 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/upb", "iot_class": "local_push", "loggers": ["upb_lib"], - "requirements": ["upb-lib==0.5.7"] + "requirements": ["upb-lib==0.5.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1d0d96bbbd7..453accf6064 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2807,7 +2807,7 @@ unifiled==0.11 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.7 +upb-lib==0.5.8 # homeassistant.components.upcloud upcloud-api==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a831e5cd59..7f0fdcf52c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2181,7 +2181,7 @@ unifi-discovery==1.2.0 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.7 +upb-lib==0.5.8 # homeassistant.components.upcloud upcloud-api==2.5.1 diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index d5d6d70bb68..54aeb00e89a 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -1,7 +1,7 @@ """Test the UPB Control config flow.""" from asyncio import TimeoutError -from unittest.mock import MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, PropertyMock, patch from homeassistant import config_entries from homeassistant.components.upb.const import DOMAIN @@ -15,11 +15,11 @@ def mocked_upb(sync_complete=True, config_ok=True): def _upb_lib_connect(callback): callback() - upb_mock = MagicMock() + upb_mock = AsyncMock() type(upb_mock).network_id = PropertyMock(return_value="42") type(upb_mock).config_ok = PropertyMock(return_value=config_ok) if sync_complete: - upb_mock.connect.side_effect = _upb_lib_connect + upb_mock.async_connect.side_effect = _upb_lib_connect return patch( "homeassistant.components.upb.config_flow.upb_lib.UpbPim", return_value=upb_mock ) From 0f69c58ba9349f726e8266b47104648435fe5f01 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:19:31 +0100 Subject: [PATCH 079/116] Bump python-kasa to 0.7.0.4 (#121791) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 337e05726ac..a345f64e4b2 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -301,5 +301,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.3"] + "requirements": ["python-kasa[speedups]==0.7.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 453accf6064..5da52fbb075 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.3 +python-kasa[speedups]==0.7.0.4 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f0fdcf52c0..23d8fd9adda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.3 +python-kasa[speedups]==0.7.0.4 # homeassistant.components.matter python-matter-server==6.2.2 From 976902f22ce0d52969a53f07d36115252fb71230 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 12 Jul 2024 15:51:18 +0200 Subject: [PATCH 080/116] Add missing translations to Roborock (#121796) --- homeassistant/components/roborock/strings.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index c7fc34386fd..5d95812e845 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -214,7 +214,8 @@ "unknown": "Unknown", "locked": "Locked", "air_drying_stopping": "Air drying stopping", - "egg_attack": "Cupid mode" + "egg_attack": "Cupid mode", + "mapping": "Mapping" } }, "total_cleaning_time": { @@ -282,7 +283,8 @@ "deep": "Deep", "deep_plus": "Deep+", "custom": "Custom", - "fast": "Fast" + "fast": "Fast", + "smart_mode": "SmartPlan" } }, "mop_intensity": { @@ -293,10 +295,12 @@ "mild": "Mild", "medium": "Medium", "moderate": "Moderate", + "max": "Max", "high": "High", "intense": "Intense", "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]", - "custom_water_flow": "Custom water flow" + "custom_water_flow": "Custom water flow", + "smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]" } } }, @@ -338,13 +342,14 @@ "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]", "gentle": "Gentle", "off": "[%key:common::state::off%]", - "max": "Max", + "max": "[%key:component::roborock::entity::select::mop_intensity::state::max%]", "max_plus": "Max plus", "medium": "Medium", "quiet": "Quiet", "silent": "Silent", "standard": "[%key:component::roborock::entity::select::mop_mode::state::standard%]", - "turbo": "Turbo" + "turbo": "Turbo", + "smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]" } } } From e0b90c4b36bc7672126b37d990482da35be438e4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 13 Jul 2024 16:10:09 +0200 Subject: [PATCH 081/116] Fix alexa does to check `current_position` correctly when handling cover range changes (#121798) --- homeassistant/components/alexa/handlers.py | 2 +- tests/components/alexa/test_smart_home.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 47e09db1166..1849dcd1862 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1497,7 +1497,7 @@ async def async_api_adjust_range( if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) service = SERVICE_SET_COVER_POSITION - if not (current := entity.attributes.get(cover.ATTR_POSITION)): + if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)): msg = f"Unable to determine {entity.entity_id} current position" raise AlexaInvalidValueError(msg) position = response_value = min(100, max(0, range_delta + current)) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index d502dce7d01..fb27c91eea7 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1979,7 +1979,7 @@ async def test_cover_position( "friendly_name": "Test cover range", "device_class": "blind", "supported_features": supported_features, - "position": position, + "current_position": position, }, ) appliance = await discovery_test(device, hass) @@ -2296,7 +2296,7 @@ async def test_cover_position_range( "friendly_name": "Test cover range", "device_class": "blind", "supported_features": 7, - "position": 30, + "current_position": 30, }, ) appliance = await discovery_test(device, hass) @@ -4658,7 +4658,7 @@ async def test_cover_semantics_position_and_tilt(hass: HomeAssistant) -> None: "friendly_name": "Test cover semantics", "device_class": "blind", "supported_features": 255, - "position": 30, + "current_position": 30, "tilt_position": 30, }, ) From 56a9167ed28a1dc4c62772ad3d30020efcbc59df Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 12 Jul 2024 09:13:55 +0200 Subject: [PATCH 082/116] Reolink media second lens (#121800) DUO lens camera distinguish between lenses for media playback --- homeassistant/components/reolink/media_source.py | 4 ++++ tests/components/reolink/test_media_source.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 7a77e482f56..ae865b77913 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -5,6 +5,7 @@ from __future__ import annotations import datetime as dt import logging +from reolink_aio.api import DUAL_LENS_MODELS from reolink_aio.enums import VodRequestType from homeassistant.components.camera import DOMAIN as CAM_DOMAIN, DynamicStreamSettings @@ -184,6 +185,9 @@ class ReolinkVODMediaSource(MediaSource): if device.name_by_user is not None: device_name = device.name_by_user + if host.api.model in DUAL_LENS_MODELS: + device_name = f"{device_name} lens {ch}" + children.append( BrowseMediaSource( domain=DOMAIN, diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 0d86106e8e5..66ed32ca823 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -54,6 +54,7 @@ TEST_FILE_NAME = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00" TEST_FILE_NAME_MP4 = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00.mp4" TEST_STREAM = "main" TEST_CHANNEL = "0" +TEST_CAM_NAME = "Cam new name" TEST_MIME_TYPE = "application/x-mpegURL" TEST_MIME_TYPE_MP4 = "video/mp4" @@ -130,6 +131,7 @@ async def test_browsing( """Test browsing the Reolink three.""" entry_id = config_entry.entry_id reolink_connect.api_version.return_value = 1 + reolink_connect.model = "Reolink TrackMix PoE" with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): assert await hass.config_entries.async_setup(entry_id) is True @@ -137,7 +139,7 @@ async def test_browsing( entries = dr.async_entries_for_config_entry(device_registry, entry_id) assert len(entries) > 0 - device_registry.async_update_device(entries[0].id, name_by_user="Cam new name") + device_registry.async_update_device(entries[0].id, name_by_user=TEST_CAM_NAME) caplog.set_level(logging.DEBUG) @@ -149,6 +151,7 @@ async def test_browsing( assert browse.title == "Reolink" assert browse.identifier is None assert browse.children[0].identifier == browse_root_id + assert browse.children[0].title == f"{TEST_CAM_NAME} lens 0" # browse resolution select browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_root_id}") From e9344ae101a8abd413e052e157f6b687c4a2333f Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 11 Jul 2024 23:12:33 +0200 Subject: [PATCH 083/116] Bump PySwitchbot to 0.48.1 (#121804) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dc858a688cb..0cbbd70a805 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.48.0"] + "requirements": ["PySwitchbot==0.48.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5da52fbb075..95dfb023ab0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -90,7 +90,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.48.0 +PySwitchbot==0.48.1 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23d8fd9adda..32d9722af73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.48.0 +PySwitchbot==0.48.1 # homeassistant.components.syncthru PySyncThru==0.7.10 From 41104324ecf125cdea7f1488a7c1338039b8076f Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 5 Jul 2024 13:26:44 +1000 Subject: [PATCH 084/116] Bump aiolifx to 1.0.4 (#121267) --- CODEOWNERS | 2 ++ homeassistant/components/lifx/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 765f1624c33..560f2efc0cc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -777,6 +777,8 @@ build.json @home-assistant/supervisor /tests/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob +/homeassistant/components/lifx/ @Djelibeybi +/tests/components/lifx/ @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core /homeassistant/components/linear_garage_door/ @IceBotYT diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 6aa7fdc6305..5e68c1bab35 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -1,7 +1,7 @@ { "domain": "lifx", "name": "LIFX", - "codeowners": [], + "codeowners": ["@Djelibeybi"], "config_flow": true, "dependencies": ["network"], "dhcp": [ @@ -48,7 +48,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.2", + "aiolifx==1.0.4", "aiolifx-effects==0.3.2", "aiolifx-themes==0.4.15" ] diff --git a/requirements_all.txt b/requirements_all.txt index 95dfb023ab0..896d541e69d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.2 +aiolifx==1.0.4 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32d9722af73..f6a7328e927 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,7 +255,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.2 +aiolifx==1.0.4 # homeassistant.components.livisi aiolivisi==0.0.19 From ad07bdb62bd0614164187fde099cb7e65ef2b0c5 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 12 Jul 2024 13:21:45 +1000 Subject: [PATCH 085/116] Bump aiolifx to 1.0.5 (#121824) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 5e68c1bab35..3d0bd1d73d1 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -48,7 +48,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.4", + "aiolifx==1.0.5", "aiolifx-effects==0.3.2", "aiolifx-themes==0.4.15" ] diff --git a/requirements_all.txt b/requirements_all.txt index 896d541e69d..5583044dd7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.4 +aiolifx==1.0.5 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6a7328e927..59afcd79a95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,7 +255,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.4 +aiolifx==1.0.5 # homeassistant.components.livisi aiolivisi==0.0.19 From a835750252ea94caf9de23d1274f84ad4d8245d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Jul 2024 08:54:38 -0500 Subject: [PATCH 086/116] Log add/remove index complete at the same level as when it starts (#121852) --- .../components/recorder/migration.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index cf003f72af4..41a429b7269 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -313,11 +313,9 @@ def _create_index( index = index_list[0] _LOGGER.debug("Creating %s index", index_name) _LOGGER.warning( - ( - "Adding index `%s` to table `%s`. Note: this can take several " - "minutes on large databases and slow computers. Please " - "be patient!" - ), + "Adding index `%s` to table `%s`. Note: this can take several " + "minutes on large databases and slow computers. Please " + "be patient!", index_name, table_name, ) @@ -331,7 +329,7 @@ def _create_index( "Index %s already exists on %s, continuing", index_name, table_name ) - _LOGGER.debug("Finished creating %s", index_name) + _LOGGER.warning("Finished adding index `%s` to table `%s`", index_name, table_name) def _execute_or_collect_error( @@ -364,11 +362,9 @@ def _drop_index( DO NOT USE THIS FUNCTION IN ANY OPERATION THAT TAKES USER INPUT. """ _LOGGER.warning( - ( - "Dropping index `%s` from table `%s`. Note: this can take several " - "minutes on large databases and slow computers. Please " - "be patient!" - ), + "Dropping index `%s` from table `%s`. Note: this can take several " + "minutes on large databases and slow computers. Please " + "be patient!", index_name, table_name, ) @@ -377,8 +373,8 @@ def _drop_index( index_to_drop = get_index_by_name(session, table_name, index_name) if index_to_drop is None: - _LOGGER.debug( - "The index %s on table %s no longer exists", index_name, table_name + _LOGGER.warning( + "The index `%s` on table `%s` no longer exists", index_name, table_name ) return @@ -395,18 +391,16 @@ def _drop_index( f"DROP INDEX {index_to_drop}", ): if _execute_or_collect_error(session_maker, query, errors): - _LOGGER.debug( - "Finished dropping index %s from table %s", index_name, table_name + _LOGGER.warning( + "Finished dropping index `%s` from table `%s`", index_name, table_name ) return if not quiet: _LOGGER.warning( - ( - "Failed to drop index `%s` from table `%s`. Schema " - "Migration will continue; this is not a " - "critical operation: %s" - ), + "Failed to drop index `%s` from table `%s`. Schema " + "Migration will continue; this is not a " + "critical operation: %s", index_name, table_name, errors, From 24ed003471d8554691e5a2a5214337c34b5ef3e5 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Mon, 15 Jul 2024 09:20:32 +0200 Subject: [PATCH 087/116] Fix opentherm_gw availability (#121892) --- homeassistant/components/opentherm_gw/__init__.py | 5 +++++ homeassistant/components/opentherm_gw/binary_sensor.py | 7 ++----- homeassistant/components/opentherm_gw/climate.py | 2 +- homeassistant/components/opentherm_gw/sensor.py | 7 ++----- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 46cc6f3daa0..a0d791fddd4 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -470,3 +470,8 @@ class OpenThermGatewayDevice: async_dispatcher_send(self.hass, self.update_signal, status) self.gateway.subscribe(handle_report) + + @property + def connected(self): + """Report whether or not we are connected to the gateway.""" + return self.gateway.connection.connected diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index ad8d09afa89..7c3760653e8 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -48,6 +48,7 @@ class OpenThermBinarySensor(BinarySensorEntity): _attr_should_poll = False _attr_entity_registry_enabled_default = False + _attr_available = False def __init__(self, gw_dev, var, source, device_class, friendly_name_format): """Initialize the binary sensor.""" @@ -85,14 +86,10 @@ class OpenThermBinarySensor(BinarySensorEntity): _LOGGER.debug("Removing OpenTherm Gateway binary sensor %s", self._attr_name) self._unsub_updates() - @property - def available(self): - """Return availability of the sensor.""" - return self._attr_is_on is not None - @callback def receive_report(self, status): """Handle status updates from the component.""" + self._attr_available = self._gateway.connected state = status[self._source].get(self._var) self._attr_is_on = None if state is None else bool(state) self.async_write_ha_state() diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 2d9f1687463..5eb1246e55f 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -138,7 +138,7 @@ class OpenThermClimate(ClimateEntity): @callback def receive_report(self, status): """Receive and handle a new report from the Gateway.""" - self._attr_available = status != gw_vars.DEFAULT_STATUS + self._attr_available = self._gateway.connected ch_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_CH_ACTIVE) flame_on = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_FLAME_ON) cooling_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 9171292c21b..8c17aca4516 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -45,6 +45,7 @@ class OpenThermSensor(SensorEntity): _attr_should_poll = False _attr_entity_registry_enabled_default = False + _attr_available = False def __init__( self, @@ -94,14 +95,10 @@ class OpenThermSensor(SensorEntity): _LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._attr_name) self._unsub_updates() - @property - def available(self): - """Return availability of the sensor.""" - return self._attr_native_value is not None - @callback def receive_report(self, status): """Handle status updates from the component.""" + self._attr_available = self._gateway.connected value = status[self._source].get(self._var) self._attr_native_value = value self.async_write_ha_state() From f9b359ae3010b9283e52525bb2f7286fc1a486a7 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 15 Jul 2024 02:10:50 -0500 Subject: [PATCH 088/116] Fix rainforest_raven closing device due to timeout (#121905) --- homeassistant/components/rainforest_raven/coordinator.py | 2 +- homeassistant/components/rainforest_raven/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainforest_raven/coordinator.py b/homeassistant/components/rainforest_raven/coordinator.py index 37e44b12eba..d08a10c2670 100644 --- a/homeassistant/components/rainforest_raven/coordinator.py +++ b/homeassistant/components/rainforest_raven/coordinator.py @@ -167,7 +167,7 @@ class RAVEnDataCoordinator(DataUpdateCoordinator): await device.synchronize() self._device_info = await device.get_device_info() except: - await device.close() + await device.abort() raise self._raven_device = device diff --git a/homeassistant/components/rainforest_raven/manifest.json b/homeassistant/components/rainforest_raven/manifest.json index bc44c3fc30c..49bd11e8880 100644 --- a/homeassistant/components/rainforest_raven/manifest.json +++ b/homeassistant/components/rainforest_raven/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/rainforest_raven", "iot_class": "local_polling", - "requirements": ["aioraven==0.6.0"], + "requirements": ["aioraven==0.7.0"], "usb": [ { "vid": "0403", diff --git a/requirements_all.txt b/requirements_all.txt index 5583044dd7c..1af4edb3b45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.6.0 +aioraven==0.7.0 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59afcd79a95..d5b8d307884 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.6.0 +aioraven==0.7.0 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 From bf89eaae2514581a8c0b28593ede59a9b5d64930 Mon Sep 17 00:00:00 2001 From: Tomasz Gorochowik Date: Mon, 15 Jul 2024 09:09:19 +0200 Subject: [PATCH 089/116] Fix enigma2 mute (#121928) --- homeassistant/components/enigma2/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 63acdd8be72..86ed9652106 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -199,7 +199,8 @@ class Enigma2Device(MediaPlayerEntity): async def async_mute_volume(self, mute: bool) -> None: """Mute or unmute.""" - await self._device.toggle_mute() + if mute != self._device.status.muted: + await self._device.toggle_mute() async def async_select_source(self, source: str) -> None: """Select input source.""" From 9bd822d693ab1d5a2a5c176d94af544a22bc8f8f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 15 Jul 2024 08:31:44 +0200 Subject: [PATCH 090/116] Fix `configuration_url` for Shelly device using IPv6 (#121939) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/coordinator.py | 3 ++- homeassistant/components/shelly/utils.py | 16 +++++++++++++++- tests/components/shelly/test_utils.py | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 33ed07c35de..c1be4577bf6 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -61,6 +61,7 @@ from .utils import ( async_create_issue_unsupported_firmware, get_block_device_sleep_period, get_device_entry_gen, + get_host, get_http_port, get_rpc_device_wakeup_period, update_device_fw_info, @@ -147,7 +148,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( model=MODEL_NAMES.get(self.model, self.model), sw_version=self.sw_version, hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}:{get_http_port(self.entry.data)}", + configuration_url=f"http://{get_host(self.entry.data[CONF_HOST])}:{get_http_port(self.entry.data)}", ) self.device_id = device_entry.id diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index bcd5a859538..bb2263c8da7 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, timedelta -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv6Address, ip_address from types import MappingProxyType from typing import Any, cast @@ -482,6 +482,20 @@ def get_http_port(data: MappingProxyType[str, Any]) -> int: return cast(int, data.get(CONF_PORT, DEFAULT_HTTP_PORT)) +def get_host(host: str) -> str: + """Get the device IP address or hostname.""" + try: + ip_object = ip_address(host) + except ValueError: + # host contains hostname + return host + + if isinstance(ip_object, IPv6Address): + return f"[{host}]" + + return host + + @callback def async_remove_shelly_rpc_entities( hass: HomeAssistant, domain: str, mac: str, keys: list[str] diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index 7c4ea8accae..5891f250fae 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -23,6 +23,7 @@ from homeassistant.components.shelly.utils import ( get_block_device_sleep_period, get_block_input_triggers, get_device_uptime, + get_host, get_number_of_channels, get_release_url, get_rpc_channel_name, @@ -274,3 +275,19 @@ def test_get_release_url( result = get_release_url(gen, model, beta) assert result is expected + + +@pytest.mark.parametrize( + ("host", "expected"), + [ + ("shelly_device.local", "shelly_device.local"), + ("192.168.178.12", "192.168.178.12"), + ( + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + ), + ], +) +def test_get_host(host: str, expected: str) -> None: + """Test get_host function.""" + assert get_host(host) == expected From 214b5efd72477a2a1a2f095e7bc9d9cb2b30603b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Jul 2024 16:23:07 -0500 Subject: [PATCH 091/116] Narrow sqlite database corruption check to ensure disk image is malformed (#121947) * Narrow sqlite database corruption check to ensure disk image is malformed The database corruption check would also replace the database when it locked externally instead of only when its malformed. This was discovered in https://github.com/home-assistant/core/issues/121909#issuecomment-2227409124 when a user did a manual index creation while HA was online * tweak * tweak * fix * fix --- homeassistant/components/recorder/core.py | 10 +++++++++- tests/components/recorder/test_init.py | 4 +++- tests/components/recorder/test_migrate.py | 4 +++- tests/components/recorder/test_purge.py | 2 +- tests/components/recorder/test_purge_v32_schema.py | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 4e5ac04c3bf..b699ea324f6 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1178,7 +1178,15 @@ class Recorder(threading.Thread): def _handle_database_error(self, err: Exception) -> bool: """Handle a database error that may result in moving away the corrupt db.""" - if isinstance(err.__cause__, sqlite3.DatabaseError): + if ( + (cause := err.__cause__) + and isinstance(cause, sqlite3.DatabaseError) + and (cause_str := str(cause)) + # Make sure we do not move away a database when its only locked + # externally by another process. sqlite does not give us a named + # exception for this so we have to check the error message. + and ("malformed" in cause_str or "not a database" in cause_str) + ): _LOGGER.exception( "Unrecoverable sqlite3 database corruption detected: %s", err ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 52947ce0c19..e1b22b2c245 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1707,7 +1707,9 @@ async def test_database_corruption_while_running( hass.states.async_set("test.lost", "on", {}) sqlite3_exception = DatabaseError("statement", {}, []) - sqlite3_exception.__cause__ = sqlite3.DatabaseError() + sqlite3_exception.__cause__ = sqlite3.DatabaseError( + "database disk image is malformed" + ) await async_wait_recording_done(hass) with patch.object( diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index cb8e402f65a..d5b26eba680 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -165,7 +165,9 @@ async def test_database_migration_encounters_corruption( assert recorder.util.async_migration_in_progress(hass) is False sqlite3_exception = DatabaseError("statement", {}, []) - sqlite3_exception.__cause__ = sqlite3.DatabaseError() + sqlite3_exception.__cause__ = sqlite3.DatabaseError( + "database disk image is malformed" + ) with ( patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1ccbaada265..1167fd4de73 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -210,7 +210,7 @@ async def test_purge_old_states_encouters_database_corruption( await async_wait_recording_done(hass) sqlite3_exception = DatabaseError("statement", {}, []) - sqlite3_exception.__cause__ = sqlite3.DatabaseError() + sqlite3_exception.__cause__ = sqlite3.DatabaseError("not a database") with ( patch( diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index fb636cfa9dc..8a641a2ce7f 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -173,7 +173,7 @@ async def test_purge_old_states_encouters_database_corruption( await async_wait_recording_done(hass) sqlite3_exception = DatabaseError("statement", {}, []) - sqlite3_exception.__cause__ = sqlite3.DatabaseError() + sqlite3_exception.__cause__ = sqlite3.DatabaseError("not a database") with ( patch( From 4b93fc61b5dfd7db6847fda7800a124250b8b5c4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 16 Jul 2024 13:47:11 +0200 Subject: [PATCH 092/116] Bump python-holidays to 0.53 (#122021) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 075285bbdb9..ebe472d7f0e 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.52", "babel==2.15.0"] + "requirements": ["holidays==0.53", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index ad609954a57..69df8080fa5 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.52"] + "requirements": ["holidays==0.53"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1af4edb3b45..a84f565dd64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.52 +holidays==0.53 # homeassistant.components.frontend home-assistant-frontend==20240710.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5b8d307884..c35ec3f64f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.52 +holidays==0.53 # homeassistant.components.frontend home-assistant-frontend==20240710.0 From d9e44bab69f564341de98fd9716c3f692aeb2449 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 16 Jul 2024 20:16:36 +0200 Subject: [PATCH 093/116] Mark UniFi power cycle button as unavailable if PoE is not enabled on port (#122035) --- homeassistant/components/unifi/button.py | 15 +++++++++-- tests/components/unifi/test_button.py | 32 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index 6684e33e532..716d3734953 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -8,7 +8,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass import secrets -from typing import Any +from typing import TYPE_CHECKING, Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -44,6 +44,17 @@ from .entity import ( async_wlan_device_info_fn, ) +if TYPE_CHECKING: + from .hub import UnifiHub + + +@callback +def async_port_power_cycle_available_fn(hub: UnifiHub, obj_id: str) -> bool: + """Check if port allows power cycle action.""" + if not async_device_available_fn(hub, obj_id): + return False + return bool(hub.api.ports[obj_id].poe_enable) + async def async_restart_device_control_fn( api: aiounifi.Controller, obj_id: str @@ -96,7 +107,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiButtonEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, device_class=ButtonDeviceClass.RESTART, api_handler_fn=lambda api: api.ports, - available_fn=async_device_available_fn, + available_fn=async_port_power_cycle_available_fn, control_fn=async_power_cycle_port_control_fn, device_info_fn=async_device_device_info_fn, name_fn=lambda port: f"{port.name} Power Cycle", diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index b58d01e7724..b7bf19aedc2 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -1,9 +1,11 @@ """UniFi Network button platform tests.""" +from copy import deepcopy from datetime import timedelta from typing import Any from unittest.mock import patch +from aiounifi.models.message import MessageKey import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass @@ -319,3 +321,33 @@ async def test_wlan_button_entities( request_data, call, ) + + +@pytest.mark.parametrize("device_payload", [DEVICE_POWER_CYCLE_POE]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_power_cycle_availability( + hass: HomeAssistant, + mock_websocket_message, + device_payload: dict[str, Any], +) -> None: + """Verify that disabling PoE marks entity as unavailable.""" + entity_id = "button.switch_port_1_power_cycle" + + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + # PoE disabled + + device_1 = deepcopy(device_payload[0]) + device_1["port_table"][0]["poe_enable"] = False + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # PoE enabled + device_1 = deepcopy(device_payload[0]) + device_1["port_table"][0]["poe_enable"] = True + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE From 002db3c3e93ef7ff72ae88d102e9cf5e74b0ef12 Mon Sep 17 00:00:00 2001 From: Harry Martland Date: Fri, 19 Jul 2024 12:47:28 +0100 Subject: [PATCH 094/116] Fix hive not updating when boosting (#122042) * fixes issue where hive does not update when boosting * formats files --- homeassistant/components/hive/climate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index cb1cc15a5bf..8f6db11babe 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -142,10 +142,10 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): self.device = await self.hive.heating.getClimate(self.device) self._attr_available = self.device["deviceData"].get("online") if self._attr_available: - self._attr_hvac_mode = HIVE_TO_HASS_STATE[self.device["status"]["mode"]] - self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION[ + self._attr_hvac_mode = HIVE_TO_HASS_STATE.get(self.device["status"]["mode"]) + self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION.get( self.device["status"]["action"] - ] + ) self._attr_current_temperature = self.device["status"][ "current_temperature" ] @@ -154,5 +154,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): self._attr_max_temp = self.device["max_temp"] if self.device["status"]["boost"] == "ON": self._attr_preset_mode = PRESET_BOOST + self._attr_hvac_mode = HVACMode.HEAT else: self._attr_preset_mode = PRESET_NONE From 977a55e3b84d8559e304b5fbb9b7e540a220d1e4 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:07:53 +0100 Subject: [PATCH 095/116] Update tplink device config during reauth flow (#122089) --- .../components/tplink/config_flow.py | 51 ++++-- tests/components/tplink/__init__.py | 46 +++-- tests/components/tplink/conftest.py | 23 ++- tests/components/tplink/test_config_flow.py | 162 +++++++++++++----- tests/components/tplink/test_init.py | 36 ++-- 5 files changed, 219 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 5608ccfa72f..a0f0ca6eb76 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping +import logging from typing import Any from kasa import ( @@ -52,6 +53,8 @@ from .const import ( DOMAIN, ) +_LOGGER = logging.getLogger(__name__) + STEP_AUTH_DATA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) @@ -88,15 +91,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): ) @callback - def _update_config_if_entry_in_setup_error( + def _get_config_updates( self, entry: ConfigEntry, host: str, config: dict - ) -> ConfigFlowResult | None: - """If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config.""" - if entry.state not in ( - ConfigEntryState.SETUP_ERROR, - ConfigEntryState.SETUP_RETRY, - ): - return None + ) -> dict | None: + """Return updates if the host or device config has changed.""" entry_data = entry.data entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG) if entry_config_dict == config and entry_data[CONF_HOST] == host: @@ -110,11 +108,31 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): != config.get(CONF_CONNECTION_TYPE) ): updates.pop(CONF_CREDENTIALS_HASH, None) - return self.async_update_reload_and_abort( - entry, - data=updates, - reason="already_configured", - ) + _LOGGER.debug( + "Connection type changed for %s from %s to: %s", + host, + entry_config_dict.get(CONF_CONNECTION_TYPE), + config.get(CONF_CONNECTION_TYPE), + ) + return updates + + @callback + def _update_config_if_entry_in_setup_error( + self, entry: ConfigEntry, host: str, config: dict + ) -> ConfigFlowResult | None: + """If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config.""" + if entry.state not in ( + ConfigEntryState.SETUP_ERROR, + ConfigEntryState.SETUP_RETRY, + ): + return None + if updates := self._get_config_updates(entry, host, config): + return self.async_update_reload_and_abort( + entry, + data=updates, + reason="already_configured", + ) + return None async def _async_handle_discovery( self, host: str, formatted_mac: str, config: dict | None = None @@ -454,7 +472,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): password = user_input[CONF_PASSWORD] credentials = Credentials(username, password) try: - await self._async_try_discover_and_update( + device = await self._async_try_discover_and_update( host, credentials=credentials, raise_on_progress=True, @@ -467,6 +485,11 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): placeholders["error"] = str(ex) else: await set_credentials(self.hass, username, password) + config = device.config.to_dict(exclude_credentials=True) + if updates := self._get_config_updates(reauth_entry, host, config): + self.hass.config_entries.async_update_entry( + reauth_entry, data=updates + ) self.hass.async_create_task( self._async_reload_requires_auth_entries(), eager_start=False ) diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index b554cf07015..c51a451c847 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -57,25 +57,26 @@ CREDENTIALS_HASH_LEGACY = "" DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True) CREDENTIALS = Credentials("foo", "bar") -CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv==" -DEVICE_CONFIG_AUTH = DeviceConfig( +CREDENTIALS_HASH_AES = "AES/abcdefghijklmnopqrstuvabcdefghijklmnopqrstuv==" +CREDENTIALS_HASH_KLAP = "KLAP/abcdefghijklmnopqrstuv==" +DEVICE_CONFIG_KLAP = DeviceConfig( IP_ADDRESS, credentials=CREDENTIALS, connection_type=DeviceConnectionParameters( - DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap ), uses_http=True, ) -DEVICE_CONFIG_AUTH2 = DeviceConfig( +DEVICE_CONFIG_AES = DeviceConfig( IP_ADDRESS2, credentials=CREDENTIALS, connection_type=DeviceConnectionParameters( - DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes ), uses_http=True, ) -DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True) -DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict(exclude_credentials=True) +DEVICE_CONFIG_DICT_KLAP = DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True) +DEVICE_CONFIG_DICT_AES = DEVICE_CONFIG_AES.to_dict(exclude_credentials=True) CREATE_ENTRY_DATA_LEGACY = { CONF_HOST: IP_ADDRESS, @@ -84,24 +85,28 @@ CREATE_ENTRY_DATA_LEGACY = { CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY, } -CREATE_ENTRY_DATA_AUTH = { +CREATE_ENTRY_DATA_KLAP = { CONF_HOST: IP_ADDRESS, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, - CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_KLAP, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, } -CREATE_ENTRY_DATA_AUTH2 = { +CREATE_ENTRY_DATA_AES = { CONF_HOST: IP_ADDRESS2, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, - CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH2, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AES, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AES, } -NEW_CONNECTION_TYPE = DeviceConnectionParameters( - DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Aes +CONNECTION_TYPE_KLAP = DeviceConnectionParameters( + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap ) -NEW_CONNECTION_TYPE_DICT = NEW_CONNECTION_TYPE.to_dict() +CONNECTION_TYPE_KLAP_DICT = CONNECTION_TYPE_KLAP.to_dict() +CONNECTION_TYPE_AES = DeviceConnectionParameters( + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes +) +CONNECTION_TYPE_AES_DICT = CONNECTION_TYPE_AES.to_dict() def _load_feature_fixtures(): @@ -187,7 +192,7 @@ def _mocked_device( device_id=DEVICE_ID, alias=ALIAS, model=MODEL, - ip_address=IP_ADDRESS, + ip_address: str | None = None, modules: list[str] | None = None, children: list[Device] | None = None, features: list[str | Feature] | None = None, @@ -202,12 +207,17 @@ def _mocked_device( device.mac = mac device.alias = alias device.model = model - device.host = ip_address device.device_id = device_id device.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} device.modules = {} device.features = {} + if not ip_address: + ip_address = IP_ADDRESS + else: + device_config.host = ip_address + device.host = ip_address + if modules: device.modules = { module_name: MODULE_TO_MOCK_GEN[module_name](device) diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index f8d933de71e..b1256f437e7 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -11,8 +11,10 @@ from homeassistant.core import HomeAssistant from . import ( CREATE_ENTRY_DATA_LEGACY, - CREDENTIALS_HASH_AUTH, - DEVICE_CONFIG_AUTH, + CREDENTIALS_HASH_AES, + CREDENTIALS_HASH_KLAP, + DEVICE_CONFIG_AES, + DEVICE_CONFIG_KLAP, IP_ADDRESS, IP_ADDRESS2, MAC_ADDRESS, @@ -32,14 +34,14 @@ def mock_discovery(): discover_single=DEFAULT, ) as mock_discovery: device = _mocked_device( - device_config=copy.deepcopy(DEVICE_CONFIG_AUTH), - credentials_hash=CREDENTIALS_HASH_AUTH, + device_config=copy.deepcopy(DEVICE_CONFIG_KLAP), + credentials_hash=CREDENTIALS_HASH_KLAP, alias=None, ) devices = { "127.0.0.1": _mocked_device( - device_config=copy.deepcopy(DEVICE_CONFIG_AUTH), - credentials_hash=CREDENTIALS_HASH_AUTH, + device_config=copy.deepcopy(DEVICE_CONFIG_KLAP), + credentials_hash=CREDENTIALS_HASH_KLAP, alias=None, ) } @@ -55,12 +57,15 @@ def mock_connect(): with patch("homeassistant.components.tplink.Device.connect") as mock_connect: devices = { IP_ADDRESS: _mocked_device( - device_config=DEVICE_CONFIG_AUTH, credentials_hash=CREDENTIALS_HASH_AUTH + device_config=DEVICE_CONFIG_KLAP, + credentials_hash=CREDENTIALS_HASH_KLAP, + ip_address=IP_ADDRESS, ), IP_ADDRESS2: _mocked_device( - device_config=DEVICE_CONFIG_AUTH, - credentials_hash=CREDENTIALS_HASH_AUTH, + device_config=DEVICE_CONFIG_AES, + credentials_hash=CREDENTIALS_HASH_AES, mac=MAC_ADDRESS2, + ip_address=IP_ADDRESS2, ), } diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index e9ae7957520..ddd67f249e6 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -1,5 +1,6 @@ """Test the tplink config flow.""" +import logging from unittest.mock import AsyncMock, patch from kasa import TimeoutError @@ -11,6 +12,7 @@ from homeassistant.components.tplink import ( DOMAIN, AuthenticationError, Credentials, + Device, DeviceConfig, KasaException, ) @@ -33,19 +35,21 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( ALIAS, - CREATE_ENTRY_DATA_AUTH, - CREATE_ENTRY_DATA_AUTH2, + CONNECTION_TYPE_KLAP_DICT, + CREATE_ENTRY_DATA_AES, + CREATE_ENTRY_DATA_KLAP, CREATE_ENTRY_DATA_LEGACY, - CREDENTIALS_HASH_AUTH, + CREDENTIALS_HASH_AES, + CREDENTIALS_HASH_KLAP, DEFAULT_ENTRY_TITLE, - DEVICE_CONFIG_DICT_AUTH, + DEVICE_CONFIG_DICT_AES, + DEVICE_CONFIG_DICT_KLAP, DEVICE_CONFIG_DICT_LEGACY, DHCP_FORMATTED_MAC_ADDRESS, IP_ADDRESS, MAC_ADDRESS, MAC_ADDRESS2, MODULE, - NEW_CONNECTION_TYPE_DICT, _mocked_device, _patch_connect, _patch_discovery, @@ -135,7 +139,7 @@ async def test_discovery_auth( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() @@ -154,7 +158,7 @@ async def test_discovery_auth( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_ENTRY_TITLE - assert result2["data"] == CREATE_ENTRY_DATA_AUTH + assert result2["data"] == CREATE_ENTRY_DATA_KLAP assert result2["context"]["unique_id"] == MAC_ADDRESS @@ -187,7 +191,7 @@ async def test_discovery_auth_errors( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() @@ -218,7 +222,7 @@ async def test_discovery_auth_errors( }, ) assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["data"] == CREATE_ENTRY_DATA_KLAP assert result3["context"]["unique_id"] == MAC_ADDRESS @@ -238,7 +242,7 @@ async def test_discovery_new_credentials( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() @@ -267,7 +271,7 @@ async def test_discovery_new_credentials( {}, ) assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["data"] == CREATE_ENTRY_DATA_KLAP assert result3["context"]["unique_id"] == MAC_ADDRESS @@ -290,7 +294,7 @@ async def test_discovery_new_credentials_invalid( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() @@ -323,7 +327,7 @@ async def test_discovery_new_credentials_invalid( }, ) assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["data"] == CREATE_ENTRY_DATA_KLAP assert result3["context"]["unique_id"] == MAC_ADDRESS @@ -543,7 +547,7 @@ async def test_manual_auth( await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE - assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["data"] == CREATE_ENTRY_DATA_KLAP assert result3["context"]["unique_id"] == MAC_ADDRESS @@ -607,7 +611,7 @@ async def test_manual_auth_errors( }, ) assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["data"] == CREATE_ENTRY_DATA_AUTH + assert result4["data"] == CREATE_ENTRY_DATA_KLAP assert result4["context"]["unique_id"] == MAC_ADDRESS await hass.async_block_till_done() @@ -791,16 +795,16 @@ async def test_integration_discovery_with_ip_change( CONF_HOST: "127.0.0.2", CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" - config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_AUTH) + config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_KLAP) mock_connect["connect"].reset_mock(side_effect=True) bulb = _mocked_device( @@ -832,8 +836,8 @@ async def test_integration_discovery_with_connection_change( mock_config_entry = MockConfigEntry( title="TPLink", domain=DOMAIN, - data=CREATE_ENTRY_DATA_AUTH, - unique_id=MAC_ADDRESS, + data=CREATE_ENTRY_DATA_AES, + unique_id=MAC_ADDRESS2, ) mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): @@ -849,13 +853,15 @@ async def test_integration_discovery_with_connection_change( ) == 0 ) - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH - assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" - assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AUTH + assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES + assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.2" + assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AES NEW_DEVICE_CONFIG = { - **DEVICE_CONFIG_DICT_AUTH, - CONF_CONNECTION_TYPE: NEW_CONNECTION_TYPE_DICT, + **DEVICE_CONFIG_DICT_KLAP, + CONF_CONNECTION_TYPE: CONNECTION_TYPE_KLAP_DICT, + CONF_HOST: "127.0.0.2", } config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG) # Reset the connect mock so when the config flow reloads the entry it succeeds @@ -870,8 +876,8 @@ async def test_integration_discovery_with_connection_change( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={ - CONF_HOST: "127.0.0.1", - CONF_MAC: MAC_ADDRESS, + CONF_HOST: "127.0.0.2", + CONF_MAC: MAC_ADDRESS2, CONF_ALIAS: ALIAS, CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG, }, @@ -880,8 +886,8 @@ async def test_integration_discovery_with_connection_change( assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG - assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" - assert CREDENTIALS_HASH_AUTH not in mock_config_entry.data + assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert CREDENTIALS_HASH_AES not in mock_config_entry.data assert mock_config_entry.state is ConfigEntryState.LOADED @@ -953,6 +959,77 @@ async def test_reauth( await hass.async_block_till_done() +async def test_reauth_update_with_encryption_change( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reauth flow.""" + orig_side_effect = mock_connect["connect"].side_effect + mock_connect["connect"].side_effect = AuthenticationError() + mock_config_entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data={**CREATE_ENTRY_DATA_AES}, + unique_id=MAC_ADDRESS2, + ) + mock_config_entry.add_to_hass(hass) + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES + assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AES + + with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + caplog.set_level(logging.DEBUG) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + [result] = flows + assert result["step_id"] == "reauth_confirm" + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES + assert CONF_CREDENTIALS_HASH not in mock_config_entry.data + + new_config = DeviceConfig( + "127.0.0.2", + credentials=None, + connection_type=Device.ConnectionParameters( + Device.Family.SmartTapoPlug, Device.EncryptionType.Klap + ), + uses_http=True, + ) + mock_discovery["mock_device"].host = "127.0.0.2" + mock_discovery["mock_device"].config = new_config + mock_discovery["mock_device"].credentials_hash = None + mock_connect["mock_devices"]["127.0.0.2"].config = new_config + mock_connect["mock_devices"]["127.0.0.2"].credentials_hash = CREDENTIALS_HASH_KLAP + + mock_connect["connect"].side_effect = orig_side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert "Connection type changed for 127.0.0.2" in caplog.text + credentials = Credentials("fake_username", "fake_password") + mock_discovery["discover_single"].assert_called_once_with( + "127.0.0.2", credentials=credentials + ) + mock_discovery["mock_device"].update.assert_called_once_with() + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == { + **DEVICE_CONFIG_DICT_KLAP, + CONF_HOST: "127.0.0.2", + } + assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_KLAP + + async def test_reauth_update_from_discovery( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -981,13 +1058,13 @@ async def test_reauth_update_from_discovery( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP async def test_reauth_update_from_discovery_with_ip_change( @@ -1017,13 +1094,13 @@ async def test_reauth_update_from_discovery_with_ip_change( CONF_HOST: "127.0.0.2", CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" @@ -1040,7 +1117,7 @@ async def test_reauth_no_update_if_config_and_ip_the_same( mock_config_entry, data={ **mock_config_entry.data, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -1051,7 +1128,7 @@ async def test_reauth_no_update_if_config_and_ip_the_same( assert len(flows) == 1 [result] = flows assert result["step_id"] == "reauth_confirm" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP discovery_result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1060,13 +1137,13 @@ async def test_reauth_no_update_if_config_and_ip_the_same( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, + CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS @@ -1214,15 +1291,20 @@ async def test_discovery_timeout_connect( async def test_reauth_update_other_flows( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, mock_discovery: AsyncMock, mock_connect: AsyncMock, ) -> None: """Test reauth updates other reauth flows.""" + mock_config_entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data={**CREATE_ENTRY_DATA_KLAP}, + unique_id=MAC_ADDRESS, + ) mock_config_entry2 = MockConfigEntry( title="TPLink", domain=DOMAIN, - data={**CREATE_ENTRY_DATA_AUTH2}, + data={**CREATE_ENTRY_DATA_AES}, unique_id=MAC_ADDRESS2, ) default_side_effect = mock_connect["connect"].side_effect @@ -1244,7 +1326,7 @@ async def test_reauth_update_other_flows( flows_by_entry_id = {flow["context"]["entry_id"]: flow for flow in flows} result = flows_by_entry_id[mock_config_entry.entry_id] assert result["step_id"] == "reauth_confirm" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index c5c5e2ce6db..5b3cf648b6e 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -33,9 +33,9 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import ( - CREATE_ENTRY_DATA_AUTH, + CREATE_ENTRY_DATA_KLAP, CREATE_ENTRY_DATA_LEGACY, - DEVICE_CONFIG_AUTH, + DEVICE_CONFIG_KLAP, DEVICE_ID, DEVICE_ID_MAC, IP_ADDRESS, @@ -178,7 +178,7 @@ async def test_config_entry_device_config( mock_config_entry = MockConfigEntry( title="TPLink", domain=DOMAIN, - data={**CREATE_ENTRY_DATA_AUTH}, + data={**CREATE_ENTRY_DATA_KLAP}, unique_id=MAC_ADDRESS, ) mock_config_entry.add_to_hass(hass) @@ -197,7 +197,7 @@ async def test_config_entry_with_stored_credentials( mock_config_entry = MockConfigEntry( title="TPLink", domain=DOMAIN, - data={**CREATE_ENTRY_DATA_AUTH}, + data={**CREATE_ENTRY_DATA_KLAP}, unique_id=MAC_ADDRESS, ) auth = { @@ -210,7 +210,7 @@ async def test_config_entry_with_stored_credentials( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED - config = DEVICE_CONFIG_AUTH + config = DEVICE_CONFIG_KLAP assert config.credentials != stored_credentials config.credentials = stored_credentials mock_connect["connect"].assert_called_once_with(config=config) @@ -223,7 +223,7 @@ async def test_config_entry_device_config_invalid( caplog: pytest.LogCaptureFixture, ) -> None: """Test that an invalid device config logs an error and loads the config entry.""" - entry_data = copy.deepcopy(CREATE_ENTRY_DATA_AUTH) + entry_data = copy.deepcopy(CREATE_ENTRY_DATA_KLAP) entry_data[CONF_DEVICE_CONFIG] = {"foo": "bar"} mock_config_entry = MockConfigEntry( title="TPLink", @@ -263,7 +263,7 @@ async def test_config_entry_errors( mock_config_entry = MockConfigEntry( title="TPLink", domain=DOMAIN, - data={**CREATE_ENTRY_DATA_AUTH}, + data={**CREATE_ENTRY_DATA_KLAP}, unique_id=MAC_ADDRESS, ) mock_config_entry.add_to_hass(hass) @@ -520,11 +520,11 @@ async def test_move_credentials_hash( from the device. """ device_config = { - **DEVICE_CONFIG_AUTH.to_dict( + **DEVICE_CONFIG_KLAP.to_dict( exclude_credentials=True, credentials_hash="theHash" ) } - entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + entry_data = {**CREATE_ENTRY_DATA_KLAP, CONF_DEVICE_CONFIG: device_config} entry = MockConfigEntry( title="TPLink", @@ -567,11 +567,11 @@ async def test_move_credentials_hash_auth_error( in async_setup_entry. """ device_config = { - **DEVICE_CONFIG_AUTH.to_dict( + **DEVICE_CONFIG_KLAP.to_dict( exclude_credentials=True, credentials_hash="theHash" ) } - entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + entry_data = {**CREATE_ENTRY_DATA_KLAP, CONF_DEVICE_CONFIG: device_config} entry = MockConfigEntry( title="TPLink", @@ -610,11 +610,11 @@ async def test_move_credentials_hash_other_error( at the end of the test. """ device_config = { - **DEVICE_CONFIG_AUTH.to_dict( + **DEVICE_CONFIG_KLAP.to_dict( exclude_credentials=True, credentials_hash="theHash" ) } - entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + entry_data = {**CREATE_ENTRY_DATA_KLAP, CONF_DEVICE_CONFIG: device_config} entry = MockConfigEntry( title="TPLink", @@ -647,9 +647,9 @@ async def test_credentials_hash( hass: HomeAssistant, ) -> None: """Test credentials_hash used to call connect.""" - device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + device_config = {**DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True)} entry_data = { - **CREATE_ENTRY_DATA_AUTH, + **CREATE_ENTRY_DATA_KLAP, CONF_DEVICE_CONFIG: device_config, CONF_CREDENTIALS_HASH: "theHash", } @@ -684,9 +684,9 @@ async def test_credentials_hash_auth_error( hass: HomeAssistant, ) -> None: """Test credentials_hash is deleted after an auth failure.""" - device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + device_config = {**DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True)} entry_data = { - **CREATE_ENTRY_DATA_AUTH, + **CREATE_ENTRY_DATA_KLAP, CONF_DEVICE_CONFIG: device_config, CONF_CREDENTIALS_HASH: "theHash", } @@ -710,7 +710,7 @@ async def test_credentials_hash_auth_error( await hass.async_block_till_done() expected_config = DeviceConfig.from_dict( - DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True, credentials_hash="theHash") + DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True, credentials_hash="theHash") ) connect_mock.assert_called_with(config=expected_config) assert entry.state is ConfigEntryState.SETUP_ERROR From a3a99cc631532d1af884dea2ba4b1517d4e78bd9 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 18 Jul 2024 23:27:03 +0300 Subject: [PATCH 096/116] Prevent connecting to a Shelly device that is already connected (#122105) --- .../components/shelly/coordinator.py | 3 +++ tests/components/shelly/conftest.py | 1 + tests/components/shelly/test_binary_sensor.py | 1 + tests/components/shelly/test_config_flow.py | 1 + tests/components/shelly/test_coordinator.py | 20 +++++++++++++++++++ tests/components/shelly/test_init.py | 2 ++ tests/components/shelly/test_sensor.py | 2 ++ tests/components/shelly/test_update.py | 1 + 8 files changed, 31 insertions(+) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index c1be4577bf6..469223a5857 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -668,6 +668,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Handle device update.""" LOGGER.debug("Shelly %s handle update, type: %s", self.name, update_type) if update_type is RpcUpdateType.ONLINE: + if self.device.connected: + LOGGER.debug("Device %s already connected", self.name) + return self.entry.async_create_background_task( self.hass, self._async_device_connect_task(), diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 14c06d3f86a..7caaae8621e 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -359,6 +359,7 @@ def _mock_rpc_device(version: str | None = None): status=MOCK_STATUS_RPC, firmware_version="some fw string", initialized=True, + connected=True, ) type(device).name = PropertyMock(return_value="Test name") return device diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 3bfbf350f7e..dc68b657796 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -263,6 +263,7 @@ async def test_rpc_sleeping_binary_sensor( ) -> None: """Test RPC online sleeping binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_cloud" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) config_entry = await init_integration(hass, 2, sleep_period=1000) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index a26c6eac405..a3040fc2eb8 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1114,6 +1114,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( caplog: pytest.LogCaptureFixture, ) -> None: """Test zeroconf discovery does not triggers refresh for sleeping device.""" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = MockConfigEntry( domain="shelly", diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 35123a2db91..d3494c094f9 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -545,6 +545,7 @@ async def test_rpc_update_entry_sleep_period( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC update entry sleep period.""" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 600) entry = await init_integration(hass, 2, sleep_period=600) register_entity( @@ -578,6 +579,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( ) -> None: """Test RPC sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) register_entity( @@ -609,6 +611,7 @@ async def test_rpc_sleeping_device_firmware_unsupported( issue_registry: ir.IssueRegistry, ) -> None: """Test RPC sleeping device firmware not supported.""" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setattr(mock_rpc_device, "firmware_supported", False) entry = await init_integration(hass, 2, sleep_period=3600) @@ -912,6 +915,7 @@ async def test_rpc_sleeping_device_connection_error( hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry ) mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setattr(mock_rpc_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -939,3 +943,19 @@ async def test_rpc_sleeping_device_connection_error( assert "Sleeping device did not update" in caplog.text assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE + + +async def test_rpc_already_connected( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC ignore connect event if already connected.""" + await init_integration(hass, 2) + + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "already connected" in caplog.text + mock_rpc_device.initialize.assert_called_once() diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 998d56fc6cc..46698c23c0a 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -279,6 +279,7 @@ async def test_sleeping_rpc_device_online( caplog: pytest.LogCaptureFixture, ) -> None: """Test sleeping RPC device online.""" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", device_sleep) entry = await init_integration(hass, 2, sleep_period=entry_sleep) assert "will resume when device is online" in caplog.text @@ -297,6 +298,7 @@ async def test_sleeping_rpc_device_online_new_firmware( caplog: pytest.LogCaptureFixture, ) -> None: """Test sleeping device Gen2 with firmware 1.0.0 or later.""" + monkeypatch.setattr(mock_rpc_device, "connected", False) entry = await init_integration(hass, 2, sleep_period=None) assert "will resume when device is online" in caplog.text diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index c62a1f6f6ca..9f510ba8fe9 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -449,6 +449,7 @@ async def test_rpc_sleeping_sensor( ) -> None: """Test RPC online sleeping sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) @@ -600,6 +601,7 @@ async def test_rpc_sleeping_update_entity_service( await async_setup_component(hass, "homeassistant", {}) entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) await init_integration(hass, 2, sleep_period=1000) diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 8448c116815..721e86559a3 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -334,6 +334,7 @@ async def test_rpc_sleeping_update( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC sleeping device update entity.""" + monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( From c518c4756b6a383bf521773a2b1f09a78279dddc Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:20:10 +0100 Subject: [PATCH 097/116] Bump tplink dependency python-kasa to 0.7.0.5 (#122119) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index a345f64e4b2..f935d019541 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -301,5 +301,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.4"] + "requirements": ["python-kasa[speedups]==0.7.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index a84f565dd64..49db85e79c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.4 +python-kasa[speedups]==0.7.0.5 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c35ec3f64f8..6af377ff73f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.4 +python-kasa[speedups]==0.7.0.5 # homeassistant.components.matter python-matter-server==6.2.2 From d0d2fd7918917b47bf348734351ecb5377e3a26d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:07:31 +0200 Subject: [PATCH 098/116] Update yt-dlp to 2024.07.16 (#122124) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index cfe44f5176b..cd312413db3 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.07.01"], + "requirements": ["yt-dlp==2024.07.16"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 49db85e79c8..1512acef5e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2951,7 +2951,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.01 +yt-dlp==2024.07.16 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6af377ff73f..a7cbf46c54b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2307,7 +2307,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.01 +yt-dlp==2024.07.16 # homeassistant.components.zamg zamg==0.3.6 From 1ef4332af6d2cc59cb233100d0e5e6425f5d0dad Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 19 Jul 2024 09:11:29 +0200 Subject: [PATCH 099/116] Fix KeyError in config flow of Bring integration (#122136) --- homeassistant/components/bring/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index 333837a20f2..997342033e4 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -58,7 +58,7 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN): ): self._abort_if_unique_id_configured() return self.async_create_entry( - title=self.info["name"] or user_input[CONF_EMAIL], data=user_input + title=self.info.get("name") or user_input[CONF_EMAIL], data=user_input ) return self.async_show_form( From a08ffdc8d3103daafd90a8551daa03d94ebf17a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Jul 2024 18:50:21 +0200 Subject: [PATCH 100/116] Bump version to 2024.7.3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8587f9e6137..f706b2d1243 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 82c29948e3c..f044551ce1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.2" +version = "2024.7.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 74d10b9824444350a4007085200ea6810a678ee2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 22 Jul 2024 01:11:05 +0200 Subject: [PATCH 101/116] Bump `aiotractive` to 0.6.0 (#121155) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> Co-authored-by: J. Nick Koston --- homeassistant/components/tractive/__init__.py | 2 +- homeassistant/components/tractive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index fd5abe24c06..4f0de7b14cd 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -132,7 +132,7 @@ async def _generate_trackables( trackable = await trackable.details() # Check that the pet has tracker linked. - if not trackable["device_id"]: + if not trackable.get("device_id"): return None if "details" not in trackable: diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 75ddf065bd7..903c5347d52 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["aiotractive"], - "requirements": ["aiotractive==0.5.6"] + "requirements": ["aiotractive==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1512acef5e8..6c87b9bd19a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.1 # homeassistant.components.tractive -aiotractive==0.5.6 +aiotractive==0.6.0 # homeassistant.components.unifi aiounifi==79 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7cbf46c54b..a0b1497a185 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -359,7 +359,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.1 # homeassistant.components.tractive -aiotractive==0.5.6 +aiotractive==0.6.0 # homeassistant.components.unifi aiounifi==79 From cf20e67f1f541a8d28703b4dceb218403c2ba1e4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 21 Jul 2024 12:36:06 +0200 Subject: [PATCH 102/116] Ensure mqtt subscriptions are in a set (#122201) --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/client.py | 8 ++++---- homeassistant/components/mqtt/models.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f057dab8bc4..2b3aa21aa22 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -251,7 +251,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mqtt_data.client.async_restore_tracked_subscriptions( mqtt_data.subscriptions_to_restore ) - mqtt_data.subscriptions_to_restore = [] + mqtt_data.subscriptions_to_restore = set() mqtt_data.reload_dispatchers.append( entry.add_update_listener(_async_config_entry_updated) ) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index f65769badfa..d242ec019c6 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -428,12 +428,12 @@ class MQTT: await self.async_init_client() @property - def subscriptions(self) -> list[Subscription]: + def subscriptions(self) -> set[Subscription]: """Return the tracked subscriptions.""" - return [ + return { *chain.from_iterable(self._simple_subscriptions.values()), *self._wildcard_subscriptions, - ] + } def cleanup(self) -> None: """Clean up listeners.""" @@ -736,7 +736,7 @@ class MQTT: @callback def async_restore_tracked_subscriptions( - self, subscriptions: list[Subscription] + self, subscriptions: set[Subscription] ) -> None: """Restore tracked subscriptions after reload.""" for subscription in subscriptions: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index e5a9a9c44da..c355510a5c2 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -423,7 +423,7 @@ class MqttData: reload_handlers: dict[str, CALLBACK_TYPE] = field(default_factory=dict) reload_schema: dict[str, VolSchemaType] = field(default_factory=dict) state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) - subscriptions_to_restore: list[Subscription] = field(default_factory=list) + subscriptions_to_restore: set[Subscription] = field(default_factory=set) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) From 683069cb9850e724594306ddd73fee92c713bbbf Mon Sep 17 00:00:00 2001 From: Alexander Schneider Date: Sun, 21 Jul 2024 14:51:10 -0700 Subject: [PATCH 103/116] Add Z-Wave discovery schema for ZVIDAR roller shades (#122332) Add discovery schema for ZVIDAR roller shades --- .../components/zwave_js/discovery.py | 9 + tests/components/zwave_js/conftest.py | 14 + .../zwave_js/fixtures/cover_zvidar_state.json | 1120 +++++++++++++++++ tests/components/zwave_js/test_discovery.py | 12 + 4 files changed, 1155 insertions(+) create mode 100644 tests/components/zwave_js/fixtures/cover_zvidar_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 0b66567c036..6798e644a02 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -579,6 +579,15 @@ DISCOVERY_SCHEMAS = [ ), entity_registry_enabled_default=False, ), + # ZVIDAR Z-CM-V01 (SmartWings/Deyi WM25L/V Z-Wave Motor for Roller Shade) + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="shade", + manufacturer_id={0x045A}, + product_id={0x0507}, + product_type={0x0904}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + ), # Vision Security ZL7432 In Wall Dual Relay Switch ZWaveDiscoverySchema( platform=Platform.SWITCH, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a2a4c217b8b..31c9c5affa5 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -472,6 +472,12 @@ def iblinds_v3_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v3_state.json")) +@pytest.fixture(name="zvidar_state", scope="package") +def zvidar_state_fixture(): + """Load the ZVIDAR node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_zvidar_state.json")) + + @pytest.fixture(name="qubino_shutter_state", scope="package") def qubino_shutter_state_fixture(): """Load the Qubino Shutter node state fixture data.""" @@ -1081,6 +1087,14 @@ def iblinds_v3_cover_fixture(client, iblinds_v3_state): return node +@pytest.fixture(name="zvidar") +def zvidar_cover_fixture(client, zvidar_state): + """Mock a ZVIDAR window cover node.""" + node = Node(client, copy.deepcopy(zvidar_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="qubino_shutter") def qubino_shutter_cover_fixture(client, qubino_shutter_state): """Mock a Qubino flush shutter node.""" diff --git a/tests/components/zwave_js/fixtures/cover_zvidar_state.json b/tests/components/zwave_js/fixtures/cover_zvidar_state.json new file mode 100644 index 00000000000..05118931026 --- /dev/null +++ b/tests/components/zwave_js/fixtures/cover_zvidar_state.json @@ -0,0 +1,1120 @@ +{ + "nodeId": 270, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": false, + "isSecure": true, + "manufacturerId": 1114, + "productId": 1287, + "productType": 2308, + "firmwareVersion": "1.10.0", + "zwavePlusVersion": 2, + "name": "Window Blind Controller", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/snapshot/build/node_modules/@zwave-js/config/config/devices/0x045a/Z-CM-V01.json", + "isEmbedded": true, + "manufacturer": "ZVIDAR", + "manufacturerId": 1114, + "label": "Z-CM-V01", + "description": "Smart Curtain Motor", + "devices": [ + { + "productType": 2308, + "productId": 1287 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "paramInformation": { + "_map": {} + }, + "compat": { + "removeCCs": {} + } + }, + "label": "Z-CM-V01", + "interviewAttempts": 0, + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [100000], + "protocolVersion": 3, + "supportsBeaming": false, + "supportsSecurity": true, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 3, + "label": "End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x045a:0x0904:0x0507:1.10.0", + "statistics": { + "commandsTX": 2, + "commandsRX": 1, + "commandsDroppedRX": 1, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "rtt": 357.6, + "lastSeen": "2024-07-21T16:42:38.086Z", + "rssi": -89, + "lwr": { + "protocolDataRate": 4, + "repeaters": [], + "rssi": -91, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2024-07-21T16:42:38.086Z", + "protocol": 1, + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": "unknown" + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Hand Button Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Hand Button Action", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Close", + "1": "Open" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Motor Direction", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Motor Direction", + "default": 1, + "min": 1, + "max": 3, + "states": { + "1": "Forward", + "2": "Opposite", + "3": "Reverse" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Manually Set Open Boundary", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Manually Set Open Boundary", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Cancel", + "1": "Start" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Manually Set Closed Boundary", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Manually Set Closed Boundary", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Cancel", + "1": "Start" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Control Motor", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Control Motor", + "default": 3, + "min": 1, + "max": 3, + "states": { + "1": "Open (Up)", + "2": "Close (Down)", + "3": "Stop" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Calibrate Limit Position", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Calibrate Limit Position", + "default": 1, + "min": 1, + "max": 3, + "states": { + "1": "Upper limit", + "2": "Lower limit", + "3": "Third limit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Delete Limit Position", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Delete Limit Position", + "default": 0, + "min": 0, + "max": 3, + "states": { + "0": "All limits", + "1": "Only upper limit", + "2": "Only lower limit", + "3": "Only third limit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Low Battery Level Alarm Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Battery Level Alarm Threshold", + "default": 10, + "min": 0, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Battery Report Interval", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Report Interval", + "default": 3600, + "min": 0, + "max": 2678400, + "unit": "seconds", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Battery Change Report Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Change Report Threshold", + "default": 5, + "min": 0, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Mains status", + "propertyName": "Power Management", + "propertyKeyName": "Mains status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Mains status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "2": "AC mains disconnected", + "3": "AC mains re-connected" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1114 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 2308 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1287 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 86 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.10"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "10.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "1.10.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 43707 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "identify", + "propertyName": "identify", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Identify", + "states": { + "true": "Identify" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Timeout", + "stateful": true, + "secret": false + } + } + ], + "endpoints": [ + { + "nodeId": 261, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 3, + "label": "End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": true + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 1179d8e843c..57841ef2a83 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -49,6 +49,18 @@ async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration) assert state +async def test_zvidar_state(hass: HomeAssistant, client, zvidar, integration) -> None: + """Test that an ZVIDAR Z-CM-V01 multilevel switch value is discovered as a cover.""" + node = zvidar + assert node.device_class.specific.label == "Unused" + + state = hass.states.get("light.window_blind_controller") + assert not state + + state = hass.states.get("cover.window_blind_controller") + assert state + + async def test_ge_12730(hass: HomeAssistant, client, ge_12730, integration) -> None: """Test GE 12730 Fan Controller v2.0 multilevel switch is discovered as a fan.""" node = ge_12730 From b63bc72450631c7d04ea6dd5430265bbb50c7e2a Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:56:46 +0200 Subject: [PATCH 104/116] Fix device class on sensor in ViCare (#122334) update device class on init --- homeassistant/components/vicare/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 0e98729e40f..0271ffc9798 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -950,6 +950,8 @@ class ViCareSensor(ViCareEntity, SensorEntity): """Initialize the sensor.""" super().__init__(device_config, api, description.key) self.entity_description = description + # run update to have device_class set depending on unit_of_measurement + self.update() @property def available(self) -> bool: From f739644735525d693e1d6a66a0a39a6256b6ef5b Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 22 Jul 2024 07:54:31 +0300 Subject: [PATCH 105/116] Goofle Generative AI: Fix string format (#122348) * Ignore format for string tool args * Add tests --- .../google_generative_ai_conversation/conversation.py | 2 ++ .../snapshots/test_conversation.ambr | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 8052ee66f40..c80581a1f57 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -81,6 +81,8 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": + if schema.get("type") == "string" and val != "enum": + continue key = "format_" elif key == "items": val = _format_schema(val) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 7f28c172970..66caf4c7218 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -449,7 +449,6 @@ description: "Test parameters" items { type_: STRING - format_: "lower" } } } From 9d6bd359c497de39b61dc8578fe180796698da5a Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 22 Jul 2024 12:11:09 +0300 Subject: [PATCH 106/116] Ensure script llm tool name does not start with a digit (#122349) * Ensure script tool name does not start with a digit * Fix test name --- homeassistant/helpers/llm.py | 5 ++++- tests/helpers/test_llm.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 506cadbf168..f386fb3ddec 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -615,6 +615,9 @@ class ScriptTool(Tool): entity_registry = er.async_get(hass) self.name = split_entity_id(script_entity_id)[1] + if self.name[0].isdigit(): + self.name = "_" + self.name + self._entity_id = script_entity_id self.parameters = vol.Schema({}) entity_entry = entity_registry.async_get(script_entity_id) if entity_entry and entity_entry.unique_id: @@ -715,7 +718,7 @@ class ScriptTool(Tool): SCRIPT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: SCRIPT_DOMAIN + "." + self.name, + ATTR_ENTITY_ID: self._entity_id, ATTR_VARIABLES: tool_input.tool_args, }, context=llm_context.context, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 81fa573852e..e1f55942d10 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -780,6 +780,46 @@ async def test_script_tool( } +async def test_script_tool_name(hass: HomeAssistant) -> None: + """Test that script tool name is not started with a digit.""" + assert await async_setup_component(hass, "homeassistant", {}) + context = Context() + llm_context = llm.LLMContext( + platform="test_platform", + context=context, + user_prompt="test_text", + language="*", + assistant="conversation", + device_id=None, + ) + + # Create a script with a unique ID + assert await async_setup_component( + hass, + "script", + { + "script": { + "123456": { + "description": "This is a test script", + "sequence": [], + "fields": { + "beer": {"description": "Number of beers", "required": True}, + }, + }, + } + }, + ) + async_expose_entity(hass, "conversation", "script.123456", True) + + api = await llm.async_get_api(hass, "assist", llm_context) + + tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)] + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "_123456" + + async def test_selector_serializer( hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: From 7135a919e3338741f4a8a816bec9f1eee02de4d4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 22 Jul 2024 11:09:03 +0200 Subject: [PATCH 107/116] Bump reolink-aio to 0.9.5 (#122366) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index ee3ebe8a13a..c329289790b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.4"] + "requirements": ["reolink-aio==0.9.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6c87b9bd19a..e8d846ef099 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.4 +reolink-aio==0.9.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0b1497a185..787b2af38fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.4 +reolink-aio==0.9.5 # homeassistant.components.rflink rflink==0.0.66 From 56f51d3e3511752c3ff8f746c64d57a5bd325606 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 23 Jul 2024 03:56:13 +0300 Subject: [PATCH 108/116] Fix gemini api format conversion (#122403) * Fix gemini api format conversion * add tests * fix tests * fix tests * fix coverage --- .../conversation.py | 18 +++++++++++++++++- .../snapshots/test_conversation.ambr | 18 ++++++++++++++++++ .../test_conversation.py | 4 +++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index c80581a1f57..69a68121c7b 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -73,6 +73,14 @@ SUPPORTED_SCHEMA_KEYS = { def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: """Format the schema to protobuf.""" + if (subschemas := schema.get("anyOf")) or (subschemas := schema.get("allOf")): + for subschema in subschemas: # Gemini API does not support anyOf and allOf keys + if "type" in subschema: # Fallback to first subschema with 'type' field + return _format_schema(subschema) + return _format_schema( + subschemas[0] + ) # Or, if not found, to any of the subschemas + result = {} for key, val in schema.items(): if key not in SUPPORTED_SCHEMA_KEYS: @@ -81,7 +89,9 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": - if schema.get("type") == "string" and val != "enum": + if (schema.get("type") == "string" and val != "enum") or ( + schema.get("type") not in ("number", "integer", "string") + ): continue key = "format_" elif key == "items": @@ -89,6 +99,12 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: elif key == "properties": val = {k: _format_schema(v) for k, v in val.items()} result[key] = val + + if result.get("type_") == "OBJECT" and not result.get("properties"): + # An object with undefined properties is not supported by Gemini API. + # Fallback to JSON string. This will probably fail for most tools that want it, + # but we don't have a better fallback strategy so far. + result["properties"] = {"json": {"type_": "STRING"}} return result diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 66caf4c7218..abd3658e869 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -442,6 +442,24 @@ description: "Test function" parameters { type_: OBJECT + properties { + key: "param3" + value { + type_: OBJECT + properties { + key: "json" + value { + type_: STRING + } + } + } + } + properties { + key: "param2" + value { + type_: NUMBER + } + } properties { key: "param1" value { diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 30016335f3b..a7ab2c1b337 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -185,7 +185,9 @@ async def test_function_call( { vol.Optional("param1", description="Test parameters"): [ vol.All(str, vol.Lower) - ] + ], + vol.Optional("param2"): vol.Any(float, int), + vol.Optional("param3"): dict, } ) From 75f0384a15d8ed2a61f8186911d8a2915f1e4cff Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Jul 2024 20:12:51 +0200 Subject: [PATCH 109/116] Fix typo in Matter lock platform (#122536) --- homeassistant/components/matter/lock.py | 4 ++-- .../matter/{test_door_lock.py => test_lock.py} | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) rename tests/components/matter/{test_door_lock.py => test_lock.py} (95%) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index ae01faa3bc7..31ae5e496ce 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -168,10 +168,10 @@ class MatterLock(MatterEntity, LockEntity): LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id) - if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched: + if lock_state == clusters.DoorLock.Enums.DlLockState.kUnlatched: self._attr_is_locked = False self._attr_is_open = True - if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked: + elif lock_state == clusters.DoorLock.Enums.DlLockState.kLocked: self._attr_is_locked = True self._attr_is_open = False elif lock_state in ( diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_lock.py similarity index 95% rename from tests/components/matter/test_door_lock.py rename to tests/components/matter/test_lock.py index 461cc1b7f3d..1180e6ee469 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_lock.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.lock import ( STATE_LOCKED, + STATE_OPEN, STATE_UNLOCKED, LockEntityFeature, ) @@ -82,12 +83,12 @@ async def test_lock( assert state assert state.state == STATE_UNLOCKED - set_node_attribute(door_lock, 1, 257, 0, 0) + set_node_attribute(door_lock, 1, 257, 0, 1) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_UNLOCKED + assert state.state == STATE_LOCKED set_node_attribute(door_lock, 1, 257, 0, None) await trigger_subscription_callback(hass, matter_client) @@ -213,9 +214,16 @@ async def test_lock_with_unbolt( assert state assert state.state == STATE_OPENING - set_node_attribute(door_lock_with_unbolt, 1, 257, 3, 0) + set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_LOCKED + assert state.state == STATE_UNLOCKED + + set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 3) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("lock.mock_door_lock_lock") + assert state + assert state.state == STATE_OPEN From 586a0b12ab36043c1fa3d7b6ce6c5e85a4204025 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 24 Jul 2024 17:19:12 +0100 Subject: [PATCH 110/116] Fix target service attribute on Mastodon integration (#122546) * Fix target * Fix --- homeassistant/components/mastodon/notify.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index f15b8c6f0ab..99999275aeb 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations import mimetypes -from typing import Any +from typing import Any, cast from mastodon import Mastodon from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError @@ -71,11 +71,15 @@ class MastodonNotificationService(BaseNotificationService): def send_message(self, message: str = "", **kwargs: Any) -> None: """Toot a message, with media perhaps.""" + + target = None + if (target_list := kwargs.get(ATTR_TARGET)) is not None: + target = cast(list[str], target_list)[0] + data = kwargs.get(ATTR_DATA) media = None mediadata = None - target = None sensitive = False content_warning = None @@ -87,7 +91,6 @@ class MastodonNotificationService(BaseNotificationService): return mediadata = self._upload_media(media) - target = data.get(ATTR_TARGET) sensitive = data.get(ATTR_MEDIA_WARNING) content_warning = data.get(ATTR_CONTENT_WARNING) From 9940d0281bc527e7b85fc7ff9cd1792807c5287a Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 25 Jul 2024 09:44:56 +1000 Subject: [PATCH 111/116] Bump aiolifx to 1.0.6 (#122569) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 3d0bd1d73d1..59b336373c2 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -48,7 +48,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.5", + "aiolifx==1.0.6", "aiolifx-effects==0.3.2", "aiolifx-themes==0.4.15" ] diff --git a/requirements_all.txt b/requirements_all.txt index e8d846ef099..1195f7b50eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.5 +aiolifx==1.0.6 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 787b2af38fc..9bfd0311b25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,7 +255,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.5 +aiolifx==1.0.6 # homeassistant.components.livisi aiolivisi==0.0.19 From aa44c54a1912ec948adffb19fe295e7e5d56ac22 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 25 Jul 2024 21:23:14 +0200 Subject: [PATCH 112/116] Bump deebot-client to 8.2.0 (#122612) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 6ca9b9e3edc..5a21facab71 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1195f7b50eb..f8c62aa75b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.1.1 +deebot-client==8.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bfd0311b25..579f24fb4bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.1.1 +deebot-client==8.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 00c3b0d888e5d5427588a83e7b05036dfba83b02 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 29 Jul 2024 11:59:13 +0200 Subject: [PATCH 113/116] Bump aiohue to version 4.7.2 (#122651) --- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/v2/hue_event.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/fixtures/v2_resources.json | 12 ++++++++++++ tests/components/hue/test_device_trigger_v2.py | 9 +++++++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index e8d214da3c8..71aabd4c204 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,6 @@ "iot_class": "local_push", "loggers": ["aiohue"], "quality_scale": "platinum", - "requirements": ["aiohue==4.7.1"], + "requirements": ["aiohue==4.7.2"], "zeroconf": ["_hue._tcp.local."] } diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index b0e0de234f1..b286a11aade 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -55,7 +55,7 @@ async def async_setup_hue_events(bridge: HueBridge): CONF_ID: slugify(f"{hue_device.metadata.name} Button"), CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.button.last_event.value, + CONF_TYPE: hue_resource.button.button_report.event.value, CONF_SUBTYPE: hue_resource.metadata.control_id, } hass.bus.async_fire(ATTR_HUE_EVENT, data) @@ -79,7 +79,7 @@ async def async_setup_hue_events(bridge: HueBridge): data = { CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.relative_rotary.last_event.action.value, + CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value, CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, diff --git a/requirements_all.txt b/requirements_all.txt index f8c62aa75b0..6c7f7bbc000 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -264,7 +264,7 @@ aioharmony==0.2.10 aiohomekit==3.1.5 # homeassistant.components.hue -aiohue==4.7.1 +aiohue==4.7.2 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 579f24fb4bb..fc830f0fe7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ aioharmony==0.2.10 aiohomekit==3.1.5 # homeassistant.components.hue -aiohue==4.7.1 +aiohue==4.7.2 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 662e1107ca9..980086d0988 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -1487,6 +1487,10 @@ "on": { "on": true }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { @@ -1498,6 +1502,10 @@ "on": { "on": true }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { @@ -1509,6 +1517,10 @@ "on": { "on": false }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index 0a89b3263c7..efdc33375a6 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -28,7 +28,12 @@ async def test_hue_event( # Emit button update event btn_event = { - "button": {"last_event": "initial_press"}, + "button": { + "button_report": { + "event": "initial_press", + "updated": "2021-10-01T12:00:00Z", + } + }, "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "metadata": {"control_id": 1}, "type": "button", @@ -41,7 +46,7 @@ async def test_hue_event( assert len(events) == 1 assert events[0].data["id"] == "wall_switch_with_2_controls_button" assert events[0].data["unique_id"] == btn_event["id"] - assert events[0].data["type"] == btn_event["button"]["last_event"] + assert events[0].data["type"] == btn_event["button"]["button_report"]["event"] assert events[0].data["subtype"] == btn_event["metadata"]["control_id"] From e5fd9819da291fef90726c1a88270586fe05bce2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 26 Jul 2024 16:59:12 +0200 Subject: [PATCH 114/116] Return unknown when data is missing in Trafikverket Weather (#122652) Return unknown when data is missing --- .../trafikverket_weatherstation/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 4bd14448546..8856482d885 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -61,7 +61,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="air_temp", translation_key="air_temperature", - value_fn=lambda data: data.air_temp or 0, + value_fn=lambda data: data.air_temp, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -69,7 +69,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="road_temp", translation_key="road_temperature", - value_fn=lambda data: data.road_temp or 0, + value_fn=lambda data: data.road_temp, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="wind_speed", - value_fn=lambda data: data.windforce or 0, + value_fn=lambda data: data.windforce, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, @@ -99,7 +99,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="wind_speed_max", translation_key="wind_speed_max", - value_fn=lambda data: data.windforcemax or 0, + value_fn=lambda data: data.windforcemax, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, @@ -107,7 +107,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="humidity", - value_fn=lambda data: data.humidity or 0, + value_fn=lambda data: data.humidity, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, @@ -115,7 +115,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="precipitation_amount", - value_fn=lambda data: data.precipitation_amount or 0, + value_fn=lambda data: data.precipitation_amount, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, state_class=SensorStateClass.MEASUREMENT, @@ -130,7 +130,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="dew_point", translation_key="dew_point", - value_fn=lambda data: data.dew_point or 0, + value_fn=lambda data: data.dew_point, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, From d51d584aed3d2adfbde15a0afe133b51a7a45d1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 04:59:31 -0500 Subject: [PATCH 115/116] Retry later on OSError during apple_tv entry setup (#122747) --- homeassistant/components/apple_tv/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 4e5c8791acd..08372aa79ae 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -60,6 +60,7 @@ AUTH_EXCEPTIONS = ( exceptions.NoCredentialsError, ) CONNECTION_TIMEOUT_EXCEPTIONS = ( + OSError, asyncio.CancelledError, TimeoutError, exceptions.ConnectionLostError, From 02c592d6afe64ea1f19f0cc1ee7a86a29537b095 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Jul 2024 14:40:02 +0200 Subject: [PATCH 116/116] Bump version to 2024.7.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f706b2d1243..71b7d79cb01 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index f044551ce1e..55f96c3e0b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.3" +version = "2024.7.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"