From 35b733fa2c851983d701c0deebea206c03c70f3c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 9 Dec 2023 16:12:05 +0100 Subject: [PATCH 001/118] Bump `aioshelly` to version 7.0.0 (#105384) * Remove get_rpc_device_sleep_period() function * Bump aioshelly version to 7.0.0 * Remove firmware compatibility check from BLE scanner * Remove firmware compatibility check from light transition * Update default fw ver * Use LightEntityFeature in tests --- homeassistant/components/shelly/__init__.py | 5 +- .../components/shelly/config_flow.py | 18 +----- homeassistant/components/shelly/const.py | 6 -- .../components/shelly/coordinator.py | 10 ---- homeassistant/components/shelly/light.py | 9 +-- homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/strings.json | 3 - homeassistant/components/shelly/utils.py | 9 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../shelly/bluetooth/test_scanner.py | 13 ----- tests/components/shelly/conftest.py | 30 ++-------- tests/components/shelly/test_config_flow.py | 56 ------------------- tests/components/shelly/test_light.py | 7 ++- 14 files changed, 18 insertions(+), 154 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b29fdcc6d19929..553d32f8e48ac7 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -49,7 +49,6 @@ get_block_device_sleep_period, get_coap_context, get_device_entry_gen, - get_rpc_device_sleep_period, get_rpc_device_wakeup_period, get_ws_context, ) @@ -266,9 +265,7 @@ def _async_device_online(_: Any, update_type: RpcUpdateType) -> None: if sleep_period is None: data = {**entry.data} - data[CONF_SLEEP_PERIOD] = get_rpc_device_sleep_period( - device.config - ) or get_rpc_device_wakeup_period(device.status) + data[CONF_SLEEP_PERIOD] = get_rpc_device_wakeup_period(device.status) hass.config_entries.async_update_entry(entry, data=data) hass.async_create_task(_async_rpc_device_setup()) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 6cde265bc2593a..98233d27b22ff6 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -12,7 +12,6 @@ InvalidAuthError, ) from aioshelly.rpc_device import RpcDevice -from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant.components.zeroconf import ZeroconfServiceInfo @@ -24,7 +23,6 @@ from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig from .const import ( - BLE_MIN_VERSION, CONF_BLE_SCANNER_MODE, CONF_SLEEP_PERIOD, DOMAIN, @@ -32,14 +30,13 @@ MODEL_WALL_DISPLAY, BLEScannerMode, ) -from .coordinator import async_reconnect_soon, get_entry_data +from .coordinator import async_reconnect_soon from .utils import ( get_block_device_sleep_period, get_coap_context, get_info_auth, get_info_gen, get_model_name, - get_rpc_device_sleep_period, get_rpc_device_wakeup_period, get_ws_context, mac_address_from_name, @@ -78,9 +75,7 @@ async def validate_input( ) await rpc_device.shutdown() - sleep_period = get_rpc_device_sleep_period( - rpc_device.config - ) or get_rpc_device_wakeup_period(rpc_device.status) + sleep_period = get_rpc_device_wakeup_period(rpc_device.status) return { "title": rpc_device.name, @@ -383,15 +378,6 @@ async def async_step_init( ) -> FlowResult: """Handle options flow.""" if user_input is not None: - entry_data = get_entry_data(self.hass)[self.config_entry.entry_id] - if user_input[CONF_BLE_SCANNER_MODE] != BLEScannerMode.DISABLED and ( - not entry_data.rpc - or AwesomeVersion(entry_data.rpc.device.version) < BLE_MIN_VERSION - ): - return self.async_abort( - reason="ble_unsupported", - description_placeholders={"ble_min_version": BLE_MIN_VERSION}, - ) return self.async_create_entry(title="", data=user_input) return self.async_show_form( diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index a90aba8db62aeb..ca1c450c9faaab 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -22,7 +22,6 @@ MODEL_VINTAGE_V2, MODEL_WALL_DISPLAY, ) -from awesomeversion import AwesomeVersion DOMAIN: Final = "shelly" @@ -33,9 +32,6 @@ DEFAULT_COAP_PORT: Final = 5683 FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})") -# Firmware 1.11.0 release date, this firmware supports light transition -LIGHT_TRANSITION_MIN_FIRMWARE_DATE: Final = 20210226 - # max light transition time in milliseconds MAX_TRANSITION_TIME: Final = 5000 @@ -187,8 +183,6 @@ SHELLY_GAS_MODELS = [MODEL_GAS] -BLE_MIN_VERSION = AwesomeVersion("0.12.0-beta2") - CONF_BLE_SCANNER_MODE = "ble_scanner_mode" diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index d1f9d6943bff5b..a7659ecc392f9b 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -13,7 +13,6 @@ from aioshelly.const import MODEL_VALVE from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from aioshelly.rpc_device import RpcDevice, RpcUpdateType -from awesomeversion import AwesomeVersion from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP @@ -33,7 +32,6 @@ ATTR_DEVICE, ATTR_GENERATION, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, - BLE_MIN_VERSION, CONF_BLE_SCANNER_MODE, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, @@ -587,14 +585,6 @@ async def _async_connect_ble_scanner(self) -> None: if ble_scanner_mode == BLEScannerMode.DISABLED and self.connected: await async_stop_scanner(self.device) return - if AwesomeVersion(self.device.version) < BLE_MIN_VERSION: - LOGGER.error( - "BLE not supported on device %s with firmware %s; upgrade to %s", - self.name, - self.device.version, - BLE_MIN_VERSION, - ) - return if await async_ensure_ble_enabled(self.device): # BLE enable required a reboot, don't bother connecting # the scanner since it will be disconnected anyway diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 829a60b3a9eb0b..2dfc5b497b1236 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -24,11 +24,9 @@ from .const import ( DUAL_MODE_LIGHT_MODELS, - FIRMWARE_PATTERN, KELVIN_MAX_VALUE, KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, - LIGHT_TRANSITION_MIN_FIRMWARE_DATE, LOGGER, MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, @@ -155,12 +153,7 @@ def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: self._attr_supported_features |= LightEntityFeature.EFFECT if coordinator.model in MODELS_SUPPORTING_LIGHT_TRANSITION: - match = FIRMWARE_PATTERN.search(coordinator.device.settings.get("fw", "")) - if ( - match is not None - and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE - ): - self._attr_supported_features |= LightEntityFeature.TRANSITION + self._attr_supported_features |= LightEntityFeature.TRANSITION @property def is_on(self) -> bool: diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b8185712d318eb..b56ce07bc30764 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==6.1.0"], + "requirements": ["aioshelly==7.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 9230ae605e0edd..330dd246c47507 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -71,9 +71,6 @@ "ble_scanner_mode": "Bluetooth scanner mode" } } - }, - "abort": { - "ble_unsupported": "Bluetooth support requires firmware version {ble_min_version} or newer." } }, "selector": { diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 6b5c59f28dbd6c..b53e3153a09679 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -267,15 +267,6 @@ def get_block_device_sleep_period(settings: dict[str, Any]) -> int: return sleep_period * 60 # minutes to seconds -def get_rpc_device_sleep_period(config: dict[str, Any]) -> int: - """Return the device sleep period in seconds or 0 for non sleeping devices. - - sys.sleep.wakeup_period value is deprecated and not available in Shelly - firmware 1.0.0 or later. - """ - return cast(int, config["sys"].get("sleep", {}).get("wakeup_period", 0)) - - def get_rpc_device_wakeup_period(status: dict[str, Any]) -> int: """Return the device wakeup period in seconds or 0 for non sleeping devices.""" return cast(int, status["sys"].get("wakeup_period", 0)) diff --git a/requirements_all.txt b/requirements_all.txt index 5e3e6a1224a725..1dccc1a5551460 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==6.1.0 +aioshelly==7.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 956d7079981696..57ec5a22df9e1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==6.1.0 +aioshelly==7.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/tests/components/shelly/bluetooth/test_scanner.py b/tests/components/shelly/bluetooth/test_scanner.py index bd44782f928c69..9fe5f77f00c946 100644 --- a/tests/components/shelly/bluetooth/test_scanner.py +++ b/tests/components/shelly/bluetooth/test_scanner.py @@ -108,19 +108,6 @@ async def test_scanner_ignores_wrong_version_and_logs( assert "Unsupported BLE scan result version: 0" in caplog.text -async def test_scanner_minimum_firmware_log_error( - hass: HomeAssistant, mock_rpc_device, monkeypatch, caplog: pytest.LogCaptureFixture -) -> None: - """Test scanner log error if device firmware incompatible.""" - monkeypatch.setattr(mock_rpc_device, "version", "0.11.0") - await init_integration( - hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} - ) - assert mock_rpc_device.initialized is True - - assert "BLE not supported on device" in caplog.text - - async def test_scanner_warns_on_corrupt_event( hass: HomeAssistant, mock_rpc_device, monkeypatch, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 6eb74e26dcb9fa..8a863a852f524d 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -157,14 +157,13 @@ def mock_light_set_state( "sys": { "ui_data": {}, "device": {"name": "Test name"}, - "wakeup_period": 0, }, } MOCK_SHELLY_COAP = { "mac": MOCK_MAC, "auth": False, - "fw": "20201124-092854/v1.9.0@57ac4ad8", + "fw": "20210715-092854/v1.11.0@57ac4ad8", "num_outputs": 2, } @@ -174,8 +173,8 @@ def mock_light_set_state( "mac": MOCK_MAC, "model": MODEL_PLUS_2PM, "gen": 2, - "fw_id": "20220830-130540/0.11.0-gfa1bc37", - "ver": "0.11.0", + "fw_id": "20230803-130540/1.0.0-gfa1bc37", + "ver": "1.0.0", "app": "Plus2PM", "auth_en": False, "auth_domain": None, @@ -290,7 +289,7 @@ def update_reply(): blocks=MOCK_BLOCKS, settings=MOCK_SETTINGS, shelly=MOCK_SHELLY_COAP, - version="0.10.0", + version="1.11.0", status=MOCK_STATUS_COAP, firmware_version="some fw string", initialized=True, @@ -314,7 +313,7 @@ def _mock_rpc_device(version: str | None = None): config=MOCK_CONFIG, event={}, shelly=MOCK_SHELLY_RPC, - version=version or "0.12.0", + version=version or "1.0.0", hostname="test-host", status=MOCK_STATUS_RPC, firmware_version="some fw string", @@ -324,23 +323,6 @@ def _mock_rpc_device(version: str | None = None): return device -@pytest.fixture -async def mock_pre_ble_rpc_device(): - """Mock rpc (Gen2, Websocket) device pre BLE.""" - with patch("aioshelly.rpc_device.RpcDevice.create") as rpc_device_mock: - - def update(): - rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( - {}, RpcUpdateType.STATUS - ) - - device = _mock_rpc_device("0.11.0") - rpc_device_mock.return_value = device - rpc_device_mock.return_value.mock_update = Mock(side_effect=update) - - yield rpc_device_mock.return_value - - @pytest.fixture async def mock_rpc_device(): """Mock rpc (Gen2, Websocket) device with BLE support.""" @@ -363,7 +345,7 @@ def disconnected(): {}, RpcUpdateType.DISCONNECTED ) - device = _mock_rpc_device("0.12.0") + device = _mock_rpc_device() rpc_device_mock.return_value = device rpc_device_mock.return_value.mock_disconnected = Mock(side_effect=disconnected) rpc_device_mock.return_value.mock_update = Mock(side_effect=update) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 9482080a1a3c26..c7ac472ada4341 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -967,62 +967,6 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device) -> None: await hass.config_entries.async_unload(entry.entry_id) -async def test_options_flow_pre_ble_device( - hass: HomeAssistant, mock_pre_ble_rpc_device -) -> None: - """Test setting ble options for gen2 devices with pre ble firmware.""" - entry = await init_integration(hass, 2) - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_BLE_SCANNER_MODE: BLEScannerMode.DISABLED, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.DISABLED - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "ble_unsupported" - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_BLE_SCANNER_MODE: BLEScannerMode.PASSIVE, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "ble_unsupported" - - await hass.config_entries.async_unload(entry.entry_id) - - async def test_zeroconf_already_configured_triggers_refresh_mac_in_name( hass: HomeAssistant, mock_rpc_device, monkeypatch ) -> None: diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index e3aea966230fec..77b65ad3bb589c 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -134,7 +134,10 @@ async def test_block_device_rgb_bulb( ColorMode.COLOR_TEMP, ColorMode.RGB, ] - assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.EFFECT + assert ( + attributes[ATTR_SUPPORTED_FEATURES] + == LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION + ) assert len(attributes[ATTR_EFFECT_LIST]) == 4 assert attributes[ATTR_EFFECT] == "Off" @@ -232,7 +235,7 @@ async def test_block_device_white_bulb( assert state.state == STATE_ON assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] - assert attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION # Turn off mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() From c96a58893495ad493a667d0b4782b7b95118f0fa Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 9 Dec 2023 13:18:59 -0500 Subject: [PATCH 002/118] Fix service missing key in Blink (#105387) * fix update service refactor service yaml * Remove leftover target --- homeassistant/components/blink/services.py | 7 +++- homeassistant/components/blink/services.yaml | 44 +++++++++++++------- homeassistant/components/blink/strings.json | 28 ++++++++++++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/blink/services.py b/homeassistant/components/blink/services.py index 12ac0d3b859d03..dae2f0ad951cb9 100644 --- a/homeassistant/components/blink/services.py +++ b/homeassistant/components/blink/services.py @@ -25,6 +25,11 @@ ) from .coordinator import BlinkUpdateCoordinator +SERVICE_UPDATE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + } +) SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( { vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), @@ -152,7 +157,7 @@ async def blink_refresh(call: ServiceCall): # Register all the above services service_mapping = [ - (blink_refresh, SERVICE_REFRESH, None), + (blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA), ( async_handle_save_video_service, SERVICE_SAVE_VIDEO, diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index f6420e7f00475c..aaecde64353cb2 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -1,18 +1,28 @@ # Describes the format for available Blink services blink_update: + fields: + device_id: + required: true + selector: + device: + integration: blink + trigger_camera: - target: - entity: - integration: blink - domain: camera + fields: + device_id: + required: true + selector: + device: + integration: blink save_video: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink name: required: true example: "Living Room" @@ -25,11 +35,12 @@ save_video: text: save_recent_clips: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink name: required: true example: "Living Room" @@ -42,11 +53,12 @@ save_recent_clips: text: send_pin: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink pin: example: "abc123" selector: diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index f47f72acb9cf02..fc0450dc8ea2aa 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -57,11 +57,23 @@ "services": { "blink_update": { "name": "Update", - "description": "Forces a refresh." + "description": "Forces a refresh.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "The Blink device id." + } + } }, "trigger_camera": { "name": "Trigger camera", - "description": "Requests camera to take new image." + "description": "Requests camera to take new image.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "The Blink device id." + } + } }, "save_video": { "name": "Save video", @@ -74,6 +86,10 @@ "filename": { "name": "File name", "description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } }, @@ -88,6 +104,10 @@ "file_path": { "name": "Output directory", "description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } }, @@ -98,6 +118,10 @@ "pin": { "name": "Pin", "description": "PIN received from blink. Leave empty if you only received a verification email." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } } From a0bf170fb45a5dbb5c0336f8a11c5771802aaa69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Dec 2023 08:41:37 -1000 Subject: [PATCH 003/118] Avoid ffmpeg subprocess for many component tests (#105354) --- tests/components/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 1ebcc864b4b805..adf79a2ef96c50 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -91,3 +91,12 @@ def tts_mutagen_mock_fixture(): from tests.components.tts.common import tts_mutagen_mock_fixture_helper yield from tts_mutagen_mock_fixture_helper() + + +@pytest.fixture(scope="session", autouse=True) +def prevent_ffmpeg_subprocess() -> Generator[None, None, None]: + """Prevent ffmpeg from creating a subprocess.""" + with patch( + "homeassistant.components.ffmpeg.FFVersion.get_version", return_value="6.0" + ): + yield From a090bcb8a55f434b2ab3ce3537f82d03a6925ca4 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 9 Dec 2023 21:35:52 +0100 Subject: [PATCH 004/118] Migrate time_date tests to use freezegun (#105409) --- tests/components/time_date/test_sensor.py | 52 +++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index 96c7edf422b99d..f9ef8a7cfe9570 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -1,29 +1,30 @@ """The tests for time_date sensor platform.""" -from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory import homeassistant.components.time_date.sensor as time_date from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -async def test_intervals(hass: HomeAssistant) -> None: +async def test_intervals(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test timing intervals of sensors.""" device = time_date.TimeDateSensor(hass, "time") now = dt_util.utc_from_timestamp(45.5) - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time == dt_util.utc_from_timestamp(60) device = time_date.TimeDateSensor(hass, "beat") now = dt_util.parse_datetime("2020-11-13 00:00:29+01:00") - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time == dt_util.parse_datetime("2020-11-13 00:01:26.4+01:00") device = time_date.TimeDateSensor(hass, "date_time") now = dt_util.utc_from_timestamp(1495068899) - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time == dt_util.utc_from_timestamp(1495068900) now = dt_util.utcnow() @@ -102,14 +103,16 @@ async def test_states_non_default_timezone(hass: HomeAssistant) -> None: assert device.state == "2017-05-17T20:54:00" -async def test_timezone_intervals(hass: HomeAssistant) -> None: +async def test_timezone_intervals( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test date sensor behavior in a timezone besides UTC.""" hass.config.set_time_zone("America/New_York") device = time_date.TimeDateSensor(hass, "date") now = dt_util.utc_from_timestamp(50000) - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() # start of local day in EST was 18000.0 # so the second day was 18000 + 86400 assert next_time.timestamp() == 104400 @@ -117,43 +120,40 @@ async def test_timezone_intervals(hass: HomeAssistant) -> None: hass.config.set_time_zone("America/Edmonton") now = dt_util.parse_datetime("2017-11-13 19:47:19-07:00") device = time_date.TimeDateSensor(hass, "date") - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2017-11-14 00:00:00-07:00") # Entering DST hass.config.set_time_zone("Europe/Prague") now = dt_util.parse_datetime("2020-03-29 00:00+01:00") - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2020-03-30 00:00+02:00") now = dt_util.parse_datetime("2020-03-29 03:00+02:00") - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2020-03-30 00:00+02:00") # Leaving DST now = dt_util.parse_datetime("2020-10-25 00:00+02:00") - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2020-10-26 00:00:00+01:00") now = dt_util.parse_datetime("2020-10-25 23:59+01:00") - with patch("homeassistant.util.dt.utcnow", return_value=now): - next_time = device.get_next_interval() + freezer.move_to(now) + next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2020-10-26 00:00:00+01:00") -@patch( - "homeassistant.util.dt.utcnow", - return_value=dt_util.parse_datetime("2017-11-14 02:47:19-00:00"), -) async def test_timezone_intervals_empty_parameter( - utcnow_mock, hass: HomeAssistant + hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Test get_interval() without parameters.""" + freezer.move_to(dt_util.parse_datetime("2017-11-14 02:47:19-00:00")) hass.config.set_time_zone("America/Edmonton") device = time_date.TimeDateSensor(hass, "date") next_time = device.get_next_interval() From 885410bcfcf5d96e0a3c0e648bbd9a0d1644873f Mon Sep 17 00:00:00 2001 From: vexofp Date: Sat, 9 Dec 2023 16:30:12 -0500 Subject: [PATCH 005/118] Prevent duplicate default SSLContext instances (#105348) Co-authored-by: J. Nick Koston --- homeassistant/util/ssl.py | 31 +++++++++++++++++++------------ tests/util/test_ssl.py | 9 +++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 2b50371606384b..6bfbec88a336d9 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -61,16 +61,11 @@ class SSLCipherList(StrEnum): @cache -def create_no_verify_ssl_context( - ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT, -) -> ssl.SSLContext: - """Return an SSL context that does not verify the server certificate. +def _create_no_verify_ssl_context(ssl_cipher_list: SSLCipherList) -> ssl.SSLContext: + # This is a copy of aiohttp's create_default_context() function, with the + # ssl verify turned off. + # https://github.com/aio-libs/aiohttp/blob/33953f110e97eecc707e1402daa8d543f38a189b/aiohttp/connector.py#L911 - This is a copy of aiohttp's create_default_context() function, with the - ssl verify turned off. - - https://github.com/aio-libs/aiohttp/blob/33953f110e97eecc707e1402daa8d543f38a189b/aiohttp/connector.py#L911 - """ sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) sslcontext.check_hostname = False sslcontext.verify_mode = ssl.CERT_NONE @@ -84,12 +79,16 @@ def create_no_verify_ssl_context( return sslcontext -@cache -def client_context( +def create_no_verify_ssl_context( ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT, ) -> ssl.SSLContext: - """Return an SSL context for making requests.""" + """Return an SSL context that does not verify the server certificate.""" + + return _create_no_verify_ssl_context(ssl_cipher_list=ssl_cipher_list) + +@cache +def _client_context(ssl_cipher_list: SSLCipherList) -> ssl.SSLContext: # Reuse environment variable definition from requests, since it's already a # requirement. If the environment variable has no value, fall back to using # certs from certifi package. @@ -104,6 +103,14 @@ def client_context( return sslcontext +def client_context( + ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT, +) -> ssl.SSLContext: + """Return an SSL context for making requests.""" + + return _client_context(ssl_cipher_list=ssl_cipher_list) + + # Create this only once and reuse it _DEFAULT_SSL_CONTEXT = client_context() _DEFAULT_NO_VERIFY_SSL_CONTEXT = create_no_verify_ssl_context() diff --git a/tests/util/test_ssl.py b/tests/util/test_ssl.py index 4d43859cc449f2..4a88e061cbce00 100644 --- a/tests/util/test_ssl.py +++ b/tests/util/test_ssl.py @@ -51,3 +51,12 @@ def test_no_verify_ssl_context(mock_sslcontext) -> None: mock_sslcontext.set_ciphers.assert_called_with( SSL_CIPHER_LISTS[SSLCipherList.INTERMEDIATE] ) + + +def test_ssl_context_caching() -> None: + """Test that SSLContext instances are cached correctly.""" + + assert client_context() is client_context(SSLCipherList.PYTHON_DEFAULT) + assert create_no_verify_ssl_context() is create_no_verify_ssl_context( + SSLCipherList.PYTHON_DEFAULT + ) From 4e1677e3f0c96472fdbfe56a2596868aac08a8ad Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 9 Dec 2023 17:33:31 -0500 Subject: [PATCH 006/118] Remove zwave_js device on device reset (#104291) * Reload zwave_js config entry on device reset * remove device * Just remove the device and don't reload * revert change to notification message * Assert device is no longer there --- homeassistant/components/zwave_js/__init__.py | 9 ++++-- tests/components/zwave_js/test_init.py | 29 +++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index a8b3d300e3ba6c..ccadc452bc70d5 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -449,7 +449,10 @@ def async_on_node_removed(self, event: dict) -> None: "remove_entity" ), ) - elif reason == RemoveNodeReason.RESET: + # We don't want to remove the device so we can keep the user customizations + return + + if reason == RemoveNodeReason.RESET: device_name = device.name_by_user or device.name or f"Node {node.node_id}" identifier = get_network_identifier_for_notification( self.hass, self.config_entry, self.driver_events.driver.controller @@ -471,8 +474,8 @@ def async_on_node_removed(self, event: dict) -> None: "Device Was Factory Reset!", f"{DOMAIN}.node_reset_and_removed.{dev_id[1]}", ) - else: - self.remove_device(device) + + self.remove_device(device) @callback def async_on_identify(self, event: dict) -> None: diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index bf015a70676fd0..75a7397cc4ed72 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1650,6 +1650,7 @@ async def test_factory_reset_node( hass: HomeAssistant, client, multisensor_6, multisensor_6_state, integration ) -> None: """Test when a node is removed because it was reset.""" + dev_reg = dr.async_get(hass) # One config entry scenario remove_event = Event( type="node removed", @@ -1670,15 +1671,25 @@ async def test_factory_reset_node( assert notifications[msg_id]["message"].startswith("`Multisensor 6`") assert "with the home ID" not in notifications[msg_id]["message"] async_dismiss(hass, msg_id) + await hass.async_block_till_done() + assert not dev_reg.async_get_device(identifiers={dev_id}) # Add mock config entry to simulate having multiple entries new_entry = MockConfigEntry(domain=DOMAIN) new_entry.add_to_hass(hass) # Re-add the node then remove it again - client.driver.controller.nodes[multisensor_6_state["nodeId"]] = Node( - client, deepcopy(multisensor_6_state) + add_event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": deepcopy(multisensor_6_state), + "result": {}, + }, ) + client.driver.controller.receive_event(add_event) + await hass.async_block_till_done() remove_event.data["node"] = deepcopy(multisensor_6_state) client.driver.controller.receive_event(remove_event) # Test case where config entry title and home ID don't match @@ -1686,16 +1697,24 @@ async def test_factory_reset_node( assert len(notifications) == 1 assert list(notifications)[0] == msg_id assert ( - "network `Mock Title`, with the home ID `3245146787`." + "network `Mock Title`, with the home ID `3245146787`" in notifications[msg_id]["message"] ) async_dismiss(hass, msg_id) # Test case where config entry title and home ID do match hass.config_entries.async_update_entry(integration, title="3245146787") - client.driver.controller.nodes[multisensor_6_state["nodeId"]] = Node( - client, deepcopy(multisensor_6_state) + add_event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": deepcopy(multisensor_6_state), + "result": {}, + }, ) + client.driver.controller.receive_event(add_event) + await hass.async_block_till_done() remove_event.data["node"] = deepcopy(multisensor_6_state) client.driver.controller.receive_event(remove_event) notifications = async_get_persistent_notifications(hass) From 327016eaebb78eb7f4bbf082393b0efd0fa0ae36 Mon Sep 17 00:00:00 2001 From: vexofp Date: Sat, 9 Dec 2023 17:45:33 -0500 Subject: [PATCH 007/118] Accept HTTP 200 through 206 as success for RESTful Switch (#105358) --- homeassistant/components/rest/switch.py | 4 ++-- tests/components/rest/test_switch.py | 30 +++++++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 102bb0249249df..f80143e2f9e659 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -171,7 +171,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: try: req = await self.set_device_state(body_on_t) - if req.status_code == HTTPStatus.OK: + if HTTPStatus.OK <= req.status_code < HTTPStatus.MULTIPLE_CHOICES: self._attr_is_on = True else: _LOGGER.error( @@ -186,7 +186,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: try: req = await self.set_device_state(body_off_t) - if req.status_code == HTTPStatus.OK: + if HTTPStatus.OK <= req.status_code < HTTPStatus.MULTIPLE_CHOICES: self._attr_is_on = False else: _LOGGER.error( diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 7ded4fb0aed51d..6224d98f694815 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -53,6 +53,22 @@ STATE_RESOURCE = RESOURCE +@pytest.fixture( + params=( + HTTPStatus.OK, + HTTPStatus.CREATED, + HTTPStatus.ACCEPTED, + HTTPStatus.NON_AUTHORITATIVE_INFORMATION, + HTTPStatus.NO_CONTENT, + HTTPStatus.RESET_CONTENT, + HTTPStatus.PARTIAL_CONTENT, + ) +) +def http_success_code(request: pytest.FixtureRequest) -> HTTPStatus: + """Fixture providing different successful HTTP response code.""" + return request.param + + async def test_setup_missing_config( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -262,11 +278,14 @@ async def test_is_on_before_update(hass: HomeAssistant) -> None: @respx.mock -async def test_turn_on_success(hass: HomeAssistant) -> None: +async def test_turn_on_success( + hass: HomeAssistant, + http_success_code: HTTPStatus, +) -> None: """Test turn_on.""" await _async_setup_test_switch(hass) - route = respx.post(RESOURCE) % HTTPStatus.OK + route = respx.post(RESOURCE) % http_success_code respx.get(RESOURCE).mock(side_effect=httpx.RequestError) await hass.services.async_call( SWITCH_DOMAIN, @@ -320,11 +339,14 @@ async def test_turn_on_timeout(hass: HomeAssistant) -> None: @respx.mock -async def test_turn_off_success(hass: HomeAssistant) -> None: +async def test_turn_off_success( + hass: HomeAssistant, + http_success_code: HTTPStatus, +) -> None: """Test turn_off.""" await _async_setup_test_switch(hass) - route = respx.post(RESOURCE) % HTTPStatus.OK + route = respx.post(RESOURCE) % http_success_code respx.get(RESOURCE).mock(side_effect=httpx.RequestError) await hass.services.async_call( SWITCH_DOMAIN, From 64a5271a51c76a8450d334fcd5920f4ef285314e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 10 Dec 2023 08:46:32 +1000 Subject: [PATCH 008/118] Add Tessie Integration (#104684) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 + homeassistant/components/tessie/__init__.py | 60 ++++ .../components/tessie/config_flow.py | 56 ++++ homeassistant/components/tessie/const.py | 21 ++ .../components/tessie/coordinator.py | 84 +++++ homeassistant/components/tessie/entity.py | 45 +++ homeassistant/components/tessie/manifest.json | 10 + homeassistant/components/tessie/sensor.py | 225 ++++++++++++++ homeassistant/components/tessie/strings.json | 92 ++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/tessie/__init__.py | 1 + tests/components/tessie/common.py | 55 ++++ tests/components/tessie/fixtures/asleep.json | 1 + tests/components/tessie/fixtures/online.json | 276 +++++++++++++++++ .../components/tessie/fixtures/vehicles.json | 292 ++++++++++++++++++ tests/components/tessie/test_config_flow.py | 139 +++++++++ tests/components/tessie/test_coordinator.py | 92 ++++++ tests/components/tessie/test_init.py | 30 ++ tests/components/tessie/test_sensor.py | 24 ++ 22 files changed, 1518 insertions(+) create mode 100644 homeassistant/components/tessie/__init__.py create mode 100644 homeassistant/components/tessie/config_flow.py create mode 100644 homeassistant/components/tessie/const.py create mode 100644 homeassistant/components/tessie/coordinator.py create mode 100644 homeassistant/components/tessie/entity.py create mode 100644 homeassistant/components/tessie/manifest.json create mode 100644 homeassistant/components/tessie/sensor.py create mode 100644 homeassistant/components/tessie/strings.json create mode 100644 tests/components/tessie/__init__.py create mode 100644 tests/components/tessie/common.py create mode 100644 tests/components/tessie/fixtures/asleep.json create mode 100644 tests/components/tessie/fixtures/online.json create mode 100644 tests/components/tessie/fixtures/vehicles.json create mode 100644 tests/components/tessie/test_config_flow.py create mode 100644 tests/components/tessie/test_coordinator.py create mode 100644 tests/components/tessie/test_init.py create mode 100644 tests/components/tessie/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1fe8bf68e78310..dad0d51ad797ae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1303,6 +1303,8 @@ build.json @home-assistant/supervisor /tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core /homeassistant/components/tesla_wall_connector/ @einarhauks /tests/components/tesla_wall_connector/ @einarhauks +/homeassistant/components/tessie/ @Bre77 +/tests/components/tessie/ @Bre77 /homeassistant/components/text/ @home-assistant/core /tests/components/text/ @home-assistant/core /homeassistant/components/tfiac/ @fredrike @mellado diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py new file mode 100644 index 00000000000000..e792780e873ec1 --- /dev/null +++ b/homeassistant/components/tessie/__init__.py @@ -0,0 +1,60 @@ +"""Tessie integration.""" +import logging + +from aiohttp import ClientError, ClientResponseError +from tessie_api import get_state_of_all_vehicles + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN +from .coordinator import TessieDataUpdateCoordinator + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Tessie config.""" + api_key = entry.data[CONF_ACCESS_TOKEN] + + try: + vehicles = await get_state_of_all_vehicles( + session=async_get_clientsession(hass), + api_key=api_key, + only_active=True, + ) + except ClientResponseError as ex: + # Reauth will go here + _LOGGER.error("Setup failed, unable to connect to Tessie: %s", ex) + return False + except ClientError as e: + raise ConfigEntryNotReady from e + + coordinators = [ + TessieDataUpdateCoordinator( + hass, + api_key=api_key, + vin=vehicle["vin"], + data=vehicle["last_state"], + ) + for vehicle in vehicles["results"] + if vehicle["last_state"] is not None + ] + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Tessie Config.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/tessie/config_flow.py b/homeassistant/components/tessie/config_flow.py new file mode 100644 index 00000000000000..c286f43c8b39b0 --- /dev/null +++ b/homeassistant/components/tessie/config_flow.py @@ -0,0 +1,56 @@ +"""Config Flow for Tessie integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from http import HTTPStatus +from typing import Any + +from aiohttp import ClientConnectionError, ClientResponseError +from tessie_api import get_state_of_all_vehicles +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +TESSIE_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) + + +class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config Tessie API connection.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: + """Get configuration from the user.""" + errors: dict[str, str] = {} + if user_input and CONF_ACCESS_TOKEN in user_input: + try: + await get_state_of_all_vehicles( + session=async_get_clientsession(self.hass), + api_key=user_input[CONF_ACCESS_TOKEN], + only_active=True, + ) + except ClientResponseError as e: + if e.status == HTTPStatus.UNAUTHORIZED: + errors[CONF_ACCESS_TOKEN] = "invalid_access_token" + else: + errors["base"] = "unknown" + except ClientConnectionError: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title="Tessie", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=TESSIE_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py new file mode 100644 index 00000000000000..dad9ba2345f2ed --- /dev/null +++ b/homeassistant/components/tessie/const.py @@ -0,0 +1,21 @@ +"""Constants used by Tessie integration.""" +from __future__ import annotations + +from enum import StrEnum + +DOMAIN = "tessie" + +MODELS = { + "model3": "Model 3", + "modelx": "Model X", + "modely": "Model Y", + "models": "Model S", +} + + +class TessieStatus(StrEnum): + """Tessie status.""" + + ASLEEP = "asleep" + ONLINE = "online" + OFFLINE = "offline" diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py new file mode 100644 index 00000000000000..7a1efb985ee41c --- /dev/null +++ b/homeassistant/components/tessie/coordinator.py @@ -0,0 +1,84 @@ +"""Tessie Data Coordinator.""" +from datetime import timedelta +from http import HTTPStatus +import logging +from typing import Any + +from aiohttp import ClientResponseError +from tessie_api import get_state + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import TessieStatus + +# This matches the update interval Tessie performs server side +TESSIE_SYNC_INTERVAL = 10 + +_LOGGER = logging.getLogger(__name__) + + +class TessieDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the Tessie API.""" + + def __init__( + self, + hass: HomeAssistant, + api_key: str, + vin: str, + data: dict[str, Any], + ) -> None: + """Initialize Tessie Data Update Coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Tessie", + update_method=self.async_update_data, + update_interval=timedelta(seconds=TESSIE_SYNC_INTERVAL), + ) + self.api_key = api_key + self.vin = vin + self.session = async_get_clientsession(hass) + self.data = self._flattern(data) + self.did_first_update = False + + async def async_update_data(self) -> dict[str, Any]: + """Update vehicle data using Tessie API.""" + try: + vehicle = await get_state( + session=self.session, + api_key=self.api_key, + vin=self.vin, + use_cache=self.did_first_update, + ) + except ClientResponseError as e: + if e.status == HTTPStatus.REQUEST_TIMEOUT: + # Vehicle is offline, only update state and dont throw error + self.data["state"] = TessieStatus.OFFLINE + return self.data + # Reauth will go here + raise e + + self.did_first_update = True + if vehicle["state"] == TessieStatus.ONLINE: + # Vehicle is online, all data is fresh + return self._flattern(vehicle) + + # Vehicle is asleep, only update state + self.data["state"] = vehicle["state"] + return self.data + + def _flattern( + self, data: dict[str, Any], parent: str | None = None + ) -> dict[str, Any]: + """Flattern the data structure.""" + result = {} + for key, value in data.items(): + if parent: + key = f"{parent}-{key}" + if isinstance(value, dict): + result.update(self._flattern(value, key)) + else: + result[key] = value + return result diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py new file mode 100644 index 00000000000000..4a14522a64cab5 --- /dev/null +++ b/homeassistant/components/tessie/entity.py @@ -0,0 +1,45 @@ +"""Tessie parent entity class.""" + + +from typing import Any + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MODELS +from .coordinator import TessieDataUpdateCoordinator + + +class TessieEntity(CoordinatorEntity[TessieDataUpdateCoordinator]): + """Parent class for Tessie Entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: TessieDataUpdateCoordinator, + key: str, + ) -> None: + """Initialize common aspects of a Tessie entity.""" + super().__init__(coordinator) + self.vin = coordinator.vin + self.key = key + + car_type = coordinator.data["vehicle_config-car_type"] + + self._attr_translation_key = key + self._attr_unique_id = f"{self.vin}-{key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.vin)}, + manufacturer="Tesla", + configuration_url="https://my.tessie.com/", + name=coordinator.data["display_name"], + model=MODELS.get(car_type, car_type), + sw_version=coordinator.data["vehicle_state-car_version"], + hw_version=coordinator.data["vehicle_config-driver_assist"], + ) + + @property + def value(self) -> Any: + """Return value from coordinator data.""" + return self.coordinator.data[self.key] diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json new file mode 100644 index 00000000000000..52fc8dd5be11d5 --- /dev/null +++ b/homeassistant/components/tessie/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "tessie", + "name": "Tessie", + "codeowners": ["@Bre77"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tessie", + "iot_class": "cloud_polling", + "loggers": ["tessie"], + "requirements": ["tessie-api==0.0.9"] +} diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py new file mode 100644 index 00000000000000..1941d8ba162065 --- /dev/null +++ b/homeassistant/components/tessie/sensor.py @@ -0,0 +1,225 @@ +"""Sensor platform for Tessie integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfLength, + UnitOfPower, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN, TessieStatus +from .coordinator import TessieDataUpdateCoordinator +from .entity import TessieEntity + +PARALLEL_UPDATES = 0 + + +@dataclass +class TessieSensorEntityDescription(SensorEntityDescription): + """Describes Tessie Sensor entity.""" + + value_fn: Callable[[StateType], StateType] = lambda x: x + + +DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="state", + options=[status.value for status in TessieStatus], + device_class=SensorDeviceClass.ENUM, + ), + TessieSensorEntityDescription( + key="charge_state-usable_battery_level", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + ), + TessieSensorEntityDescription( + key="charge_state-charge_energy_added", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + suggested_display_precision=1, + ), + TessieSensorEntityDescription( + key="charge_state-charger_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="charge_state-charger_voltage", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="charge_state-charger_actual_current", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="charge_state-charge_rate", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="charge_state-battery_range", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + suggested_display_precision=1, + ), + TessieSensorEntityDescription( + key="drive_state-speed", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, + ), + TessieSensorEntityDescription( + key="drive_state-power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="drive_state-shift_state", + icon="mdi:car-shift-pattern", + options=["p", "d", "r", "n"], + device_class=SensorDeviceClass.ENUM, + value_fn=lambda x: x.lower() if isinstance(x, str) else x, + ), + TessieSensorEntityDescription( + key="vehicle_state-odometer", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="vehicle_state-tpms_pressure_fl", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="vehicle_state-tpms_pressure_fr", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="vehicle_state-tpms_pressure_rl", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="vehicle_state-tpms_pressure_rr", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="climate_state-inside_temp", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + ), + TessieSensorEntityDescription( + key="climate_state-outside_temp", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + ), + TessieSensorEntityDescription( + key="climate_state-driver_temp_setting", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="climate_state-passenger_temp_setting", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tessie sensor platform from a config entry.""" + coordinators = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + TessieSensorEntity(coordinator, description) + for coordinator in coordinators + for description in DESCRIPTIONS + if description.key in coordinator.data + ) + + +class TessieSensorEntity(TessieEntity, SensorEntity): + """Base class for Tessie metric sensors.""" + + entity_description: TessieSensorEntityDescription + + def __init__( + self, + coordinator: TessieDataUpdateCoordinator, + description: TessieSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, description.key) + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.value) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json new file mode 100644 index 00000000000000..2069e46cecc378 --- /dev/null +++ b/homeassistant/components/tessie/strings.json @@ -0,0 +1,92 @@ +{ + "config": { + "error": { + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + }, + "description": "Enter your access token from [my.tessie.com/settings/api](https://my.tessie.com/settings/api)." + } + } + }, + "entity": { + "sensor": { + "state": { + "name": "Status", + "state": { + "online": "Online", + "asleep": "Asleep", + "offline": "Offline" + } + }, + "charge_state-usable_battery_level": { + "name": "Battery Level" + }, + "charge_state-charge_energy_added": { + "name": "Charge Energy Added" + }, + "charge_state-charger_power": { + "name": "Charger Power" + }, + "charge_state-charger_voltage": { + "name": "Charger Voltage" + }, + "charge_state-charger_actual_current": { + "name": "Charger Current" + }, + "charge_state-charge_rate": { + "name": "Charge Rate" + }, + "charge_state-battery_range": { + "name": "Battery Range" + }, + "drive_state-speed": { + "name": "Speed" + }, + "drive_state-power": { + "name": "Power" + }, + "drive_state-shift_state": { + "name": "Shift State", + "state": { + "p": "Park", + "d": "Drive", + "r": "Reverse", + "n": "Neutral" + } + }, + "vehicle_state-odometer": { + "name": "Odometer" + }, + "vehicle_state-tpms_pressure_fl": { + "name": "Tyre Pressure Front Left" + }, + "vehicle_state-tpms_pressure_fr": { + "name": "Tyre Pressure Front Right" + }, + "vehicle_state-tpms_pressure_rl": { + "name": "Tyre Pressure Rear Left" + }, + "vehicle_state-tpms_pressure_rr": { + "name": "Tyre Pressure Rear Right" + }, + "climate_state-inside_temp": { + "name": "Inside Temperature" + }, + "climate_state-outside_temp": { + "name": "Outside Temperature" + }, + "climate_state-driver_temp_setting": { + "name": "Driver Temperature Setting" + }, + "climate_state-passenger_temp_setting": { + "name": "Passenger Temperature Setting" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 164aa2acdd213b..975bfc60688d2b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -490,6 +490,7 @@ "tautulli", "tellduslive", "tesla_wall_connector", + "tessie", "thermobeacon", "thermopro", "thread", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 89c5ee6a80db0b..33e2229eb2ece3 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5802,6 +5802,12 @@ } } }, + "tessie": { + "name": "Tessie", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "tfiac": { "name": "Tfiac", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 1dccc1a5551460..d05699120cda2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2603,6 +2603,9 @@ tesla-powerwall==0.3.19 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.2 +# homeassistant.components.tessie +tessie-api==0.0.9 + # homeassistant.components.tensorflow # tf-models-official==2.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57ec5a22df9e1b..afe214608ec5ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1943,6 +1943,9 @@ tesla-powerwall==0.3.19 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.2 +# homeassistant.components.tessie +tessie-api==0.0.9 + # homeassistant.components.thermobeacon thermobeacon-ble==0.6.0 diff --git a/tests/components/tessie/__init__.py b/tests/components/tessie/__init__.py new file mode 100644 index 00000000000000..df17fe027d9e80 --- /dev/null +++ b/tests/components/tessie/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tessie integration.""" diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py new file mode 100644 index 00000000000000..572a687a6e5d61 --- /dev/null +++ b/tests/components/tessie/common.py @@ -0,0 +1,55 @@ +"""Tessie common helpers for tests.""" + +from http import HTTPStatus +from unittest.mock import patch + +from aiohttp import ClientConnectionError, ClientResponseError +from aiohttp.client import RequestInfo + +from homeassistant.components.tessie.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_json_object_fixture + +TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN) +TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN) +TEST_VEHICLE_STATE_ASLEEP = load_json_object_fixture("asleep.json", DOMAIN) + +TEST_CONFIG = {CONF_ACCESS_TOKEN: "1234567890"} +TESSIE_URL = "https://api.tessie.com/" + +TEST_REQUEST_INFO = RequestInfo( + url=TESSIE_URL, method="GET", headers={}, real_url=TESSIE_URL +) + +ERROR_AUTH = ClientResponseError( + request_info=TEST_REQUEST_INFO, history=None, status=HTTPStatus.UNAUTHORIZED +) +ERROR_TIMEOUT = ClientResponseError( + request_info=TEST_REQUEST_INFO, history=None, status=HTTPStatus.REQUEST_TIMEOUT +) +ERROR_UNKNOWN = ClientResponseError( + request_info=TEST_REQUEST_INFO, history=None, status=HTTPStatus.BAD_REQUEST +) +ERROR_CONNECTION = ClientConnectionError() + + +async def setup_platform(hass: HomeAssistant, side_effect=None): + """Set up the Tessie platform.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_CONFIG, + ) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.tessie.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + side_effect=side_effect, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/tessie/fixtures/asleep.json b/tests/components/tessie/fixtures/asleep.json new file mode 100644 index 00000000000000..4f78efafcf185f --- /dev/null +++ b/tests/components/tessie/fixtures/asleep.json @@ -0,0 +1 @@ +{ "state": "asleep" } diff --git a/tests/components/tessie/fixtures/online.json b/tests/components/tessie/fixtures/online.json new file mode 100644 index 00000000000000..8fbab1ab948af9 --- /dev/null +++ b/tests/components/tessie/fixtures/online.json @@ -0,0 +1,276 @@ +{ + "user_id": 234567890, + "vehicle_id": 345678901, + "vin": "VINVINVIN", + "color": null, + "access_type": "OWNER", + "granular_access": { + "hide_private": false + }, + "tokens": ["beef", "c0ffee"], + "state": "online", + "in_service": false, + "id_s": "123456789", + "calendar_enabled": true, + "api_version": 67, + "backseat_token": null, + "backseat_token_updated_at": null, + "ble_autopair_enrolled": false, + "charge_state": { + "battery_heater_on": false, + "battery_level": 75, + "battery_range": 263.68, + "charge_amps": 32, + "charge_current_request": 32, + "charge_current_request_max": 32, + "charge_enable_request": true, + "charge_energy_added": 18.47, + "charge_limit_soc": 80, + "charge_limit_soc_max": 100, + "charge_limit_soc_min": 50, + "charge_limit_soc_std": 80, + "charge_miles_added_ideal": 84, + "charge_miles_added_rated": 84, + "charge_port_cold_weather_mode": false, + "charge_port_color": "", + "charge_port_door_open": true, + "charge_port_latch": "Engaged", + "charge_rate": 30.6, + "charger_actual_current": 32, + "charger_phases": 1, + "charger_pilot_current": 32, + "charger_power": 7, + "charger_voltage": 224, + "charging_state": "Charging", + "conn_charge_cable": "IEC", + "est_battery_range": 324.73, + "fast_charger_brand": "", + "fast_charger_present": false, + "fast_charger_type": "ACSingleWireCAN", + "ideal_battery_range": 263.68, + "max_range_charge_counter": 0, + "minutes_to_full_charge": 30, + "not_enough_power_to_heat": null, + "off_peak_charging_enabled": false, + "off_peak_charging_times": "all_week", + "off_peak_hours_end_time": 900, + "preconditioning_enabled": false, + "preconditioning_times": "all_week", + "scheduled_charging_mode": "StartAt", + "scheduled_charging_pending": false, + "scheduled_charging_start_time": 1701216000, + "scheduled_charging_start_time_app": 600, + "scheduled_charging_start_time_minutes": 600, + "scheduled_departure_time": 1694899800, + "scheduled_departure_time_minutes": 450, + "supercharger_session_trip_planner": false, + "time_to_full_charge": 0.5, + "timestamp": 1701139037461, + "trip_charging": false, + "usable_battery_level": 75, + "user_charge_enable_request": null + }, + "climate_state": { + "allow_cabin_overheat_protection": true, + "auto_seat_climate_left": true, + "auto_seat_climate_right": true, + "auto_steering_wheel_heat": true, + "battery_heater": false, + "battery_heater_no_power": null, + "cabin_overheat_protection": "On", + "cabin_overheat_protection_actively_cooling": false, + "climate_keeper_mode": "off", + "cop_activation_temperature": "High", + "defrost_mode": 0, + "driver_temp_setting": 22.5, + "fan_status": 0, + "hvac_auto_request": "On", + "inside_temp": 30.4, + "is_auto_conditioning_on": false, + "is_climate_on": false, + "is_front_defroster_on": false, + "is_preconditioning": false, + "is_rear_defroster_on": false, + "left_temp_direction": 234, + "max_avail_temp": 28, + "min_avail_temp": 15, + "outside_temp": 30.5, + "passenger_temp_setting": 22.5, + "remote_heater_control_enabled": false, + "right_temp_direction": 234, + "seat_heater_left": 0, + "seat_heater_rear_center": 0, + "seat_heater_rear_left": 0, + "seat_heater_rear_right": 0, + "seat_heater_right": 0, + "side_mirror_heaters": false, + "steering_wheel_heat_level": 0, + "steering_wheel_heater": false, + "supports_fan_only_cabin_overheat_protection": true, + "timestamp": 1701139037461, + "wiper_blade_heater": false + }, + "drive_state": { + "active_route_latitude": 30.2226265, + "active_route_longitude": -97.6236871, + "active_route_traffic_minutes_delay": 0, + "gps_as_of": 1701129612, + "heading": 185, + "latitude": -30.222626, + "longitude": -97.6236871, + "native_latitude": -30.222626, + "native_location_supported": 1, + "native_longitude": -97.6236871, + "native_type": "wgs", + "power": -7, + "shift_state": null, + "speed": null, + "timestamp": 1701139037461 + }, + "gui_settings": { + "gui_24_hour_time": false, + "gui_charge_rate_units": "kW", + "gui_distance_units": "km/hr", + "gui_range_display": "Rated", + "gui_temperature_units": "C", + "gui_tirepressure_units": "Psi", + "show_range_units": false, + "timestamp": 1701139037461 + }, + "vehicle_config": { + "aux_park_lamps": "Eu", + "badge_version": 1, + "can_accept_navigation_requests": true, + "can_actuate_trunks": true, + "car_special_type": "base", + "car_type": "model3", + "charge_port_type": "CCS", + "cop_user_set_temp_supported": false, + "dashcam_clip_save_supported": true, + "default_charge_to_max": false, + "driver_assist": "TeslaAP3", + "ece_restrictions": false, + "efficiency_package": "M32021", + "eu_vehicle": true, + "exterior_color": "DeepBlue", + "exterior_trim": "Black", + "exterior_trim_override": "", + "has_air_suspension": false, + "has_ludicrous_mode": false, + "has_seat_cooling": false, + "headlamp_type": "Global", + "interior_trim_type": "White2", + "key_version": 2, + "motorized_charge_port": true, + "paint_color_override": "0,9,25,0.7,0.04", + "performance_package": "Base", + "plg": true, + "pws": false, + "rear_drive_unit": "PM216MOSFET", + "rear_seat_heaters": 1, + "rear_seat_type": 0, + "rhd": true, + "roof_color": "RoofColorGlass", + "seat_type": null, + "spoiler_type": "None", + "sun_roof_installed": null, + "supports_qr_pairing": false, + "third_row_seats": "None", + "timestamp": 1701139037461, + "trim_badging": "74d", + "use_range_badging": true, + "utc_offset": 36000, + "webcam_selfie_supported": true, + "webcam_supported": true, + "wheel_type": "Pinwheel18CapKit" + }, + "vehicle_state": { + "api_version": 67, + "autopark_state_v2": "unavailable", + "calendar_supported": true, + "car_version": "2023.38.6 c1f85ddb415f", + "center_display_state": 0, + "dashcam_clip_save_available": true, + "dashcam_state": "Recording", + "df": 0, + "dr": 0, + "fd_window": 0, + "feature_bitmask": "fbdffbff,7f", + "fp_window": 0, + "ft": 0, + "is_user_present": false, + "locked": true, + "media_info": { + "audio_volume": 2.3333, + "audio_volume_increment": 0.333333, + "audio_volume_max": 10.333333, + "media_playback_status": "Stopped", + "now_playing_album": "", + "now_playing_artist": "", + "now_playing_duration": 0, + "now_playing_elapsed": 0, + "now_playing_source": "Spotify", + "now_playing_station": "", + "now_playing_title": "" + }, + "media_state": { + "remote_control_enabled": false + }, + "notifications_supported": true, + "odometer": 5454.495383, + "parsed_calendar_supported": true, + "pf": 0, + "pr": 0, + "rd_window": 0, + "remote_start": false, + "remote_start_enabled": true, + "remote_start_supported": true, + "rp_window": 0, + "rt": 0, + "santa_mode": 0, + "sentry_mode": false, + "sentry_mode_available": true, + "service_mode": false, + "service_mode_plus": false, + "software_update": { + "download_perc": 0, + "expected_duration_sec": 2700, + "install_perc": 1, + "status": "", + "version": " " + }, + "speed_limit_mode": { + "active": false, + "current_limit_mph": 74.564543, + "max_limit_mph": 120, + "min_limit_mph": 50, + "pin_code_set": true + }, + "timestamp": 1701139037461, + "tpms_hard_warning_fl": false, + "tpms_hard_warning_fr": false, + "tpms_hard_warning_rl": false, + "tpms_hard_warning_rr": false, + "tpms_last_seen_pressure_time_fl": 1701062077, + "tpms_last_seen_pressure_time_fr": 1701062047, + "tpms_last_seen_pressure_time_rl": 1701062077, + "tpms_last_seen_pressure_time_rr": 1701062047, + "tpms_pressure_fl": 2.975, + "tpms_pressure_fr": 2.975, + "tpms_pressure_rl": 2.95, + "tpms_pressure_rr": 2.95, + "tpms_rcp_front_value": 2.9, + "tpms_rcp_rear_value": 2.9, + "tpms_soft_warning_fl": false, + "tpms_soft_warning_fr": false, + "tpms_soft_warning_rl": false, + "tpms_soft_warning_rr": false, + "valet_mode": false, + "valet_pin_needed": false, + "vehicle_name": "Test", + "vehicle_self_test_progress": 0, + "vehicle_self_test_requested": false, + "webcam_available": true + }, + "display_name": "Test" +} diff --git a/tests/components/tessie/fixtures/vehicles.json b/tests/components/tessie/fixtures/vehicles.json new file mode 100644 index 00000000000000..9cc833a1cb22c9 --- /dev/null +++ b/tests/components/tessie/fixtures/vehicles.json @@ -0,0 +1,292 @@ +{ + "results": [ + { + "vin": "VINVINVIN", + "is_active": true, + "is_archived_manually": false, + "last_charge_created_at": null, + "last_charge_updated_at": null, + "last_drive_created_at": null, + "last_drive_updated_at": null, + "last_idle_created_at": null, + "last_idle_updated_at": null, + "last_state": { + "id": 123456789, + "user_id": 234567890, + "vehicle_id": 345678901, + "vin": "VINVINVIN", + "color": null, + "access_type": "OWNER", + "granular_access": { + "hide_private": false + }, + "tokens": ["beef", "c0ffee"], + "state": "online", + "in_service": false, + "id_s": "123456789", + "calendar_enabled": true, + "api_version": 67, + "backseat_token": null, + "backseat_token_updated_at": null, + "ble_autopair_enrolled": false, + "charge_state": { + "battery_heater_on": false, + "battery_level": 75, + "battery_range": 263.68, + "charge_amps": 32, + "charge_current_request": 32, + "charge_current_request_max": 32, + "charge_enable_request": true, + "charge_energy_added": 18.47, + "charge_limit_soc": 80, + "charge_limit_soc_max": 100, + "charge_limit_soc_min": 50, + "charge_limit_soc_std": 80, + "charge_miles_added_ideal": 84, + "charge_miles_added_rated": 84, + "charge_port_cold_weather_mode": false, + "charge_port_color": "", + "charge_port_door_open": true, + "charge_port_latch": "Engaged", + "charge_rate": 30.6, + "charger_actual_current": 32, + "charger_phases": 1, + "charger_pilot_current": 32, + "charger_power": 7, + "charger_voltage": 224, + "charging_state": "Charging", + "conn_charge_cable": "IEC", + "est_battery_range": 324.73, + "fast_charger_brand": "", + "fast_charger_present": false, + "fast_charger_type": "ACSingleWireCAN", + "ideal_battery_range": 263.68, + "max_range_charge_counter": 0, + "minutes_to_full_charge": 30, + "not_enough_power_to_heat": null, + "off_peak_charging_enabled": false, + "off_peak_charging_times": "all_week", + "off_peak_hours_end_time": 900, + "preconditioning_enabled": false, + "preconditioning_times": "all_week", + "scheduled_charging_mode": "StartAt", + "scheduled_charging_pending": false, + "scheduled_charging_start_time": 1701216000, + "scheduled_charging_start_time_app": 600, + "scheduled_charging_start_time_minutes": 600, + "scheduled_departure_time": 1694899800, + "scheduled_departure_time_minutes": 450, + "supercharger_session_trip_planner": false, + "time_to_full_charge": 0.5, + "timestamp": 1701139037461, + "trip_charging": false, + "usable_battery_level": 75, + "user_charge_enable_request": null + }, + "climate_state": { + "allow_cabin_overheat_protection": true, + "auto_seat_climate_left": true, + "auto_seat_climate_right": true, + "auto_steering_wheel_heat": true, + "battery_heater": false, + "battery_heater_no_power": null, + "cabin_overheat_protection": "On", + "cabin_overheat_protection_actively_cooling": false, + "climate_keeper_mode": "off", + "cop_activation_temperature": "High", + "defrost_mode": 0, + "driver_temp_setting": 22.5, + "fan_status": 0, + "hvac_auto_request": "On", + "inside_temp": 30.4, + "is_auto_conditioning_on": false, + "is_climate_on": false, + "is_front_defroster_on": false, + "is_preconditioning": false, + "is_rear_defroster_on": false, + "left_temp_direction": 234, + "max_avail_temp": 28, + "min_avail_temp": 15, + "outside_temp": 30.5, + "passenger_temp_setting": 22.5, + "remote_heater_control_enabled": false, + "right_temp_direction": 234, + "seat_heater_left": 0, + "seat_heater_rear_center": 0, + "seat_heater_rear_left": 0, + "seat_heater_rear_right": 0, + "seat_heater_right": 0, + "side_mirror_heaters": false, + "steering_wheel_heat_level": 0, + "steering_wheel_heater": false, + "supports_fan_only_cabin_overheat_protection": true, + "timestamp": 1701139037461, + "wiper_blade_heater": false + }, + "drive_state": { + "active_route_latitude": 30.2226265, + "active_route_longitude": -97.6236871, + "active_route_traffic_minutes_delay": 0, + "gps_as_of": 1701129612, + "heading": 185, + "latitude": -30.222626, + "longitude": -97.6236871, + "native_latitude": -30.222626, + "native_location_supported": 1, + "native_longitude": -97.6236871, + "native_type": "wgs", + "power": -7, + "shift_state": null, + "speed": null, + "timestamp": 1701139037461 + }, + "gui_settings": { + "gui_24_hour_time": false, + "gui_charge_rate_units": "kW", + "gui_distance_units": "km/hr", + "gui_range_display": "Rated", + "gui_temperature_units": "C", + "gui_tirepressure_units": "Psi", + "show_range_units": false, + "timestamp": 1701139037461 + }, + "vehicle_config": { + "aux_park_lamps": "Eu", + "badge_version": 1, + "can_accept_navigation_requests": true, + "can_actuate_trunks": true, + "car_special_type": "base", + "car_type": "model3", + "charge_port_type": "CCS", + "cop_user_set_temp_supported": false, + "dashcam_clip_save_supported": true, + "default_charge_to_max": false, + "driver_assist": "TeslaAP3", + "ece_restrictions": false, + "efficiency_package": "M32021", + "eu_vehicle": true, + "exterior_color": "DeepBlue", + "exterior_trim": "Black", + "exterior_trim_override": "", + "has_air_suspension": false, + "has_ludicrous_mode": false, + "has_seat_cooling": false, + "headlamp_type": "Global", + "interior_trim_type": "White2", + "key_version": 2, + "motorized_charge_port": true, + "paint_color_override": "0,9,25,0.7,0.04", + "performance_package": "Base", + "plg": true, + "pws": false, + "rear_drive_unit": "PM216MOSFET", + "rear_seat_heaters": 1, + "rear_seat_type": 0, + "rhd": true, + "roof_color": "RoofColorGlass", + "seat_type": null, + "spoiler_type": "None", + "sun_roof_installed": null, + "supports_qr_pairing": false, + "third_row_seats": "None", + "timestamp": 1701139037461, + "trim_badging": "74d", + "use_range_badging": true, + "utc_offset": 36000, + "webcam_selfie_supported": true, + "webcam_supported": true, + "wheel_type": "Pinwheel18CapKit" + }, + "vehicle_state": { + "api_version": 67, + "autopark_state_v2": "unavailable", + "calendar_supported": true, + "car_version": "2023.38.6 c1f85ddb415f", + "center_display_state": 0, + "dashcam_clip_save_available": true, + "dashcam_state": "Recording", + "df": 0, + "dr": 0, + "fd_window": 0, + "feature_bitmask": "fbdffbff,7f", + "fp_window": 0, + "ft": 0, + "is_user_present": false, + "locked": true, + "media_info": { + "audio_volume": 2.3333, + "audio_volume_increment": 0.333333, + "audio_volume_max": 10.333333, + "media_playback_status": "Stopped", + "now_playing_album": "", + "now_playing_artist": "", + "now_playing_duration": 0, + "now_playing_elapsed": 0, + "now_playing_source": "Spotify", + "now_playing_station": "", + "now_playing_title": "" + }, + "media_state": { + "remote_control_enabled": false + }, + "notifications_supported": true, + "odometer": 5454.495383, + "parsed_calendar_supported": true, + "pf": 0, + "pr": 0, + "rd_window": 0, + "remote_start": false, + "remote_start_enabled": true, + "remote_start_supported": true, + "rp_window": 0, + "rt": 0, + "santa_mode": 0, + "sentry_mode": false, + "sentry_mode_available": true, + "service_mode": false, + "service_mode_plus": false, + "software_update": { + "download_perc": 0, + "expected_duration_sec": 2700, + "install_perc": 1, + "status": "", + "version": " " + }, + "speed_limit_mode": { + "active": false, + "current_limit_mph": 74.564543, + "max_limit_mph": 120, + "min_limit_mph": 50, + "pin_code_set": true + }, + "timestamp": 1701139037461, + "tpms_hard_warning_fl": false, + "tpms_hard_warning_fr": false, + "tpms_hard_warning_rl": false, + "tpms_hard_warning_rr": false, + "tpms_last_seen_pressure_time_fl": 1701062077, + "tpms_last_seen_pressure_time_fr": 1701062047, + "tpms_last_seen_pressure_time_rl": 1701062077, + "tpms_last_seen_pressure_time_rr": 1701062047, + "tpms_pressure_fl": 2.975, + "tpms_pressure_fr": 2.975, + "tpms_pressure_rl": 2.95, + "tpms_pressure_rr": 2.95, + "tpms_rcp_front_value": 2.9, + "tpms_rcp_rear_value": 2.9, + "tpms_soft_warning_fl": false, + "tpms_soft_warning_fr": false, + "tpms_soft_warning_rl": false, + "tpms_soft_warning_rr": false, + "valet_mode": false, + "valet_pin_needed": false, + "vehicle_name": "Test", + "vehicle_self_test_progress": 0, + "vehicle_self_test_requested": false, + "webcam_available": true + }, + "display_name": "Test" + } + } + ] +} diff --git a/tests/components/tessie/test_config_flow.py b/tests/components/tessie/test_config_flow.py new file mode 100644 index 00000000000000..edf2f8914ae801 --- /dev/null +++ b/tests/components/tessie/test_config_flow.py @@ -0,0 +1,139 @@ +"""Test the Tessie config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.tessie.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .common import ( + ERROR_AUTH, + ERROR_CONNECTION, + ERROR_UNKNOWN, + TEST_CONFIG, + TEST_STATE_OF_ALL_VEHICLES, +) + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result1["type"] == FlowResultType.FORM + assert not result1["errors"] + + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + ) as mock_get_state_of_all_vehicles, patch( + "homeassistant.components.tessie.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, + ) + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tessie" + assert result2["data"] == TEST_CONFIG + + +async def test_form_invalid_access_token(hass: HomeAssistant) -> None: + """Test invalid auth is handled.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + side_effect=ERROR_AUTH, + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} + + # Complete the flow + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + TEST_CONFIG, + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + + +async def test_form_invalid_response(hass: HomeAssistant) -> None: + """Test invalid auth is handled.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + side_effect=ERROR_UNKNOWN, + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + # Complete the flow + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + TEST_CONFIG, + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + + +async def test_form_network_issue(hass: HomeAssistant) -> None: + """Test network issues are handled.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + side_effect=ERROR_CONNECTION, + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + # Complete the flow + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + TEST_CONFIG, + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py new file mode 100644 index 00000000000000..43b6489c39d3c4 --- /dev/null +++ b/tests/components/tessie/test_coordinator.py @@ -0,0 +1,92 @@ +"""Test the Tessie sensor platform.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL +from homeassistant.components.tessie.sensor import TessieStatus +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .common import ( + ERROR_CONNECTION, + ERROR_TIMEOUT, + ERROR_UNKNOWN, + TEST_VEHICLE_STATE_ASLEEP, + TEST_VEHICLE_STATE_ONLINE, + setup_platform, +) + +from tests.common import async_fire_time_changed + +WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) + + +@pytest.fixture +def mock_get_state(): + """Mock get_state function.""" + with patch( + "homeassistant.components.tessie.coordinator.get_state", + ) as mock_get_state: + yield mock_get_state + + +async def test_coordinator_online(hass: HomeAssistant, mock_get_state) -> None: + """Tests that the coordinator handles online vehciles.""" + + mock_get_state.return_value = TEST_VEHICLE_STATE_ONLINE + await setup_platform(hass) + + async_fire_time_changed(hass, utcnow() + WAIT) + await hass.async_block_till_done() + mock_get_state.assert_called_once() + assert hass.states.get("sensor.test_status").state == TessieStatus.ONLINE + + +async def test_coordinator_asleep(hass: HomeAssistant, mock_get_state) -> None: + """Tests that the coordinator handles asleep vehicles.""" + + mock_get_state.return_value = TEST_VEHICLE_STATE_ASLEEP + await setup_platform(hass) + + async_fire_time_changed(hass, utcnow() + WAIT) + await hass.async_block_till_done() + mock_get_state.assert_called_once() + assert hass.states.get("sensor.test_status").state == TessieStatus.ASLEEP + + +async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_state) -> None: + """Tests that the coordinator handles client errors.""" + + mock_get_state.side_effect = ERROR_UNKNOWN + await setup_platform(hass) + + async_fire_time_changed(hass, utcnow() + WAIT) + await hass.async_block_till_done() + mock_get_state.assert_called_once() + assert hass.states.get("sensor.test_status").state == STATE_UNAVAILABLE + + +async def test_coordinator_timeout(hass: HomeAssistant, mock_get_state) -> None: + """Tests that the coordinator handles timeout errors.""" + + mock_get_state.side_effect = ERROR_TIMEOUT + await setup_platform(hass) + + async_fire_time_changed(hass, utcnow() + WAIT) + await hass.async_block_till_done() + mock_get_state.assert_called_once() + assert hass.states.get("sensor.test_status").state == TessieStatus.OFFLINE + + +async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> None: + """Tests that the coordinator handles connection errors.""" + + mock_get_state.side_effect = ERROR_CONNECTION + await setup_platform(hass) + async_fire_time_changed(hass, utcnow() + WAIT) + await hass.async_block_till_done() + mock_get_state.assert_called_once() + assert hass.states.get("sensor.test_status").state == STATE_UNAVAILABLE diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py new file mode 100644 index 00000000000000..409ece97a24ec4 --- /dev/null +++ b/tests/components/tessie/test_init.py @@ -0,0 +1,30 @@ +"""Test the Tessie init.""" + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .common import ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform + + +async def test_load_unload(hass: HomeAssistant) -> None: + """Test load and unload.""" + + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_unknown_failure(hass: HomeAssistant) -> None: + """Test init with an authentication failure.""" + + entry = await setup_platform(hass, side_effect=ERROR_UNKNOWN) + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_connection_failure(hass: HomeAssistant) -> None: + """Test init with a network connection failure.""" + + entry = await setup_platform(hass, side_effect=ERROR_CONNECTION) + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/tessie/test_sensor.py b/tests/components/tessie/test_sensor.py new file mode 100644 index 00000000000000..b9371032d0e564 --- /dev/null +++ b/tests/components/tessie/test_sensor.py @@ -0,0 +1,24 @@ +"""Test the Tessie sensor platform.""" +from homeassistant.components.tessie.sensor import DESCRIPTIONS +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .common import TEST_VEHICLE_STATE_ONLINE, setup_platform + + +async def test_sensors(hass: HomeAssistant) -> None: + """Tests that the sensors are correct.""" + + assert len(hass.states.async_all("sensor")) == 0 + + await setup_platform(hass) + + assert len(hass.states.async_all("sensor")) == len(DESCRIPTIONS) + + assert hass.states.get("sensor.test_battery_level").state == str( + TEST_VEHICLE_STATE_ONLINE["charge_state"]["battery_level"] + ) + assert hass.states.get("sensor.test_charge_energy_added").state == str( + TEST_VEHICLE_STATE_ONLINE["charge_state"]["charge_energy_added"] + ) + assert hass.states.get("sensor.test_shift_state").state == STATE_UNKNOWN From a8148cea65454b79b44ab1c7da15d9b57d39f805 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 9 Dec 2023 23:47:19 +0100 Subject: [PATCH 009/118] Migrate roku tests to use freezegun (#105418) --- tests/components/roku/test_media_player.py | 29 +++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 5d4568ce7ac929..c186741aac93b9 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError @@ -153,6 +154,7 @@ async def test_availability( hass: HomeAssistant, mock_roku: MagicMock, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, error: RokuError, ) -> None: """Test entity availability.""" @@ -160,23 +162,22 @@ async def test_availability( future = now + timedelta(minutes=1) mock_config_entry.add_to_hass(hass) - with patch("homeassistant.util.dt.utcnow", return_value=now): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + freezer.move_to(now) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - with patch("homeassistant.util.dt.utcnow", return_value=future): - mock_roku.update.side_effect = error - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE + freezer.move_to(future) + mock_roku.update.side_effect = error + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE future += timedelta(minutes=1) - - with patch("homeassistant.util.dt.utcnow", return_value=future): - mock_roku.update.side_effect = None - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert hass.states.get(MAIN_ENTITY_ID).state == STATE_IDLE + freezer.move_to(future) + mock_roku.update.side_effect = None + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert hass.states.get(MAIN_ENTITY_ID).state == STATE_IDLE async def test_supported_features( From 7b32e4142ebd4be4c2085ead50f06066b2be39ef Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Sun, 10 Dec 2023 06:15:48 +0100 Subject: [PATCH 010/118] Make API init async in Minecraft Server (#105403) * Make api init async * Remove duplicate assignment of address and set server to None in constructor --- .../components/minecraft_server/__init__.py | 27 +++++---- .../components/minecraft_server/api.py | 39 +++++++++--- .../minecraft_server/config_flow.py | 6 +- .../minecraft_server/coordinator.py | 12 +++- .../snapshots/test_binary_sensor.ambr | 8 +-- .../snapshots/test_sensor.ambr | 60 +++++++++---------- .../minecraft_server/test_binary_sensor.py | 57 ++++++++++++++---- .../minecraft_server/test_config_flow.py | 8 +-- .../minecraft_server/test_diagnostics.py | 7 ++- .../components/minecraft_server/test_init.py | 34 ++++++++--- .../minecraft_server/test_sensor.py | 52 +++++++++++++--- 11 files changed, 219 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 4e5ab9290f0b00..0e2debda33ec80 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -30,13 +30,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Minecraft Server from a config entry.""" - # Check and create API instance. + # Create API instance. + api = MinecraftServer( + hass, + entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION), + entry.data[CONF_ADDRESS], + ) + + # Initialize API instance. try: - api = await hass.async_add_executor_job( - MinecraftServer, - entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION), - entry.data[CONF_ADDRESS], - ) + await api.async_initialize() except MinecraftServerAddressError as error: raise ConfigEntryError( f"Server address in configuration entry is invalid: {error}" @@ -102,9 +105,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_data = config_entry.data # Migrate config entry. + address = config_data[CONF_HOST] + api = MinecraftServer(hass, MinecraftServerType.JAVA_EDITION, address) + try: - address = config_data[CONF_HOST] - MinecraftServer(MinecraftServerType.JAVA_EDITION, address) + await api.async_initialize() host_only_lookup_success = True except MinecraftServerAddressError as error: host_only_lookup_success = False @@ -114,9 +119,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) if not host_only_lookup_success: + address = f"{config_data[CONF_HOST]}:{config_data[CONF_PORT]}" + api = MinecraftServer(hass, MinecraftServerType.JAVA_EDITION, address) + try: - address = f"{config_data[CONF_HOST]}:{config_data[CONF_PORT]}" - MinecraftServer(MinecraftServerType.JAVA_EDITION, address) + await api.async_initialize() except MinecraftServerAddressError as error: _LOGGER.exception( "Can't migrate configuration entry due to error while parsing server address, try again later: %s", diff --git a/homeassistant/components/minecraft_server/api.py b/homeassistant/components/minecraft_server/api.py index 4ab7865f369a0d..fc872d37bdef33 100644 --- a/homeassistant/components/minecraft_server/api.py +++ b/homeassistant/components/minecraft_server/api.py @@ -9,6 +9,8 @@ from mcstatus import BedrockServer, JavaServer from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +from homeassistant.core import HomeAssistant + _LOGGER = logging.getLogger(__name__) LOOKUP_TIMEOUT: float = 10 @@ -52,35 +54,51 @@ class MinecraftServerConnectionError(Exception): """Raised when no data can be fechted from the server.""" +class MinecraftServerNotInitializedError(Exception): + """Raised when APIs are used although server instance is not initialized yet.""" + + class MinecraftServer: """Minecraft Server wrapper class for 3rd party library mcstatus.""" - _server: BedrockServer | JavaServer + _server: BedrockServer | JavaServer | None - def __init__(self, server_type: MinecraftServerType, address: str) -> None: + def __init__( + self, hass: HomeAssistant, server_type: MinecraftServerType, address: str + ) -> None: """Initialize server instance.""" + self._server = None + self._hass = hass + self._server_type = server_type + self._address = address + + async def async_initialize(self) -> None: + """Perform async initialization of server instance.""" try: - if server_type == MinecraftServerType.JAVA_EDITION: - self._server = JavaServer.lookup(address, timeout=LOOKUP_TIMEOUT) + if self._server_type == MinecraftServerType.JAVA_EDITION: + self._server = await JavaServer.async_lookup(self._address) else: - self._server = BedrockServer.lookup(address, timeout=LOOKUP_TIMEOUT) + self._server = await self._hass.async_add_executor_job( + BedrockServer.lookup, self._address + ) except (ValueError, LifetimeTimeout) as error: raise MinecraftServerAddressError( - f"Lookup of '{address}' failed: {self._get_error_message(error)}" + f"Lookup of '{self._address}' failed: {self._get_error_message(error)}" ) from error self._server.timeout = DATA_UPDATE_TIMEOUT - self._address = address _LOGGER.debug( - "%s server instance created with address '%s'", server_type, address + "%s server instance created with address '%s'", + self._server_type, + self._address, ) async def async_is_online(self) -> bool: """Check if the server is online, supporting both Java and Bedrock Edition servers.""" try: await self.async_get_data() - except MinecraftServerConnectionError: + except (MinecraftServerConnectionError, MinecraftServerNotInitializedError): return False return True @@ -89,6 +107,9 @@ async def async_get_data(self) -> MinecraftServerData: """Get updated data from the server, supporting both Java and Bedrock Edition servers.""" status_response: BedrockStatusResponse | JavaStatusResponse + if self._server is None: + raise MinecraftServerNotInitializedError() + try: status_response = await self._server.async_status(tries=DATA_UPDATE_RETRIES) except OSError as error: diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py index f064a4ac1ef4bf..045133421fba89 100644 --- a/homeassistant/components/minecraft_server/config_flow.py +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -35,10 +35,10 @@ async def async_step_user(self, user_input=None) -> FlowResult: # Some Bedrock Edition servers mimic a Java Edition server, therefore check for a Bedrock Edition server first. for server_type in MinecraftServerType: + api = MinecraftServer(self.hass, server_type, address) + try: - api = await self.hass.async_add_executor_job( - MinecraftServer, server_type, address - ) + await api.async_initialize() except MinecraftServerAddressError: pass else: diff --git a/homeassistant/components/minecraft_server/coordinator.py b/homeassistant/components/minecraft_server/coordinator.py index f7a60318c64f41..e498375cafc17a 100644 --- a/homeassistant/components/minecraft_server/coordinator.py +++ b/homeassistant/components/minecraft_server/coordinator.py @@ -7,7 +7,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .api import MinecraftServer, MinecraftServerConnectionError, MinecraftServerData +from .api import ( + MinecraftServer, + MinecraftServerConnectionError, + MinecraftServerData, + MinecraftServerNotInitializedError, +) SCAN_INTERVAL = timedelta(seconds=60) @@ -32,5 +37,8 @@ async def _async_update_data(self) -> MinecraftServerData: """Get updated data from the server.""" try: return await self._api.async_get_data() - except MinecraftServerConnectionError as error: + except ( + MinecraftServerConnectionError, + MinecraftServerNotInitializedError, + ) as error: raise UpdateFailed(error) from error diff --git a/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr b/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr index ef03e36343b930..2a62fea7f357dc 100644 --- a/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr +++ b/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_binary_sensor[bedrock_mock_config_entry-BedrockServer-status_response1] +# name: test_binary_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', @@ -13,7 +13,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[java_mock_config_entry-JavaServer-status_response0] +# name: test_binary_sensor[java_mock_config_entry-JavaServer-async_lookup-status_response0] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', @@ -27,7 +27,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1] +# name: test_binary_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', @@ -41,7 +41,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor_update[java_mock_config_entry-JavaServer-status_response0] +# name: test_binary_sensor_update[java_mock_config_entry-JavaServer-async_lookup-status_response0] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', diff --git a/tests/components/minecraft_server/snapshots/test_sensor.ambr b/tests/components/minecraft_server/snapshots/test_sensor.ambr index fed0ae93c6669b..b0f77f27b80b0c 100644 --- a/tests/components/minecraft_server/snapshots/test_sensor.ambr +++ b/tests/components/minecraft_server/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1] +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', @@ -13,7 +13,7 @@ 'state': '5', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].1 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].1 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', @@ -27,7 +27,7 @@ 'state': '3', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].2 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].2 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', @@ -41,7 +41,7 @@ 'state': '10', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].3 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].3 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', @@ -54,7 +54,7 @@ 'state': 'Dummy MOTD', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].4 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].4 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', @@ -67,7 +67,7 @@ 'state': 'Dummy Version', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].5 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].5 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', @@ -80,7 +80,7 @@ 'state': '123', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].6 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].6 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Map name', @@ -93,7 +93,7 @@ 'state': 'Dummy Map Name', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].7 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].7 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Game mode', @@ -106,7 +106,7 @@ 'state': 'Dummy Game Mode', }) # --- -# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].8 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].8 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Edition', @@ -119,7 +119,7 @@ 'state': 'MCPE', }) # --- -# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0] +# name: test_sensor[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', @@ -133,7 +133,7 @@ 'state': '5', }) # --- -# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].1 +# name: test_sensor[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].1 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', @@ -152,7 +152,7 @@ 'state': '3', }) # --- -# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].2 +# name: test_sensor[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].2 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', @@ -166,7 +166,7 @@ 'state': '10', }) # --- -# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].3 +# name: test_sensor[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].3 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', @@ -179,7 +179,7 @@ 'state': 'Dummy MOTD', }) # --- -# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].4 +# name: test_sensor[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].4 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', @@ -192,7 +192,7 @@ 'state': 'Dummy Version', }) # --- -# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].5 +# name: test_sensor[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].5 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', @@ -205,7 +205,7 @@ 'state': '123', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1] +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', @@ -219,7 +219,7 @@ 'state': '5', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].1 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].1 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', @@ -233,7 +233,7 @@ 'state': '3', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].2 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].2 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', @@ -247,7 +247,7 @@ 'state': '10', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].3 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].3 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', @@ -260,7 +260,7 @@ 'state': 'Dummy MOTD', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].4 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].4 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', @@ -273,7 +273,7 @@ 'state': 'Dummy Version', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].5 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].5 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', @@ -286,7 +286,7 @@ 'state': '123', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].6 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].6 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Map name', @@ -299,7 +299,7 @@ 'state': 'Dummy Map Name', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].7 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].7 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Game mode', @@ -312,7 +312,7 @@ 'state': 'Dummy Game Mode', }) # --- -# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].8 +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-lookup-status_response1-entity_ids1].8 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Edition', @@ -325,7 +325,7 @@ 'state': 'MCPE', }) # --- -# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0] +# name: test_sensor_update[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', @@ -339,7 +339,7 @@ 'state': '5', }) # --- -# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].1 +# name: test_sensor_update[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].1 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', @@ -358,7 +358,7 @@ 'state': '3', }) # --- -# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].2 +# name: test_sensor_update[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].2 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', @@ -372,7 +372,7 @@ 'state': '10', }) # --- -# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].3 +# name: test_sensor_update[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].3 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', @@ -385,7 +385,7 @@ 'state': 'Dummy MOTD', }) # --- -# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].4 +# name: test_sensor_update[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].4 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', @@ -398,7 +398,7 @@ 'state': 'Dummy Version', }) # --- -# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].5 +# name: test_sensor_update[java_mock_config_entry-JavaServer-async_lookup-status_response0-entity_ids0].5 StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', diff --git a/tests/components/minecraft_server/test_binary_sensor.py b/tests/components/minecraft_server/test_binary_sensor.py index 9fae35b113df89..4db564bc143dc3 100644 --- a/tests/components/minecraft_server/test_binary_sensor.py +++ b/tests/components/minecraft_server/test_binary_sensor.py @@ -22,16 +22,27 @@ @pytest.mark.parametrize( - ("mock_config_entry", "server", "status_response"), + ("mock_config_entry", "server", "lookup_function_name", "status_response"), [ - ("java_mock_config_entry", JavaServer, TEST_JAVA_STATUS_RESPONSE), - ("bedrock_mock_config_entry", BedrockServer, TEST_BEDROCK_STATUS_RESPONSE), + ( + "java_mock_config_entry", + JavaServer, + "async_lookup", + TEST_JAVA_STATUS_RESPONSE, + ), + ( + "bedrock_mock_config_entry", + BedrockServer, + "lookup", + TEST_BEDROCK_STATUS_RESPONSE, + ), ], ) async def test_binary_sensor( hass: HomeAssistant, mock_config_entry: str, server: JavaServer | BedrockServer, + lookup_function_name: str, status_response: JavaStatusResponse | BedrockStatusResponse, request: pytest.FixtureRequest, snapshot: SnapshotAssertion, @@ -41,7 +52,7 @@ async def test_binary_sensor( mock_config_entry.add_to_hass(hass) with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", @@ -53,16 +64,27 @@ async def test_binary_sensor( @pytest.mark.parametrize( - ("mock_config_entry", "server", "status_response"), + ("mock_config_entry", "server", "lookup_function_name", "status_response"), [ - ("java_mock_config_entry", JavaServer, TEST_JAVA_STATUS_RESPONSE), - ("bedrock_mock_config_entry", BedrockServer, TEST_BEDROCK_STATUS_RESPONSE), + ( + "java_mock_config_entry", + JavaServer, + "async_lookup", + TEST_JAVA_STATUS_RESPONSE, + ), + ( + "bedrock_mock_config_entry", + BedrockServer, + "lookup", + TEST_BEDROCK_STATUS_RESPONSE, + ), ], ) async def test_binary_sensor_update( hass: HomeAssistant, mock_config_entry: str, server: JavaServer | BedrockServer, + lookup_function_name: str, status_response: JavaStatusResponse | BedrockStatusResponse, request: pytest.FixtureRequest, snapshot: SnapshotAssertion, @@ -73,7 +95,7 @@ async def test_binary_sensor_update( mock_config_entry.add_to_hass(hass) with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", @@ -88,16 +110,27 @@ async def test_binary_sensor_update( @pytest.mark.parametrize( - ("mock_config_entry", "server", "status_response"), + ("mock_config_entry", "server", "lookup_function_name", "status_response"), [ - ("java_mock_config_entry", JavaServer, TEST_JAVA_STATUS_RESPONSE), - ("bedrock_mock_config_entry", BedrockServer, TEST_BEDROCK_STATUS_RESPONSE), + ( + "java_mock_config_entry", + JavaServer, + "async_lookup", + TEST_JAVA_STATUS_RESPONSE, + ), + ( + "bedrock_mock_config_entry", + BedrockServer, + "lookup", + TEST_BEDROCK_STATUS_RESPONSE, + ), ], ) async def test_binary_sensor_update_failure( hass: HomeAssistant, mock_config_entry: str, server: JavaServer | BedrockServer, + lookup_function_name: str, status_response: JavaStatusResponse | BedrockStatusResponse, request: pytest.FixtureRequest, freezer: FrozenDateTimeFactory, @@ -107,7 +140,7 @@ async def test_binary_sensor_update_failure( mock_config_entry.add_to_hass(hass) with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 785905492c1088..2a0208f2251099 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -41,7 +41,7 @@ async def test_address_validation_failure(hass: HomeAssistant) -> None: "homeassistant.components.minecraft_server.api.BedrockServer.lookup", side_effect=ValueError, ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", side_effect=ValueError, ): result = await hass.config_entries.flow.async_init( @@ -58,7 +58,7 @@ async def test_java_connection_failure(hass: HomeAssistant) -> None: "homeassistant.components.minecraft_server.api.BedrockServer.lookup", side_effect=ValueError, ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), ), patch( "homeassistant.components.minecraft_server.api.JavaServer.async_status", @@ -95,7 +95,7 @@ async def test_java_connection(hass: HomeAssistant) -> None: "homeassistant.components.minecraft_server.api.BedrockServer.lookup", side_effect=ValueError, ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), ), patch( "homeassistant.components.minecraft_server.api.JavaServer.async_status", @@ -138,7 +138,7 @@ async def test_recovery(hass: HomeAssistant) -> None: "homeassistant.components.minecraft_server.api.BedrockServer.lookup", side_effect=ValueError, ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", side_effect=ValueError, ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/minecraft_server/test_diagnostics.py b/tests/components/minecraft_server/test_diagnostics.py index 6979325fa0c8d3..80b5c91c1fb4a1 100644 --- a/tests/components/minecraft_server/test_diagnostics.py +++ b/tests/components/minecraft_server/test_diagnostics.py @@ -42,9 +42,14 @@ async def test_config_entry_diagnostics( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) + if server.__name__ == "JavaServer": + lookup_function_name = "async_lookup" + else: + lookup_function_name = "lookup" + # Setup mock entry. with patch( - f"mcstatus.server.{server.__name__}.lookup", + f"mcstatus.server.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"mcstatus.server.{server.__name__}.async_status", diff --git a/tests/components/minecraft_server/test_init.py b/tests/components/minecraft_server/test_init.py index 018fdac542e9a5..5b0d9509d692df 100644 --- a/tests/components/minecraft_server/test_init.py +++ b/tests/components/minecraft_server/test_init.py @@ -122,7 +122,7 @@ async def test_setup_and_unload_entry( java_mock_config_entry.add_to_hass(hass) with patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), ), patch( "homeassistant.components.minecraft_server.api.JavaServer.async_status", @@ -138,14 +138,14 @@ async def test_setup_and_unload_entry( assert java_mock_config_entry.state == ConfigEntryState.NOT_LOADED -async def test_setup_entry_failure( +async def test_setup_entry_lookup_failure( hass: HomeAssistant, java_mock_config_entry: MockConfigEntry ) -> None: - """Test failed entry setup.""" + """Test lookup failure in entry setup.""" java_mock_config_entry.add_to_hass(hass) with patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", side_effect=ValueError, ): assert not await hass.config_entries.async_setup( @@ -156,6 +156,24 @@ async def test_setup_entry_failure( assert java_mock_config_entry.state == ConfigEntryState.SETUP_ERROR +async def test_setup_entry_init_failure( + hass: HomeAssistant, java_mock_config_entry: MockConfigEntry +) -> None: + """Test init failure in entry setup.""" + java_mock_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.minecraft_server.api.MinecraftServer.async_initialize", + side_effect=None, + ): + assert not await hass.config_entries.async_setup( + java_mock_config_entry.entry_id + ) + + await hass.async_block_till_done() + assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY + + async def test_setup_entry_not_ready( hass: HomeAssistant, java_mock_config_entry: MockConfigEntry ) -> None: @@ -163,7 +181,7 @@ async def test_setup_entry_not_ready( java_mock_config_entry.add_to_hass(hass) with patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), ), patch( "homeassistant.components.minecraft_server.api.JavaServer.async_status", @@ -196,7 +214,7 @@ async def test_entry_migration( # Trigger migration. with patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", side_effect=[ ValueError, # async_migrate_entry JavaServer(host=TEST_HOST, port=TEST_PORT), # async_migrate_entry @@ -258,7 +276,7 @@ async def test_entry_migration_host_only( # Trigger migration. with patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), ), patch( "homeassistant.components.minecraft_server.api.JavaServer.async_status", @@ -293,7 +311,7 @@ async def test_entry_migration_v3_failure( # Trigger migration. with patch( - "homeassistant.components.minecraft_server.api.JavaServer.lookup", + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", side_effect=[ ValueError, # async_migrate_entry ValueError, # async_migrate_entry diff --git a/tests/components/minecraft_server/test_sensor.py b/tests/components/minecraft_server/test_sensor.py index 006c735e034ef7..7d599669d71e27 100644 --- a/tests/components/minecraft_server/test_sensor.py +++ b/tests/components/minecraft_server/test_sensor.py @@ -55,17 +55,25 @@ @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("mock_config_entry", "server", "status_response", "entity_ids"), + ( + "mock_config_entry", + "server", + "lookup_function_name", + "status_response", + "entity_ids", + ), [ ( "java_mock_config_entry", JavaServer, + "async_lookup", TEST_JAVA_STATUS_RESPONSE, JAVA_SENSOR_ENTITIES, ), ( "bedrock_mock_config_entry", BedrockServer, + "lookup", TEST_BEDROCK_STATUS_RESPONSE, BEDROCK_SENSOR_ENTITIES, ), @@ -75,6 +83,7 @@ async def test_sensor( hass: HomeAssistant, mock_config_entry: str, server: JavaServer | BedrockServer, + lookup_function_name: str, status_response: JavaStatusResponse | BedrockStatusResponse, entity_ids: list[str], request: pytest.FixtureRequest, @@ -85,7 +94,7 @@ async def test_sensor( mock_config_entry.add_to_hass(hass) with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", @@ -98,17 +107,25 @@ async def test_sensor( @pytest.mark.parametrize( - ("mock_config_entry", "server", "status_response", "entity_ids"), + ( + "mock_config_entry", + "server", + "lookup_function_name", + "status_response", + "entity_ids", + ), [ ( "java_mock_config_entry", JavaServer, + "async_lookup", TEST_JAVA_STATUS_RESPONSE, JAVA_SENSOR_ENTITIES_DISABLED_BY_DEFAULT, ), ( "bedrock_mock_config_entry", BedrockServer, + "lookup", TEST_BEDROCK_STATUS_RESPONSE, BEDROCK_SENSOR_ENTITIES_DISABLED_BY_DEFAULT, ), @@ -118,6 +135,7 @@ async def test_sensor_disabled_by_default( hass: HomeAssistant, mock_config_entry: str, server: JavaServer | BedrockServer, + lookup_function_name: str, status_response: JavaStatusResponse | BedrockStatusResponse, entity_ids: list[str], request: pytest.FixtureRequest, @@ -127,7 +145,7 @@ async def test_sensor_disabled_by_default( mock_config_entry.add_to_hass(hass) with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", @@ -141,17 +159,25 @@ async def test_sensor_disabled_by_default( @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("mock_config_entry", "server", "status_response", "entity_ids"), + ( + "mock_config_entry", + "server", + "lookup_function_name", + "status_response", + "entity_ids", + ), [ ( "java_mock_config_entry", JavaServer, + "async_lookup", TEST_JAVA_STATUS_RESPONSE, JAVA_SENSOR_ENTITIES, ), ( "bedrock_mock_config_entry", BedrockServer, + "lookup", TEST_BEDROCK_STATUS_RESPONSE, BEDROCK_SENSOR_ENTITIES, ), @@ -161,6 +187,7 @@ async def test_sensor_update( hass: HomeAssistant, mock_config_entry: str, server: JavaServer | BedrockServer, + lookup_function_name: str, status_response: JavaStatusResponse | BedrockStatusResponse, entity_ids: list[str], request: pytest.FixtureRequest, @@ -172,7 +199,7 @@ async def test_sensor_update( mock_config_entry.add_to_hass(hass) with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", @@ -189,17 +216,25 @@ async def test_sensor_update( @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("mock_config_entry", "server", "status_response", "entity_ids"), + ( + "mock_config_entry", + "server", + "lookup_function_name", + "status_response", + "entity_ids", + ), [ ( "java_mock_config_entry", JavaServer, + "async_lookup", TEST_JAVA_STATUS_RESPONSE, JAVA_SENSOR_ENTITIES, ), ( "bedrock_mock_config_entry", BedrockServer, + "lookup", TEST_BEDROCK_STATUS_RESPONSE, BEDROCK_SENSOR_ENTITIES, ), @@ -209,6 +244,7 @@ async def test_sensor_update_failure( hass: HomeAssistant, mock_config_entry: str, server: JavaServer | BedrockServer, + lookup_function_name: str, status_response: JavaStatusResponse | BedrockStatusResponse, entity_ids: list[str], request: pytest.FixtureRequest, @@ -219,7 +255,7 @@ async def test_sensor_update_failure( mock_config_entry.add_to_hass(hass) with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", return_value=server(host=TEST_HOST, port=TEST_PORT), ), patch( f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", From 1cc47c0553562d295ebec2d9671454824551947a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 10 Dec 2023 17:37:57 +1000 Subject: [PATCH 011/118] Add reauth to Tessie (#105419) * First pass at Tessie * Working POC * async_step_reauth * Config Flow tests * WIP * Add test requirement * correctly gen test requirements * 100% coverage * Remove remnants of copy paste * Add TPMS * Fix docstring * Remove redundant line * Fix some more copied docstrings * Grammar * Create reusable StrEnum * Streamline get * Add a couple more sensors * Removed need for a model * Move MODELS * Remove DOMAIN from config flow * Add translation strings * Remove unused parameter * Simplify error handling * Refactor coordinator to class * Add missing types * Add icon to shift state * Simplify setdefault Co-authored-by: J. Nick Koston * Use walrus for async_unload_platforms Co-authored-by: J. Nick Koston * Reformat entity init Co-authored-by: J. Nick Koston * Remove Unique ID * Better Config Flow Tests * Fix all remaining tests * Standardise docstring * Remove redudnant test * Set TessieDataUpdateCoordinator on entity * Correct some sensors * add error types * Make shift state a ENUM sensor * Add more sensors * Fix translation string * Add precision suggestions * Move session from init to coordinator * Add api_key type * Remove api_key parameter * Meta changes * Update TessieEntity and TessieSensor translations * Goodbye translation keys * bump tessie-api to 0.0.9 * Fix only_active to be True * Per vehicle coordinator * Rework coordinator * Fix coverage * WIP * The grand rework * Add comments * Use ENUM more * Add ENUM translations * Update homeassistant/components/tessie/sensor.py Co-authored-by: J. Nick Koston * Add entity_category * Remove reauth * Remove session * Update homeassistant/components/tessie/__init__.py Co-authored-by: J. Nick Koston * Add property tag * Add error text * Complete config flow tests * Fix property and rename * Fix init test * Reworked coordinator tests * Add extra checks * Update homeassistant/components/tessie/__init__.py Co-authored-by: J. Nick Koston * Update homeassistant/components/tessie/coordinator.py Co-authored-by: J. Nick Koston * Apply suggestions from code review Co-authored-by: J. Nick Koston * Ruff fix * Update homeassistant/components/tessie/config_flow.py Co-authored-by: J. Nick Koston * Remove future ENUMs Co-authored-by: J. Nick Koston * Ruff fix * Update homeassistant/components/tessie/config_flow.py Co-authored-by: J. Nick Koston * Remove reauth and already configured strings * Lowercase sensor values for translation * Update homeassistant/components/tessie/__init__.py Co-authored-by: J. Nick Koston * Fixed, before using lambda * Add value lambda * fix lambda * Fix config flow test * @bdraco fix for 500 errors * format * Add reauth * Reuse string in reauth * Ruff * remove redundant check * Improve error tests --------- Co-authored-by: J. Nick Koston --- homeassistant/components/tessie/__init__.py | 10 +- .../components/tessie/config_flow.py | 48 ++++++++- .../components/tessie/coordinator.py | 5 +- homeassistant/components/tessie/strings.json | 7 ++ tests/components/tessie/test_config_flow.py | 101 ++++++++++++++++++ tests/components/tessie/test_coordinator.py | 12 +++ tests/components/tessie/test_init.py | 9 +- 7 files changed, 185 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index e792780e873ec1..ac77c3cc09e7c3 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -1,4 +1,5 @@ """Tessie integration.""" +from http import HTTPStatus import logging from aiohttp import ClientError, ClientResponseError @@ -7,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -28,9 +29,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_key=api_key, only_active=True, ) - except ClientResponseError as ex: - # Reauth will go here - _LOGGER.error("Setup failed, unable to connect to Tessie: %s", ex) + except ClientResponseError as e: + if e.status == HTTPStatus.UNAUTHORIZED: + raise ConfigEntryAuthFailed from e + _LOGGER.error("Setup failed, unable to connect to Tessie: %s", e) return False except ClientError as e: raise ConfigEntryNotReady from e diff --git a/homeassistant/components/tessie/config_flow.py b/homeassistant/components/tessie/config_flow.py index c286f43c8b39b0..4379a810309c8a 100644 --- a/homeassistant/components/tessie/config_flow.py +++ b/homeassistant/components/tessie/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -24,12 +25,16 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize.""" + self._reauth_entry: ConfigEntry | None = None + async def async_step_user( self, user_input: Mapping[str, Any] | None = None ) -> FlowResult: """Get configuration from the user.""" errors: dict[str, str] = {} - if user_input and CONF_ACCESS_TOKEN in user_input: + if user_input: try: await get_state_of_all_vehicles( session=async_get_clientsession(self.hass), @@ -54,3 +59,44 @@ async def async_step_user( data_schema=TESSIE_SCHEMA, errors=errors, ) + + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + """Handle re-auth.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: + """Get update API Key from the user.""" + errors: dict[str, str] = {} + assert self._reauth_entry + if user_input: + try: + await get_state_of_all_vehicles( + session=async_get_clientsession(self.hass), + api_key=user_input[CONF_ACCESS_TOKEN], + ) + except ClientResponseError as e: + if e.status == HTTPStatus.UNAUTHORIZED: + errors["base"] = "invalid_access_token" + else: + errors["base"] = "unknown" + except ClientConnectionError: + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=TESSIE_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 7a1efb985ee41c..7a2a8c71c5676b 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -8,6 +8,7 @@ from tessie_api import get_state from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -57,7 +58,9 @@ async def async_update_data(self) -> dict[str, Any]: # Vehicle is offline, only update state and dont throw error self.data["state"] = TessieStatus.OFFLINE return self.data - # Reauth will go here + if e.status == HTTPStatus.UNAUTHORIZED: + # Auth Token is no longer valid + raise ConfigEntryAuthFailed from e raise e self.did_first_update = True diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 2069e46cecc378..5d57075241ca0f 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -11,6 +11,13 @@ "access_token": "[%key:common::config_flow::data::access_token%]" }, "description": "Enter your access token from [my.tessie.com/settings/api](https://my.tessie.com/settings/api)." + }, + "reauth_confirm": { + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + }, + "description": "[%key:component::tessie::config::step::user::description%]", + "title": "[%key:common::config_flow::title::reauth%]" } } }, diff --git a/tests/components/tessie/test_config_flow.py b/tests/components/tessie/test_config_flow.py index edf2f8914ae801..d1977a1319359b 100644 --- a/tests/components/tessie/test_config_flow.py +++ b/tests/components/tessie/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant import config_entries from homeassistant.components.tessie.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN @@ -14,8 +16,21 @@ ERROR_UNKNOWN, TEST_CONFIG, TEST_STATE_OF_ALL_VEHICLES, + setup_platform, ) +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_get_state_of_all_vehicles(): + """Mock get_state_of_all_vehicles function.""" + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + ) as mock_get_state_of_all_vehicles: + yield mock_get_state_of_all_vehicles + async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -137,3 +152,89 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: TEST_CONFIG, ) assert result3["type"] == FlowResultType.CREATE_ENTRY + + +async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None: + """Test reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + data=TEST_CONFIG, + ) + + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "reauth_confirm" + assert not result1["errors"] + + with patch( + "homeassistant.components.tessie.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, + ) + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == TEST_CONFIG + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (ERROR_AUTH, {"base": "invalid_access_token"}), + (ERROR_UNKNOWN, {"base": "unknown"}), + (ERROR_CONNECTION, {"base": "cannot_connect"}), + ], +) +async def test_reauth_errors( + hass: HomeAssistant, mock_get_state_of_all_vehicles, side_effect, error +) -> None: + """Test reauth flows that failscript/.""" + + mock_entry = await setup_platform(hass) + mock_get_state_of_all_vehicles.side_effect = side_effect + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=TEST_CONFIG, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_CONFIG, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == error + + # Complete the flow + mock_get_state_of_all_vehicles.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + TEST_CONFIG, + ) + assert "errors" not in result3 + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "reauth_successful" + assert mock_entry.data == TEST_CONFIG diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 43b6489c39d3c4..8fe92454c36175 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -11,6 +11,7 @@ from homeassistant.util.dt import utcnow from .common import ( + ERROR_AUTH, ERROR_CONNECTION, ERROR_TIMEOUT, ERROR_UNKNOWN, @@ -81,6 +82,17 @@ async def test_coordinator_timeout(hass: HomeAssistant, mock_get_state) -> None: assert hass.states.get("sensor.test_status").state == TessieStatus.OFFLINE +async def test_coordinator_auth(hass: HomeAssistant, mock_get_state) -> None: + """Tests that the coordinator handles timeout errors.""" + + mock_get_state.side_effect = ERROR_AUTH + await setup_platform(hass) + + async_fire_time_changed(hass, utcnow() + WAIT) + await hass.async_block_till_done() + mock_get_state.assert_called_once() + + async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> None: """Tests that the coordinator handles connection errors.""" diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index 409ece97a24ec4..8c12979b9d56b2 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -3,7 +3,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from .common import ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform +from .common import ERROR_AUTH, ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform async def test_load_unload(hass: HomeAssistant) -> None: @@ -16,6 +16,13 @@ async def test_load_unload(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED +async def test_auth_failure(hass: HomeAssistant) -> None: + """Test init with an authentication failure.""" + + entry = await setup_platform(hass, side_effect=ERROR_AUTH) + assert entry.state is ConfigEntryState.SETUP_ERROR + + async def test_unknown_failure(hass: HomeAssistant) -> None: """Test init with an authentication failure.""" From ff85d0c290707607b6bf0615d81a47cf907671eb Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 10 Dec 2023 09:25:16 +0100 Subject: [PATCH 012/118] Migrate mqtt tests to use freezegun (#105414) --- tests/components/mqtt/test_common.py | 15 +++---- tests/components/mqtt/test_init.py | 67 ++++++++++++++-------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 0664f6e8d6f2c1..cb5ff53d7e905c 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import ANY, MagicMock, patch +from freezegun import freeze_time import pytest import voluptuous as vol import yaml @@ -31,6 +32,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_mqtt_message from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient @@ -1320,9 +1322,8 @@ async def help_test_entity_debug_info_max_messages( "subscriptions" ] - start_dt = datetime(2019, 1, 1, 0, 0, 0) - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt + start_dt = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(start_dt): for i in range(0, debug_info.STORED_MESSAGES + 1): async_fire_mqtt_message(hass, "test-topic", f"{i}") @@ -1396,7 +1397,7 @@ async def help_test_entity_debug_info_message( debug_info_data = debug_info.info_for_device(hass, device.id) - start_dt = datetime(2019, 1, 1, 0, 0, 0) + start_dt = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) if state_topic is not None: assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 @@ -1404,8 +1405,7 @@ async def help_test_entity_debug_info_message( "subscriptions" ] - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt + with freeze_time(start_dt): async_fire_mqtt_message(hass, str(state_topic), state_payload) debug_info_data = debug_info.info_for_device(hass, device.id) @@ -1426,8 +1426,7 @@ async def help_test_entity_debug_info_message( expected_transmissions = [] if service: # Trigger an outgoing MQTT message - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt + with freeze_time(start_dt): if service: service_data = {ATTR_ENTITY_ID: f"{domain}.beer_test"} if service_parameters: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index d31570548f0e69..98e2c9b71fe4a8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -8,6 +8,7 @@ from typing import Any, TypedDict from unittest.mock import ANY, MagicMock, call, mock_open, patch +from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol @@ -40,6 +41,7 @@ from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow from .test_common import help_all_subscribe_calls @@ -3256,6 +3258,7 @@ async def test_debug_info_wildcard( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test debug info.""" await mqtt_mock_entry() @@ -3279,10 +3282,9 @@ async def test_debug_info_wildcard( "subscriptions" ] - start_dt = datetime(2019, 1, 1, 0, 0, 0) - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt - async_fire_mqtt_message(hass, "sensor/abc", "123") + start_dt = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) + freezer.move_to(start_dt) + async_fire_mqtt_message(hass, "sensor/abc", "123") debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 @@ -3304,6 +3306,7 @@ async def test_debug_info_filter_same( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test debug info removes messages with same timestamp.""" await mqtt_mock_entry() @@ -3327,14 +3330,13 @@ async def test_debug_info_filter_same( "subscriptions" ] - dt1 = datetime(2019, 1, 1, 0, 0, 0) - dt2 = datetime(2019, 1, 1, 0, 0, 1) - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = dt1 - async_fire_mqtt_message(hass, "sensor/abc", "123") - async_fire_mqtt_message(hass, "sensor/abc", "123") - dt_utcnow.return_value = dt2 - async_fire_mqtt_message(hass, "sensor/abc", "123") + dt1 = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) + dt2 = datetime(2019, 1, 1, 0, 0, 1, tzinfo=dt_util.UTC) + freezer.move_to(dt1) + async_fire_mqtt_message(hass, "sensor/abc", "123") + async_fire_mqtt_message(hass, "sensor/abc", "123") + freezer.move_to(dt2) + async_fire_mqtt_message(hass, "sensor/abc", "123") debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 @@ -3364,6 +3366,7 @@ async def test_debug_info_same_topic( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test debug info.""" await mqtt_mock_entry() @@ -3388,10 +3391,9 @@ async def test_debug_info_same_topic( "subscriptions" ] - start_dt = datetime(2019, 1, 1, 0, 0, 0) - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt - async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) + start_dt = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) + freezer.move_to(start_dt) + async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 @@ -3408,16 +3410,16 @@ async def test_debug_info_same_topic( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - start_dt = datetime(2019, 1, 1, 0, 0, 0) - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt - async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) + start_dt = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) + freezer.move_to(start_dt) + async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) async def test_debug_info_qos_retain( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test debug info.""" await mqtt_mock_entry() @@ -3441,19 +3443,18 @@ async def test_debug_info_qos_retain( "subscriptions" ] - start_dt = datetime(2019, 1, 1, 0, 0, 0) - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt - # simulate the first message was replayed from the broker with retained flag - async_fire_mqtt_message(hass, "sensor/abc", "123", qos=0, retain=True) - # simulate an update message - async_fire_mqtt_message(hass, "sensor/abc", "123", qos=0, retain=False) - # simpulate someone else subscribed and retained messages were replayed - async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=True) - # simulate an update message - async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=False) - # simulate an update message - async_fire_mqtt_message(hass, "sensor/abc", "123", qos=2, retain=False) + start_dt = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) + freezer.move_to(start_dt) + # simulate the first message was replayed from the broker with retained flag + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=0, retain=True) + # simulate an update message + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=0, retain=False) + # simpulate someone else subscribed and retained messages were replayed + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=True) + # simulate an update message + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=False) + # simulate an update message + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=2, retain=False) debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 From 6a3c422d2f2313d9558f6d2cf1758092320b897c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Dec 2023 13:38:10 +0100 Subject: [PATCH 013/118] Improve Amazon Alexa endpoint validation (#105287) * Improve Amazon Alexa endpoint validation * Add source comment --------- Co-authored-by: Jan Bouwhuis --- homeassistant/components/alexa/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 219553b3563873..2a9637772b114a 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -36,6 +36,15 @@ CONF_SMART_HOME = "smart_home" DEFAULT_LOCALE = "en-US" +# Alexa Smart Home API send events gateway endpoints +# https://developer.amazon.com/en-US/docs/alexa/smarthome/send-events.html#endpoints +VALID_ENDPOINTS = [ + "https://api.amazonalexa.com/v3/events", + "https://api.eu.amazonalexa.com/v3/events", + "https://api.fe.amazonalexa.com/v3/events", +] + + ALEXA_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_DESCRIPTION): cv.string, @@ -46,7 +55,7 @@ SMART_HOME_SCHEMA = vol.Schema( { - vol.Optional(CONF_ENDPOINT): cv.string, + vol.Optional(CONF_ENDPOINT): vol.All(vol.Lower, vol.In(VALID_ENDPOINTS)), vol.Optional(CONF_CLIENT_ID): cv.string, vol.Optional(CONF_CLIENT_SECRET): cv.string, vol.Optional(CONF_LOCALE, default=DEFAULT_LOCALE): vol.In( From 063ac53f01c9627b067e0fd8a82ace3725c330e5 Mon Sep 17 00:00:00 2001 From: Florian B Date: Sun, 10 Dec 2023 17:23:05 +0100 Subject: [PATCH 014/118] Fix adding/updating todo items with due date in CalDAV integration (#105435) * refactor: return date/datetime for due date * fix: explicitly set due date on vTODO component Using `set_due` automatically handles converting the Python-native date/datetime values to the correct representation required by RFC5545. * fix: fix tests with changed due date handling * fix: item.due may not be a str * refactor: keep local timezone of due datetime * refactor: reorder import statement To make ruff happy. * fix: fix false-positive mypy error --- homeassistant/components/caldav/todo.py | 10 +++++----- tests/components/caldav/test_todo.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/caldav/todo.py b/homeassistant/components/caldav/todo.py index 1bd24dc542af70..b7089c3da65d2c 100644 --- a/homeassistant/components/caldav/todo.py +++ b/homeassistant/components/caldav/todo.py @@ -98,10 +98,7 @@ def _to_ics_fields(item: TodoItem) -> dict[str, Any]: if status := item.status: item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION") if due := item.due: - if isinstance(due, datetime): - item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ") - else: - item_data["due"] = due.strftime("%Y%m%d") + item_data["due"] = due if description := item.description: item_data["description"] = description return item_data @@ -162,7 +159,10 @@ async def async_update_todo_item(self, item: TodoItem) -> None: except (requests.ConnectionError, DAVError) as err: raise HomeAssistantError(f"CalDAV lookup error: {err}") from err vtodo = todo.icalendar_component # type: ignore[attr-defined] - vtodo.update(**_to_ics_fields(item)) + updated_fields = _to_ics_fields(item) + if "due" in updated_fields: + todo.set_due(updated_fields.pop("due")) # type: ignore[attr-defined] + vtodo.update(**updated_fields) try: await self.hass.async_add_executor_job( partial( diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 6e92f211463538..a90529297beb40 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -1,4 +1,5 @@ """The tests for the webdav todo component.""" +from datetime import UTC, date, datetime from typing import Any from unittest.mock import MagicMock, Mock @@ -200,12 +201,16 @@ async def test_supported_components( ), ( {"due_date": "2023-11-18"}, - {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118"}, + {"status": "NEEDS-ACTION", "summary": "Cheese", "due": date(2023, 11, 18)}, {**RESULT_ITEM, "due": "2023-11-18"}, ), ( {"due_datetime": "2023-11-18T08:30:00-06:00"}, - {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118T143000Z"}, + { + "status": "NEEDS-ACTION", + "summary": "Cheese", + "due": datetime(2023, 11, 18, 14, 30, 00, tzinfo=UTC), + }, {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, ), ( @@ -311,13 +316,13 @@ async def test_add_item_failure( ), ( {"due_date": "2023-11-18"}, - ["SUMMARY:Cheese", "DUE:20231118"], + ["SUMMARY:Cheese", "DUE;VALUE=DATE:20231118"], "1", {**RESULT_ITEM, "due": "2023-11-18"}, ), ( {"due_datetime": "2023-11-18T08:30:00-06:00"}, - ["SUMMARY:Cheese", "DUE:20231118T143000Z"], + ["SUMMARY:Cheese", "DUE;TZID=America/Regina:20231118T083000"], "1", {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, ), From a7155b154eb873ea43b0f52da3e21b19bf019a25 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 10 Dec 2023 17:27:01 +0100 Subject: [PATCH 015/118] Fix alexa calling not featured cover services (#105444) * Fix alexa calls not supported cover services * Follow up comment and additional tests --- homeassistant/components/alexa/handlers.py | 19 +- tests/components/alexa/test_smart_home.py | 401 +++++++++++++++++---- 2 files changed, 333 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f99b0231e4d4be..2796c10795b895 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1304,13 +1304,14 @@ async def async_api_set_range( service = None data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} range_value = directive.payload["rangeValue"] + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Cover Position if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": range_value = int(range_value) - if range_value == 0: + if supported & cover.CoverEntityFeature.CLOSE and range_value == 0: service = cover.SERVICE_CLOSE_COVER - elif range_value == 100: + elif supported & cover.CoverEntityFeature.OPEN and range_value == 100: service = cover.SERVICE_OPEN_COVER else: service = cover.SERVICE_SET_COVER_POSITION @@ -1319,9 +1320,9 @@ async def async_api_set_range( # Cover Tilt elif instance == f"{cover.DOMAIN}.tilt": range_value = int(range_value) - if range_value == 0: + if supported & cover.CoverEntityFeature.CLOSE_TILT and range_value == 0: service = cover.SERVICE_CLOSE_COVER_TILT - elif range_value == 100: + elif supported & cover.CoverEntityFeature.OPEN_TILT and range_value == 100: service = cover.SERVICE_OPEN_COVER_TILT else: service = cover.SERVICE_SET_COVER_TILT_POSITION @@ -1332,13 +1333,11 @@ async def async_api_set_range( range_value = int(range_value) if range_value == 0: service = fan.SERVICE_TURN_OFF + elif supported & fan.FanEntityFeature.SET_SPEED: + service = fan.SERVICE_SET_PERCENTAGE + data[fan.ATTR_PERCENTAGE] = range_value else: - supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported and fan.FanEntityFeature.SET_SPEED: - service = fan.SERVICE_SET_PERCENTAGE - data[fan.ATTR_PERCENTAGE] = range_value - else: - service = fan.SERVICE_TURN_ON + service = fan.SERVICE_TURN_ON # Humidifier target humidity elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7a1abe96110a14..0a5b1f79f72a04 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -6,7 +6,7 @@ from homeassistant.components.alexa import smart_home, state_report import homeassistant.components.camera as camera -from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.config import async_process_ha_core_config @@ -1884,7 +1884,91 @@ async def test_group(hass: HomeAssistant) -> None: ) -async def test_cover_position_range(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("position", "position_attr_in_service_call", "supported_features", "service_call"), + [ + ( + 30, + 30, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ( + 0, + None, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.close_cover", + ), + ( + 99, + 99, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ( + 100, + None, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.open_cover", + ), + ( + 0, + 0, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 60, + 60, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 0, + 0, + CoverEntityFeature.SET_POSITION | CoverEntityFeature.OPEN, + "cover.set_cover_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_POSITION | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ], + ids=[ + "position_30_open_close", + "position_0_open_close", + "position_99_open_close", + "position_100_open_close", + "position_0_no_open_close", + "position_60_no_open_close", + "position_100_no_open_close", + "position_0_no_close", + "position_100_no_open", + ], +) +async def test_cover_position( + hass: HomeAssistant, + position: int, + position_attr_in_service_call: int | None, + supported_features: CoverEntityFeature, + service_call: str, +) -> None: """Test cover discovery and position using rangeController.""" device = ( "cover.test_range", @@ -1892,8 +1976,8 @@ async def test_cover_position_range(hass: HomeAssistant) -> None: { "friendly_name": "Test cover range", "device_class": "blind", - "supported_features": 7, - "position": 30, + "supported_features": supported_features, + "position": position, }, ) appliance = await discovery_test(device, hass) @@ -1969,58 +2053,112 @@ async def test_cover_position_range(hass: HomeAssistant) -> None: "range": {"minimumValue": 1, "maximumValue": 100}, } in position_state_mappings - call, _ = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.set_cover_position", - hass, - payload={"rangeValue": 50}, - instance="cover.position", - ) - assert call.data["position"] == 50 - call, msg = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", "cover#test_range", - "cover.close_cover", + service_call, hass, - payload={"rangeValue": 0}, + payload={"rangeValue": position}, instance="cover.position", ) + assert call.data.get("position") == position_attr_in_service_call properties = msg["context"]["properties"][0] assert properties["name"] == "rangeValue" assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 0 + assert properties["value"] == position - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.open_cover", - hass, - payload={"rangeValue": 100}, - instance="cover.position", + +async def test_cover_position_range( + hass: HomeAssistant, +) -> None: + """Test cover discovery and position range using rangeController. + + Also tests an invalid cover position being handled correctly. + """ + + device = ( + "cover.test_range", + "open", + { + "friendly_name": "Test cover range", + "device_class": "blind", + "supported_features": 7, + "position": 30, + }, ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 + appliance = await discovery_test(device, hass) - call, msg = await assert_request_calls_service( + assert appliance["endpointId"] == "cover#test_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover range" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", "Alexa.RangeController", - "AdjustRangeValue", - "cover#test_range", - "cover.open_cover", - hass, - payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, - instance="cover.position", + "Alexa.EndpointHealth", + "Alexa", ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.position" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "rangeValue"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Position", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Opening"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent" + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 100 + assert supported_range["precision"] == 1 + + # Assert for Position Semantics + position_semantics = range_capability["semantics"] + assert position_semantics is not None + + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in position_action_mappings + + position_state_mappings = position_semantics["stateMappings"] + assert position_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in position_state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in position_state_mappings call, msg = await assert_request_calls_service( "Alexa.RangeController", @@ -3435,7 +3573,100 @@ async def test_presence_sensor(hass: HomeAssistant) -> None: assert {"name": "humanPresenceDetectionState"} in properties["supported"] -async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ( + "tilt_position", + "tilt_position_attr_in_service_call", + "supported_features", + "service_call", + ), + [ + ( + 30, + 30, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.set_cover_tilt_position", + ), + ( + 0, + None, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.close_cover_tilt", + ), + ( + 99, + 99, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.set_cover_tilt_position", + ), + ( + 100, + None, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.open_cover_tilt", + ), + ( + 0, + 0, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 60, + 60, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 0, + 0, + CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.OPEN_TILT, + "cover.set_cover_tilt_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.CLOSE_TILT, + "cover.set_cover_tilt_position", + ), + ], + ids=[ + "tilt_position_30_open_close", + "tilt_position_0_open_close", + "tilt_position_99_open_close", + "tilt_position_100_open_close", + "tilt_position_0_no_open_close", + "tilt_position_60_no_open_close", + "tilt_position_100_no_open_close", + "tilt_position_0_no_close", + "tilt_position_100_no_open", + ], +) +async def test_cover_tilt_position( + hass: HomeAssistant, + tilt_position: int, + tilt_position_attr_in_service_call: int | None, + supported_features: CoverEntityFeature, + service_call: str, +) -> None: """Test cover discovery and tilt position using rangeController.""" device = ( "cover.test_tilt_range", @@ -3443,8 +3674,8 @@ async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: { "friendly_name": "Test cover tilt range", "device_class": "blind", - "supported_features": 240, - "tilt_position": 30, + "supported_features": supported_features, + "tilt_position": tilt_position, }, ) appliance = await discovery_test(device, hass) @@ -3474,58 +3705,74 @@ async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: state_mappings = semantics["stateMappings"] assert state_mappings is not None - call, _ = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_tilt_range", - "cover.set_cover_tilt_position", - hass, - payload={"rangeValue": 50}, - instance="cover.tilt", - ) - assert call.data["tilt_position"] == 50 - call, msg = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", "cover#test_tilt_range", - "cover.close_cover_tilt", + service_call, hass, - payload={"rangeValue": 0}, + payload={"rangeValue": tilt_position}, instance="cover.tilt", ) + assert call.data.get("tilt_position") == tilt_position_attr_in_service_call properties = msg["context"]["properties"][0] assert properties["name"] == "rangeValue" assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 0 + assert properties["value"] == tilt_position - call, msg = await assert_request_calls_service( + +async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: + """Test cover discovery and tilt position range using rangeController. + + Also tests and invalid tilt position being handled correctly. + """ + device = ( + "cover.test_tilt_range", + "open", + { + "friendly_name": "Test cover tilt range", + "device_class": "blind", + "supported_features": 240, + "tilt_position": 30, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_tilt_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover tilt range" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", "Alexa.RangeController", - "SetRangeValue", - "cover#test_tilt_range", - "cover.open_cover_tilt", - hass, - payload={"rangeValue": 100}, - instance="cover.tilt", + "Alexa.EndpointHealth", + "Alexa", ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - call, msg = await assert_request_calls_service( + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.tilt" + + semantics = range_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + + call, _ = await assert_request_calls_service( "Alexa.RangeController", - "AdjustRangeValue", + "SetRangeValue", "cover#test_tilt_range", - "cover.open_cover_tilt", + "cover.set_cover_tilt_position", hass, - payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, + payload={"rangeValue": 50}, instance="cover.tilt", ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 + assert call.data["tilt_position"] == 50 call, msg = await assert_request_calls_service( "Alexa.RangeController", From 4752d37df70263979327ea6e8abdcf80414adca4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Dec 2023 14:09:58 -0800 Subject: [PATCH 016/118] Fix fitbit oauth reauth debug logging (#105450) --- homeassistant/components/fitbit/application_credentials.py | 5 ++++- tests/components/fitbit/test_init.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fitbit/application_credentials.py b/homeassistant/components/fitbit/application_credentials.py index caf0384eca269a..caa47351f45674 100644 --- a/homeassistant/components/fitbit/application_credentials.py +++ b/homeassistant/components/fitbit/application_credentials.py @@ -60,7 +60,10 @@ async def _post(self, data: dict[str, Any]) -> dict[str, Any]: resp.raise_for_status() except aiohttp.ClientResponseError as err: if _LOGGER.isEnabledFor(logging.DEBUG): - error_body = await resp.text() if not session.closed else "" + try: + error_body = await resp.text() + except aiohttp.ClientError: + error_body = "" _LOGGER.debug( "Client response error status=%s, body=%s", err.status, error_body ) diff --git a/tests/components/fitbit/test_init.py b/tests/components/fitbit/test_init.py index b6bf75c1c69b24..3ed3695ff3d5a2 100644 --- a/tests/components/fitbit/test_init.py +++ b/tests/components/fitbit/test_init.py @@ -107,18 +107,21 @@ async def test_token_refresh_success( @pytest.mark.parametrize("token_expiration_time", [12345]) +@pytest.mark.parametrize("closing", [True, False]) async def test_token_requires_reauth( hass: HomeAssistant, integration_setup: Callable[[], Awaitable[bool]], config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, setup_credentials: None, + closing: bool, ) -> None: """Test where token is expired and the refresh attempt requires reauth.""" aioclient_mock.post( OAUTH2_TOKEN, status=HTTPStatus.UNAUTHORIZED, + closing=closing, ) assert not await integration_setup() From b5af987a180a213a74b9ffb3c05ab5acafe808d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 10 Dec 2023 23:16:06 +0100 Subject: [PATCH 017/118] Bump hatasmota to 0.8.0 (#105440) * Bump hatasmota to 0.8.0 * Keep devices with deep sleep support always available * Add tests --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/mixins.py | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/tasmota/test_binary_sensor.py | 29 +++++ tests/components/tasmota/test_common.py | 120 ++++++++++++++++++ tests/components/tasmota/test_cover.py | 36 ++++++ tests/components/tasmota/test_fan.py | 27 ++++ tests/components/tasmota/test_light.py | 27 ++++ tests/components/tasmota/test_sensor.py | 38 ++++++ tests/components/tasmota/test_switch.py | 25 ++++ 11 files changed, 313 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 42fc849a2cf10d..2ce81772774f7a 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.7.3"] + "requirements": ["HATasmota==0.8.0"] } diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index 21030b8c14b07c..48dbe51fd6752a 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -112,8 +112,11 @@ class TasmotaAvailability(TasmotaEntity): def __init__(self, **kwds: Any) -> None: """Initialize the availability mixin.""" - self._available = False super().__init__(**kwds) + if self._tasmota_entity.deep_sleep_enabled: + self._available = True + else: + self._available = False async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" @@ -122,6 +125,8 @@ async def async_added_to_hass(self) -> None: async_subscribe_connection_status(self.hass, self.async_mqtt_connected) ) await super().async_added_to_hass() + if self._tasmota_entity.deep_sleep_enabled: + await self._tasmota_entity.poll_status() async def availability_updated(self, available: bool) -> None: """Handle updated availability.""" @@ -135,6 +140,8 @@ def async_mqtt_connected(self, _: bool) -> None: if not self.hass.is_stopping: if not mqtt_connected(self.hass): self._available = False + elif self._tasmota_entity.deep_sleep_enabled: + self._available = True self.async_write_ha_state() @property diff --git a/requirements_all.txt b/requirements_all.txt index d05699120cda2d..0a3ddf78dc7015 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.7.3 +HATasmota==0.8.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index afe214608ec5ab..5e5b7bbe6136fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -25,7 +25,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.7.3 +HATasmota==0.8.0 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 2bfb4a9d5e2436..d5f1e4d7101709 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -31,6 +31,8 @@ help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -313,6 +315,21 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + config["swn"][0] = "Test" + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.BINARY_SENSOR, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -323,6 +340,18 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.BINARY_SENSOR, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability when deep sleep is enabled.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + config["swn"][0] = "Test" + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.BINARY_SENSOR, config + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index a184f650faea81..1f414cb4e5abb3 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -4,6 +4,7 @@ from unittest.mock import ANY from hatasmota.const import ( + CONF_DEEP_SLEEP, CONF_MAC, CONF_OFFLINE, CONF_ONLINE, @@ -188,6 +189,76 @@ async def help_test_availability_when_connection_lost( assert state.state != STATE_UNAVAILABLE +async def help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + domain, + config, + sensor_config=None, + object_id="tasmota_test", +): + """Test availability after MQTT disconnection when deep sleep is enabled. + + This is a test helper for the TasmotaAvailability mixin. + """ + config[CONF_DEEP_SLEEP] = 1 + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + if sensor_config: + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + # Device online + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + # Disconnected from MQTT server -> state changed to unavailable + mqtt_mock.connected = False + await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state == STATE_UNAVAILABLE + + # Reconnected to MQTT server -> state no longer unavailable + mqtt_mock.connected = True + await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + # Receive LWT again + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_offline(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async def help_test_availability( hass, mqtt_mock, @@ -236,6 +307,55 @@ async def help_test_availability( assert state.state == STATE_UNAVAILABLE +async def help_test_deep_sleep_availability( + hass, + mqtt_mock, + domain, + config, + sensor_config=None, + object_id="tasmota_test", +): + """Test availability when deep sleep is enabled. + + This is a test helper for the TasmotaAvailability mixin. + """ + config[CONF_DEEP_SLEEP] = 1 + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + if sensor_config: + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_offline(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async def help_test_availability_discovery_update( hass, mqtt_mock, diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index e2bdc8b2ca728c..cae65521e21eab 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -22,6 +22,8 @@ help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -663,6 +665,27 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + Platform.COVER, + config, + object_id="test_cover_1", + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -676,6 +699,19 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 2a50e2d43b57c4..05e3151be2e6fe 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -22,6 +22,8 @@ help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -232,6 +234,20 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -243,6 +259,17 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 27b7bd1a82a643..50f11fb7757210 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -22,6 +22,8 @@ help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -1669,6 +1671,21 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (Dimmer) + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.LIGHT, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -1679,6 +1696,16 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.LIGHT, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (Dimmer) + await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.LIGHT, config) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2f50a84ffdd1a1..dc4820779a6671 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -28,6 +28,8 @@ help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -1222,6 +1224,26 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) + await help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + Platform.SENSOR, + config, + sensor_config, + "tasmota_dht11_temperature", + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -1238,6 +1260,22 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) + await help_test_deep_sleep_availability( + hass, + mqtt_mock, + Platform.SENSOR, + config, + sensor_config, + "tasmota_dht11_temperature", + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 54d94b46fe89d1..1a16f372fc98b3 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -20,6 +20,8 @@ help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -158,6 +160,20 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.SWITCH, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -167,6 +183,15 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.SWITCH, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.SWITCH, config) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: From 58d9d0daa5b759424ba399c2437e8bbbda793b60 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Sun, 10 Dec 2023 17:30:24 -0500 Subject: [PATCH 018/118] Add reauth to A. O. Smith integration (#105320) * Add reauth to A. O. Smith integration * Validate reauth uses the same email address * Only show password field during reauth --- .../components/aosmith/config_flow.py | 76 ++++++++++++--- .../components/aosmith/coordinator.py | 5 +- homeassistant/components/aosmith/strings.json | 10 +- tests/components/aosmith/conftest.py | 2 +- tests/components/aosmith/test_config_flow.py | 96 ++++++++++++++++++- 5 files changed, 170 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/aosmith/config_flow.py b/homeassistant/components/aosmith/config_flow.py index 4ee298970702fc..36a1c215d68b31 100644 --- a/homeassistant/components/aosmith/config_flow.py +++ b/homeassistant/components/aosmith/config_flow.py @@ -1,6 +1,7 @@ """Config flow for A. O. Smith integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -22,6 +23,29 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + _reauth_email: str | None + + def __init__(self): + """Start the config flow.""" + self._reauth_email = None + + async def _async_validate_credentials( + self, email: str, password: str + ) -> str | None: + """Validate the credentials. Return an error string, or None if successful.""" + session = aiohttp_client.async_get_clientsession(self.hass) + client = AOSmithAPIClient(email, password, session) + + try: + await client.get_devices() + except AOSmithInvalidCredentialsException: + return "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return "unknown" + + return None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -32,30 +56,56 @@ async def async_step_user( await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() - session = aiohttp_client.async_get_clientsession(self.hass) - client = AOSmithAPIClient( - user_input[CONF_EMAIL], user_input[CONF_PASSWORD], session + error = await self._async_validate_credentials( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] ) - - try: - await client.get_devices() - except AOSmithInvalidCredentialsException: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: + if error is None: return self.async_create_entry( title=user_input[CONF_EMAIL], data=user_input ) + errors["base"] = error + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_EMAIL): str, + vol.Required(CONF_EMAIL, default=self._reauth_email): str, vol.Required(CONF_PASSWORD): str, } ), errors=errors, ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth if the user credentials have changed.""" + self._reauth_email = entry_data[CONF_EMAIL] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user's reauth credentials.""" + errors: dict[str, str] = {} + if user_input is not None and self._reauth_email is not None: + email = self._reauth_email + password = user_input[CONF_PASSWORD] + entry_id = self.context["entry_id"] + + if entry := self.hass.config_entries.async_get_entry(entry_id): + error = await self._async_validate_credentials(email, password) + if error is None: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | user_input, + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + errors["base"] = error + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + description_placeholders={CONF_EMAIL: self._reauth_email}, + errors=errors, + ) diff --git a/homeassistant/components/aosmith/coordinator.py b/homeassistant/components/aosmith/coordinator.py index 80cf85bc59a5a0..bdd144569dd5e8 100644 --- a/homeassistant/components/aosmith/coordinator.py +++ b/homeassistant/components/aosmith/coordinator.py @@ -10,6 +10,7 @@ ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, FAST_INTERVAL, REGULAR_INTERVAL @@ -29,7 +30,9 @@ async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch latest data from API.""" try: devices = await self.client.get_devices() - except (AOSmithInvalidCredentialsException, AOSmithUnknownException) as err: + except AOSmithInvalidCredentialsException as err: + raise ConfigEntryAuthFailed from err + except AOSmithUnknownException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err mode_pending = any( diff --git a/homeassistant/components/aosmith/strings.json b/homeassistant/components/aosmith/strings.json index 157895e04f8e3d..26de264bab9f03 100644 --- a/homeassistant/components/aosmith/strings.json +++ b/homeassistant/components/aosmith/strings.json @@ -7,6 +7,13 @@ "password": "[%key:common::config_flow::data::password%]" }, "description": "Please enter your A. O. Smith credentials." + }, + "reauth_confirm": { + "description": "Please update your password for {email}", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -14,7 +21,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/tests/components/aosmith/conftest.py b/tests/components/aosmith/conftest.py index 509e15024a964e..f0ece65d56f143 100644 --- a/tests/components/aosmith/conftest.py +++ b/tests/components/aosmith/conftest.py @@ -24,7 +24,7 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, data=FIXTURE_USER_INPUT, - unique_id="unique_id", + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], ) diff --git a/tests/components/aosmith/test_config_flow.py b/tests/components/aosmith/test_config_flow.py index ff09f23ccbb04f..5d3e986e05e7c1 100644 --- a/tests/components/aosmith/test_config_flow.py +++ b/tests/components/aosmith/test_config_flow.py @@ -1,15 +1,18 @@ """Test the A. O. Smith config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch +from freezegun.api import FrozenDateTimeFactory from py_aosmith import AOSmithInvalidCredentialsException import pytest from homeassistant import config_entries -from homeassistant.components.aosmith.const import DOMAIN -from homeassistant.const import CONF_EMAIL +from homeassistant.components.aosmith.const import DOMAIN, REGULAR_INTERVAL +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.aosmith.conftest import FIXTURE_USER_INPUT @@ -82,3 +85,90 @@ async def test_form_exception( assert result3["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] assert result3["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow( + freezer: FrozenDateTimeFactory, + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test reauth works.""" + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + mock_client.get_devices.side_effect = AOSmithInvalidCredentialsException( + "Authentication error" + ) + freezer.tick(REGULAR_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", + return_value=[], + ), patch("homeassistant.components.aosmith.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + {CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD]}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_flow_retry( + freezer: FrozenDateTimeFactory, + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test reauth works with retry.""" + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + mock_client.get_devices.side_effect = AOSmithInvalidCredentialsException( + "Authentication error" + ) + freezer.tick(REGULAR_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + # First attempt at reauth - authentication fails again + with patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", + side_effect=AOSmithInvalidCredentialsException("Authentication error"), + ): + result2 = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + {CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD]}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + # Second attempt at reauth - authentication succeeds + with patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", + return_value=[], + ), patch("homeassistant.components.aosmith.async_setup_entry", return_value=True): + result3 = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + {CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD]}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "reauth_successful" From 77c722630ed8a19b9f29afa11c1a9b53fb2b1210 Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Sun, 10 Dec 2023 23:59:54 +0100 Subject: [PATCH 019/118] Check if heat area exists when setting up valve opening and battery sensors in moehlenhoff alpha2 (#105437) Check whether the referenced heat area exists when setting up valve opening and battery sensors --- homeassistant/components/moehlenhoff_alpha2/binary_sensor.py | 1 + homeassistant/components/moehlenhoff_alpha2/sensor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py index 8acc88d83141e9..5cdca72fa55226 100644 --- a/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py +++ b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py @@ -27,6 +27,7 @@ async def async_setup_entry( Alpha2IODeviceBatterySensor(coordinator, io_device_id) for io_device_id, io_device in coordinator.data["io_devices"].items() if io_device["_HEATAREA_ID"] + and io_device["_HEATAREA_ID"] in coordinator.data["heat_areas"] ) diff --git a/homeassistant/components/moehlenhoff_alpha2/sensor.py b/homeassistant/components/moehlenhoff_alpha2/sensor.py index e41c6b041f61be..2c2e44f451d955 100644 --- a/homeassistant/components/moehlenhoff_alpha2/sensor.py +++ b/homeassistant/components/moehlenhoff_alpha2/sensor.py @@ -25,7 +25,7 @@ async def async_setup_entry( Alpha2HeatControlValveOpeningSensor(coordinator, heat_control_id) for heat_control_id, heat_control in coordinator.data["heat_controls"].items() if heat_control["INUSE"] - and heat_control["_HEATAREA_ID"] + and heat_control["_HEATAREA_ID"] in coordinator.data["heat_areas"] and heat_control.get("ACTOR_PERCENT") is not None ) From 72c6eb888593cb8c0882d94130cb4823e7c0a045 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Dec 2023 15:46:27 -0800 Subject: [PATCH 020/118] Bump ical to 6.1.1 (#105462) --- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index d7b16ee3bef7ff..f5a24e07b0cd1a 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==6.1.0"] + "requirements": ["ical==6.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 4c3a8e10a6219c..335a89eab3c4c2 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==6.1.0"] + "requirements": ["ical==6.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0a3ddf78dc7015..eb038bc6c2b320 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1064,7 +1064,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.1.0 +ical==6.1.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e5b7bbe6136fb..44ec0de5322ae1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,7 +842,7 @@ ibeacon-ble==1.0.1 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.1.0 +ical==6.1.1 # homeassistant.components.ping icmplib==3.0 From c634e3f0ca3baf725beb165b851b537d65e20e84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Dec 2023 20:47:53 -1000 Subject: [PATCH 021/118] Bump zeroconf to 0.128.4 (#105465) * Bump zeroconf to 0.128.3 significant bug fixes changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.128.0...0.128.3 * .4 --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 8351212f0b819b..6738431b304d0a 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.128.0"] + "requirements": ["zeroconf==0.128.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a44f93324b60a..2f1373b61d9835 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -58,7 +58,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.128.0 +zeroconf==0.128.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index eb038bc6c2b320..eea1c6243beade 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2832,7 +2832,7 @@ zamg==0.3.3 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.128.0 +zeroconf==0.128.4 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44ec0de5322ae1..fdce9d3c5546d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2127,7 +2127,7 @@ yt-dlp==2023.11.16 zamg==0.3.3 # homeassistant.components.zeroconf -zeroconf==0.128.0 +zeroconf==0.128.4 # homeassistant.components.zeversolar zeversolar==0.3.1 From c89c2f939263327f1683b7d8f534b382efe54bd5 Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Sun, 10 Dec 2023 23:40:13 -0800 Subject: [PATCH 022/118] Bump pylitejet to v0.6.0 (#105472) --- homeassistant/components/litejet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index 136880257ce268..8525bb9ff178d8 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["pylitejet"], "quality_scale": "platinum", - "requirements": ["pylitejet==0.5.0"] + "requirements": ["pylitejet==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index eea1c6243beade..97e0f899ad56c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1872,7 +1872,7 @@ pylgnetcast==0.3.7 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.5.0 +pylitejet==0.6.0 # homeassistant.components.litterrobot pylitterbot==2023.4.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdce9d3c5546d4..184f0eb8cb5fb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1416,7 +1416,7 @@ pylaunches==1.4.0 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.5.0 +pylitejet==0.6.0 # homeassistant.components.litterrobot pylitterbot==2023.4.9 From fbfe434e8baf88e23d30c34aa3ad57b3e4fe0128 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 11 Dec 2023 09:09:23 +0100 Subject: [PATCH 023/118] Migrate tag & tts tests to use freezegun (#105411) --- tests/components/tag/test_event.py | 20 ++++++++++------ tests/components/tag/test_init.py | 11 +++++---- tests/components/tts/test_init.py | 38 +++++++++++++++--------------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/tests/components/tag/test_event.py b/tests/components/tag/test_event.py index 7112a0cda4ffc7..0338ed504d776d 100644 --- a/tests/components/tag/test_event.py +++ b/tests/components/tag/test_event.py @@ -1,6 +1,6 @@ """Tests for the tag component.""" -from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.tag import DOMAIN, EVENT_TAG_SCANNED, async_scan_tag @@ -40,7 +40,10 @@ async def _storage(items=None): async def test_named_tag_scanned_event( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, storage_setup_named_tag + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + storage_setup_named_tag, ) -> None: """Test scanning named tag triggering event.""" assert await storage_setup_named_tag() @@ -50,8 +53,8 @@ async def test_named_tag_scanned_event( events = async_capture_events(hass, EVENT_TAG_SCANNED) now = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=now): - await async_scan_tag(hass, TEST_TAG_ID, TEST_DEVICE_ID) + freezer.move_to(now) + await async_scan_tag(hass, TEST_TAG_ID, TEST_DEVICE_ID) assert len(events) == 1 @@ -83,7 +86,10 @@ async def _storage(items=None): async def test_unnamed_tag_scanned_event( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, storage_setup_unnamed_tag + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + storage_setup_unnamed_tag, ) -> None: """Test scanning named tag triggering event.""" assert await storage_setup_unnamed_tag() @@ -93,8 +99,8 @@ async def test_unnamed_tag_scanned_event( events = async_capture_events(hass, EVENT_TAG_SCANNED) now = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=now): - await async_scan_tag(hass, TEST_TAG_ID, TEST_DEVICE_ID) + freezer.move_to(now) + await async_scan_tag(hass, TEST_TAG_ID, TEST_DEVICE_ID) assert len(events) == 1 diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 5d54f31b13a01d..d7f77c0d2e2b9c 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -1,6 +1,6 @@ """Tests for the tag component.""" -from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.tag import DOMAIN, TAGS, async_scan_tag @@ -76,7 +76,10 @@ async def test_ws_update( async def test_tag_scanned( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, storage_setup + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + storage_setup, ) -> None: """Test scanning tags.""" assert await storage_setup() @@ -93,8 +96,8 @@ async def test_tag_scanned( assert "test tag" in result now = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=now): - await async_scan_tag(hass, "new tag", "some_scanner") + freezer.move_to(now) + await async_scan_tag(hass, "new tag", "some_scanner") await client.send_json({"id": 7, "type": f"{DOMAIN}/list"}) resp = await client.receive_json() diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 5be56edbc32a0f..990d8d273ed2da 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import ffmpeg, tts @@ -78,6 +79,7 @@ async def test_config_entry_unload( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_tts_entity: MockTTSEntity, + freezer: FrozenDateTimeFactory, ) -> None: """Test we can unload config entry.""" entity_id = f"{tts.DOMAIN}.{TEST_DOMAIN}" @@ -93,26 +95,24 @@ async def test_config_entry_unload( calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) now = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=now): - await hass.services.async_call( - tts.DOMAIN, - "speak", - { - ATTR_ENTITY_ID: entity_id, - tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - blocking=True, - ) - assert len(calls) == 1 + freezer.move_to(now) + await hass.services.async_call( + tts.DOMAIN, + "speak", + { + ATTR_ENTITY_ID: entity_id, + tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 - assert ( - await retrieve_media( - hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID] - ) - == HTTPStatus.OK - ) - await hass.async_block_till_done() + assert ( + await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + == HTTPStatus.OK + ) + await hass.async_block_till_done() state = hass.states.get(entity_id) assert state is not None From 3e3f9cf09237bbd2e6c24069e8e51f80e0818c2c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:29:50 +0100 Subject: [PATCH 024/118] Bump plugwise to v0.35.3 (#105442) --- homeassistant/components/plugwise/const.py | 19 ++++++++++++++++++- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 3 +-- homeassistant/components/plugwise/select.py | 3 +-- .../components/plugwise/strings.json | 5 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../all_data.json | 19 ++++++++++++------- .../anna_heatpump_heating/all_data.json | 2 +- .../fixtures/m_adam_cooling/all_data.json | 4 ++-- .../fixtures/m_adam_heating/all_data.json | 4 ++-- .../m_anna_heatpump_cooling/all_data.json | 2 +- .../m_anna_heatpump_idle/all_data.json | 2 +- .../plugwise/snapshots/test_diagnostics.ambr | 9 +++++++-- 14 files changed, 53 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 34bb5c926aef99..f5677c0b4a9d7c 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging -from typing import Final +from typing import Final, Literal from homeassistant.const import Platform @@ -36,6 +36,23 @@ "stretch": "Stretch", } +NumberType = Literal[ + "maximum_boiler_temperature", + "max_dhw_temperature", + "temperature_offset", +] + +SelectType = Literal[ + "select_dhw_mode", + "select_regulation_mode", + "select_schedule", +] +SelectOptionsType = Literal[ + "dhw_modes", + "regulation_modes", + "available_schedules", +] + # Default directives DEFAULT_MAX_TEMP: Final = 30 DEFAULT_MIN_TEMP: Final = 4 diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index bb2b428bf197c3..92923e98d2c23a 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.34.5"], + "requirements": ["plugwise==0.35.3"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 2c87edddf0413d..c21ecbd94c7941 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from plugwise import Smile -from plugwise.constants import NumberType from homeassistant.components.number import ( NumberDeviceClass, @@ -18,7 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, NumberType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index c12ca67155485c..eef873703c188a 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from plugwise import Smile -from plugwise.constants import SelectOptionsType, SelectType from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -13,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, SelectOptionsType, SelectType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 5348a1dc4840cf..addd1ceadb156e 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -108,7 +108,10 @@ } }, "select_schedule": { - "name": "Thermostat schedule" + "name": "Thermostat schedule", + "state": { + "off": "Off" + } } }, "sensor": { diff --git a/requirements_all.txt b/requirements_all.txt index 97e0f899ad56c4..6b75e99ca9e69e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1486,7 +1486,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.5 +plugwise==0.35.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 184f0eb8cb5fb7..08e7c060269b70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.5 +plugwise==0.35.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 279fe6b8a43bfe..f97182782e6bef 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -112,7 +112,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -251,7 +252,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-08-02T02:00:00+02:00", @@ -334,7 +336,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -344,7 +347,7 @@ "model": "Lisa", "name": "Zone Lisa Bios", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "battery": 67, "setpoint": 13.0, @@ -373,7 +376,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "thermostatic_radiator_valve", "firmware": "2019-03-27T01:00:00+01:00", @@ -383,7 +387,7 @@ "model": "Tom/Floor", "name": "CV Kraan Garage", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "battery": 68, "setpoint": 5.5, @@ -414,7 +418,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 9ef93d63bdd3cb..d655f95c79b398 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 2e1063d14d377d..7b570a6cf61f1d 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -52,7 +52,7 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "active_preset": "asleep", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "cooling", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", @@ -102,7 +102,7 @@ "e2f4322d57924fa090fbbc48b3a140dc": { "active_preset": "home", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 81d60bed9d41a1..57259047698035 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -80,7 +80,7 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "active_preset": "asleep", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "preheating", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", @@ -124,7 +124,7 @@ "e2f4322d57924fa090fbbc48b3a140dc": { "active_preset": "home", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index 844eae4c2f7082..92c95f6c5a9dd2 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index f6be6f3518856d..be400b9bc98476 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 29f23a137fb07f..c2bbea9418a10f 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -115,6 +115,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', @@ -260,6 +261,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-08-02T02:00:00+02:00', @@ -349,6 +351,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', @@ -364,7 +367,7 @@ 'vacation', 'no_frost', ]), - 'select_schedule': 'None', + 'select_schedule': 'off', 'sensors': dict({ 'battery': 67, 'setpoint': 13.0, @@ -394,6 +397,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'thermostatic_radiator_valve', 'firmware': '2019-03-27T01:00:00+01:00', @@ -409,7 +413,7 @@ 'vacation', 'no_frost', ]), - 'select_schedule': 'None', + 'select_schedule': 'off', 'sensors': dict({ 'battery': 68, 'setpoint': 5.5, @@ -441,6 +445,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', From b4731674f8128b43117e0557733bb38960364019 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 11 Dec 2023 10:42:16 +0100 Subject: [PATCH 025/118] Migrate octoprint tests to use freezegun (#105408) --- tests/components/octoprint/test_sensor.py | 42 +++++++++++------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index 2ba657c77d5210..3d3efd04da09dc 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -1,6 +1,7 @@ """The tests for Octoptint binary sensor module.""" from datetime import UTC, datetime -from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -8,7 +9,7 @@ from . import init_integration -async def test_sensors(hass: HomeAssistant) -> None: +async def test_sensors(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test the underlying sensors.""" printer = { "state": { @@ -22,11 +23,8 @@ async def test_sensors(hass: HomeAssistant) -> None: "progress": {"completion": 50, "printTime": 600, "printTimeLeft": 6000}, "state": "Printing", } - with patch( - "homeassistant.util.dt.utcnow", - return_value=datetime(2020, 2, 20, 9, 10, 13, 543, tzinfo=UTC), - ): - await init_integration(hass, "sensor", printer=printer, job=job) + freezer.move_to(datetime(2020, 2, 20, 9, 10, 13, 543, tzinfo=UTC)) + await init_integration(hass, "sensor", printer=printer, job=job) entity_registry = er.async_get(hass) @@ -80,7 +78,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry.unique_id == "Estimated Finish Time-uuid" -async def test_sensors_no_target_temp(hass: HomeAssistant) -> None: +async def test_sensors_no_target_temp( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test the underlying sensors.""" printer = { "state": { @@ -89,10 +89,8 @@ async def test_sensors_no_target_temp(hass: HomeAssistant) -> None: }, "temperature": {"tool1": {"actual": 18.83136, "target": None}}, } - with patch( - "homeassistant.util.dt.utcnow", return_value=datetime(2020, 2, 20, 9, 10, 0) - ): - await init_integration(hass, "sensor", printer=printer) + freezer.move_to(datetime(2020, 2, 20, 9, 10, 0)) + await init_integration(hass, "sensor", printer=printer) entity_registry = er.async_get(hass) @@ -111,7 +109,9 @@ async def test_sensors_no_target_temp(hass: HomeAssistant) -> None: assert entry.unique_id == "target tool1 temp-uuid" -async def test_sensors_paused(hass: HomeAssistant) -> None: +async def test_sensors_paused( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test the underlying sensors.""" printer = { "state": { @@ -125,10 +125,8 @@ async def test_sensors_paused(hass: HomeAssistant) -> None: "progress": {"completion": 50, "printTime": 600, "printTimeLeft": 6000}, "state": "Paused", } - with patch( - "homeassistant.util.dt.utcnow", return_value=datetime(2020, 2, 20, 9, 10, 0) - ): - await init_integration(hass, "sensor", printer=printer, job=job) + freezer.move_to(datetime(2020, 2, 20, 9, 10, 0)) + await init_integration(hass, "sensor", printer=printer, job=job) entity_registry = er.async_get(hass) @@ -147,17 +145,17 @@ async def test_sensors_paused(hass: HomeAssistant) -> None: assert entry.unique_id == "Estimated Finish Time-uuid" -async def test_sensors_printer_disconnected(hass: HomeAssistant) -> None: +async def test_sensors_printer_disconnected( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test the underlying sensors.""" job = { "job": {}, "progress": {"completion": 50, "printTime": 600, "printTimeLeft": 6000}, "state": "Paused", } - with patch( - "homeassistant.util.dt.utcnow", return_value=datetime(2020, 2, 20, 9, 10, 0) - ): - await init_integration(hass, "sensor", printer=None, job=job) + freezer.move_to(datetime(2020, 2, 20, 9, 10, 0)) + await init_integration(hass, "sensor", printer=None, job=job) entity_registry = er.async_get(hass) From 47819bde4fb10f54753f6c5445bf94547639d329 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 11 Dec 2023 10:47:51 +0100 Subject: [PATCH 026/118] Migrate sonarr tests to use freezegun (#105410) --- tests/components/sonarr/test_sensor.py | 34 ++++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 9f27e59365785f..e44081f94bf767 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,8 +1,9 @@ """Tests for the Sonarr sensor platform.""" from datetime import timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from aiopyarr import ArrException +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -122,6 +123,7 @@ async def test_disabled_by_default_sensors( async def test_availability( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_config_entry: MockConfigEntry, mock_sonarr: MagicMock, ) -> None: @@ -129,9 +131,9 @@ async def test_availability( now = dt_util.utcnow() mock_config_entry.add_to_hass(hass) - with patch("homeassistant.util.dt.utcnow", return_value=now): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + freezer.move_to(now) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" @@ -140,9 +142,9 @@ async def test_availability( mock_sonarr.async_get_calendar.side_effect = ArrException future = now + timedelta(minutes=1) - with patch("homeassistant.util.dt.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + freezer.move_to(future) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE @@ -151,9 +153,9 @@ async def test_availability( mock_sonarr.async_get_calendar.side_effect = None future += timedelta(minutes=1) - with patch("homeassistant.util.dt.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + freezer.move_to(future) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" @@ -162,9 +164,9 @@ async def test_availability( mock_sonarr.async_get_calendar.side_effect = ArrException future += timedelta(minutes=1) - with patch("homeassistant.util.dt.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + freezer.move_to(future) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE @@ -173,9 +175,9 @@ async def test_availability( mock_sonarr.async_get_calendar.side_effect = None future += timedelta(minutes=1) - with patch("homeassistant.util.dt.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + freezer.move_to(future) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" From 32681acc799df70fe28218fac206a597b1f2078b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 11 Dec 2023 12:09:43 +0100 Subject: [PATCH 027/118] Remove Aftership import issue when entry already exists (#105476) --- .../components/aftership/config_flow.py | 23 +++---------------- .../components/aftership/strings.json | 4 ---- .../components/aftership/test_config_flow.py | 10 +++++--- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/aftership/config_flow.py b/homeassistant/components/aftership/config_flow.py index 3da6ac9e3d5a92..9457809150187a 100644 --- a/homeassistant/components/aftership/config_flow.py +++ b/homeassistant/components/aftership/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -51,25 +51,6 @@ async def async_step_user( async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import configuration from yaml.""" - try: - self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]}) - except AbortFlow as err: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_import_issue_already_configured", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_already_configured", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "AfterShip", - }, - ) - raise err - async_create_issue( self.hass, HOMEASSISTANT_DOMAIN, @@ -84,6 +65,8 @@ async def async_step_import(self, config: dict[str, Any]) -> FlowResult: "integration_title": "AfterShip", }, ) + + self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]}) return self.async_create_entry( title=config.get(CONF_NAME, "AfterShip"), data={CONF_API_KEY: config[CONF_API_KEY]}, diff --git a/homeassistant/components/aftership/strings.json b/homeassistant/components/aftership/strings.json index b49c19976a6354..ace8eb6d2d3a0b 100644 --- a/homeassistant/components/aftership/strings.json +++ b/homeassistant/components/aftership/strings.json @@ -49,10 +49,6 @@ } }, "issues": { - "deprecated_yaml_import_issue_already_configured": { - "title": "The {integration_title} YAML configuration import failed", - "description": "Configuring {integration_title} using YAML is being removed but the YAML configuration was already imported.\n\nRemove the YAML configuration and restart Home Assistant." - }, "deprecated_yaml_import_issue_cannot_connect": { "title": "The {integration_title} YAML configuration import failed", "description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." diff --git a/tests/components/aftership/test_config_flow.py b/tests/components/aftership/test_config_flow.py index 2ac5919a5555ad..4668e7a61e4f24 100644 --- a/tests/components/aftership/test_config_flow.py +++ b/tests/components/aftership/test_config_flow.py @@ -77,7 +77,9 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non } -async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None: +async def test_import_flow( + hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_setup_entry +) -> None: """Test importing yaml config.""" with patch( @@ -95,11 +97,12 @@ async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None: assert result["data"] == { CONF_API_KEY: "yaml-api-key", } - issue_registry = ir.async_get(hass) assert len(issue_registry.issues) == 1 -async def test_import_flow_already_exists(hass: HomeAssistant) -> None: +async def test_import_flow_already_exists( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: """Test importing yaml config where entry already exists.""" entry = MockConfigEntry(domain=DOMAIN, data={CONF_API_KEY: "yaml-api-key"}) entry.add_to_hass(hass) @@ -108,3 +111,4 @@ async def test_import_flow_already_exists(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + assert len(issue_registry.issues) == 1 From cedac41407f9502e57f768b729f03c6032372d41 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Mon, 11 Dec 2023 13:18:23 +0100 Subject: [PATCH 028/118] Bump python-holidays to 0.38 (#105482) --- 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 f73577bddee1fc..50536bc201d0f9 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.37", "babel==2.13.1"] + "requirements": ["holidays==0.38", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index dd2df87234f7a5..92face1ecdb9b7 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.37"] + "requirements": ["holidays==0.38"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6b75e99ca9e69e..7f57fdf2a0462a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1021,7 +1021,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.37 +holidays==0.38 # homeassistant.components.frontend home-assistant-frontend==20231208.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08e7c060269b70..b993ee71a64a3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.37 +holidays==0.38 # homeassistant.components.frontend home-assistant-frontend==20231208.2 From c0314cd05c53401ad80d12ca667ea6937645cac2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Dec 2023 14:06:29 +0100 Subject: [PATCH 029/118] Make Workday UI setup nicer (#105407) --- homeassistant/components/workday/config_flow.py | 10 +++++----- homeassistant/components/workday/strings.json | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 348bb0c2fbacfb..9ae319772768cf 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -155,7 +155,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: DATA_SCHEMA_OPT = vol.Schema( { - vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): SelectSelector( + vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): SelectSelector( SelectSelectorConfig( options=ALLOWED_DAYS, multiple=True, @@ -163,10 +163,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: translation_key="days", ) ), - vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): NumberSelector( - NumberSelectorConfig(min=-10, max=10, step=1, mode=NumberSelectorMode.BOX) - ), - vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): SelectSelector( + vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): SelectSelector( SelectSelectorConfig( options=ALLOWED_DAYS, multiple=True, @@ -174,6 +171,9 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: translation_key="days", ) ), + vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): NumberSelector( + NumberSelectorConfig(min=-10, max=10, step=1, mode=NumberSelectorMode.BOX) + ), vol.Optional(CONF_ADD_HOLIDAYS, default=[]): SelectSelector( SelectSelectorConfig( options=[], diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index 20e7cd26fd6701..7e8439af5ea6f6 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -23,13 +23,13 @@ "language": "Language for named holidays" }, "data_description": { - "excludes": "List of workdays to exclude", - "days_offset": "Days offset", - "workdays": "List of workdays", + "excludes": "List of workdays to exclude, notice the keyword `holiday` and read the documentation on how to use it correctly", + "days_offset": "Days offset from current day", + "workdays": "List of working days", "add_holidays": "Add custom holidays as YYYY-MM-DD or as range using `,` as separator", "remove_holidays": "Remove holidays as YYYY-MM-DD, as range using `,` as separator or by using partial of name", - "province": "State, Territory, Province, Region of Country", - "language": "Choose the language you want to configure named holidays after" + "province": "State, territory, province or region of country", + "language": "Language to use when configuring named holiday exclusions" } } }, From 1242456ff1fc8f70ff48503f91d6d54d9a46cfbc Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 11 Dec 2023 17:47:26 +0300 Subject: [PATCH 030/118] Bump openai end switch from dall-e-2 to dall-e-3 (#104998) * Bump openai * Fix tests * Apply suggestions from code review * Undo conftest changes * Raise repasir issue * Explicitly use async mock for chat.completions.create It is not always detected correctly as async because it uses a decorator * removed duplicated message * ruff * Compatibility with old pydantic versions * Compatibility with old pydantic versions * More tests * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Apply suggestions from code review --------- Co-authored-by: Paulus Schoutsen --- .../openai_conversation/__init__.py | 71 ++++--- .../openai_conversation/config_flow.py | 10 +- .../openai_conversation/manifest.json | 2 +- .../openai_conversation/services.yaml | 30 ++- .../openai_conversation/strings.json | 14 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../openai_conversation/conftest.py | 2 +- .../openai_conversation/test_config_flow.py | 23 ++- .../openai_conversation/test_init.py | 178 +++++++++++++++--- 10 files changed, 266 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 054ccbdbe37c45..b0762979ca217c 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -1,12 +1,10 @@ """The OpenAI Conversation integration.""" from __future__ import annotations -from functools import partial import logging from typing import Literal import openai -from openai import error import voluptuous as vol from homeassistant.components import conversation @@ -23,7 +21,13 @@ HomeAssistantError, TemplateError, ) -from homeassistant.helpers import config_validation as cv, intent, selector, template +from homeassistant.helpers import ( + config_validation as cv, + intent, + issue_registry as ir, + selector, + template, +) from homeassistant.helpers.typing import ConfigType from homeassistant.util import ulid @@ -52,17 +56,38 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def render_image(call: ServiceCall) -> ServiceResponse: """Render an image with dall-e.""" + client = hass.data[DOMAIN][call.data["config_entry"]] + + if call.data["size"] in ("256", "512", "1024"): + ir.async_create_issue( + hass, + DOMAIN, + "image_size_deprecated_format", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + is_persistent=True, + learn_more_url="https://www.home-assistant.io/integrations/openai_conversation/", + severity=ir.IssueSeverity.WARNING, + translation_key="image_size_deprecated_format", + ) + size = "1024x1024" + else: + size = call.data["size"] + try: - response = await openai.Image.acreate( - api_key=hass.data[DOMAIN][call.data["config_entry"]], + response = await client.images.generate( + model="dall-e-3", prompt=call.data["prompt"], + size=size, + quality=call.data["quality"], + style=call.data["style"], + response_format="url", n=1, - size=f'{call.data["size"]}x{call.data["size"]}', ) - except error.OpenAIError as err: + except openai.OpenAIError as err: raise HomeAssistantError(f"Error generating image: {err}") from err - return response["data"][0] + return response.data[0].model_dump(exclude={"b64_json"}) hass.services.async_register( DOMAIN, @@ -76,7 +101,11 @@ async def render_image(call: ServiceCall) -> ServiceResponse: } ), vol.Required("prompt"): cv.string, - vol.Optional("size", default="512"): vol.In(("256", "512", "1024")), + vol.Optional("size", default="1024x1024"): vol.In( + ("1024x1024", "1024x1792", "1792x1024", "256", "512", "1024") + ), + vol.Optional("quality", default="standard"): vol.In(("standard", "hd")), + vol.Optional("style", default="vivid"): vol.In(("vivid", "natural")), } ), supports_response=SupportsResponse.ONLY, @@ -86,21 +115,16 @@ async def render_image(call: ServiceCall) -> ServiceResponse: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenAI Conversation from a config entry.""" + client = openai.AsyncOpenAI(api_key=entry.data[CONF_API_KEY]) try: - await hass.async_add_executor_job( - partial( - openai.Model.list, - api_key=entry.data[CONF_API_KEY], - request_timeout=10, - ) - ) - except error.AuthenticationError as err: + await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list) + except openai.AuthenticationError as err: _LOGGER.error("Invalid API key: %s", err) return False - except error.OpenAIError as err: + except openai.OpenAIError as err: raise ConfigEntryNotReady(err) from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data[CONF_API_KEY] + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry)) return True @@ -160,9 +184,10 @@ async def async_process( _LOGGER.debug("Prompt for %s: %s", model, messages) + client = self.hass.data[DOMAIN][self.entry.entry_id] + try: - result = await openai.ChatCompletion.acreate( - api_key=self.entry.data[CONF_API_KEY], + result = await client.chat.completions.create( model=model, messages=messages, max_tokens=max_tokens, @@ -170,7 +195,7 @@ async def async_process( temperature=temperature, user=conversation_id, ) - except error.OpenAIError as err: + except openai.OpenAIError as err: intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, @@ -181,7 +206,7 @@ async def async_process( ) _LOGGER.debug("Response %s", result) - response = result["choices"][0]["message"] + response = result.choices[0].message.model_dump(include={"role", "content"}) messages.append(response) self.history[conversation_id] = messages diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index 9c5ef32d796ad6..ef1e498d061c08 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -1,14 +1,12 @@ """Config flow for OpenAI Conversation integration.""" from __future__ import annotations -from functools import partial import logging import types from types import MappingProxyType from typing import Any import openai -from openai import error import voluptuous as vol from homeassistant import config_entries @@ -59,8 +57,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - openai.api_key = data[CONF_API_KEY] - await hass.async_add_executor_job(partial(openai.Model.list, request_timeout=10)) + client = openai.AsyncOpenAI(api_key=data[CONF_API_KEY]) + await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -81,9 +79,9 @@ async def async_step_user( try: await validate_input(self.hass, user_input) - except error.APIConnectionError: + except openai.APIConnectionError: errors["base"] = "cannot_connect" - except error.AuthenticationError: + except openai.AuthenticationError: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 88d347355e9ed1..5138be96b55e01 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==0.27.2"] + "requirements": ["openai==1.3.8"] } diff --git a/homeassistant/components/openai_conversation/services.yaml b/homeassistant/components/openai_conversation/services.yaml index 81818fb3e71f13..3db71cae38383d 100644 --- a/homeassistant/components/openai_conversation/services.yaml +++ b/homeassistant/components/openai_conversation/services.yaml @@ -11,12 +11,30 @@ generate_image: text: multiline: true size: - required: true - example: "512" - default: "512" + required: false + example: "1024x1024" + default: "1024x1024" + selector: + select: + options: + - "1024x1024" + - "1024x1792" + - "1792x1024" + quality: + required: false + example: "standard" + default: "standard" + selector: + select: + options: + - "standard" + - "hd" + style: + required: false + example: "vivid" + default: "vivid" selector: select: options: - - "256" - - "512" - - "1024" + - "vivid" + - "natural" diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 542fe06dd5687b..1a7d5a03c6532d 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -43,8 +43,22 @@ "size": { "name": "Size", "description": "The size of the image to generate" + }, + "quality": { + "name": "Quality", + "description": "The quality of the image that will be generated" + }, + "style": { + "name": "Style", + "description": "The style of the generated image" } } } + }, + "issues": { + "image_size_deprecated_format": { + "title": "Deprecated size format for image generation service", + "description": "OpenAI is now using Dall-E 3 to generate images when calling `openai_conversation.generate_image`, which supports different sizes. Valid values are now \"1024x1024\", \"1024x1792\", \"1792x1024\". The old values of \"256\", \"512\", \"1024\" are currently interpreted as \"1024x1024\".\nPlease update your scripts or automations with the new parameters." + } } } diff --git a/requirements_all.txt b/requirements_all.txt index 7f57fdf2a0462a..003d557c73fc52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1393,7 +1393,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==0.27.2 +openai==1.3.8 # homeassistant.components.opencv # opencv-python-headless==4.6.0.66 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b993ee71a64a3f..9bda1b89845faa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1087,7 +1087,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==0.27.2 +openai==1.3.8 # homeassistant.components.openerz openerz-api==0.2.0 diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index 40f2eb33f08347..a83c660e509ca1 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -25,7 +25,7 @@ def mock_config_entry(hass): async def mock_init_component(hass, mock_config_entry): """Initialize integration.""" with patch( - "openai.Model.list", + "openai.resources.models.AsyncModels.list", ): assert await async_setup_component(hass, "openai_conversation", {}) await hass.async_block_till_done() diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 43dfc26ca825f5..dd218e88c126f1 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -1,7 +1,8 @@ """Test the OpenAI Conversation config flow.""" from unittest.mock import patch -from openai.error import APIConnectionError, AuthenticationError, InvalidRequestError +from httpx import Response +from openai import APIConnectionError, AuthenticationError, BadRequestError import pytest from homeassistant import config_entries @@ -32,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: assert result["errors"] is None with patch( - "homeassistant.components.openai_conversation.config_flow.openai.Model.list", + "homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list", ), patch( "homeassistant.components.openai_conversation.async_setup_entry", return_value=True, @@ -76,9 +77,19 @@ async def test_options( @pytest.mark.parametrize( ("side_effect", "error"), [ - (APIConnectionError(""), "cannot_connect"), - (AuthenticationError, "invalid_auth"), - (InvalidRequestError, "unknown"), + (APIConnectionError(request=None), "cannot_connect"), + ( + AuthenticationError( + response=Response(status_code=None, request=""), body=None, message=None + ), + "invalid_auth", + ), + ( + BadRequestError( + response=Response(status_code=None, request=""), body=None, message=None + ), + "unknown", + ), ], ) async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> None: @@ -88,7 +99,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non ) with patch( - "homeassistant.components.openai_conversation.config_flow.openai.Model.list", + "homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list", side_effect=side_effect, ): result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 61fe33e54699d1..d3a06cabeb39a4 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,7 +1,18 @@ """Tests for the OpenAI integration.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -from openai import error +from httpx import Response +from openai import ( + APIConnectionError, + AuthenticationError, + BadRequestError, + RateLimitError, +) +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.completion_usage import CompletionUsage +from openai.types.image import Image +from openai.types.images_response import ImagesResponse import pytest from syrupy.assertion import SnapshotAssertion @@ -9,6 +20,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry as ar, device_registry as dr, intent +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -94,17 +106,30 @@ async def test_default_prompt( suggested_area="Test Area 2", ) with patch( - "openai.ChatCompletion.acreate", - return_value={ - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello, how can I help you?", - } - } - ] - }, + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="Hello, how can I help you?", + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="gpt-3.5-turbo-0613", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ), ) as mock_create: result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id @@ -119,7 +144,11 @@ async def test_error_handling( ) -> None: """Test that the default prompt works.""" with patch( - "openai.ChatCompletion.acreate", side_effect=error.ServiceUnavailableError + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + side_effect=RateLimitError( + response=Response(status_code=None, request=""), body=None, message=None + ), ): result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id @@ -140,8 +169,11 @@ async def test_template_error( }, ) with patch( - "openai.Model.list", - ), patch("openai.ChatCompletion.acreate"): + "openai.resources.models.AsyncModels.list", + ), patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() result = await conversation.async_converse( @@ -169,15 +201,67 @@ async def test_conversation_agent( [ ( {"prompt": "Picture of a dog"}, - {"prompt": "Picture of a dog", "size": "512x512"}, + { + "prompt": "Picture of a dog", + "size": "1024x1024", + "quality": "standard", + "style": "vivid", + }, + ), + ( + { + "prompt": "Picture of a dog", + "size": "1024x1792", + "quality": "hd", + "style": "vivid", + }, + { + "prompt": "Picture of a dog", + "size": "1024x1792", + "quality": "hd", + "style": "vivid", + }, + ), + ( + { + "prompt": "Picture of a dog", + "size": "1792x1024", + "quality": "standard", + "style": "natural", + }, + { + "prompt": "Picture of a dog", + "size": "1792x1024", + "quality": "standard", + "style": "natural", + }, ), ( {"prompt": "Picture of a dog", "size": "256"}, - {"prompt": "Picture of a dog", "size": "256x256"}, + { + "prompt": "Picture of a dog", + "size": "1024x1024", + "quality": "standard", + "style": "vivid", + }, + ), + ( + {"prompt": "Picture of a dog", "size": "512"}, + { + "prompt": "Picture of a dog", + "size": "1024x1024", + "quality": "standard", + "style": "vivid", + }, ), ( {"prompt": "Picture of a dog", "size": "1024"}, - {"prompt": "Picture of a dog", "size": "1024x1024"}, + { + "prompt": "Picture of a dog", + "size": "1024x1024", + "quality": "standard", + "style": "vivid", + }, ), ], ) @@ -190,11 +274,22 @@ async def test_generate_image_service( ) -> None: """Test generate image service.""" service_data["config_entry"] = mock_config_entry.entry_id - expected_args["api_key"] = mock_config_entry.data["api_key"] + expected_args["model"] = "dall-e-3" + expected_args["response_format"] = "url" expected_args["n"] = 1 with patch( - "openai.Image.acreate", return_value={"data": [{"url": "A"}]} + "openai.resources.images.AsyncImages.generate", + return_value=ImagesResponse( + created=1700000000, + data=[ + Image( + b64_json=None, + revised_prompt="A clear and detailed picture of an ordinary canine", + url="A", + ) + ], + ), ) as mock_create: response = await hass.services.async_call( "openai_conversation", @@ -204,7 +299,10 @@ async def test_generate_image_service( return_response=True, ) - assert response == {"url": "A"} + assert response == { + "url": "A", + "revised_prompt": "A clear and detailed picture of an ordinary canine", + } assert len(mock_create.mock_calls) == 1 assert mock_create.mock_calls[0][2] == expected_args @@ -216,7 +314,10 @@ async def test_generate_image_service_error( ) -> None: """Test generate image service handles errors.""" with patch( - "openai.Image.acreate", side_effect=error.ServiceUnavailableError("Reason") + "openai.resources.images.AsyncImages.generate", + side_effect=RateLimitError( + response=Response(status_code=None, request=""), body=None, message="Reason" + ), ), pytest.raises(HomeAssistantError, match="Error generating image: Reason"): await hass.services.async_call( "openai_conversation", @@ -228,3 +329,34 @@ async def test_generate_image_service_error( blocking=True, return_response=True, ) + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (APIConnectionError(request=None), "Connection error"), + ( + AuthenticationError( + response=Response(status_code=None, request=""), body=None, message=None + ), + "Invalid API key", + ), + ( + BadRequestError( + response=Response(status_code=None, request=""), body=None, message=None + ), + "openai_conversation integration not ready yet: None", + ), + ], +) +async def test_init_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, caplog, side_effect, error +) -> None: + """Test initialization errors.""" + with patch( + "openai.resources.models.AsyncModels.list", + side_effect=side_effect, + ): + assert await async_setup_component(hass, "openai_conversation", {}) + await hass.async_block_till_done() + assert error in caplog.text From 44e54e11d8e390e68849e758b2e599ad7a860bba Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 11 Dec 2023 15:58:51 +0100 Subject: [PATCH 031/118] Follow Alpine 3.18 raspberrypi package updates (#105486) Alpine 3.18 renamed the packages raspberrypi and raspberrypi-libs to raspberrypi-userland and raspberrypi-userland-libs respectively. Follow that rename. With this moderniziation raspistill and friends now gets deployed to /usr/bin, which makes any symlinks obsolete. Note that there is and was never a 64-bit variant of raspistill. So these symlinks were essentially useless all along. This effectively doesn't change anything for users: Alpine automatically installed the renamed package already and Home Assistant Core picked up the raspistill binary from /usr/bin already. --- machine/raspberrypi | 11 ++--------- machine/raspberrypi2 | 11 ++--------- machine/raspberrypi3 | 11 ++--------- machine/raspberrypi3-64 | 11 ++--------- machine/raspberrypi4 | 11 ++--------- machine/raspberrypi4-64 | 11 ++--------- machine/yellow | 11 ++--------- 7 files changed, 14 insertions(+), 63 deletions(-) diff --git a/machine/raspberrypi b/machine/raspberrypi index 3cce504661e38d..2ed3b3c8e442bf 100644 --- a/machine/raspberrypi +++ b/machine/raspberrypi @@ -4,12 +4,5 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi \ - raspberrypi-libs - -## -# Set symlinks for raspberry pi camera binaries. -RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ - && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ - && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ - && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv + raspberrypi-userland \ + raspberrypi-userland-libs diff --git a/machine/raspberrypi2 b/machine/raspberrypi2 index c49db40b40875e..2ed3b3c8e442bf 100644 --- a/machine/raspberrypi2 +++ b/machine/raspberrypi2 @@ -4,12 +4,5 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi \ - raspberrypi-libs - -## -# Set symlinks for raspberry pi binaries. -RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ - && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ - && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ - && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv + raspberrypi-userland \ + raspberrypi-userland-libs diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index c49db40b40875e..2ed3b3c8e442bf 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -4,12 +4,5 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi \ - raspberrypi-libs - -## -# Set symlinks for raspberry pi binaries. -RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ - && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ - && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ - && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv + raspberrypi-userland \ + raspberrypi-userland-libs diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index c49db40b40875e..2ed3b3c8e442bf 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -4,12 +4,5 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi \ - raspberrypi-libs - -## -# Set symlinks for raspberry pi binaries. -RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ - && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ - && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ - && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv + raspberrypi-userland \ + raspberrypi-userland-libs diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index c49db40b40875e..2ed3b3c8e442bf 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -4,12 +4,5 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi \ - raspberrypi-libs - -## -# Set symlinks for raspberry pi binaries. -RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ - && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ - && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ - && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv + raspberrypi-userland \ + raspberrypi-userland-libs diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index c49db40b40875e..2ed3b3c8e442bf 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -4,12 +4,5 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi \ - raspberrypi-libs - -## -# Set symlinks for raspberry pi binaries. -RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ - && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ - && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ - && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv + raspberrypi-userland \ + raspberrypi-userland-libs diff --git a/machine/yellow b/machine/yellow index c49db40b40875e..2ed3b3c8e442bf 100644 --- a/machine/yellow +++ b/machine/yellow @@ -4,12 +4,5 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi \ - raspberrypi-libs - -## -# Set symlinks for raspberry pi binaries. -RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ - && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ - && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ - && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv + raspberrypi-userland \ + raspberrypi-userland-libs From 3963f59121423c1e68f818cd528a319d19c04a0f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 11 Dec 2023 16:12:32 +0100 Subject: [PATCH 032/118] Reduce modbus validator for "swap" (remove special handling) (#105021) --- homeassistant/components/modbus/validators.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 7dc5a91a2fa109..5e2129bd90a81c 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -120,34 +120,30 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: slave_count = config.get(CONF_SLAVE_COUNT, config.get(CONF_VIRTUAL_COUNT)) validator = DEFAULT_STRUCT_FORMAT[data_type].validate_parm swap_type = config.get(CONF_SWAP) + swap_dict = { + CONF_SWAP_BYTE: validator.swap_byte, + CONF_SWAP_WORD: validator.swap_word, + CONF_SWAP_WORD_BYTE: validator.swap_word, + } + swap_type_validator = swap_dict[swap_type] if swap_type else OPTIONAL for entry in ( (count, validator.count, CONF_COUNT), (structure, validator.structure, CONF_STRUCTURE), ( slave_count, validator.slave_count, - f"{CONF_VIRTUAL_COUNT} / {CONF_SLAVE_COUNT}", + f"{CONF_VIRTUAL_COUNT} / {CONF_SLAVE_COUNT}:", ), + (swap_type, swap_type_validator, f"{CONF_SWAP}:{swap_type}"), ): if entry[0] is None: if entry[1] == DEMANDED: - error = f"{name}: `{entry[2]}:` missing, demanded with `{CONF_DATA_TYPE}: {data_type}`" + error = f"{name}: `{entry[2]}` missing, demanded with `{CONF_DATA_TYPE}: {data_type}`" raise vol.Invalid(error) elif entry[1] == ILLEGAL: - error = ( - f"{name}: `{entry[2]}:` illegal with `{CONF_DATA_TYPE}: {data_type}`" - ) + error = f"{name}: `{entry[2]}` illegal with `{CONF_DATA_TYPE}: {data_type}`" raise vol.Invalid(error) - if swap_type: - swap_type_validator = { - CONF_SWAP_BYTE: validator.swap_byte, - CONF_SWAP_WORD: validator.swap_word, - CONF_SWAP_WORD_BYTE: validator.swap_word, - }[swap_type] - if swap_type_validator == ILLEGAL: - error = f"{name}: `{CONF_SWAP}:{swap_type}` illegal with `{CONF_DATA_TYPE}: {data_type}`" - raise vol.Invalid(error) if config[CONF_DATA_TYPE] == DataType.CUSTOM: try: size = struct.calcsize(structure) From 4c0fda9ca032732d096782de1f2788b73c773cf4 Mon Sep 17 00:00:00 2001 From: Alex Thompson Date: Mon, 11 Dec 2023 10:27:02 -0500 Subject: [PATCH 033/118] Fix Lyric LCC thermostats auto mode (#104853) --- homeassistant/components/lyric/climate.py | 109 +++++++++++++--------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index f01e4c4fe55ecd..e2504232c689a9 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import enum import logging from time import localtime, strftime, time from typing import Any @@ -151,6 +152,13 @@ async def async_setup_entry( ) +class LyricThermostatType(enum.Enum): + """Lyric thermostats are classified as TCC or LCC devices.""" + + TCC = enum.auto() + LCC = enum.auto() + + class LyricClimate(LyricDeviceEntity, ClimateEntity): """Defines a Honeywell Lyric climate entity.""" @@ -201,8 +209,10 @@ def __init__( # Setup supported features if device.changeableValues.thermostatSetpointStatus: self._attr_supported_features = SUPPORT_FLAGS_LCC + self._attr_thermostat_type = LyricThermostatType.LCC else: self._attr_supported_features = SUPPORT_FLAGS_TCC + self._attr_thermostat_type = LyricThermostatType.TCC # Setup supported fan modes if device_fan_modes := device.settings.attributes.get("fan", {}).get( @@ -365,56 +375,69 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" _LOGGER.debug("HVAC mode: %s", hvac_mode) try: - if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: - # If the system is off, turn it to Heat first then to Auto, - # otherwise it turns to. - # Auto briefly and then reverts to Off (perhaps related to - # heatCoolMode). This is the behavior that happens with the - # native app as well, so likely a bug in the api itself - if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF: - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[LYRIC_HVAC_MODE_COOL], - ) - await self._update_thermostat( - self.location, - self.device, - mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=False, - ) - # Sleep 3 seconds before proceeding - await asyncio.sleep(3) - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - ) - await self._update_thermostat( - self.location, - self.device, - mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=True, - ) - else: - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[self.device.changeableValues.mode], - ) - await self._update_thermostat( - self.location, self.device, autoChangeoverActive=True - ) - else: + match self._attr_thermostat_type: + case LyricThermostatType.TCC: + await self._async_set_hvac_mode_tcc(hvac_mode) + case LyricThermostatType.LCC: + await self._async_set_hvac_mode_lcc(hvac_mode) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() + + async def _async_set_hvac_mode_tcc(self, hvac_mode: HVACMode) -> None: + if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: + # If the system is off, turn it to Heat first then to Auto, + # otherwise it turns to. + # Auto briefly and then reverts to Off (perhaps related to + # heatCoolMode). This is the behavior that happens with the + # native app as well, so likely a bug in the api itself + if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF: _LOGGER.debug( - "HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode] + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_COOL], ) await self._update_thermostat( self.location, self.device, - mode=LYRIC_HVAC_MODES[hvac_mode], + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], autoChangeoverActive=False, ) - except LYRIC_EXCEPTIONS as exception: - _LOGGER.error(exception) - await self.coordinator.async_refresh() + # Sleep 3 seconds before proceeding + await asyncio.sleep(3) + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + ) + await self._update_thermostat( + self.location, + self.device, + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + autoChangeoverActive=True, + ) + else: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[self.device.changeableValues.mode], + ) + await self._update_thermostat( + self.location, self.device, autoChangeoverActive=True + ) + else: + _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + autoChangeoverActive=False, + ) + + async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None: + _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode.""" From 94fd7d0353a8ddca0809d631c394af42fd6d7887 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Dec 2023 16:48:12 +0100 Subject: [PATCH 034/118] Improve test of config entry store (#105487) * Improve test of config entry store * Tweak test --- tests/snapshots/test_config_entries.ambr | 18 ++++++++++ tests/test_config_entries.py | 46 +++++++++++++++++++----- 2 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 tests/snapshots/test_config_entries.ambr diff --git a/tests/snapshots/test_config_entries.ambr b/tests/snapshots/test_config_entries.ambr new file mode 100644 index 00000000000000..beaa60cf762898 --- /dev/null +++ b/tests/snapshots/test_config_entries.ambr @@ -0,0 +1,18 @@ +# serializer version: 1 +# name: test_as_dict + dict({ + 'data': dict({ + }), + 'disabled_by': None, + 'domain': 'test', + 'entry_id': 'mock-entry', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 1, + }) +# --- diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index f63972c79e8618..40e3b3b4c3c162 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries, data_entry_flow, loader from homeassistant.components import dhcp @@ -667,14 +668,43 @@ async def async_step_user(self, user_input=None): for orig, loaded in zip( hass.config_entries.async_entries(), manager.async_entries() ): - assert orig.version == loaded.version - assert orig.domain == loaded.domain - assert orig.title == loaded.title - assert orig.data == loaded.data - assert orig.source == loaded.source - assert orig.unique_id == loaded.unique_id - assert orig.pref_disable_new_entities == loaded.pref_disable_new_entities - assert orig.pref_disable_polling == loaded.pref_disable_polling + assert orig.as_dict() == loaded.as_dict() + + +async def test_as_dict(snapshot: SnapshotAssertion) -> None: + """Test ConfigEntry.as_dict.""" + + # Ensure as_dict is not overridden + assert MockConfigEntry.as_dict is config_entries.ConfigEntry.as_dict + + excluded_from_dict = { + "supports_unload", + "supports_remove_device", + "state", + "_setup_lock", + "update_listeners", + "reason", + "_async_cancel_retry_setup", + "_on_unload", + "reload_lock", + "_reauth_lock", + "_tasks", + "_background_tasks", + "_integration_for_domain", + "_tries", + "_setup_again_job", + } + + entry = MockConfigEntry(entry_id="mock-entry") + + # Make sure the expected keys are present + dict_repr = entry.as_dict() + for key in config_entries.ConfigEntry.__slots__: + assert key in dict_repr or key in excluded_from_dict + assert not (key in dict_repr and key in excluded_from_dict) + + # Make sure the dict representation is as expected + assert dict_repr == snapshot async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: From b71f488d3e544fbc51567dec43cba108a02bfd7b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 11 Dec 2023 17:04:07 +0100 Subject: [PATCH 035/118] Update pylint to 3.0.3 (#105491) --- homeassistant/components/improv_ble/config_flow.py | 2 +- homeassistant/components/mqtt/__init__.py | 1 - homeassistant/components/zha/__init__.py | 2 +- requirements_test.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index bfc86ac01629f6..762f37ef5d4e67 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -405,7 +405,7 @@ async def _try_call(func: Coroutine[Any, Any, _T]) -> _T: raise AbortFlow("characteristic_missing") from err except improv_ble_errors.CommandFailed: raise - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.exception("Unexpected exception") raise AbortFlow("unknown") from err diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 16f584db011b4c..593d5bbd2029c5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -247,7 +247,6 @@ async def async_check_config_schema( schema(config) except vol.Invalid as exc: integration = await async_get_integration(hass, DOMAIN) - # pylint: disable-next=protected-access message = conf_util.format_schema_error( hass, exc, domain, config, integration.documentation ) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 2046070d6a52d0..340e0db40a696b 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -182,7 +182,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) from exc except TransientConnectionError as exc: raise ConfigEntryNotReady from exc - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: _LOGGER.debug( "Couldn't start coordinator (attempt %s of %s)", attempt + 1, diff --git a/requirements_test.txt b/requirements_test.txt index f8918dc73f446e..9b30c0e40a111b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,7 +14,7 @@ mock-open==1.4.0 mypy==1.7.1 pre-commit==3.5.0 pydantic==1.10.12 -pylint==3.0.2 +pylint==3.0.3 pylint-per-file-ignores==1.2.1 pipdeptree==2.11.0 pytest-asyncio==0.21.0 From 80607f77509f8e38f368c9c4ccfaba7dda028290 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 11 Dec 2023 10:18:46 -0600 Subject: [PATCH 036/118] Disconnect before reconnecting to satellite (#105500) Disconnect before reconnecting --- homeassistant/components/wyoming/satellite.py | 26 ++++++++++++++++--- tests/components/wyoming/test_satellite.py | 23 ++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 94f61c17047e3b..2c93b762015bd1 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -71,11 +71,11 @@ async def run(self) -> None: while self.is_running: try: # Check if satellite has been disabled - if not self.device.is_enabled: + while not self.device.is_enabled: await self.on_disabled() if not self.is_running: # Satellite was stopped while waiting to be enabled - break + return # Connect and run pipeline loop await self._run_once() @@ -87,7 +87,7 @@ async def run(self) -> None: # Ensure sensor is off self.device.set_is_active(False) - await self.on_stopped() + await self.on_stopped() def stop(self) -> None: """Signal satellite task to stop running.""" @@ -130,6 +130,7 @@ def _enabled_changed(self) -> None: self._audio_queue.put_nowait(None) self._enabled_changed_event.set() + self._enabled_changed_event.clear() def _pipeline_changed(self) -> None: """Run when device pipeline changes.""" @@ -255,9 +256,17 @@ async def _run_once(self) -> None: chunk = AudioChunk.from_event(client_event) chunk = self._chunk_converter.convert(chunk) self._audio_queue.put_nowait(chunk.audio) + elif AudioStop.is_type(client_event.type): + # Stop pipeline + _LOGGER.debug("Client requested pipeline to stop") + self._audio_queue.put_nowait(b"") + break else: _LOGGER.debug("Unexpected event from satellite: %s", client_event) + # Ensure task finishes + await _pipeline_task + _LOGGER.debug("Pipeline finished") def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None: @@ -348,12 +357,23 @@ def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None: async def _connect(self) -> None: """Connect to satellite over TCP.""" + await self._disconnect() + _LOGGER.debug( "Connecting to satellite at %s:%s", self.service.host, self.service.port ) self._client = AsyncTcpClient(self.service.host, self.service.port) await self._client.connect() + async def _disconnect(self) -> None: + """Disconnect if satellite is currently connected.""" + if self._client is None: + return + + _LOGGER.debug("Disconnecting from satellite") + await self._client.disconnect() + self._client = None + async def _stream_tts(self, media_id: str) -> None: """Stream TTS WAV audio to satellite in chunks.""" assert self._client is not None diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 50252007aa5bc2..83e4d98d971046 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -322,11 +322,12 @@ def make_disabled_satellite( hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService ): satellite = original_make_satellite(hass, config_entry, service) - satellite.device.is_enabled = False + satellite.device.set_is_enabled(False) return satellite async def on_disabled(self): + self.device.set_is_enabled(True) on_disabled_event.set() with patch( @@ -368,11 +369,19 @@ async def on_restart(self): async def test_satellite_reconnect(hass: HomeAssistant) -> None: """Test satellite reconnect call after connection refused.""" - on_reconnect_event = asyncio.Event() + num_reconnects = 0 + reconnect_event = asyncio.Event() + stopped_event = asyncio.Event() async def on_reconnect(self): - self.stop() - on_reconnect_event.set() + nonlocal num_reconnects + num_reconnects += 1 + if num_reconnects >= 2: + reconnect_event.set() + self.stop() + + async def on_stopped(self): + stopped_event.set() with patch( "homeassistant.components.wyoming.data.load_wyoming_info", @@ -383,10 +392,14 @@ async def on_reconnect(self): ), patch( "homeassistant.components.wyoming.satellite.WyomingSatellite.on_reconnect", on_reconnect, + ), patch( + "homeassistant.components.wyoming.satellite.WyomingSatellite.on_stopped", + on_stopped, ): await setup_config_entry(hass) async with asyncio.timeout(1): - await on_reconnect_event.wait() + await reconnect_event.wait() + await stopped_event.wait() async def test_satellite_disconnect_before_pipeline(hass: HomeAssistant) -> None: From 6c2b3ef950c7f738274f0a2f172dca3894b52db7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 11 Dec 2023 17:30:24 +0100 Subject: [PATCH 037/118] Update typing-extensions to 4.9.0 (#105490) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2f1373b61d9835..4ee6c9ba3ead0f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ PyYAML==6.0.1 requests==2.31.0 scapy==2.5.0 SQLAlchemy==2.0.23 -typing-extensions>=4.8.0,<5.0 +typing-extensions>=4.9.0,<5.0 ulid-transform==0.9.0 voluptuous-serialize==2.6.0 voluptuous==0.13.1 diff --git a/pyproject.toml b/pyproject.toml index 9e9e8de4916277..7b1b025ee242bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "python-slugify==4.0.1", "PyYAML==6.0.1", "requests==2.31.0", - "typing-extensions>=4.8.0,<5.0", + "typing-extensions>=4.9.0,<5.0", "ulid-transform==0.9.0", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", diff --git a/requirements.txt b/requirements.txt index 1b5b8d63c54562..250a0948714e6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ pip>=21.3.1 python-slugify==4.0.1 PyYAML==6.0.1 requests==2.31.0 -typing-extensions>=4.8.0,<5.0 +typing-extensions>=4.9.0,<5.0 ulid-transform==0.9.0 voluptuous==0.13.1 voluptuous-serialize==2.6.0 From d1ea04152a1f6b569212b84c7f85ded1376ce2f6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 11 Dec 2023 17:37:15 +0100 Subject: [PATCH 038/118] Bump reolink_aio to 0.8.3 (#105489) --- 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 e03fa28b7cecfc..7dc81e83b53788 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.8.2"] + "requirements": ["reolink-aio==0.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 003d557c73fc52..7b76caf4c28fd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2354,7 +2354,7 @@ renault-api==0.2.0 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.2 +reolink-aio==0.8.3 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bda1b89845faa..2ca0e164808778 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1766,7 +1766,7 @@ renault-api==0.2.0 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.2 +reolink-aio==0.8.3 # homeassistant.components.rflink rflink==0.0.65 From 837ce99e3095c4490930361e80988c5ccca71041 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 11 Dec 2023 17:39:48 +0100 Subject: [PATCH 039/118] Add Raspberry Pi 5 specific container image (#105488) --- .github/workflows/builder.yml | 5 +++-- machine/raspberrypi5-64 | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 machine/raspberrypi5-64 diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 7c3a42aaaa1009..a646510582af9d 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -197,7 +197,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.09.0 + uses: home-assistant/builder@2023.12.0 with: args: | $BUILD_ARGS \ @@ -247,6 +247,7 @@ jobs: - raspberrypi3-64 - raspberrypi4 - raspberrypi4-64 + - raspberrypi5-64 - tinker - yellow - green @@ -273,7 +274,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.09.0 + uses: home-assistant/builder@2023.12.0 with: args: | $BUILD_ARGS \ diff --git a/machine/raspberrypi5-64 b/machine/raspberrypi5-64 new file mode 100644 index 00000000000000..2ed3b3c8e442bf --- /dev/null +++ b/machine/raspberrypi5-64 @@ -0,0 +1,8 @@ +ARG \ + BUILD_FROM + +FROM $BUILD_FROM + +RUN apk --no-cache add \ + raspberrypi-userland \ + raspberrypi-userland-libs From bb0d082b25913bb154c2da0314994b4b9fe4538b Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 11 Dec 2023 19:17:06 +0100 Subject: [PATCH 040/118] Correctly report unavailable battery for value 255 of percentage (#104566) * Ignore unavailable battery level for zha * Adjust unavailable test --- homeassistant/components/zha/sensor.py | 2 +- tests/components/zha/test_sensor.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 4fe96109c4625d..4ec4c11ef530d7 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -231,7 +231,7 @@ def create_entity( def formatter(value: int) -> int | None: """Return the state of the entity.""" # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - if not isinstance(value, numbers.Number) or value == -1: + if not isinstance(value, numbers.Number) or value == -1 or value == 255: return None value = round(value / 2) return value diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 7c11077c55d44e..59b8bb1293ef36 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -260,11 +260,12 @@ async def async_test_powerconfiguration2(hass, cluster, entity_id): """Test powerconfiguration/battery sensor.""" await send_attributes_report(hass, cluster, {33: -1}) assert_state(hass, entity_id, STATE_UNKNOWN, "%") - assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.9 - assert hass.states.get(entity_id).attributes["battery_quantity"] == 3 - assert hass.states.get(entity_id).attributes["battery_size"] == "AAA" - await send_attributes_report(hass, cluster, {32: 20}) - assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0 + + await send_attributes_report(hass, cluster, {33: 255}) + assert_state(hass, entity_id, STATE_UNKNOWN, "%") + + await send_attributes_report(hass, cluster, {33: 98}) + assert_state(hass, entity_id, "49", "%") async def async_test_device_temperature(hass, cluster, entity_id): From dd338799d4b8a05983ddf6819132ae0b875bc041 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Dec 2023 20:00:55 +0100 Subject: [PATCH 041/118] Make it possible to inherit EntityDescription in frozen and mutable dataclasses (#105211) --- .../bluetooth/passive_update_processor.py | 4 +- homeassistant/components/iotawatt/sensor.py | 20 +-- .../components/litterrobot/vacuum.py | 2 +- homeassistant/components/zwave_js/sensor.py | 114 ++++++++-------- homeassistant/helpers/entity.py | 19 ++- homeassistant/util/frozen_dataclass_compat.py | 127 ++++++++++++++++++ tests/helpers/snapshots/test_entity.ambr | 45 +++++++ tests/helpers/test_entity.py | 51 ++++++- 8 files changed, 307 insertions(+), 75 deletions(-) create mode 100644 homeassistant/util/frozen_dataclass_compat.py create mode 100644 tests/helpers/snapshots/test_entity.ambr diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 8da0d2c462b6a3..eeccf081b55cc0 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -94,7 +94,7 @@ def deserialize_entity_description( ) -> EntityDescription: """Deserialize an entity description.""" result: dict[str, Any] = {} - for field in cached_fields(descriptions_class): # type: ignore[arg-type] + for field in cached_fields(descriptions_class): field_name = field.name # It would be nice if field.type returned the actual # type instead of a str so we could avoid writing this @@ -114,7 +114,7 @@ def serialize_entity_description(description: EntityDescription) -> dict[str, An as_dict = dataclasses.asdict(description) return { field.name: as_dict[field.name] - for field in cached_fields(type(description)) # type: ignore[arg-type] + for field in cached_fields(type(description)) if field.default != as_dict.get(field.name) } diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index 27ecc1574e3b60..7dd26c462010ab 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -45,14 +45,14 @@ class IotaWattSensorEntityDescription(SensorEntityDescription): ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = { "Amps": IotaWattSensorEntityDescription( - "Amps", + key="Amps", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, ), "Hz": IotaWattSensorEntityDescription( - "Hz", + key="Hz", native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, @@ -60,7 +60,7 @@ class IotaWattSensorEntityDescription(SensorEntityDescription): entity_registry_enabled_default=False, ), "PF": IotaWattSensorEntityDescription( - "PF", + key="PF", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER_FACTOR, @@ -68,40 +68,40 @@ class IotaWattSensorEntityDescription(SensorEntityDescription): entity_registry_enabled_default=False, ), "Watts": IotaWattSensorEntityDescription( - "Watts", + key="Watts", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, ), "WattHours": IotaWattSensorEntityDescription( - "WattHours", + key="WattHours", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY, ), "VA": IotaWattSensorEntityDescription( - "VA", + key="VA", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.APPARENT_POWER, entity_registry_enabled_default=False, ), "VAR": IotaWattSensorEntityDescription( - "VAR", + key="VAR", native_unit_of_measurement=VOLT_AMPERE_REACTIVE, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash", entity_registry_enabled_default=False, ), "VARh": IotaWattSensorEntityDescription( - "VARh", + key="VARh", native_unit_of_measurement=VOLT_AMPERE_REACTIVE_HOURS, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash", entity_registry_enabled_default=False, ), "Volts": IotaWattSensorEntityDescription( - "Volts", + key="Volts", native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, @@ -125,7 +125,7 @@ def _create_entity(key: str) -> IotaWattSensor: created.add(key) data = coordinator.data["sensors"][key] description = ENTITY_DESCRIPTION_KEY_MAP.get( - data.getUnit(), IotaWattSensorEntityDescription("base_sensor") + data.getUnit(), IotaWattSensorEntityDescription(key="base_sensor") ) return IotaWattSensor( diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 4b1a8effb98ab6..a86f1e4be002c8 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -47,7 +47,7 @@ } LITTER_BOX_ENTITY = StateVacuumEntityDescription( - "litter_box", translation_key="litter_box" + key="litter_box", translation_key="litter_box" ) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 8d42bcfb36698f..56ed3f010b8d93 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -111,20 +111,20 @@ tuple[str, str], SensorEntityDescription ] = { (ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription( - ENTITY_DESC_KEY_BATTERY, + key=ENTITY_DESC_KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), (ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription( - ENTITY_DESC_KEY_CURRENT, + key=ENTITY_DESC_KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), (ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription( - ENTITY_DESC_KEY_VOLTAGE, + key=ENTITY_DESC_KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -133,7 +133,7 @@ ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.MILLIVOLT, ): SensorEntityDescription( - ENTITY_DESC_KEY_VOLTAGE, + key=ENTITY_DESC_KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, @@ -142,67 +142,67 @@ ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, UnitOfEnergy.KILO_WATT_HOUR, ): SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, + key=ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), (ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription( - ENTITY_DESC_KEY_POWER, + key=ENTITY_DESC_KEY_POWER, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), (ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription( - ENTITY_DESC_KEY_POWER_FACTOR, + key=ENTITY_DESC_KEY_POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), (ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( - ENTITY_DESC_KEY_CO, + key=ENTITY_DESC_KEY_CO, device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), (ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( - ENTITY_DESC_KEY_CO2, + key=ENTITY_DESC_KEY_CO2, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), (ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription( - ENTITY_DESC_KEY_HUMIDITY, + key=ENTITY_DESC_KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), (ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription( - ENTITY_DESC_KEY_ILLUMINANCE, + key=ENTITY_DESC_KEY_ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LIGHT_LUX, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription( - ENTITY_DESC_KEY_PRESSURE, + key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.KPA, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription( - ENTITY_DESC_KEY_PRESSURE, + key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.PSI, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription( - ENTITY_DESC_KEY_PRESSURE, + key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.INHG, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription( - ENTITY_DESC_KEY_PRESSURE, + key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.MMHG, @@ -211,7 +211,7 @@ ENTITY_DESC_KEY_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ): SensorEntityDescription( - ENTITY_DESC_KEY_SIGNAL_STRENGTH, + key=ENTITY_DESC_KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -219,7 +219,7 @@ native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), (ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription( - ENTITY_DESC_KEY_TEMPERATURE, + key=ENTITY_DESC_KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -228,7 +228,7 @@ ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.FAHRENHEIT, ): SensorEntityDescription( - ENTITY_DESC_KEY_TEMPERATURE, + key=ENTITY_DESC_KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, @@ -237,7 +237,7 @@ ENTITY_DESC_KEY_TARGET_TEMPERATURE, UnitOfTemperature.CELSIUS, ): SensorEntityDescription( - ENTITY_DESC_KEY_TARGET_TEMPERATURE, + key=ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), @@ -245,7 +245,7 @@ ENTITY_DESC_KEY_TARGET_TEMPERATURE, UnitOfTemperature.FAHRENHEIT, ): SensorEntityDescription( - ENTITY_DESC_KEY_TARGET_TEMPERATURE, + key=ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, ), @@ -253,13 +253,13 @@ ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, UnitOfTime.SECONDS, ): SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, + key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, name="Energy production time", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, ), (ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, UnitOfTime.HOURS): SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, + key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, ), @@ -267,7 +267,7 @@ ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY, UnitOfEnergy.WATT_HOUR, ): SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY, + key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY, name="Energy production today", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -277,7 +277,7 @@ ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL, UnitOfEnergy.WATT_HOUR, ): SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL, + key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL, name="Energy production total", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -287,7 +287,7 @@ ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER, UnitOfPower.WATT, ): SensorEntityDescription( - ENTITY_DESC_KEY_POWER, + key=ENTITY_DESC_KEY_POWER, name="Energy production power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -298,41 +298,41 @@ # These descriptions are without device class. ENTITY_DESCRIPTION_KEY_MAP = { ENTITY_DESC_KEY_CO: SensorEntityDescription( - ENTITY_DESC_KEY_CO, + key=ENTITY_DESC_KEY_CO, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_MEASUREMENT, + key=ENTITY_DESC_KEY_ENERGY_MEASUREMENT, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( - ENTITY_DESC_KEY_HUMIDITY, + key=ENTITY_DESC_KEY_HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( - ENTITY_DESC_KEY_ILLUMINANCE, + key=ENTITY_DESC_KEY_ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( - ENTITY_DESC_KEY_POWER_FACTOR, + key=ENTITY_DESC_KEY_POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( - ENTITY_DESC_KEY_SIGNAL_STRENGTH, + key=ENTITY_DESC_KEY_SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription( - ENTITY_DESC_KEY_MEASUREMENT, + key=ENTITY_DESC_KEY_MEASUREMENT, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription( - ENTITY_DESC_KEY_TOTAL_INCREASING, + key=ENTITY_DESC_KEY_TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING, ), ENTITY_DESC_KEY_UV_INDEX: SensorEntityDescription( - ENTITY_DESC_KEY_UV_INDEX, + key=ENTITY_DESC_KEY_UV_INDEX, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, ), @@ -342,80 +342,80 @@ # Controller statistics descriptions ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ SensorEntityDescription( - "messagesTX", + key="messagesTX", name="Successful messages (TX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "messagesRX", + key="messagesRX", name="Successful messages (RX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "messagesDroppedTX", + key="messagesDroppedTX", name="Messages dropped (TX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "messagesDroppedRX", + key="messagesDroppedRX", name="Messages dropped (RX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "NAK", + key="NAK", name="Messages not accepted", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "CAN", name="Collisions", state_class=SensorStateClass.TOTAL + key="CAN", name="Collisions", state_class=SensorStateClass.TOTAL ), SensorEntityDescription( - "timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL + key="timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL ), SensorEntityDescription( - "timeoutResponse", + key="timeoutResponse", name="Timed out responses", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "timeoutCallback", + key="timeoutCallback", name="Timed out callbacks", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "backgroundRSSI.channel0.average", + key="backgroundRSSI.channel0.average", name="Average background RSSI (channel 0)", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, ), SensorEntityDescription( - "backgroundRSSI.channel0.current", + key="backgroundRSSI.channel0.current", name="Current background RSSI (channel 0)", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( - "backgroundRSSI.channel1.average", + key="backgroundRSSI.channel1.average", name="Average background RSSI (channel 1)", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, ), SensorEntityDescription( - "backgroundRSSI.channel1.current", + key="backgroundRSSI.channel1.current", name="Current background RSSI (channel 1)", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( - "backgroundRSSI.channel2.average", + key="backgroundRSSI.channel2.average", name="Average background RSSI (channel 2)", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, ), SensorEntityDescription( - "backgroundRSSI.channel2.current", + key="backgroundRSSI.channel2.current", name="Current background RSSI (channel 2)", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -426,39 +426,39 @@ # Node statistics descriptions ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [ SensorEntityDescription( - "commandsRX", + key="commandsRX", name="Successful commands (RX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "commandsTX", + key="commandsTX", name="Successful commands (TX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "commandsDroppedRX", + key="commandsDroppedRX", name="Commands dropped (RX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "commandsDroppedTX", + key="commandsDroppedTX", name="Commands dropped (TX)", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "timeoutResponse", + key="timeoutResponse", name="Timed out responses", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( - "rtt", + key="rtt", name="Round Trip Time", native_unit_of_measurement=UnitOfTime.MILLISECONDS, device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( - "rssi", + key="rssi", name="RSSI", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -478,7 +478,7 @@ def get_entity_description( ENTITY_DESCRIPTION_KEY_MAP.get( data_description_key, SensorEntityDescription( - "base_sensor", native_unit_of_measurement=data.unit_of_measurement + key="base_sensor", native_unit_of_measurement=data.unit_of_measurement ), ), ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7877ca0e6135db..6446a4fe6d61bb 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -4,7 +4,7 @@ from abc import ABC import asyncio from collections.abc import Coroutine, Iterable, Mapping, MutableMapping -from dataclasses import dataclass +import dataclasses from datetime import timedelta from enum import Enum, auto import functools as ft @@ -23,6 +23,7 @@ final, ) +from typing_extensions import dataclass_transform import voluptuous as vol from homeassistant.backports.functools import cached_property @@ -51,6 +52,7 @@ ) from homeassistant.loader import async_suggest_report_issue, bind_hass from homeassistant.util import ensure_unique_string, slugify +from homeassistant.util.frozen_dataclass_compat import FrozenOrThawed from . import device_registry as dr, entity_registry as er from .device_registry import DeviceInfo, EventDeviceRegistryUpdatedData @@ -218,8 +220,17 @@ class EntityPlatformState(Enum): REMOVED = auto() -@dataclass(slots=True) -class EntityDescription: +@dataclass_transform( + field_specifiers=(dataclasses.field, dataclasses.Field), + kw_only_default=True, # Set to allow setting kw_only in child classes +) +class _EntityDescriptionBase: + """Add PEP 681 decorator (dataclass transform).""" + + +class EntityDescription( + _EntityDescriptionBase, metaclass=FrozenOrThawed, frozen_or_thawed=True +): """A class that describes Home Assistant entities.""" # This is the key identifier for this entity @@ -1245,7 +1256,7 @@ def _suggest_report_issue(self) -> str: ) -@dataclass(slots=True) +@dataclasses.dataclass(slots=True) class ToggleEntityDescription(EntityDescription): """A class that describes toggle entities.""" diff --git a/homeassistant/util/frozen_dataclass_compat.py b/homeassistant/util/frozen_dataclass_compat.py new file mode 100644 index 00000000000000..96053844ab5828 --- /dev/null +++ b/homeassistant/util/frozen_dataclass_compat.py @@ -0,0 +1,127 @@ +"""Utility to create classes from which frozen or mutable dataclasses can be derived. + +This module enabled a non-breaking transition from mutable to frozen dataclasses +derived from EntityDescription and sub classes thereof. +""" +from __future__ import annotations + +import dataclasses +import sys +from typing import Any + + +def _class_fields(cls: type, kw_only: bool) -> list[tuple[str, Any, Any]]: + """Return a list of dataclass fields. + + Extracted from dataclasses._process_class. + """ + # pylint: disable=protected-access + cls_annotations = cls.__dict__.get("__annotations__", {}) + + cls_fields: list[dataclasses.Field[Any]] = [] + + _dataclasses = sys.modules[dataclasses.__name__] + for name, _type in cls_annotations.items(): + # See if this is a marker to change the value of kw_only. + if dataclasses._is_kw_only(type, _dataclasses) or ( # type: ignore[attr-defined] + isinstance(_type, str) + and dataclasses._is_type( # type: ignore[attr-defined] + _type, + cls, + _dataclasses, + dataclasses.KW_ONLY, + dataclasses._is_kw_only, # type: ignore[attr-defined] + ) + ): + kw_only = True + else: + # Otherwise it's a field of some type. + cls_fields.append(dataclasses._get_field(cls, name, _type, kw_only)) # type: ignore[attr-defined] + + return [(field.name, field.type, field) for field in cls_fields] + + +class FrozenOrThawed(type): + """Metaclass which which makes classes which behave like a dataclass. + + This allows child classes to be either mutable or frozen dataclasses. + """ + + def _make_dataclass(cls, name: str, bases: tuple[type, ...], kw_only: bool) -> None: + class_fields = _class_fields(cls, kw_only) + dataclass_bases = [] + for base in bases: + dataclass_bases.append(getattr(base, "_dataclass", base)) + cls._dataclass = dataclasses.make_dataclass( + f"{name}_dataclass", class_fields, bases=tuple(dataclass_bases), frozen=True + ) + + def __new__( + mcs, # noqa: N804 ruff bug, ruff does not understand this is a metaclass + name: str, + bases: tuple[type, ...], + namespace: dict[Any, Any], + frozen_or_thawed: bool = False, + **kwargs: Any, + ) -> Any: + """Pop frozen_or_thawed and store it in the namespace.""" + namespace["_FrozenOrThawed__frozen_or_thawed"] = frozen_or_thawed + return super().__new__(mcs, name, bases, namespace) + + def __init__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[Any, Any], + **kwargs: Any, + ) -> None: + """Optionally create a dataclass and store it in cls._dataclass. + + A dataclass will be created if frozen_or_thawed is set, if not we assume the + class will be a real dataclass, i.e. it's decorated with @dataclass. + """ + if not namespace["_FrozenOrThawed__frozen_or_thawed"]: + parent = cls.__mro__[1] + # This class is a real dataclass, optionally inject the parent's annotations + if dataclasses.is_dataclass(parent) or not hasattr(parent, "_dataclass"): + # Rely on dataclass inheritance + return + # Parent is not a dataclass, inject its annotations + cls.__annotations__ = ( + parent._dataclass.__annotations__ | cls.__annotations__ + ) + return + + # First try without setting the kw_only flag, and if that fails, try setting it + try: + cls._make_dataclass(name, bases, False) + except TypeError: + cls._make_dataclass(name, bases, True) + + def __delattr__(self: object, name: str) -> None: + """Delete an attribute. + + If self is a real dataclass, this is called if the dataclass is not frozen. + If self is not a real dataclass, forward to cls._dataclass.__delattr. + """ + if dataclasses.is_dataclass(self): + return object.__delattr__(self, name) + return self._dataclass.__delattr__(self, name) # type: ignore[attr-defined, no-any-return] + + def __setattr__(self: object, name: str, value: Any) -> None: + """Set an attribute. + + If self is a real dataclass, this is called if the dataclass is not frozen. + If self is not a real dataclass, forward to cls._dataclass.__setattr__. + """ + if dataclasses.is_dataclass(self): + return object.__setattr__(self, name, value) + return self._dataclass.__setattr__(self, name, value) # type: ignore[attr-defined, no-any-return] + + # Set generated dunder methods from the dataclass + # MyPy doesn't understand what's happening, so we ignore it + cls.__delattr__ = __delattr__ # type: ignore[assignment, method-assign] + cls.__eq__ = cls._dataclass.__eq__ # type: ignore[method-assign] + cls.__init__ = cls._dataclass.__init__ # type: ignore[misc] + cls.__repr__ = cls._dataclass.__repr__ # type: ignore[method-assign] + cls.__setattr__ = __setattr__ # type: ignore[assignment, method-assign] diff --git a/tests/helpers/snapshots/test_entity.ambr b/tests/helpers/snapshots/test_entity.ambr new file mode 100644 index 00000000000000..3b04286b62f703 --- /dev/null +++ b/tests/helpers/snapshots/test_entity.ambr @@ -0,0 +1,45 @@ +# serializer version: 1 +# name: test_entity_description_as_dataclass + EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=, translation_key=None, unit_of_measurement=None) +# --- +# name: test_entity_description_as_dataclass.1 + "EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=, translation_key=None, unit_of_measurement=None)" +# --- +# name: test_extending_entity_description + dict({ + 'device_class': None, + 'entity_category': None, + 'entity_registry_enabled_default': True, + 'entity_registry_visible_default': True, + 'extra': 'foo', + 'force_update': False, + 'has_entity_name': False, + 'icon': None, + 'key': 'blah', + 'name': 'name', + 'translation_key': None, + 'unit_of_measurement': None, + }) +# --- +# name: test_extending_entity_description.1 + "test_extending_entity_description..FrozenEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')" +# --- +# name: test_extending_entity_description.2 + dict({ + 'device_class': None, + 'entity_category': None, + 'entity_registry_enabled_default': True, + 'entity_registry_visible_default': True, + 'extra': 'foo', + 'force_update': False, + 'has_entity_name': False, + 'icon': None, + 'key': 'blah', + 'name': 'name', + 'translation_key': None, + 'unit_of_measurement': None, + }) +# --- +# name: test_extending_entity_description.3 + "test_extending_entity_description..ThawedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')" +# --- diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 4076afcfad0505..66ba9f947c9302 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant.const import ( @@ -966,7 +967,7 @@ async def test_entity_description_fallback() -> None: ent_with_description = entity.Entity() ent_with_description.entity_description = entity.EntityDescription(key="test") - for field in dataclasses.fields(entity.EntityDescription): + for field in dataclasses.fields(entity.EntityDescription._dataclass): if field.name == "key": continue @@ -1657,3 +1658,51 @@ async def async_will_remove_from_hass(self): assert len(result) == 2 assert len(ent.added_calls) == 3 assert len(ent.remove_calls) == 2 + + +def test_entity_description_as_dataclass(snapshot: SnapshotAssertion): + """Test EntityDescription behaves like a dataclass.""" + + obj = entity.EntityDescription("blah", device_class="test") + with pytest.raises(dataclasses.FrozenInstanceError): + obj.name = "mutate" + with pytest.raises(dataclasses.FrozenInstanceError): + delattr(obj, "name") + + assert obj == snapshot + assert obj == entity.EntityDescription("blah", device_class="test") + assert repr(obj) == snapshot + + +def test_extending_entity_description(snapshot: SnapshotAssertion): + """Test extending entity descriptions.""" + + @dataclasses.dataclass(frozen=True) + class FrozenEntityDescription(entity.EntityDescription): + extra: str = None + + obj = FrozenEntityDescription("blah", extra="foo", name="name") + assert obj == snapshot + assert obj == FrozenEntityDescription("blah", extra="foo", name="name") + assert repr(obj) == snapshot + + # Try mutating + with pytest.raises(dataclasses.FrozenInstanceError): + obj.name = "mutate" + with pytest.raises(dataclasses.FrozenInstanceError): + delattr(obj, "name") + + @dataclasses.dataclass + class ThawedEntityDescription(entity.EntityDescription): + extra: str = None + + obj = ThawedEntityDescription("blah", extra="foo", name="name") + assert obj == snapshot + assert obj == ThawedEntityDescription("blah", extra="foo", name="name") + assert repr(obj) == snapshot + + # Try mutating + obj.name = "mutate" + assert obj.name == "mutate" + delattr(obj, "key") + assert not hasattr(obj, "key") From 0dc61b34930f6fdafa3532050987431a3d317b69 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Mon, 11 Dec 2023 20:30:19 +0100 Subject: [PATCH 042/118] Add typing in Melcloud config flow (#105510) * Add typing in config flow * Patching functions with typing --- homeassistant/components/melcloud/config_flow.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index b19e268a4c380c..9293c9bb3d5b6e 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -63,7 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entry: config_entries.ConfigEntry | None = None - async def _create_entry(self, username: str, token: str): + async def _create_entry(self, username: str, token: str) -> FlowResult: """Register new entry.""" await self.async_set_unique_id(username) try: @@ -81,7 +81,7 @@ async def _create_client( *, password: str | None = None, token: str | None = None, - ): + ) -> FlowResult: """Create client.""" try: async with asyncio.timeout(10): @@ -113,7 +113,9 @@ async def _create_client( return await self._create_entry(username, acquired_token) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """User initiated config flow.""" if user_input is None: return self.async_show_form( @@ -125,7 +127,7 @@ async def async_step_user(self, user_input=None): username = user_input[CONF_USERNAME] return await self._create_client(username, password=user_input[CONF_PASSWORD]) - async def async_step_import(self, user_input): + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Import a config entry.""" result = await self._create_client( user_input[CONF_USERNAME], token=user_input[CONF_TOKEN] From e890671192bc69b789f07399e16edf2a4092110d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Dec 2023 10:42:00 -1000 Subject: [PATCH 043/118] Relocate Bluetooth manager to habluetooth library (#105110) * Relocate Bluetooth manager to habluetooth library * Relocate Bluetooth manager to habluetooth library * Relocate Bluetooth manager to habluetooth library * fixes * fix patching time * fix more tests * fix more tests * split * Bump habluetooth to 0.7.0 changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v0.6.1...v0.7.0 This is the big change that will move the manager so the HA PR that will follow this will be a bit larger than the rest of them since the manager is connected to everything * fix types * fix types * fix types * fix patch targets * fix flakey logbook tests (will need another PR) * mock shutdown * bump again * value can be a float now * Revert "value can be a float now" This reverts commit b7e7127143bd2947345c7590fc2727aa47e28d88. * float --- .../components/bluetooth/__init__.py | 13 +- homeassistant/components/bluetooth/api.py | 9 +- .../components/bluetooth/base_scanner.py | 14 +- homeassistant/components/bluetooth/manager.py | 653 +----------------- homeassistant/components/bluetooth/models.py | 9 +- homeassistant/components/bluetooth/usage.py | 51 -- .../components/bluetooth/wrappers.py | 391 ----------- tests/components/bluetooth/__init__.py | 8 +- .../bluetooth/test_advertisement_tracker.py | 47 +- tests/components/bluetooth/test_init.py | 2 +- tests/components/bluetooth/test_manager.py | 25 +- tests/components/bluetooth/test_models.py | 5 +- .../test_passive_update_coordinator.py | 18 +- .../test_passive_update_processor.py | 11 +- tests/components/bluetooth/test_usage.py | 55 +- tests/components/bluetooth/test_wrappers.py | 36 +- tests/components/bthome/test_binary_sensor.py | 17 +- tests/components/bthome/test_sensor.py | 17 +- tests/components/govee_ble/test_sensor.py | 12 +- tests/components/oralb/test_sensor.py | 12 +- .../components/private_ble_device/__init__.py | 7 +- .../private_ble_device/test_sensor.py | 4 +- .../components/qingping/test_binary_sensor.py | 7 +- tests/components/qingping/test_sensor.py | 7 +- tests/components/sensorpush/test_sensor.py | 7 +- .../xiaomi_ble/test_binary_sensor.py | 17 +- tests/components/xiaomi_ble/test_sensor.py | 17 +- 27 files changed, 145 insertions(+), 1326 deletions(-) delete mode 100644 homeassistant/components/bluetooth/usage.py delete mode 100644 homeassistant/components/bluetooth/wrappers.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 329b597d515bb2..4a53347e826ae9 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -21,11 +21,15 @@ adapter_unique_name, get_adapters, ) +from bluetooth_data_tools import monotonic_time_coarse as MONOTONIC_TIME from habluetooth import ( + BaseHaScanner, + BluetoothScannerDevice, BluetoothScanningMode, HaBluetoothConnector, HaScanner, ScannerStartError, + set_manager, ) from home_assistant_bluetooth import BluetoothServiceInfo, BluetoothServiceInfoBleak @@ -65,11 +69,7 @@ async_set_fallback_availability_interval, async_track_unavailable, ) -from .base_scanner import ( - BaseHaScanner, - BluetoothScannerDevice, - HomeAssistantRemoteScanner, -) +from .base_scanner import HomeAssistantRemoteScanner from .const import ( BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, @@ -81,7 +81,7 @@ LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, ) -from .manager import MONOTONIC_TIME, HomeAssistantBluetoothManager +from .manager import HomeAssistantBluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import BluetoothCallback, BluetoothChange from .storage import BluetoothStorage @@ -146,6 +146,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: manager = HomeAssistantBluetoothManager( hass, integration_matcher, bluetooth_adapters, bluetooth_storage, slot_manager ) + set_manager(manager) await manager.async_setup() hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, lambda event: manager.async_stop() diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index afdd26a20011b8..4acb8d91c842dd 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -9,17 +9,20 @@ from collections.abc import Callable, Iterable from typing import TYPE_CHECKING, cast -from habluetooth import BluetoothScanningMode +from habluetooth import ( + BaseHaScanner, + BluetoothScannerDevice, + BluetoothScanningMode, + HaBleakScannerWrapper, +) from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback -from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import DATA_MANAGER from .manager import HomeAssistantBluetoothManager from .match import BluetoothCallbackMatcher from .models import BluetoothCallback, BluetoothChange, ProcessAdvertisementCallback -from .wrappers import HaBleakScannerWrapper if TYPE_CHECKING: from bleak.backends.device import BLEDevice diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index 8267a73fd71e3d..b8e1e909ad2880 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -2,13 +2,10 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass from typing import Any -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from bluetooth_adapters import DiscoveredDeviceAdvertisementData -from habluetooth import BaseHaRemoteScanner, BaseHaScanner, HaBluetoothConnector +from habluetooth import BaseHaRemoteScanner, HaBluetoothConnector from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -22,15 +19,6 @@ from . import models -@dataclass(slots=True) -class BluetoothScannerDevice: - """Data for a bluetooth device from a given scanner.""" - - scanner: BaseHaScanner - ble_device: BLEDevice - advertisement: AdvertisementData - - class HomeAssistantRemoteScanner(BaseHaRemoteScanner): """Home Assistant remote BLE scanner. diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 777d0ebe317d4f..848460455ca736 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -1,22 +1,13 @@ """The bluetooth integration.""" from __future__ import annotations -import asyncio from collections.abc import Callable, Iterable import itertools import logging -from typing import TYPE_CHECKING, Any, Final -from bleak.backends.scanner import AdvertisementDataCallback -from bleak_retry_connector import NO_RSSI_VALUE, RSSI_SWITCH_THRESHOLD, BleakSlotManager -from bluetooth_adapters import ( - ADAPTER_ADDRESS, - ADAPTER_PASSIVE_SCAN, - AdapterDetails, - BluetoothAdapters, -) -from bluetooth_data_tools import monotonic_time_coarse -from habluetooth import TRACKER_BUFFERING_WOBBLE_SECONDS, AdvertisementTracker +from bleak_retry_connector import BleakSlotManager +from bluetooth_adapters import BluetoothAdapters +from habluetooth import BluetoothManager from homeassistant import config_entries from homeassistant.const import EVENT_LOGGING_CHANGED @@ -28,11 +19,6 @@ ) from homeassistant.helpers import discovery_flow -from .base_scanner import BaseHaScanner, BluetoothScannerDevice -from .const import ( - FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, - UNAVAILABLE_TRACK_SECONDS, -) from .match import ( ADDRESS, CALLBACK, @@ -45,642 +31,17 @@ ) from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .storage import BluetoothStorage -from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_load_history_from_system -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData - - -FILTER_UUIDS: Final = "UUIDs" - -APPLE_MFR_ID: Final = 76 -APPLE_IBEACON_START_BYTE: Final = 0x02 # iBeacon (tilt_ble) -APPLE_HOMEKIT_START_BYTE: Final = 0x06 # homekit_controller -APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker -APPLE_HOMEKIT_NOTIFY_START_BYTE: Final = 0x11 # homekit_controller -APPLE_START_BYTES_WANTED: Final = { - APPLE_IBEACON_START_BYTE, - APPLE_HOMEKIT_START_BYTE, - APPLE_HOMEKIT_NOTIFY_START_BYTE, - APPLE_DEVICE_ID_START_BYTE, -} - -MONOTONIC_TIME: Final = monotonic_time_coarse - _LOGGER = logging.getLogger(__name__) -def _dispatch_bleak_callback( - callback: AdvertisementDataCallback | None, - filters: dict[str, set[str]], - device: BLEDevice, - advertisement_data: AdvertisementData, -) -> None: - """Dispatch the callback.""" - if not callback: - # Callback destroyed right before being called, ignore - return - - if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( - advertisement_data.service_uuids - ): - return - - try: - callback(device, advertisement_data) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in callback: %s", callback) - - -class BluetoothManager: - """Manage Bluetooth.""" - - __slots__ = ( - "_cancel_unavailable_tracking", - "_advertisement_tracker", - "_fallback_intervals", - "_intervals", - "_unavailable_callbacks", - "_connectable_unavailable_callbacks", - "_bleak_callbacks", - "_all_history", - "_connectable_history", - "_non_connectable_scanners", - "_connectable_scanners", - "_adapters", - "_sources", - "_bluetooth_adapters", - "storage", - "slot_manager", - "_debug", - "shutdown", - "_loop", - ) - - def __init__( - self, - bluetooth_adapters: BluetoothAdapters, - storage: BluetoothStorage, - slot_manager: BleakSlotManager, - ) -> None: - """Init bluetooth manager.""" - self._cancel_unavailable_tracking: asyncio.TimerHandle | None = None - - self._advertisement_tracker = AdvertisementTracker() - self._fallback_intervals = self._advertisement_tracker.fallback_intervals - self._intervals = self._advertisement_tracker.intervals - - self._unavailable_callbacks: dict[ - str, list[Callable[[BluetoothServiceInfoBleak], None]] - ] = {} - self._connectable_unavailable_callbacks: dict[ - str, list[Callable[[BluetoothServiceInfoBleak], None]] - ] = {} - - self._bleak_callbacks: list[ - tuple[AdvertisementDataCallback, dict[str, set[str]]] - ] = [] - self._all_history: dict[str, BluetoothServiceInfoBleak] = {} - self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {} - self._non_connectable_scanners: list[BaseHaScanner] = [] - self._connectable_scanners: list[BaseHaScanner] = [] - self._adapters: dict[str, AdapterDetails] = {} - self._sources: dict[str, BaseHaScanner] = {} - self._bluetooth_adapters = bluetooth_adapters - self.storage = storage - self.slot_manager = slot_manager - self._debug = _LOGGER.isEnabledFor(logging.DEBUG) - self.shutdown = False - self._loop: asyncio.AbstractEventLoop | None = None - - @property - def supports_passive_scan(self) -> bool: - """Return if passive scan is supported.""" - return any(adapter[ADAPTER_PASSIVE_SCAN] for adapter in self._adapters.values()) - - def async_scanner_count(self, connectable: bool = True) -> int: - """Return the number of scanners.""" - if connectable: - return len(self._connectable_scanners) - return len(self._connectable_scanners) + len(self._non_connectable_scanners) - - async def async_diagnostics(self) -> dict[str, Any]: - """Diagnostics for the manager.""" - scanner_diagnostics = await asyncio.gather( - *[ - scanner.async_diagnostics() - for scanner in itertools.chain( - self._non_connectable_scanners, self._connectable_scanners - ) - ] - ) - return { - "adapters": self._adapters, - "slot_manager": self.slot_manager.diagnostics(), - "scanners": scanner_diagnostics, - "connectable_history": [ - service_info.as_dict() - for service_info in self._connectable_history.values() - ], - "all_history": [ - service_info.as_dict() for service_info in self._all_history.values() - ], - "advertisement_tracker": self._advertisement_tracker.async_diagnostics(), - } - - def _find_adapter_by_address(self, address: str) -> str | None: - for adapter, details in self._adapters.items(): - if details[ADAPTER_ADDRESS] == address: - return adapter - return None - - def async_scanner_by_source(self, source: str) -> BaseHaScanner | None: - """Return the scanner for a source.""" - return self._sources.get(source) - - async def async_get_bluetooth_adapters( - self, cached: bool = True - ) -> dict[str, AdapterDetails]: - """Get bluetooth adapters.""" - if not self._adapters or not cached: - if not cached: - await self._bluetooth_adapters.refresh() - self._adapters = self._bluetooth_adapters.adapters - return self._adapters - - async def async_get_adapter_from_address(self, address: str) -> str | None: - """Get adapter from address.""" - if adapter := self._find_adapter_by_address(address): - return adapter - await self._bluetooth_adapters.refresh() - self._adapters = self._bluetooth_adapters.adapters - return self._find_adapter_by_address(address) - - async def async_setup(self) -> None: - """Set up the bluetooth manager.""" - self._loop = asyncio.get_running_loop() - await self._bluetooth_adapters.refresh() - install_multiple_bleak_catcher() - self.async_setup_unavailable_tracking() - - def async_stop(self) -> None: - """Stop the Bluetooth integration at shutdown.""" - _LOGGER.debug("Stopping bluetooth manager") - self.shutdown = True - if self._cancel_unavailable_tracking: - self._cancel_unavailable_tracking.cancel() - self._cancel_unavailable_tracking = None - uninstall_multiple_bleak_catcher() - - def async_scanner_devices_by_address( - self, address: str, connectable: bool - ) -> list[BluetoothScannerDevice]: - """Get BluetoothScannerDevice by address.""" - if not connectable: - scanners: Iterable[BaseHaScanner] = itertools.chain( - self._connectable_scanners, self._non_connectable_scanners - ) - else: - scanners = self._connectable_scanners - return [ - BluetoothScannerDevice(scanner, *device_adv) - for scanner in scanners - if ( - device_adv := scanner.discovered_devices_and_advertisement_data.get( - address - ) - ) - ] - - def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]: - """Return all of discovered addresses. - - Include addresses from all the scanners including duplicates. - """ - yield from itertools.chain.from_iterable( - scanner.discovered_devices_and_advertisement_data - for scanner in self._connectable_scanners - ) - if not connectable: - yield from itertools.chain.from_iterable( - scanner.discovered_devices_and_advertisement_data - for scanner in self._non_connectable_scanners - ) - - def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]: - """Return all of combined best path to discovered from all the scanners.""" - histories = self._connectable_history if connectable else self._all_history - return [history.device for history in histories.values()] - - def async_setup_unavailable_tracking(self) -> None: - """Set up the unavailable tracking.""" - self._schedule_unavailable_tracking() - - def _schedule_unavailable_tracking(self) -> None: - """Schedule the unavailable tracking.""" - if TYPE_CHECKING: - assert self._loop is not None - loop = self._loop - self._cancel_unavailable_tracking = loop.call_at( - loop.time() + UNAVAILABLE_TRACK_SECONDS, self._async_check_unavailable - ) - - def _async_check_unavailable(self) -> None: - """Watch for unavailable devices and cleanup state history.""" - monotonic_now = MONOTONIC_TIME() - connectable_history = self._connectable_history - all_history = self._all_history - tracker = self._advertisement_tracker - intervals = tracker.intervals - - for connectable in (True, False): - if connectable: - unavailable_callbacks = self._connectable_unavailable_callbacks - else: - unavailable_callbacks = self._unavailable_callbacks - history = connectable_history if connectable else all_history - disappeared = set(history).difference( - self._async_all_discovered_addresses(connectable) - ) - for address in disappeared: - if not connectable: - # - # For non-connectable devices we also check the device has exceeded - # the advertising interval before we mark it as unavailable - # since it may have gone to sleep and since we do not need an active - # connection to it we can only determine its availability - # by the lack of advertisements - if advertising_interval := ( - intervals.get(address) or self._fallback_intervals.get(address) - ): - advertising_interval += TRACKER_BUFFERING_WOBBLE_SECONDS - else: - advertising_interval = ( - FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS - ) - time_since_seen = monotonic_now - all_history[address].time - if time_since_seen <= advertising_interval: - continue - - # The second loop (connectable=False) is responsible for removing - # the device from all the interval tracking since it is no longer - # available for both connectable and non-connectable - tracker.async_remove_fallback_interval(address) - tracker.async_remove_address(address) - self._address_disappeared(address) - - service_info = history.pop(address) - - if not (callbacks := unavailable_callbacks.get(address)): - continue - - for callback in callbacks: - try: - callback(service_info) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in unavailable callback") - - self._schedule_unavailable_tracking() - - def _address_disappeared(self, address: str) -> None: - """Call when an address disappears from the stack. - - This method is intended to be overridden by subclasses. - """ - - def _prefer_previous_adv_from_different_source( - self, - old: BluetoothServiceInfoBleak, - new: BluetoothServiceInfoBleak, - ) -> bool: - """Prefer previous advertisement from a different source if it is better.""" - if new.time - old.time > ( - stale_seconds := self._intervals.get( - new.address, - self._fallback_intervals.get( - new.address, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS - ), - ) - ): - # If the old advertisement is stale, any new advertisement is preferred - if self._debug: - _LOGGER.debug( - ( - "%s (%s): Switching from %s to %s (time elapsed:%s > stale" - " seconds:%s)" - ), - new.name, - new.address, - self._async_describe_source(old), - self._async_describe_source(new), - new.time - old.time, - stale_seconds, - ) - return False - if (new.rssi or NO_RSSI_VALUE) - RSSI_SWITCH_THRESHOLD > ( - old.rssi or NO_RSSI_VALUE - ): - # If new advertisement is RSSI_SWITCH_THRESHOLD more, - # the new one is preferred. - if self._debug: - _LOGGER.debug( - ( - "%s (%s): Switching from %s to %s (new rssi:%s - threshold:%s >" - " old rssi:%s)" - ), - new.name, - new.address, - self._async_describe_source(old), - self._async_describe_source(new), - new.rssi, - RSSI_SWITCH_THRESHOLD, - old.rssi, - ) - return False - return True - - def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: - """Handle a new advertisement from any scanner. - - Callbacks from all the scanners arrive here. - """ - - # Pre-filter noisy apple devices as they can account for 20-35% of the - # traffic on a typical network. - if ( - (manufacturer_data := service_info.manufacturer_data) - and APPLE_MFR_ID in manufacturer_data - and manufacturer_data[APPLE_MFR_ID][0] not in APPLE_START_BYTES_WANTED - and len(manufacturer_data) == 1 - and not service_info.service_data - ): - return - - address = service_info.device.address - all_history = self._all_history - connectable = service_info.connectable - connectable_history = self._connectable_history - old_connectable_service_info = connectable and connectable_history.get(address) - source = service_info.source - # This logic is complex due to the many combinations of scanners - # that are supported. - # - # We need to handle multiple connectable and non-connectable scanners - # and we need to handle the case where a device is connectable on one scanner - # but not on another. - # - # The device may also be connectable only by a scanner that has worse - # signal strength than a non-connectable scanner. - # - # all_history - the history of all advertisements from all scanners with the - # best advertisement from each scanner - # connectable_history - the history of all connectable advertisements from all - # scanners with the best advertisement from each - # connectable scanner - # - if ( - (old_service_info := all_history.get(address)) - and source != old_service_info.source - and (scanner := self._sources.get(old_service_info.source)) - and scanner.scanning - and self._prefer_previous_adv_from_different_source( - old_service_info, service_info - ) - ): - # If we are rejecting the new advertisement and the device is connectable - # but not in the connectable history or the connectable source is the same - # as the new source, we need to add it to the connectable history - if connectable: - if old_connectable_service_info and ( - # If its the same as the preferred source, we are done - # as we know we prefer the old advertisement - # from the check above - (old_connectable_service_info is old_service_info) - # If the old connectable source is different from the preferred - # source, we need to check it as well to see if we prefer - # the old connectable advertisement - or ( - source != old_connectable_service_info.source - and ( - connectable_scanner := self._sources.get( - old_connectable_service_info.source - ) - ) - and connectable_scanner.scanning - and self._prefer_previous_adv_from_different_source( - old_connectable_service_info, service_info - ) - ) - ): - return - - connectable_history[address] = service_info - - return - - if connectable: - connectable_history[address] = service_info - - all_history[address] = service_info - - # Track advertisement intervals to determine when we need to - # switch adapters or mark a device as unavailable - tracker = self._advertisement_tracker - if (last_source := tracker.sources.get(address)) and last_source != source: - # Source changed, remove the old address from the tracker - tracker.async_remove_address(address) - if address not in tracker.intervals: - tracker.async_collect(service_info) - - # If the advertisement data is the same as the last time we saw it, we - # don't need to do anything else unless its connectable and we are missing - # connectable history for the device so we can make it available again - # after unavailable callbacks. - if ( - # Ensure its not a connectable device missing from connectable history - not (connectable and not old_connectable_service_info) - # Than check if advertisement data is the same - and old_service_info - and not ( - service_info.manufacturer_data != old_service_info.manufacturer_data - or service_info.service_data != old_service_info.service_data - or service_info.service_uuids != old_service_info.service_uuids - or service_info.name != old_service_info.name - ) - ): - return - - if not connectable and old_connectable_service_info: - # Since we have a connectable path and our BleakClient will - # route any connection attempts to the connectable path, we - # mark the service_info as connectable so that the callbacks - # will be called and the device can be discovered. - service_info = BluetoothServiceInfoBleak( - name=service_info.name, - address=service_info.address, - rssi=service_info.rssi, - manufacturer_data=service_info.manufacturer_data, - service_data=service_info.service_data, - service_uuids=service_info.service_uuids, - source=service_info.source, - device=service_info.device, - advertisement=service_info.advertisement, - connectable=True, - time=service_info.time, - ) - - if (connectable or old_connectable_service_info) and ( - bleak_callbacks := self._bleak_callbacks - ): - # Bleak callbacks must get a connectable device - device = service_info.device - advertisement_data = service_info.advertisement - for callback_filters in bleak_callbacks: - _dispatch_bleak_callback(*callback_filters, device, advertisement_data) - - self._discover_service_info(service_info) - - def _discover_service_info(self, service_info: BluetoothServiceInfoBleak) -> None: - """Discover a new service info. - - This method is intended to be overridden by subclasses. - """ - - def _async_describe_source(self, service_info: BluetoothServiceInfoBleak) -> str: - """Describe a source.""" - if scanner := self._sources.get(service_info.source): - description = scanner.name - else: - description = service_info.source - if service_info.connectable: - description += " [connectable]" - return description - - def async_track_unavailable( - self, - callback: Callable[[BluetoothServiceInfoBleak], None], - address: str, - connectable: bool, - ) -> Callable[[], None]: - """Register a callback.""" - if connectable: - unavailable_callbacks = self._connectable_unavailable_callbacks - else: - unavailable_callbacks = self._unavailable_callbacks - unavailable_callbacks.setdefault(address, []).append(callback) - - def _async_remove_callback() -> None: - unavailable_callbacks[address].remove(callback) - if not unavailable_callbacks[address]: - del unavailable_callbacks[address] - - return _async_remove_callback - - def async_ble_device_from_address( - self, address: str, connectable: bool - ) -> BLEDevice | None: - """Return the BLEDevice if present.""" - histories = self._connectable_history if connectable else self._all_history - if history := histories.get(address): - return history.device - return None - - def async_address_present(self, address: str, connectable: bool) -> bool: - """Return if the address is present.""" - histories = self._connectable_history if connectable else self._all_history - return address in histories - - def async_discovered_service_info( - self, connectable: bool - ) -> Iterable[BluetoothServiceInfoBleak]: - """Return all the discovered services info.""" - histories = self._connectable_history if connectable else self._all_history - return histories.values() - - def async_last_service_info( - self, address: str, connectable: bool - ) -> BluetoothServiceInfoBleak | None: - """Return the last service info for an address.""" - histories = self._connectable_history if connectable else self._all_history - return histories.get(address) - - def async_register_scanner( - self, - scanner: BaseHaScanner, - connectable: bool, - connection_slots: int | None = None, - ) -> CALLBACK_TYPE: - """Register a new scanner.""" - _LOGGER.debug("Registering scanner %s", scanner.name) - if connectable: - scanners = self._connectable_scanners - else: - scanners = self._non_connectable_scanners - - def _unregister_scanner() -> None: - _LOGGER.debug("Unregistering scanner %s", scanner.name) - self._advertisement_tracker.async_remove_source(scanner.source) - scanners.remove(scanner) - del self._sources[scanner.source] - if connection_slots: - self.slot_manager.remove_adapter(scanner.adapter) - - scanners.append(scanner) - self._sources[scanner.source] = scanner - if connection_slots: - self.slot_manager.register_adapter(scanner.adapter, connection_slots) - return _unregister_scanner - - def async_register_bleak_callback( - self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] - ) -> CALLBACK_TYPE: - """Register a callback.""" - callback_entry = (callback, filters) - self._bleak_callbacks.append(callback_entry) - - def _remove_callback() -> None: - self._bleak_callbacks.remove(callback_entry) - - # Replay the history since otherwise we miss devices - # that were already discovered before the callback was registered - # or we are in passive mode - for history in self._connectable_history.values(): - _dispatch_bleak_callback( - callback, filters, history.device, history.advertisement - ) - - return _remove_callback - - def async_release_connection_slot(self, device: BLEDevice) -> None: - """Release a connection slot.""" - self.slot_manager.release_slot(device) - - def async_allocate_connection_slot(self, device: BLEDevice) -> bool: - """Allocate a connection slot.""" - return self.slot_manager.allocate_slot(device) - - def async_get_learned_advertising_interval(self, address: str) -> float | None: - """Get the learned advertising interval for a MAC address.""" - return self._intervals.get(address) - - def async_get_fallback_availability_interval(self, address: str) -> float | None: - """Get the fallback availability timeout for a MAC address.""" - return self._fallback_intervals.get(address) - - def async_set_fallback_availability_interval( - self, address: str, interval: float - ) -> None: - """Override the fallback availability timeout for a MAC address.""" - self._fallback_intervals[address] = interval - - class HomeAssistantBluetoothManager(BluetoothManager): """Manage Bluetooth for Home Assistant.""" __slots__ = ( "hass", + "storage", "_integration_matcher", "_callback_index", "_cancel_logging_listener", @@ -696,13 +57,15 @@ def __init__( ) -> None: """Init bluetooth manager.""" self.hass = hass + self.storage = storage self._integration_matcher = integration_matcher self._callback_index = BluetoothCallbackMatcherIndex() self._cancel_logging_listener: CALLBACK_TYPE | None = None - super().__init__(bluetooth_adapters, storage, slot_manager) + super().__init__(bluetooth_adapters, slot_manager) + self._async_logging_changed() @hass_callback - def _async_logging_changed(self, event: Event) -> None: + def _async_logging_changed(self, event: Event | None = None) -> None: """Handle logging change.""" self._debug = _LOGGER.isEnabledFor(logging.DEBUG) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a35c5be6daf17a..001a47767a1ca2 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -3,18 +3,15 @@ from collections.abc import Callable from enum import Enum -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING -from bluetooth_data_tools import monotonic_time_coarse from home_assistant_bluetooth import BluetoothServiceInfoBleak if TYPE_CHECKING: - from .manager import BluetoothManager + from .manager import HomeAssistantBluetoothManager -MANAGER: BluetoothManager | None = None - -MONOTONIC_TIME: Final = monotonic_time_coarse +MANAGER: HomeAssistantBluetoothManager | None = None BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py deleted file mode 100644 index d89f0b5b684280..00000000000000 --- a/homeassistant/components/bluetooth/usage.py +++ /dev/null @@ -1,51 +0,0 @@ -"""bluetooth usage utility to handle multiple instances.""" - -from __future__ import annotations - -import bleak -from bleak.backends.service import BleakGATTServiceCollection -import bleak_retry_connector - -from .wrappers import HaBleakClientWrapper, HaBleakScannerWrapper - -ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner -ORIGINAL_BLEAK_CLIENT = bleak.BleakClient -ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT_WITH_SERVICE_CACHE = ( - bleak_retry_connector.BleakClientWithServiceCache -) -ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT = bleak_retry_connector.BleakClient - - -def install_multiple_bleak_catcher() -> None: - """Wrap the bleak classes to return the shared instance. - - In case multiple instances are detected. - """ - bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] - bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] - bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] # noqa: E501 - bleak_retry_connector.BleakClient = HaBleakClientWrapper # type: ignore[misc] # noqa: E501 - - -def uninstall_multiple_bleak_catcher() -> None: - """Unwrap the bleak classes.""" - bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] - bleak.BleakClient = ORIGINAL_BLEAK_CLIENT # type: ignore[misc] - bleak_retry_connector.BleakClientWithServiceCache = ( # type: ignore[misc] - ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT_WITH_SERVICE_CACHE - ) - bleak_retry_connector.BleakClient = ( # type: ignore[misc] - ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT - ) - - -class HaBleakClientWithServiceCache(HaBleakClientWrapper): - """A BleakClient that implements service caching.""" - - def set_cached_services(self, services: BleakGATTServiceCollection | None) -> None: - """Set the cached services. - - No longer used since bleak 0.17+ has service caching built-in. - - This was only kept for backwards compatibility. - """ diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py deleted file mode 100644 index e3c08a035a8fb3..00000000000000 --- a/homeassistant/components/bluetooth/wrappers.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Bleak wrappers for bluetooth.""" -from __future__ import annotations - -import asyncio -from collections.abc import Callable -import contextlib -from dataclasses import dataclass -from functools import partial -import inspect -import logging -from typing import TYPE_CHECKING, Any, Final - -from bleak import BleakClient, BleakError -from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import ( - AdvertisementData, - AdvertisementDataCallback, - BaseBleakScanner, -) -from bleak_retry_connector import ( - NO_RSSI_VALUE, - ble_device_description, - clear_cache, - device_source, -) - -from homeassistant.core import CALLBACK_TYPE, callback as hass_callback -from homeassistant.helpers.frame import report - -from . import models -from .base_scanner import BaseHaScanner, BluetoothScannerDevice - -FILTER_UUIDS: Final = "UUIDs" -_LOGGER = logging.getLogger(__name__) - - -if TYPE_CHECKING: - from .manager import BluetoothManager - - -@dataclass(slots=True) -class _HaWrappedBleakBackend: - """Wrap bleak backend to make it usable by Home Assistant.""" - - device: BLEDevice - scanner: BaseHaScanner - client: type[BaseBleakClient] - source: str | None - - -class HaBleakScannerWrapper(BaseBleakScanner): - """A wrapper that uses the single instance.""" - - def __init__( - self, - *args: Any, - detection_callback: AdvertisementDataCallback | None = None, - service_uuids: list[str] | None = None, - **kwargs: Any, - ) -> None: - """Initialize the BleakScanner.""" - self._detection_cancel: CALLBACK_TYPE | None = None - self._mapped_filters: dict[str, set[str]] = {} - self._advertisement_data_callback: AdvertisementDataCallback | None = None - self._background_tasks: set[asyncio.Task] = set() - remapped_kwargs = { - "detection_callback": detection_callback, - "service_uuids": service_uuids or [], - **kwargs, - } - self._map_filters(*args, **remapped_kwargs) - super().__init__( - detection_callback=detection_callback, service_uuids=service_uuids or [] - ) - - @classmethod - async def discover(cls, timeout: float = 5.0, **kwargs: Any) -> list[BLEDevice]: - """Discover devices.""" - assert models.MANAGER is not None - return list(models.MANAGER.async_discovered_devices(True)) - - async def stop(self, *args: Any, **kwargs: Any) -> None: - """Stop scanning for devices.""" - - async def start(self, *args: Any, **kwargs: Any) -> None: - """Start scanning for devices.""" - - def _map_filters(self, *args: Any, **kwargs: Any) -> bool: - """Map the filters.""" - mapped_filters = {} - if filters := kwargs.get("filters"): - if filter_uuids := filters.get(FILTER_UUIDS): - mapped_filters[FILTER_UUIDS] = set(filter_uuids) - else: - _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) - if service_uuids := kwargs.get("service_uuids"): - mapped_filters[FILTER_UUIDS] = set(service_uuids) - if mapped_filters == self._mapped_filters: - return False - self._mapped_filters = mapped_filters - return True - - def set_scanning_filter(self, *args: Any, **kwargs: Any) -> None: - """Set the filters to use.""" - if self._map_filters(*args, **kwargs): - self._setup_detection_callback() - - def _cancel_callback(self) -> None: - """Cancel callback.""" - if self._detection_cancel: - self._detection_cancel() - self._detection_cancel = None - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return a list of discovered devices.""" - assert models.MANAGER is not None - return list(models.MANAGER.async_discovered_devices(True)) - - def register_detection_callback( - self, callback: AdvertisementDataCallback | None - ) -> Callable[[], None]: - """Register a detection callback. - - The callback is called when a device is discovered or has a property changed. - - This method takes the callback and registers it with the long running scanner. - """ - self._advertisement_data_callback = callback - self._setup_detection_callback() - assert self._detection_cancel is not None - return self._detection_cancel - - def _setup_detection_callback(self) -> None: - """Set up the detection callback.""" - if self._advertisement_data_callback is None: - return - callback = self._advertisement_data_callback - self._cancel_callback() - super().register_detection_callback(self._advertisement_data_callback) - assert models.MANAGER is not None - - if not inspect.iscoroutinefunction(callback): - detection_callback = callback - else: - - def detection_callback( - ble_device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - task = asyncio.create_task(callback(ble_device, advertisement_data)) - self._background_tasks.add(task) - task.add_done_callback(self._background_tasks.discard) - - self._detection_cancel = models.MANAGER.async_register_bleak_callback( - detection_callback, self._mapped_filters - ) - - def __del__(self) -> None: - """Delete the BleakScanner.""" - if self._detection_cancel: - # Nothing to do if event loop is already closed - with contextlib.suppress(RuntimeError): - asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) - - -def _rssi_sorter_with_connection_failure_penalty( - device: BluetoothScannerDevice, - connection_failure_count: dict[BaseHaScanner, int], - rssi_diff: int, -) -> float: - """Get a sorted list of scanner, device, advertisement data. - - Adjusting for previous connection failures. - - When a connection fails, we want to try the next best adapter so we - apply a penalty to the RSSI value to make it less likely to be chosen - for every previous connection failure. - - We use the 51% of the RSSI difference between the first and second - best adapter as the penalty. This ensures we will always try the - best adapter twice before moving on to the next best adapter since - the first failure may be a transient service resolution issue. - """ - base_rssi = device.advertisement.rssi or NO_RSSI_VALUE - if connect_failures := connection_failure_count.get(device.scanner): - if connect_failures > 1 and not rssi_diff: - rssi_diff = 1 - return base_rssi - (rssi_diff * connect_failures * 0.51) - return base_rssi - - -class HaBleakClientWrapper(BleakClient): - """Wrap the BleakClient to ensure it does not shutdown our scanner. - - If an address is passed into BleakClient instead of a BLEDevice, - bleak will quietly start a new scanner under the hood to resolve - the address. This can cause a conflict with our scanner. We need - to handle translating the address to the BLEDevice in this case - to avoid the whole stack from getting stuck in an in progress state - when an integration does this. - """ - - def __init__( # pylint: disable=super-init-not-called - self, - address_or_ble_device: str | BLEDevice, - disconnected_callback: Callable[[BleakClient], None] | None = None, - *args: Any, - timeout: float = 10.0, - **kwargs: Any, - ) -> None: - """Initialize the BleakClient.""" - if isinstance(address_or_ble_device, BLEDevice): - self.__address = address_or_ble_device.address - else: - report( - "attempted to call BleakClient with an address instead of a BLEDevice", - exclude_integrations={"bluetooth"}, - error_if_core=False, - ) - self.__address = address_or_ble_device - self.__disconnected_callback = disconnected_callback - self.__timeout = timeout - self.__connect_failures: dict[BaseHaScanner, int] = {} - self._backend: BaseBleakClient | None = None # type: ignore[assignment] - - @property - def is_connected(self) -> bool: - """Return True if the client is connected to a device.""" - return self._backend is not None and self._backend.is_connected - - async def clear_cache(self) -> bool: - """Clear the GATT cache.""" - if self._backend is not None and hasattr(self._backend, "clear_cache"): - return await self._backend.clear_cache() # type: ignore[no-any-return] - return await clear_cache(self.__address) - - def set_disconnected_callback( - self, - callback: Callable[[BleakClient], None] | None, - **kwargs: Any, - ) -> None: - """Set the disconnect callback.""" - self.__disconnected_callback = callback - if self._backend: - self._backend.set_disconnected_callback( - self._make_disconnected_callback(callback), - **kwargs, - ) - - def _make_disconnected_callback( - self, callback: Callable[[BleakClient], None] | None - ) -> Callable[[], None] | None: - """Make the disconnected callback. - - https://github.com/hbldh/bleak/pull/1256 - The disconnected callback needs to get the top level - BleakClientWrapper instance, not the backend instance. - - The signature of the callback for the backend is: - Callable[[], None] - - To make this work we need to wrap the callback in a partial - that passes the BleakClientWrapper instance as the first - argument. - """ - return None if callback is None else partial(callback, self) - - async def connect(self, **kwargs: Any) -> bool: - """Connect to the specified GATT server.""" - assert models.MANAGER is not None - manager = models.MANAGER - if manager.shutdown: - raise BleakError("Bluetooth is already shutdown") - if debug_logging := _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("%s: Looking for backend to connect", self.__address) - wrapped_backend = self._async_get_best_available_backend_and_device(manager) - device = wrapped_backend.device - scanner = wrapped_backend.scanner - self._backend = wrapped_backend.client( - device, - disconnected_callback=self._make_disconnected_callback( - self.__disconnected_callback - ), - timeout=self.__timeout, - ) - if debug_logging: - # Only lookup the description if we are going to log it - description = ble_device_description(device) - _, adv = scanner.discovered_devices_and_advertisement_data[device.address] - rssi = adv.rssi - _LOGGER.debug( - "%s: Connecting via %s (last rssi: %s)", description, scanner.name, rssi - ) - connected = None - try: - connected = await super().connect(**kwargs) - finally: - # If we failed to connect and its a local adapter (no source) - # we release the connection slot - if not connected: - self.__connect_failures[scanner] = ( - self.__connect_failures.get(scanner, 0) + 1 - ) - if not wrapped_backend.source: - manager.async_release_connection_slot(device) - - if debug_logging: - _LOGGER.debug( - "%s: Connected via %s (last rssi: %s)", description, scanner.name, rssi - ) - return connected - - @hass_callback - def _async_get_backend_for_ble_device( - self, manager: BluetoothManager, scanner: BaseHaScanner, ble_device: BLEDevice - ) -> _HaWrappedBleakBackend | None: - """Get the backend for a BLEDevice.""" - if not (source := device_source(ble_device)): - # If client is not defined in details - # its the client for this platform - if not manager.async_allocate_connection_slot(ble_device): - return None - cls = get_platform_client_backend_type() - return _HaWrappedBleakBackend(ble_device, scanner, cls, source) - - # Make sure the backend can connect to the device - # as some backends have connection limits - if not scanner.connector or not scanner.connector.can_connect(): - return None - - return _HaWrappedBleakBackend( - ble_device, scanner, scanner.connector.client, source - ) - - @hass_callback - def _async_get_best_available_backend_and_device( - self, manager: BluetoothManager - ) -> _HaWrappedBleakBackend: - """Get a best available backend and device for the given address. - - This method will return the backend with the best rssi - that has a free connection slot. - """ - address = self.__address - devices = manager.async_scanner_devices_by_address(self.__address, True) - sorted_devices = sorted( - devices, - key=lambda device: device.advertisement.rssi or NO_RSSI_VALUE, - reverse=True, - ) - - # If we have connection failures we adjust the rssi sorting - # to prefer the adapter/scanner with the less failures so - # we don't keep trying to connect with an adapter - # that is failing - if self.__connect_failures and len(sorted_devices) > 1: - # We use the rssi diff between to the top two - # to adjust the rssi sorter so that each failure - # will reduce the rssi sorter by the diff amount - rssi_diff = ( - sorted_devices[0].advertisement.rssi - - sorted_devices[1].advertisement.rssi - ) - adjusted_rssi_sorter = partial( - _rssi_sorter_with_connection_failure_penalty, - connection_failure_count=self.__connect_failures, - rssi_diff=rssi_diff, - ) - sorted_devices = sorted( - devices, - key=adjusted_rssi_sorter, - reverse=True, - ) - - for device in sorted_devices: - if backend := self._async_get_backend_for_ble_device( - manager, device.scanner, device.ble_device - ): - return backend - - raise BleakError( - "No backend with an available connection slot that can reach address" - f" {address} was found" - ) - - async def disconnect(self) -> bool: - """Disconnect from the device.""" - if self._backend is None: - return True - return await self._backend.disconnect() diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 5261e7371f35f6..5ad4b5a6c310c6 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -10,7 +10,7 @@ from bleak import BleakClient from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import DEFAULT_ADDRESS -from habluetooth import BaseHaScanner, BluetoothManager +from habluetooth import BaseHaScanner, BluetoothManager, get_manager from homeassistant.components.bluetooth import ( DOMAIN, @@ -18,7 +18,6 @@ BluetoothServiceInfo, BluetoothServiceInfoBleak, async_get_advertisement_callback, - models, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -60,9 +59,6 @@ def patch_bluetooth_time(mock_time: float) -> None: """Patch the bluetooth time.""" with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=mock_time, - ), patch( "homeassistant.components.bluetooth.MONOTONIC_TIME", return_value=mock_time ), patch( "habluetooth.base_scanner.monotonic_time_coarse", return_value=mock_time @@ -104,7 +100,7 @@ def generate_ble_device( def _get_manager() -> BluetoothManager: """Return the bluetooth manager.""" - return models.MANAGER + return get_manager() def inject_advertisement( diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py index 8681287baa23ef..190b05e60e8163 100644 --- a/tests/components/bluetooth/test_advertisement_tracker.py +++ b/tests/components/bluetooth/test_advertisement_tracker.py @@ -1,7 +1,6 @@ """Tests for the Bluetooth integration advertisement tracking.""" from datetime import timedelta import time -from unittest.mock import patch from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED import pytest @@ -25,6 +24,7 @@ generate_ble_device, inject_advertisement_with_time_and_source, inject_advertisement_with_time_and_source_connectable, + patch_bluetooth_time, ) from tests.common import async_fire_time_changed @@ -70,9 +70,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: ) monotonic_now = start_monotonic_time + ((ADVERTISING_TIMES_NEEDED - 1) * 2) - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -123,9 +122,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: monotonic_now = start_monotonic_time + ( (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS ) - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -189,9 +187,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: monotonic_now = start_monotonic_time + ( (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS ) - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -245,9 +242,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: monotonic_now = start_monotonic_time + ( (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS ) - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -321,9 +317,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: monotonic_now = start_monotonic_time + ( (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS ) - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -402,9 +397,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: monotonic_now = start_monotonic_time + ( (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS ) - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -415,9 +409,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: cancel_scanner() # Now that the scanner is gone we should go back to the stack default timeout - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -427,9 +420,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: assert switchbot_device_went_unavailable is False # Now that the scanner is gone we should go back to the stack default timeout - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, @@ -484,9 +476,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: ) monotonic_now = start_monotonic_time + UNAVAILABLE_TRACK_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 63ff735ca4391c..52624e67996d91 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -8,6 +8,7 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import DEFAULT_ADDRESS from habluetooth import scanner +from habluetooth.wrappers import HaBleakScannerWrapper import pytest from homeassistant.components import bluetooth @@ -35,7 +36,6 @@ SERVICE_DATA_UUID, SERVICE_UUID, ) -from homeassistant.components.bluetooth.wrappers import HaBleakScannerWrapper from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 361f0cd008f1a2..33683977ef028c 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -7,6 +7,7 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import AdvertisementHistory +from habluetooth.manager import FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS import pytest from homeassistant.components import bluetooth @@ -31,9 +32,6 @@ SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) -from homeassistant.components.bluetooth.manager import ( - FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, -) from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -48,6 +46,7 @@ inject_advertisement_with_source, inject_advertisement_with_time_and_source, inject_advertisement_with_time_and_source_connectable, + patch_bluetooth_time, ) from tests.common import async_fire_time_changed, load_fixture @@ -962,9 +961,8 @@ def _unavailable_callback(service_info: BluetoothServiceInfoBleak) -> None: return_value=[{"flow_id": "mock_flow_id"}], ) as mock_async_progress_by_init_data_type, patch.object( hass.config_entries.flow, "async_abort" - ) as mock_async_abort, patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + ) as mock_async_abort, patch_bluetooth_time( + monotonic_now + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -1105,9 +1103,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: ) monotonic_now = start_monotonic_time + 2 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -1170,9 +1167,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: # Check that device hasn't expired after a day monotonic_now = start_monotonic_time + 86400 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -1184,9 +1180,8 @@ def _switchbot_device_unavailable_callback(_address: str) -> None: # Try again after it has expired monotonic_now = start_monotonic_time + 604800 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + with patch_bluetooth_time( + monotonic_now + UNAVAILABLE_TRACK_SECONDS, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 8cffbe685b6590..7499f312cef939 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -7,6 +7,7 @@ from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData +from habluetooth.wrappers import HaBleakClientWrapper, HaBleakScannerWrapper import pytest from homeassistant.components.bluetooth import ( @@ -14,10 +15,6 @@ HaBluetoothConnector, HomeAssistantRemoteScanner, ) -from homeassistant.components.bluetooth.wrappers import ( - HaBleakClientWrapper, - HaBleakScannerWrapper, -) from homeassistant.core import HomeAssistant from . import ( diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 86f0ee4b5defbd..b6e50ebc565aaa 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -22,7 +22,11 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import inject_bluetooth_service_info, patch_all_discovered_devices +from . import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, + patch_bluetooth_time, +) from tests.common import async_fire_time_changed @@ -159,10 +163,9 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, - ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] + ): async_fire_time_changed( hass, dt_util.utcnow() @@ -176,9 +179,8 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 8cc76e01d8c13e..345c4b62b7eda8 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -48,6 +48,7 @@ inject_bluetooth_service_info, inject_bluetooth_service_info_bleak, patch_all_discovered_devices, + patch_bluetooth_time, ) from tests.common import ( @@ -471,9 +472,8 @@ def _async_generate_mock_data( assert processor.available is True monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) @@ -490,9 +490,8 @@ def _async_generate_mock_data( monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 12bdba66d75c9e..0edff02aa0e50b 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -2,17 +2,12 @@ from unittest.mock import patch import bleak -import bleak_retry_connector -import pytest - -from homeassistant.components.bluetooth.usage import ( +from habluetooth.usage import ( install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher, ) -from homeassistant.components.bluetooth.wrappers import ( - HaBleakClientWrapper, - HaBleakScannerWrapper, -) +from habluetooth.wrappers import HaBleakClientWrapper, HaBleakScannerWrapper + from homeassistant.core import HomeAssistant from . import generate_ble_device @@ -57,47 +52,3 @@ async def test_wrapping_bleak_client( instance = bleak.BleakClient(MOCK_BLE_DEVICE) assert not isinstance(instance, HaBleakClientWrapper) - - -async def test_bleak_client_reports_with_address( - hass: HomeAssistant, enable_bluetooth: None, caplog: pytest.LogCaptureFixture -) -> None: - """Test we report when we pass an address to BleakClient.""" - install_multiple_bleak_catcher() - - instance = bleak.BleakClient("00:00:00:00:00:00") - - assert "BleakClient with an address instead of a BLEDevice" in caplog.text - - assert isinstance(instance, HaBleakClientWrapper) - - uninstall_multiple_bleak_catcher() - - caplog.clear() - - instance = bleak.BleakClient("00:00:00:00:00:00") - - assert not isinstance(instance, HaBleakClientWrapper) - assert "BleakClient with an address instead of a BLEDevice" not in caplog.text - - -async def test_bleak_retry_connector_client_reports_with_address( - hass: HomeAssistant, enable_bluetooth: None, caplog: pytest.LogCaptureFixture -) -> None: - """Test we report when we pass an address to BleakClientWithServiceCache.""" - install_multiple_bleak_catcher() - - instance = bleak_retry_connector.BleakClientWithServiceCache("00:00:00:00:00:00") - - assert "BleakClient with an address instead of a BLEDevice" in caplog.text - - assert isinstance(instance, HaBleakClientWrapper) - - uninstall_multiple_bleak_catcher() - - caplog.clear() - - instance = bleak_retry_connector.BleakClientWithServiceCache("00:00:00:00:00:00") - - assert not isinstance(instance, HaBleakClientWrapper) - assert "BleakClient with an address instead of a BLEDevice" not in caplog.text diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index d3c2e1b54dbe02..1d294d90d7675f 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -2,30 +2,40 @@ from __future__ import annotations from collections.abc import Callable +from contextlib import contextmanager from unittest.mock import patch import bleak from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from bleak.exc import BleakError +from habluetooth.usage import ( + install_multiple_bleak_catcher, + uninstall_multiple_bleak_catcher, +) import pytest from homeassistant.components.bluetooth import ( MONOTONIC_TIME, BluetoothServiceInfoBleak, HaBluetoothConnector, + HomeAssistantBluetoothManager, HomeAssistantRemoteScanner, async_get_advertisement_callback, ) -from homeassistant.components.bluetooth.usage import ( - install_multiple_bleak_catcher, - uninstall_multiple_bleak_catcher, -) from homeassistant.core import HomeAssistant from . import _get_manager, generate_advertisement_data, generate_ble_device +@contextmanager +def mock_shutdown(manager: HomeAssistantBluetoothManager) -> None: + """Mock shutdown of the HomeAssistantBluetoothManager.""" + manager.shutdown = True + yield + manager.shutdown = False + + class FakeScanner(HomeAssistantRemoteScanner): """Fake scanner.""" @@ -133,7 +143,7 @@ def install_bleak_catcher_fixture(): def mock_platform_client_fixture(): """Fixture that mocks the platform client.""" with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClient, ): yield @@ -143,7 +153,7 @@ def mock_platform_client_fixture(): def mock_platform_client_that_fails_to_connect_fixture(): """Fixture that mocks the platform client that fails to connect.""" with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClientFailsToConnect, ): yield @@ -153,7 +163,7 @@ def mock_platform_client_that_fails_to_connect_fixture(): def mock_platform_client_that_raises_on_connect_fixture(): """Fixture that mocks the platform client that fails to connect.""" with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClientRaisesOnConnect, ): yield @@ -332,27 +342,27 @@ async def connect(self, *args, **kwargs): return True with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClientFailsHCI0Only, ): assert await client.connect() is False with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClientFailsHCI0Only, ): assert await client.connect() is False # After two tries we should switch to hci1 with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClientFailsHCI0Only, ): assert await client.connect() is True # ..and we remember that hci1 works as long as the client doesn't change with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClientFailsHCI0Only, ): assert await client.connect() is True @@ -361,7 +371,7 @@ async def connect(self, *args, **kwargs): client = bleak.BleakClient(ble_device) with patch( - "homeassistant.components.bluetooth.wrappers.get_platform_client_backend_type", + "habluetooth.wrappers.get_platform_client_backend_type", return_value=FakeBleakClientFailsHCI0Only, ): assert await client.connect() is False @@ -382,7 +392,7 @@ async def test_raise_after_shutdown( hass ) # hci0 has 2 slots, hci1 has 1 slot - with patch.object(manager, "shutdown", True): + with mock_shutdown(manager): ble_device = hci0_device_advs["00:00:00:00:00:01"][0] client = bleak.BleakClient(ble_device) with pytest.raises(BleakError, match="shutdown"): diff --git a/tests/components/bthome/test_binary_sensor.py b/tests/components/bthome/test_binary_sensor.py index 168988e510faba..c38bec3ba44903 100644 --- a/tests/components/bthome/test_binary_sensor.py +++ b/tests/components/bthome/test_binary_sensor.py @@ -2,7 +2,6 @@ from datetime import timedelta import logging import time -from unittest.mock import patch import pytest @@ -25,6 +24,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info, patch_all_discovered_devices, + patch_bluetooth_time, ) _LOGGER = logging.getLogger(__name__) @@ -236,10 +236,7 @@ async def test_unavailable(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, - ), patch_all_discovered_devices([]): + with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices([]): async_fire_time_changed( hass, dt_util.utcnow() @@ -290,10 +287,7 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, - ), patch_all_discovered_devices([]): + with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices([]): async_fire_time_changed( hass, dt_util.utcnow() @@ -344,10 +338,7 @@ async def test_sleepy_device_restores_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, - ), patch_all_discovered_devices([]): + with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices([]): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index c1f8e26ccb2acb..0b6e7a42cfb47c 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -2,7 +2,6 @@ from datetime import timedelta import logging import time -from unittest.mock import patch import pytest @@ -25,6 +24,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info, patch_all_discovered_devices, + patch_bluetooth_time, ) _LOGGER = logging.getLogger(__name__) @@ -1150,10 +1150,7 @@ async def test_unavailable(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, - ), patch_all_discovered_devices([]): + with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices([]): async_fire_time_changed( hass, dt_util.utcnow() @@ -1206,10 +1203,7 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, - ), patch_all_discovered_devices([]): + with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices([]): async_fire_time_changed( hass, dt_util.utcnow() @@ -1262,10 +1256,7 @@ async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, - ), patch_all_discovered_devices([]): + with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices([]): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 185ae2404dada5..5e7ca299fb6174 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -1,7 +1,6 @@ """Test the Govee BLE sensors.""" from datetime import timedelta import time -from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -27,6 +26,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info, patch_all_discovered_devices, + patch_bluetooth_time, ) @@ -112,9 +112,8 @@ async def test_gvh5178_multi_sensor(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, @@ -139,9 +138,8 @@ async def test_gvh5178_multi_sensor(hass: HomeAssistant) -> None: assert primary_temp_sensor.state == "1.0" # Fastforward time without BLE advertisements - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 49de6db6e136a3..b48ccad2fe2aa4 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -2,7 +2,6 @@ from datetime import timedelta import time -from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -24,6 +23,7 @@ inject_bluetooth_service_info, inject_bluetooth_service_info_bleak, patch_all_discovered_devices, + patch_bluetooth_time, ) @@ -63,9 +63,8 @@ async def test_sensors( # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, @@ -114,9 +113,8 @@ async def test_sensors_io_series_4( # Fast-forward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, diff --git a/tests/components/private_ble_device/__init__.py b/tests/components/private_ble_device/__init__.py index df9929293a1526..967f422872ba83 100644 --- a/tests/components/private_ble_device/__init__.py +++ b/tests/components/private_ble_device/__init__.py @@ -2,7 +2,6 @@ from datetime import timedelta import time -from unittest.mock import patch from home_assistant_bluetooth import BluetoothServiceInfoBleak @@ -16,6 +15,7 @@ generate_advertisement_data, generate_ble_device, inject_bluetooth_service_info_bleak, + patch_bluetooth_time, ) MAC_RPA_VALID_1 = "40:01:02:0a:c4:a6" @@ -70,9 +70,8 @@ async def async_inject_broadcast( async def async_move_time_forwards(hass: HomeAssistant, offset: float): """Mock time advancing from now to now+offset.""" - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=time.monotonic() + offset, + with patch_bluetooth_time( + time.monotonic() + offset, ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=offset)) await hass.async_block_till_done() diff --git a/tests/components/private_ble_device/test_sensor.py b/tests/components/private_ble_device/test_sensor.py index e35643d7626586..a517578990947a 100644 --- a/tests/components/private_ble_device/test_sensor.py +++ b/tests/components/private_ble_device/test_sensor.py @@ -81,7 +81,7 @@ async def test_estimated_broadcast_interval( "sensor.private_ble_device_000000_estimated_broadcast_interval" ) assert state - assert state.state == "90" + assert state.state == "90.0" # Learned broadcast interval takes over from fallback interval @@ -104,4 +104,4 @@ async def test_estimated_broadcast_interval( "sensor.private_ble_device_000000_estimated_broadcast_interval" ) assert state - assert state.state == "10" + assert state.state == "10.0" diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py index 9b83cd8c59074d..f201b3b55ff256 100644 --- a/tests/components/qingping/test_binary_sensor.py +++ b/tests/components/qingping/test_binary_sensor.py @@ -1,7 +1,6 @@ """Test the Qingping binary sensors.""" from datetime import timedelta import time -from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -17,6 +16,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info, patch_all_discovered_devices, + patch_bluetooth_time, ) @@ -72,9 +72,8 @@ async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, diff --git a/tests/components/qingping/test_sensor.py b/tests/components/qingping/test_sensor.py index 2fedbba9e5c3b8..12e3ec85c52452 100644 --- a/tests/components/qingping/test_sensor.py +++ b/tests/components/qingping/test_sensor.py @@ -1,7 +1,6 @@ """Test the Qingping sensors.""" from datetime import timedelta import time -from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -22,6 +21,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info, patch_all_discovered_devices, + patch_bluetooth_time, ) @@ -82,9 +82,8 @@ async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index e00b626b20b2b9..2e7a08673096db 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -1,7 +1,6 @@ """Test the SensorPush sensors.""" from datetime import timedelta import time -from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -22,6 +21,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info, patch_all_discovered_devices, + patch_bluetooth_time, ) @@ -55,9 +55,8 @@ async def test_sensors(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 32d1fea7f62225..14ea3e44af8e17 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -2,7 +2,6 @@ from datetime import timedelta import time -from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -23,6 +22,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info_bleak, patch_all_discovered_devices, + patch_bluetooth_time, ) @@ -294,9 +294,8 @@ async def test_unavailable(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, @@ -347,9 +346,8 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, @@ -400,9 +398,8 @@ async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index b0ddd99a7c264f..ceca08a68eed43 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -1,7 +1,6 @@ """Test Xiaomi BLE sensors.""" from datetime import timedelta import time -from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -28,6 +27,7 @@ from tests.components.bluetooth import ( inject_bluetooth_service_info_bleak, patch_all_discovered_devices, + patch_bluetooth_time, ) @@ -692,9 +692,8 @@ async def test_unavailable(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, @@ -739,9 +738,8 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, @@ -788,9 +786,8 @@ async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=monotonic_now, + with patch_bluetooth_time( + monotonic_now, ), patch_all_discovered_devices([]): async_fire_time_changed( hass, From a187a39f0b27acafc04b9444f461a58e2847be30 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 11 Dec 2023 22:06:16 +0100 Subject: [PATCH 044/118] Add config flow to Suez water (#104730) * Add config flow to Suez water * fix tests * Complete coverage * Change version to 2024.7 * Fix final test * Add issue when import is successful * Move hassdata * Do unique_id * Remove import issue when entry already exists * Remove import issue when entry already exists --- .coveragerc | 3 +- CODEOWNERS | 1 + .../components/suez_water/__init__.py | 49 +++- .../components/suez_water/config_flow.py | 166 +++++++++++++ homeassistant/components/suez_water/const.py | 5 + .../components/suez_water/manifest.json | 1 + homeassistant/components/suez_water/sensor.py | 33 +-- .../components/suez_water/strings.json | 35 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/suez_water/__init__.py | 1 + tests/components/suez_water/conftest.py | 14 ++ .../components/suez_water/test_config_flow.py | 223 ++++++++++++++++++ 14 files changed, 519 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/suez_water/config_flow.py create mode 100644 homeassistant/components/suez_water/const.py create mode 100644 homeassistant/components/suez_water/strings.json create mode 100644 tests/components/suez_water/__init__.py create mode 100644 tests/components/suez_water/conftest.py create mode 100644 tests/components/suez_water/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c012c8e686e859..7c74ed57505522 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1233,7 +1233,8 @@ omit = homeassistant/components/stream/hls.py homeassistant/components/stream/worker.py homeassistant/components/streamlabswater/* - homeassistant/components/suez_water/* + homeassistant/components/suez_water/__init__.py + homeassistant/components/suez_water/sensor.py homeassistant/components/supervisord/sensor.py homeassistant/components/supla/* homeassistant/components/surepetcare/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index dad0d51ad797ae..17b5909471da90 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1251,6 +1251,7 @@ build.json @home-assistant/supervisor /homeassistant/components/subaru/ @G-Two /tests/components/subaru/ @G-Two /homeassistant/components/suez_water/ @ooii +/tests/components/suez_water/ @ooii /homeassistant/components/sun/ @Swamp-Ig /tests/components/sun/ @Swamp-Ig /homeassistant/components/sunweg/ @rokam diff --git a/homeassistant/components/suez_water/__init__.py b/homeassistant/components/suez_water/__init__.py index a2d07a8d0a4de1..66c3981705c369 100644 --- a/homeassistant/components/suez_water/__init__.py +++ b/homeassistant/components/suez_water/__init__.py @@ -1 +1,48 @@ -"""France Suez Water integration.""" +"""The Suez Water integration.""" +from __future__ import annotations + +from pysuez import SuezClient +from pysuez.client import PySuezError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady + +from .const import CONF_COUNTER_ID, DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Suez Water from a config entry.""" + + def get_client() -> SuezClient: + try: + client = SuezClient( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_COUNTER_ID], + provider=None, + ) + if not client.check_credentials(): + raise ConfigEntryError + return client + except PySuezError: + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = await hass.async_add_executor_job(get_client) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/suez_water/config_flow.py b/homeassistant/components/suez_water/config_flow.py new file mode 100644 index 00000000000000..1dd79c017e01ea --- /dev/null +++ b/homeassistant/components/suez_water/config_flow.py @@ -0,0 +1,166 @@ +"""Config flow for Suez Water integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pysuez import SuezClient +from pysuez.client import PySuezError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue + +from .const import CONF_COUNTER_ID, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=suez_water"} +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_COUNTER_ID): str, + } +) + + +def validate_input(data: dict[str, Any]) -> None: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + try: + client = SuezClient( + data[CONF_USERNAME], + data[CONF_PASSWORD], + data[CONF_COUNTER_ID], + provider=None, + ) + if not client.check_credentials(): + raise InvalidAuth + except PySuezError: + raise CannotConnect + + +class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Suez Water.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() + try: + await self.hass.async_add_executor_job(validate_input, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Import the yaml config.""" + await self.async_set_unique_id(user_input[CONF_USERNAME]) + try: + self._abort_if_unique_id_configured() + except AbortFlow as err: + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Suez Water", + }, + ) + raise err + try: + await self.hass.async_add_executor_job(validate_input, user_input) + except CannotConnect: + async_create_issue( + self.hass, + DOMAIN, + "deprecated_yaml_import_issue_cannot_connect", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml_import_issue_cannot_connect", + translation_placeholders=ISSUE_PLACEHOLDER, + ) + return self.async_abort(reason="cannot_connect") + except InvalidAuth: + async_create_issue( + self.hass, + DOMAIN, + "deprecated_yaml_import_issue_invalid_auth", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml_import_issue_invalid_auth", + translation_placeholders=ISSUE_PLACEHOLDER, + ) + return self.async_abort(reason="invalid_auth") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + async_create_issue( + self.hass, + DOMAIN, + "deprecated_yaml_import_issue_unknown", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml_import_issue_unknown", + translation_placeholders=ISSUE_PLACEHOLDER, + ) + return self.async_abort(reason="unknown") + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Suez Water", + }, + ) + return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/suez_water/const.py b/homeassistant/components/suez_water/const.py new file mode 100644 index 00000000000000..7afc0d3ce3ea51 --- /dev/null +++ b/homeassistant/components/suez_water/const.py @@ -0,0 +1,5 @@ +"""Constants for the Suez Water integration.""" + +DOMAIN = "suez_water" + +CONF_COUNTER_ID = "counter_id" diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 3da91c4aa521f7..4503d7a1119177 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -2,6 +2,7 @@ "domain": "suez_water", "name": "Suez Water", "codeowners": ["@ooii"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/suez_water", "iot_class": "cloud_polling", "loggers": ["pysuez", "regex"], diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index d0c1bba211e160..fc5b804137d871 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -13,18 +13,19 @@ SensorDeviceClass, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfVolume from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import CONF_COUNTER_ID, DOMAIN + _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=12) -CONF_COUNTER_ID = "counter_id" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, @@ -41,21 +42,23 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the sensor platform.""" - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - counter_id = config[CONF_COUNTER_ID] - try: - client = SuezClient(username, password, counter_id, provider=None) - - if not client.check_credentials(): - _LOGGER.warning("Wrong username and/or password") - return + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - except PySuezError: - _LOGGER.warning("Unable to create Suez Client") - return - add_entities([SuezSensor(client)], True) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Suez Water sensor from a config entry.""" + client = hass.data[DOMAIN][entry.entry_id] + async_add_entities([SuezSensor(client)], True) class SuezSensor(SensorEntity): diff --git a/homeassistant/components/suez_water/strings.json b/homeassistant/components/suez_water/strings.json new file mode 100644 index 00000000000000..09df3ead17f3f4 --- /dev/null +++ b/homeassistant/components/suez_water/strings.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "counter_id": "Counter id" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_invalid_auth": { + "title": "The Suez water YAML configuration import failed", + "description": "Configuring Suez water using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The Suez water YAML configuration import failed", + "description": "Configuring Suez water using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Suez water works and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_unknown": { + "title": "The Suez water YAML configuration import failed", + "description": "Configuring Suez water using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the imported configuration is correct and remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 975bfc60688d2b..5936ac01b68f14 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -471,6 +471,7 @@ "stookalert", "stookwijzer", "subaru", + "suez_water", "sun", "sunweg", "surepetcare", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 33e2229eb2ece3..af822143d50380 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5537,7 +5537,7 @@ "suez_water": { "name": "Suez Water", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "sun": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ca0e164808778..10fe491ec04635 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1602,6 +1602,9 @@ pyspcwebgw==0.7.0 # homeassistant.components.squeezebox pysqueezebox==0.7.1 +# homeassistant.components.suez_water +pysuez==0.2.0 + # homeassistant.components.switchbee pyswitchbee==1.8.0 diff --git a/tests/components/suez_water/__init__.py b/tests/components/suez_water/__init__.py new file mode 100644 index 00000000000000..4605e06344add2 --- /dev/null +++ b/tests/components/suez_water/__init__.py @@ -0,0 +1 @@ +"""Tests for the Suez Water integration.""" diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py new file mode 100644 index 00000000000000..8a67cfe97d7c3f --- /dev/null +++ b/tests/components/suez_water/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the Suez Water tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.suez_water.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py new file mode 100644 index 00000000000000..265598e5c645de --- /dev/null +++ b/tests/components/suez_water/test_config_flow.py @@ -0,0 +1,223 @@ +"""Test the Suez Water config flow.""" +from unittest.mock import AsyncMock, patch + +from pysuez.client import PySuezError +import pytest + +from homeassistant import config_entries +from homeassistant.components.suez_water.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + +MOCK_DATA = { + "username": "test-username", + "password": "test-password", + "counter_id": "test-counter", +} + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch("homeassistant.components.suez_water.config_flow.SuezClient"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-username" + assert result["result"].unique_id == "test-username" + assert result["data"] == MOCK_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.suez_water.config_flow.SuezClient.__init__", + return_value=None, + ), patch( + "homeassistant.components.suez_water.config_flow.SuezClient.check_credentials", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_DATA, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + with patch("homeassistant.components.suez_water.config_flow.SuezClient"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-username" + assert result["result"].unique_id == "test-username" + assert result["data"] == MOCK_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_already_configured(hass: HomeAssistant) -> None: + """Test we abort when entry is already configured.""" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-username", + data=MOCK_DATA, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_DATA, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), [(PySuezError, "cannot_connect"), (Exception, "unknown")] +) +async def test_form_error( + hass: HomeAssistant, mock_setup_entry: AsyncMock, exception: Exception, error: str +) -> None: + """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.suez_water.config_flow.SuezClient", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_DATA, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": error} + + with patch( + "homeassistant.components.suez_water.config_flow.SuezClient", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_DATA, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"] == MOCK_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import( + hass: HomeAssistant, mock_setup_entry: AsyncMock, issue_registry: ir.IssueRegistry +) -> None: + """Test import flow.""" + with patch("homeassistant.components.suez_water.config_flow.SuezClient"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-username" + assert result["result"].unique_id == "test-username" + assert result["data"] == MOCK_DATA + assert len(mock_setup_entry.mock_calls) == 1 + assert len(issue_registry.issues) == 1 + + +@pytest.mark.parametrize( + ("exception", "reason"), [(PySuezError, "cannot_connect"), (Exception, "unknown")] +) +async def test_import_error( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + exception: Exception, + reason: str, + issue_registry: ir.IssueRegistry, +) -> None: + """Test we handle errors while importing.""" + + with patch( + "homeassistant.components.suez_water.config_flow.SuezClient", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == reason + assert len(issue_registry.issues) == 1 + + +async def test_importing_invalid_auth( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test we handle invalid auth when importing.""" + + with patch( + "homeassistant.components.suez_water.config_flow.SuezClient.__init__", + return_value=None, + ), patch( + "homeassistant.components.suez_water.config_flow.SuezClient.check_credentials", + return_value=False, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "invalid_auth" + assert len(issue_registry.issues) == 1 + + +async def test_import_already_configured( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test we abort import when entry is already configured.""" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-username", + data=MOCK_DATA, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(issue_registry.issues) == 1 From 662e19999d0ed5b0baf4414b38458c3706babb3e Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Mon, 11 Dec 2023 22:28:04 +0100 Subject: [PATCH 045/118] Add Fastdotcom DataUpdateCoordinator (#104839) * Adding DataUpdateCoordinator * Updating and adding test cases * Optimizing test * Fix typing * Prevent speedtest at startup * Removing typing on Coordinator * Update homeassistant/components/fastdotcom/coordinator.py Co-authored-by: G Johansson * Putting back typing * Update homeassistant/components/fastdotcom/__init__.py Co-authored-by: G Johansson * Adding proper StateType typing * Fix linting * Stricter typing * Creating proper test case for coordinator * Fixing typo * Patching librbary * Adding unavailable state test * Putting back in asserts * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Coordinator workable proposal * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Working test cases * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Update tests/components/fastdotcom/test_coordinator.py Co-authored-by: G Johansson * Fixing tests and context * Fix the freezer interval to 59 minutes * Fix test --------- Co-authored-by: G Johansson --- .../components/fastdotcom/__init__.py | 55 ++++++------------- .../components/fastdotcom/coordinator.py | 31 +++++++++++ homeassistant/components/fastdotcom/sensor.py | 53 +++++++----------- .../components/fastdotcom/test_config_flow.py | 5 +- .../components/fastdotcom/test_coordinator.py | 54 ++++++++++++++++++ 5 files changed, 122 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/fastdotcom/coordinator.py create mode 100644 tests/components/fastdotcom/test_coordinator.py diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 2fe5b3ccafc6a5..e872c3f501d1a9 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,22 +1,18 @@ """Support for testing internet speed via Fast.com.""" from __future__ import annotations -from datetime import datetime, timedelta import logging -from typing import Any -from fastdotcom import fast_com import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, Event, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from .const import CONF_MANUAL, DATA_UPDATED, DEFAULT_INTERVAL, DOMAIN, PLATFORMS +from .const import CONF_MANUAL, DEFAULT_INTERVAL, DOMAIN, PLATFORMS +from .coordinator import FastdotcomDataUpdateCoordindator _LOGGER = logging.getLogger(__name__) @@ -48,21 +44,20 @@ async def async_setup_platform(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up the Fast.com component.""" - data = hass.data[DOMAIN] = SpeedtestData(hass) + """Set up Fast.com from a config entry.""" + coordinator = FastdotcomDataUpdateCoordindator(hass) - entry.async_on_unload( - async_track_time_interval(hass, data.update, timedelta(hours=DEFAULT_INTERVAL)) - ) - # Run an initial update to get a starting state - await data.update() - - async def update(service_call: ServiceCall | None = None) -> None: - """Service call to manually update the data.""" - await data.update() + async def _request_refresh(event: Event) -> None: + """Request a refresh.""" + await coordinator.async_request_refresh() - hass.services.async_register(DOMAIN, "speedtest", update) + if hass.state == CoreState.running: + await coordinator.async_config_entry_first_refresh() + else: + # Don't start the speedtest when HA is starting up + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _request_refresh) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS, @@ -73,23 +68,7 @@ async def update(service_call: ServiceCall | None = None) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Fast.com config entry.""" + hass.services.async_remove(DOMAIN, "speedtest") if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data.pop(DOMAIN) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class SpeedtestData: - """Get the latest data from Fast.com.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the data object.""" - self.data: dict[str, Any] | None = None - self._hass = hass - - async def update(self, now: datetime | None = None) -> None: - """Get the latest data from fast.com.""" - _LOGGER.debug("Executing Fast.com speedtest") - fast_com_data = await self._hass.async_add_executor_job(fast_com) - self.data = {"download": fast_com_data} - _LOGGER.debug("Fast.com speedtest finished, with mbit/s: %s", fast_com_data) - dispatcher_send(self._hass, DATA_UPDATED) diff --git a/homeassistant/components/fastdotcom/coordinator.py b/homeassistant/components/fastdotcom/coordinator.py new file mode 100644 index 00000000000000..692a85d2edaf35 --- /dev/null +++ b/homeassistant/components/fastdotcom/coordinator.py @@ -0,0 +1,31 @@ +"""DataUpdateCoordinator for the Fast.com integration.""" +from __future__ import annotations + +from datetime import timedelta + +from fastdotcom import fast_com + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_INTERVAL, DOMAIN, LOGGER + + +class FastdotcomDataUpdateCoordindator(DataUpdateCoordinator[float]): + """Class to manage fetching Fast.com data API.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the coordinator for Fast.com.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(hours=DEFAULT_INTERVAL), + ) + + async def _async_update_data(self) -> float: + """Run an executor job to retrieve Fast.com data.""" + try: + return await self.hass.async_add_executor_job(fast_com) + except Exception as exc: + raise UpdateFailed(f"Error communicating with Fast.com: {exc}") from exc diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 939ab4a40e5224..b82b20defb5857 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -1,8 +1,6 @@ """Support for Fast.com internet speed testing sensor.""" from __future__ import annotations -from typing import Any - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -10,12 +8,12 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfDataRate -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DATA_UPDATED, DOMAIN +from .const import DOMAIN +from .coordinator import FastdotcomDataUpdateCoordindator async def async_setup_entry( @@ -24,11 +22,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fast.com sensor.""" - async_add_entities([SpeedtestSensor(entry.entry_id, hass.data[DOMAIN])]) + coordinator: FastdotcomDataUpdateCoordindator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([SpeedtestSensor(entry.entry_id, coordinator)]) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SpeedtestSensor(RestoreEntity, SensorEntity): +class SpeedtestSensor( + CoordinatorEntity[FastdotcomDataUpdateCoordindator], SensorEntity +): """Implementation of a Fast.com sensor.""" _attr_name = "Fast.com Download" @@ -38,31 +38,16 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): _attr_icon = "mdi:speedometer" _attr_should_poll = False - def __init__(self, entry_id: str, speedtest_data: dict[str, Any]) -> None: + def __init__( + self, entry_id: str, coordinator: FastdotcomDataUpdateCoordindator + ) -> None: """Initialize the sensor.""" - self._speedtest_data = speedtest_data + super().__init__(coordinator) self._attr_unique_id = entry_id - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - - self.async_on_remove( - async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update - ) - ) - - if not (state := await self.async_get_last_state()): - return - self._attr_native_value = state.state - - def update(self) -> None: - """Get the latest data and update the states.""" - if (data := self._speedtest_data.data) is None: # type: ignore[attr-defined] - return - self._attr_native_value = data["download"] - - @callback - def _schedule_immediate_update(self) -> None: - self.async_schedule_update_ha_state(True) + @property + def native_value( + self, + ) -> float: + """Return the state of the sensor.""" + return self.coordinator.data diff --git a/tests/components/fastdotcom/test_config_flow.py b/tests/components/fastdotcom/test_config_flow.py index 4314a7688d8edd..17e75935dae8df 100644 --- a/tests/components/fastdotcom/test_config_flow.py +++ b/tests/components/fastdotcom/test_config_flow.py @@ -57,10 +57,7 @@ async def test_single_instance_allowed( async def test_import_flow_success(hass: HomeAssistant) -> None: """Test import flow.""" - with patch( - "homeassistant.components.fastdotcom.__init__.SpeedtestData", - return_value={"download": "50"}, - ), patch("homeassistant.components.fastdotcom.sensor.SpeedtestSensor"): + with patch("homeassistant.components.fastdotcom.coordinator.fast_com"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, diff --git a/tests/components/fastdotcom/test_coordinator.py b/tests/components/fastdotcom/test_coordinator.py new file mode 100644 index 00000000000000..254301950fb6ab --- /dev/null +++ b/tests/components/fastdotcom/test_coordinator.py @@ -0,0 +1,54 @@ +"""Test the FastdotcomDataUpdateCoordindator.""" +from datetime import timedelta +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.fastdotcom.const import DOMAIN +from homeassistant.components.fastdotcom.coordinator import DEFAULT_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_fastdotcom_data_update_coordinator( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test the update coordinator.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="UNIQUE_TEST_ID", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=5.0 + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fast_com_download") + assert state is not None + assert state.state == "5.0" + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=10.0 + ): + freezer.tick(timedelta(hours=DEFAULT_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fast_com_download") + assert state.state == "10.0" + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", + side_effect=Exception("Test error"), + ): + freezer.tick(timedelta(hours=DEFAULT_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fast_com_download") + assert state.state is STATE_UNAVAILABLE From bf939298262d6d7ee30dddc92fe4c769f513f5c4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Dec 2023 22:58:56 +0100 Subject: [PATCH 046/118] Add support for Shelly Gen3 devices (#104874) * Add support for Gen3 devices * Add RPC_GENERATIONS const * Add gen3 to tests * More tests * Add BLOCK_GENERATIONS const * Use *_GENERATIONS constants from aioshelly --- homeassistant/components/shelly/__init__.py | 5 +++-- .../components/shelly/binary_sensor.py | 4 +++- homeassistant/components/shelly/button.py | 4 +++- homeassistant/components/shelly/climate.py | 3 ++- homeassistant/components/shelly/config_flow.py | 17 ++++++++++------- homeassistant/components/shelly/cover.py | 3 ++- homeassistant/components/shelly/event.py | 4 ++-- homeassistant/components/shelly/light.py | 4 ++-- homeassistant/components/shelly/sensor.py | 3 ++- homeassistant/components/shelly/switch.py | 4 ++-- homeassistant/components/shelly/update.py | 3 ++- homeassistant/components/shelly/utils.py | 6 ++++-- tests/components/shelly/test_config_flow.py | 14 ++++++++++++++ tests/components/shelly/test_init.py | 8 ++++---- 14 files changed, 55 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 553d32f8e48ac7..0fab86f7f4f8da 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -6,6 +6,7 @@ from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.common import ConnectionOptions +from aioshelly.const import RPC_GENERATIONS from aioshelly.exceptions import ( DeviceConnectionError, InvalidAuthError, @@ -123,7 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: get_entry_data(hass)[entry.entry_id] = ShellyEntryData() - if get_device_entry_gen(entry) == 2: + if get_device_entry_gen(entry) in RPC_GENERATIONS: return await _async_setup_rpc_entry(hass, entry) return await _async_setup_block_entry(hass, entry) @@ -313,7 +314,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.data.get(CONF_SLEEP_PERIOD): platforms = RPC_PLATFORMS - if get_device_entry_gen(entry) == 2: + if get_device_entry_gen(entry) in RPC_GENERATIONS: if unload_ok := await hass.config_entries.async_unload_platforms( entry, platforms ): diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index a5889cd11a75c8..caed52279da3e6 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -4,6 +4,8 @@ from dataclasses import dataclass from typing import Final, cast +from aioshelly.const import RPC_GENERATIONS + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -224,7 +226,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for device.""" - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: if config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_rpc( hass, diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index edc33c9a8a05d6..e5cc6b6580b1f4 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -5,6 +5,8 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar +from aioshelly.const import RPC_GENERATIONS + from homeassistant.components.button import ( ButtonDeviceClass, ButtonEntity, @@ -126,7 +128,7 @@ def _async_migrate_unique_ids( return async_migrate_unique_ids(entity_entry, coordinator) coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: coordinator = get_entry_data(hass)[config_entry.entry_id].rpc else: coordinator = get_entry_data(hass)[config_entry.entry_id].block diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 6a592c904f6c79..9ac603a7fb0601 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -6,6 +6,7 @@ from typing import Any, cast from aioshelly.block_device import Block +from aioshelly.const import RPC_GENERATIONS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from homeassistant.components.climate import ( @@ -51,7 +52,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate device.""" - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: return async_setup_rpc_entry(hass, config_entry, async_add_entities) coordinator = get_entry_data(hass)[config_entry.entry_id].block diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 98233d27b22ff6..68b0f1f8cccb18 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -6,6 +6,7 @@ from aioshelly.block_device import BlockDevice from aioshelly.common import ConnectionOptions, get_info +from aioshelly.const import BLOCK_GENERATIONS, RPC_GENERATIONS from aioshelly.exceptions import ( DeviceConnectionError, FirmwareUnsupported, @@ -66,7 +67,9 @@ async def validate_input( """ options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) - if get_info_gen(info) == 2: + gen = get_info_gen(info) + + if gen in RPC_GENERATIONS: ws_context = await get_ws_context(hass) rpc_device = await RpcDevice.create( async_get_clientsession(hass), @@ -81,7 +84,7 @@ async def validate_input( "title": rpc_device.name, CONF_SLEEP_PERIOD: sleep_period, "model": rpc_device.shelly.get("model"), - "gen": 2, + "gen": gen, } # Gen1 @@ -96,7 +99,7 @@ async def validate_input( "title": block_device.name, CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), "model": block_device.model, - "gen": 1, + "gen": gen, } @@ -165,7 +168,7 @@ async def async_step_credentials( """Handle the credentials step.""" errors: dict[str, str] = {} if user_input is not None: - if get_info_gen(self.info) == 2: + if get_info_gen(self.info) in RPC_GENERATIONS: user_input[CONF_USERNAME] = "admin" try: device_info = await validate_input( @@ -194,7 +197,7 @@ async def async_step_credentials( else: user_input = {} - if get_info_gen(self.info) == 2: + if get_info_gen(self.info) in RPC_GENERATIONS: schema = { vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD)): str, } @@ -331,7 +334,7 @@ async def async_step_reauth_confirm( await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_abort(reason="reauth_successful") - if self.entry.data.get("gen", 1) == 1: + if self.entry.data.get("gen", 1) in BLOCK_GENERATIONS: schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, @@ -360,7 +363,7 @@ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" return ( - config_entry.data.get("gen") == 2 + config_entry.data.get("gen") in RPC_GENERATIONS and not config_entry.data.get(CONF_SLEEP_PERIOD) and config_entry.data.get("model") != MODEL_WALL_DISPLAY ) diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 95f387f8f97875..4390790c7942b6 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -4,6 +4,7 @@ from typing import Any, cast from aioshelly.block_device import Block +from aioshelly.const import RPC_GENERATIONS from homeassistant.components.cover import ( ATTR_POSITION, @@ -26,7 +27,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up covers for device.""" - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: return async_setup_rpc_entry(hass, config_entry, async_add_entities) return async_setup_block_entry(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index af323c82a2420c..e93303d7191abe 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, Final from aioshelly.block_device import Block -from aioshelly.const import MODEL_I3 +from aioshelly.const import MODEL_I3, RPC_GENERATIONS from homeassistant.components.event import ( DOMAIN as EVENT_DOMAIN, @@ -80,7 +80,7 @@ async def async_setup_entry( coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: coordinator = get_entry_data(hass)[config_entry.entry_id].rpc if TYPE_CHECKING: assert coordinator diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 2dfc5b497b1236..7e49dc78e4d3e3 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -4,7 +4,7 @@ from typing import Any, cast from aioshelly.block_device import Block -from aioshelly.const import MODEL_BULB +from aioshelly.const import MODEL_BULB, RPC_GENERATIONS from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -53,7 +53,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up lights for device.""" - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: return async_setup_rpc_entry(hass, config_entry, async_add_entities) return async_setup_block_entry(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 99ccd9ab2ff289..4518135214cc05 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -6,6 +6,7 @@ from typing import Final, cast from aioshelly.block_device import Block +from aioshelly.const import RPC_GENERATIONS from homeassistant.components.sensor import ( RestoreSensor, @@ -925,7 +926,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for device.""" - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: if config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_rpc( hass, diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 5a398182e4d1cf..5ef39cd33af0fc 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -5,7 +5,7 @@ from typing import Any, cast from aioshelly.block_device import Block -from aioshelly.const import MODEL_2, MODEL_25, MODEL_GAS +from aioshelly.const import MODEL_2, MODEL_25, MODEL_GAS, RPC_GENERATIONS from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -49,7 +49,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches for device.""" - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: return async_setup_rpc_entry(hass, config_entry, async_add_entities) return async_setup_block_entry(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 9e52a292108f2d..975b61e631ac5f 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -6,6 +6,7 @@ import logging from typing import Any, Final, cast +from aioshelly.const import RPC_GENERATIONS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.components.update import ( @@ -119,7 +120,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up update entities for Shelly component.""" - if get_device_entry_gen(config_entry) == 2: + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: if config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_rpc( hass, diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index b53e3153a09679..7d475bf5ef8f21 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -7,12 +7,14 @@ from aiohttp.web import Request, WebSocketResponse from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.const import ( + BLOCK_GENERATIONS, MODEL_1L, MODEL_DIMMER, MODEL_DIMMER_2, MODEL_EM3, MODEL_I3, MODEL_NAMES, + RPC_GENERATIONS, ) from aioshelly.rpc_device import RpcDevice, WsServer @@ -284,7 +286,7 @@ def get_info_gen(info: dict[str, Any]) -> int: def get_model_name(info: dict[str, Any]) -> str: """Return the device model name.""" - if get_info_gen(info) == 2: + if get_info_gen(info) in RPC_GENERATIONS: return cast(str, MODEL_NAMES.get(info["model"], info["model"])) return cast(str, MODEL_NAMES.get(info["type"], info["type"])) @@ -420,4 +422,4 @@ def get_release_url(gen: int, model: str, beta: bool) -> str | None: if beta or model in DEVICES_WITHOUT_FIRMWARE_CHANGELOG: return None - return GEN1_RELEASE_URL if gen == 1 else GEN2_RELEASE_URL + return GEN1_RELEASE_URL if gen in BLOCK_GENERATIONS else GEN2_RELEASE_URL diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index c7ac472ada4341..1bccd3570cf61a 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -55,6 +55,7 @@ [ (1, MODEL_1), (2, MODEL_PLUS_2PM), + (3, MODEL_PLUS_2PM), ], ) async def test_form( @@ -109,6 +110,12 @@ async def test_form( {"password": "test2 password"}, "admin", ), + ( + 3, + MODEL_PLUS_2PM, + {"password": "test2 password"}, + "admin", + ), ], ) async def test_form_auth( @@ -465,6 +472,11 @@ async def test_form_auth_errors_test_connection_gen2( MODEL_PLUS_2PM, {"mac": "test-mac", "model": MODEL_PLUS_2PM, "auth": False, "gen": 2}, ), + ( + 3, + MODEL_PLUS_2PM, + {"mac": "test-mac", "model": MODEL_PLUS_2PM, "auth": False, "gen": 3}, + ), ], ) async def test_zeroconf( @@ -742,6 +754,7 @@ async def test_zeroconf_require_auth(hass: HomeAssistant, mock_block_device) -> [ (1, {"username": "test user", "password": "test1 password"}), (2, {"password": "test2 password"}), + (3, {"password": "test2 password"}), ], ) async def test_reauth_successful( @@ -780,6 +793,7 @@ async def test_reauth_successful( [ (1, {"username": "test user", "password": "test1 password"}), (2, {"password": "test2 password"}), + (3, {"password": "test2 password"}), ], ) async def test_reauth_unsuccessful(hass: HomeAssistant, gen, user_input) -> None: diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 2ead9cba198020..8f6599b39e46e9 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -41,7 +41,7 @@ async def test_custom_coap_port( assert "Starting CoAP context with UDP port 7632" in caplog.text -@pytest.mark.parametrize("gen", [1, 2]) +@pytest.mark.parametrize("gen", [1, 2, 3]) async def test_shared_device_mac( hass: HomeAssistant, gen, @@ -74,7 +74,7 @@ async def test_setup_entry_not_shelly( assert "probably comes from a custom integration" in caplog.text -@pytest.mark.parametrize("gen", [1, 2]) +@pytest.mark.parametrize("gen", [1, 2, 3]) async def test_device_connection_error( hass: HomeAssistant, gen, mock_block_device, mock_rpc_device, monkeypatch ) -> None: @@ -90,7 +90,7 @@ async def test_device_connection_error( assert entry.state == ConfigEntryState.SETUP_RETRY -@pytest.mark.parametrize("gen", [1, 2]) +@pytest.mark.parametrize("gen", [1, 2, 3]) async def test_mac_mismatch_error( hass: HomeAssistant, gen, mock_block_device, mock_rpc_device, monkeypatch ) -> None: @@ -106,7 +106,7 @@ async def test_mac_mismatch_error( assert entry.state == ConfigEntryState.SETUP_RETRY -@pytest.mark.parametrize("gen", [1, 2]) +@pytest.mark.parametrize("gen", [1, 2, 3]) async def test_device_auth_error( hass: HomeAssistant, gen, mock_block_device, mock_rpc_device, monkeypatch ) -> None: From d4cf0490161a63511b8eb404496c97dd1cd3893a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Dec 2023 23:10:11 +0100 Subject: [PATCH 047/118] Remove unneeded class _EntityDescriptionBase (#105518) --- homeassistant/helpers/entity.py | 13 +------------ homeassistant/util/frozen_dataclass_compat.py | 6 ++++++ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 6446a4fe6d61bb..dad0e2e00f357d 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -23,7 +23,6 @@ final, ) -from typing_extensions import dataclass_transform import voluptuous as vol from homeassistant.backports.functools import cached_property @@ -220,17 +219,7 @@ class EntityPlatformState(Enum): REMOVED = auto() -@dataclass_transform( - field_specifiers=(dataclasses.field, dataclasses.Field), - kw_only_default=True, # Set to allow setting kw_only in child classes -) -class _EntityDescriptionBase: - """Add PEP 681 decorator (dataclass transform).""" - - -class EntityDescription( - _EntityDescriptionBase, metaclass=FrozenOrThawed, frozen_or_thawed=True -): +class EntityDescription(metaclass=FrozenOrThawed, frozen_or_thawed=True): """A class that describes Home Assistant entities.""" # This is the key identifier for this entity diff --git a/homeassistant/util/frozen_dataclass_compat.py b/homeassistant/util/frozen_dataclass_compat.py index 96053844ab5828..e62e0a34cf141f 100644 --- a/homeassistant/util/frozen_dataclass_compat.py +++ b/homeassistant/util/frozen_dataclass_compat.py @@ -9,6 +9,8 @@ import sys from typing import Any +from typing_extensions import dataclass_transform + def _class_fields(cls: type, kw_only: bool) -> list[tuple[str, Any, Any]]: """Return a list of dataclass fields. @@ -41,6 +43,10 @@ def _class_fields(cls: type, kw_only: bool) -> list[tuple[str, Any, Any]]: return [(field.name, field.type, field) for field in cls_fields] +@dataclass_transform( + field_specifiers=(dataclasses.field, dataclasses.Field), + kw_only_default=True, # Set to allow setting kw_only in child classes +) class FrozenOrThawed(type): """Metaclass which which makes classes which behave like a dataclass. From bf9c2a08b79e62965ef338d6fd9274ff65556b71 Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Tue, 12 Dec 2023 04:42:52 +0100 Subject: [PATCH 048/118] Bump caldav to 1.3.8 (#105508) * Bump caldav to 1.3.8 1.3.8 fixes a bug where duplicate STATUS properties would be emitted for a single VTODO depending on the case of the arguments used. That bug meant that even though that is the intended API usage, passing lowercase for the status argument name would be rejected by caldav servers checking conformance with the spec which forbids duplicate STATUS. This in turn prevented HomeAssistant to add new items to a caldav todo list. Bump the requirements to 1.3.8 to repair that feature * Update global requirements --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index a7365515758e1d..619523ae7a1528 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"], - "requirements": ["caldav==1.3.6"] + "requirements": ["caldav==1.3.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7b76caf4c28fd5..90b3435979bd2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -607,7 +607,7 @@ btsmarthub-devicelist==0.2.3 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.3.6 +caldav==1.3.8 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10fe491ec04635..c4568fdd26e988 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -506,7 +506,7 @@ bthome-ble==3.2.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.3.6 +caldav==1.3.8 # homeassistant.components.coinbase coinbase==2.1.0 From 8922c9325949f8ab8aa69d827be5ee1d11f8292f Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 12 Dec 2023 16:30:54 +1000 Subject: [PATCH 049/118] Improve tests in Tessie (#105430) --- .../components/tessie/config_flow.py | 2 +- tests/components/tessie/conftest.py | 28 +++++ tests/components/tessie/test_config_flow.py | 116 ++++-------------- tests/components/tessie/test_coordinator.py | 12 -- 4 files changed, 51 insertions(+), 107 deletions(-) create mode 100644 tests/components/tessie/conftest.py diff --git a/homeassistant/components/tessie/config_flow.py b/homeassistant/components/tessie/config_flow.py index 4379a810309c8a..3e3207b264b2ed 100644 --- a/homeassistant/components/tessie/config_flow.py +++ b/homeassistant/components/tessie/config_flow.py @@ -81,7 +81,7 @@ async def async_step_reauth_confirm( ) except ClientResponseError as e: if e.status == HTTPStatus.UNAUTHORIZED: - errors["base"] = "invalid_access_token" + errors[CONF_ACCESS_TOKEN] = "invalid_access_token" else: errors["base"] = "unknown" except ClientConnectionError: diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py new file mode 100644 index 00000000000000..c7a344d54c56f0 --- /dev/null +++ b/tests/components/tessie/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for Tessie.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from .common import TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE + + +@pytest.fixture +def mock_get_state(): + """Mock get_state function.""" + with patch( + "homeassistant.components.tessie.coordinator.get_state", + return_value=TEST_VEHICLE_STATE_ONLINE, + ) as mock_get_state: + yield mock_get_state + + +@pytest.fixture +def mock_get_state_of_all_vehicles(): + """Mock get_state_of_all_vehicles function.""" + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + ) as mock_get_state_of_all_vehicles: + yield mock_get_state_of_all_vehicles diff --git a/tests/components/tessie/test_config_flow.py b/tests/components/tessie/test_config_flow.py index d1977a1319359b..182468e200c69a 100644 --- a/tests/components/tessie/test_config_flow.py +++ b/tests/components/tessie/test_config_flow.py @@ -15,24 +15,13 @@ ERROR_CONNECTION, ERROR_UNKNOWN, TEST_CONFIG, - TEST_STATE_OF_ALL_VEHICLES, setup_platform, ) from tests.common import MockConfigEntry -@pytest.fixture -def mock_get_state_of_all_vehicles(): - """Mock get_state_of_all_vehicles function.""" - with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - return_value=TEST_STATE_OF_ALL_VEHICLES, - ) as mock_get_state_of_all_vehicles: - yield mock_get_state_of_all_vehicles - - -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None: """Test we get the form.""" result1 = await hass.config_entries.flow.async_init( @@ -42,9 +31,6 @@ async def test_form(hass: HomeAssistant) -> None: assert not result1["errors"] with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - return_value=TEST_STATE_OF_ALL_VEHICLES, - ) as mock_get_state_of_all_vehicles, patch( "homeassistant.components.tessie.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -61,96 +47,38 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["data"] == TEST_CONFIG -async def test_form_invalid_access_token(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (ERROR_AUTH, {CONF_ACCESS_TOKEN: "invalid_access_token"}), + (ERROR_UNKNOWN, {"base": "unknown"}), + (ERROR_CONNECTION, {"base": "cannot_connect"}), + ], +) +async def test_form_errors( + hass: HomeAssistant, side_effect, error, mock_get_state_of_all_vehicles +) -> None: """Test invalid auth is handled.""" result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - side_effect=ERROR_AUTH, - ): - result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], - TEST_CONFIG, - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} - - # Complete the flow - with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - return_value=TEST_STATE_OF_ALL_VEHICLES, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - TEST_CONFIG, - ) - assert result3["type"] == FlowResultType.CREATE_ENTRY - - -async def test_form_invalid_response(hass: HomeAssistant) -> None: - """Test invalid auth is handled.""" - - result1 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + mock_get_state_of_all_vehicles.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, ) - with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - side_effect=ERROR_UNKNOWN, - ): - result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], - TEST_CONFIG, - ) - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"] == error # Complete the flow - with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - return_value=TEST_STATE_OF_ALL_VEHICLES, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - TEST_CONFIG, - ) - assert result3["type"] == FlowResultType.CREATE_ENTRY - - -async def test_form_network_issue(hass: HomeAssistant) -> None: - """Test network issues are handled.""" - - result1 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + mock_get_state_of_all_vehicles.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + TEST_CONFIG, ) - - with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - side_effect=ERROR_CONNECTION, - ): - result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], - TEST_CONFIG, - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - - # Complete the flow - with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", - return_value=TEST_STATE_OF_ALL_VEHICLES, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - TEST_CONFIG, - ) assert result3["type"] == FlowResultType.CREATE_ENTRY @@ -196,7 +124,7 @@ async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> No @pytest.mark.parametrize( ("side_effect", "error"), [ - (ERROR_AUTH, {"base": "invalid_access_token"}), + (ERROR_AUTH, {CONF_ACCESS_TOKEN: "invalid_access_token"}), (ERROR_UNKNOWN, {"base": "unknown"}), (ERROR_CONNECTION, {"base": "cannot_connect"}), ], diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 8fe92454c36175..50a9f2f773392f 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -1,8 +1,5 @@ """Test the Tessie sensor platform.""" from datetime import timedelta -from unittest.mock import patch - -import pytest from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL from homeassistant.components.tessie.sensor import TessieStatus @@ -25,15 +22,6 @@ WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) -@pytest.fixture -def mock_get_state(): - """Mock get_state function.""" - with patch( - "homeassistant.components.tessie.coordinator.get_state", - ) as mock_get_state: - yield mock_get_state - - async def test_coordinator_online(hass: HomeAssistant, mock_get_state) -> None: """Tests that the coordinator handles online vehciles.""" From 54c218c139cf36e559a96c9598657618f209af81 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 12 Dec 2023 07:17:09 +0000 Subject: [PATCH 050/118] Updates V2C sensor icons (#105534) update icons --- homeassistant/components/v2c/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index 0c860943922b1f..ed642510a34596 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -41,6 +41,7 @@ class V2CSensorEntityDescription(SensorEntityDescription, V2CRequiredKeysMixin): V2CSensorEntityDescription( key="charge_power", translation_key="charge_power", + icon="mdi:ev-station", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, @@ -49,6 +50,7 @@ class V2CSensorEntityDescription(SensorEntityDescription, V2CRequiredKeysMixin): V2CSensorEntityDescription( key="charge_energy", translation_key="charge_energy", + icon="mdi:ev-station", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, @@ -57,6 +59,7 @@ class V2CSensorEntityDescription(SensorEntityDescription, V2CRequiredKeysMixin): V2CSensorEntityDescription( key="charge_time", translation_key="charge_time", + icon="mdi:timer", native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.DURATION, @@ -65,6 +68,7 @@ class V2CSensorEntityDescription(SensorEntityDescription, V2CRequiredKeysMixin): V2CSensorEntityDescription( key="house_power", translation_key="house_power", + icon="mdi:home-lightning-bolt", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, @@ -73,6 +77,7 @@ class V2CSensorEntityDescription(SensorEntityDescription, V2CRequiredKeysMixin): V2CSensorEntityDescription( key="fv_power", translation_key="fv_power", + icon="mdi:solar-power-variant", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, @@ -99,7 +104,6 @@ class V2CSensorBaseEntity(V2CBaseEntity, SensorEntity): """Defines a base v2c sensor entity.""" entity_description: V2CSensorEntityDescription - _attr_icon = "mdi:ev-station" def __init__( self, From 324aa171c60f64ffa94afb42cbee5f7fd81114f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 08:20:26 +0100 Subject: [PATCH 051/118] Bump sigstore/cosign-installer from 3.2.0 to 3.3.0 (#105537) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index a646510582af9d..378208fbdf445a 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -331,7 +331,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Install Cosign - uses: sigstore/cosign-installer@v3.2.0 + uses: sigstore/cosign-installer@v3.3.0 with: cosign-release: "v2.0.2" From f4ee2a1ab41e560ad654949a6f161a43c2c3e2eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Dec 2023 21:24:24 -1000 Subject: [PATCH 052/118] Bump anyio to 4.1.0 (#105529) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4ee6c9ba3ead0f..1fce2f8092ba96 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -108,7 +108,7 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==4.0.0 +anyio==4.1.0 h11==0.14.0 httpcore==0.18.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f6835fdbaf147f..5356ee8663b27e 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -103,7 +103,7 @@ # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==4.0.0 +anyio==4.1.0 h11==0.14.0 httpcore==0.18.0 From a66c9bb7b66f9c3327e795b20034156650574195 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 12 Dec 2023 08:28:08 +0100 Subject: [PATCH 053/118] Update stale doc strings in entity platform tests (#105526) --- tests/components/binary_sensor/test_init.py | 4 ++-- tests/components/sensor/test_init.py | 2 +- tests/components/vacuum/test_init.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 437a2e1efa6adc..074ecb4434a79e 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -102,7 +102,7 @@ async def async_setup_entry_platform( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up test stt platform via config entry.""" + """Set up test binary_sensor platform via config entry.""" async_add_entities([entity1, entity2, entity3, entity4]) mock_platform( @@ -172,7 +172,7 @@ async def async_setup_entry_platform( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up test stt platform via config entry.""" + """Set up test binary_sensor platform via config entry.""" async_add_entities([entity1, entity2]) mock_platform( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index fc714a543bf668..9164bb442c3f49 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -2424,7 +2424,7 @@ async def async_setup_entry_platform( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up test stt platform via config entry.""" + """Set up test sensor platform via config entry.""" async_add_entities([entity1, entity2, entity3, entity4]) mock_platform( diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 7c5c0de1674762..3cf77d4f42015a 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -89,7 +89,7 @@ async def async_setup_entry_platform( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up test stt platform via config entry.""" + """Set up test vacuum platform via config entry.""" async_add_entities([entity1]) mock_platform( From 319d6db55b7169465bf86361d2dd50bed12b3042 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 12 Dec 2023 08:29:10 +0100 Subject: [PATCH 054/118] Migrate device_sun_light_trigger tests to use freezegun (#105520) --- .../device_sun_light_trigger/test_init.py | 167 +++++++++--------- 1 file changed, 85 insertions(+), 82 deletions(-) diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 724ae612f0db93..ada1c03a92367c 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -2,6 +2,7 @@ from datetime import datetime from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import ( @@ -72,13 +73,15 @@ async def scanner(hass, enable_custom_integrations): return scanner -async def test_lights_on_when_sun_sets(hass: HomeAssistant, scanner) -> None: +async def test_lights_on_when_sun_sets( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, scanner +) -> None: """Test lights go on when there is someone home and the sun sets.""" test_time = datetime(2017, 4, 5, 1, 2, 3, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=test_time): - assert await async_setup_component( - hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}} - ) + freezer.move_to(test_time) + assert await async_setup_component( + hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}} + ) await hass.services.async_call( light.DOMAIN, @@ -88,9 +91,9 @@ async def test_lights_on_when_sun_sets(hass: HomeAssistant, scanner) -> None: ) test_time = test_time.replace(hour=3) - with patch("homeassistant.util.dt.utcnow", return_value=test_time): - async_fire_time_changed(hass, test_time) - await hass.async_block_till_done() + freezer.move_to(test_time) + async_fire_time_changed(hass, test_time) + await hass.async_block_till_done() assert all( hass.states.get(ent_id).state == STATE_ON @@ -128,22 +131,22 @@ async def test_lights_turn_off_when_everyone_leaves( async def test_lights_turn_on_when_coming_home_after_sun_set( - hass: HomeAssistant, scanner + hass: HomeAssistant, freezer: FrozenDateTimeFactory, scanner ) -> None: """Test lights turn on when coming home after sun set.""" test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=test_time): - await hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "all"}, blocking=True - ) + freezer.move_to(test_time) + await hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "all"}, blocking=True + ) - assert await async_setup_component( - hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}} - ) + assert await async_setup_component( + hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}} + ) - hass.states.async_set(f"{DOMAIN}.device_2", STATE_HOME) + hass.states.async_set(f"{DOMAIN}.device_2", STATE_HOME) - await hass.async_block_till_done() + await hass.async_block_till_done() assert all( hass.states.get(ent_id).state == light.STATE_ON @@ -152,85 +155,85 @@ async def test_lights_turn_on_when_coming_home_after_sun_set( async def test_lights_turn_on_when_coming_home_after_sun_set_person( - hass: HomeAssistant, scanner + hass: HomeAssistant, freezer: FrozenDateTimeFactory, scanner ) -> None: """Test lights turn on when coming home after sun set.""" device_1 = f"{DOMAIN}.device_1" device_2 = f"{DOMAIN}.device_2" test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=test_time): - await hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "all"}, blocking=True - ) - hass.states.async_set(device_1, STATE_NOT_HOME) - hass.states.async_set(device_2, STATE_NOT_HOME) - await hass.async_block_till_done() + freezer.move_to(test_time) + await hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "all"}, blocking=True + ) + hass.states.async_set(device_1, STATE_NOT_HOME) + hass.states.async_set(device_2, STATE_NOT_HOME) + await hass.async_block_till_done() - assert all( - not light.is_on(hass, ent_id) - for ent_id in hass.states.async_entity_ids("light") - ) - assert hass.states.get(device_1).state == "not_home" - assert hass.states.get(device_2).state == "not_home" + assert all( + not light.is_on(hass, ent_id) + for ent_id in hass.states.async_entity_ids("light") + ) + assert hass.states.get(device_1).state == "not_home" + assert hass.states.get(device_2).state == "not_home" - assert await async_setup_component( - hass, - "person", - {"person": [{"id": "me", "name": "Me", "device_trackers": [device_1]}]}, - ) + assert await async_setup_component( + hass, + "person", + {"person": [{"id": "me", "name": "Me", "device_trackers": [device_1]}]}, + ) - assert await async_setup_component(hass, "group", {}) - await hass.async_block_till_done() - await group.Group.async_create_group( - hass, - "person_me", - created_by_service=False, - entity_ids=["person.me"], - icon=None, - mode=None, - object_id=None, - order=None, - ) + assert await async_setup_component(hass, "group", {}) + await hass.async_block_till_done() + await group.Group.async_create_group( + hass, + "person_me", + created_by_service=False, + entity_ids=["person.me"], + icon=None, + mode=None, + object_id=None, + order=None, + ) - assert await async_setup_component( - hass, - device_sun_light_trigger.DOMAIN, - {device_sun_light_trigger.DOMAIN: {"device_group": "group.person_me"}}, - ) + assert await async_setup_component( + hass, + device_sun_light_trigger.DOMAIN, + {device_sun_light_trigger.DOMAIN: {"device_group": "group.person_me"}}, + ) - assert all( - hass.states.get(ent_id).state == STATE_OFF - for ent_id in hass.states.async_entity_ids("light") - ) - assert hass.states.get(device_1).state == "not_home" - assert hass.states.get(device_2).state == "not_home" - assert hass.states.get("person.me").state == "not_home" + assert all( + hass.states.get(ent_id).state == STATE_OFF + for ent_id in hass.states.async_entity_ids("light") + ) + assert hass.states.get(device_1).state == "not_home" + assert hass.states.get(device_2).state == "not_home" + assert hass.states.get("person.me").state == "not_home" - # Unrelated device has no impact - hass.states.async_set(device_2, STATE_HOME) - await hass.async_block_till_done() + # Unrelated device has no impact + hass.states.async_set(device_2, STATE_HOME) + await hass.async_block_till_done() - assert all( - hass.states.get(ent_id).state == STATE_OFF - for ent_id in hass.states.async_entity_ids("light") - ) - assert hass.states.get(device_1).state == "not_home" - assert hass.states.get(device_2).state == "home" - assert hass.states.get("person.me").state == "not_home" + assert all( + hass.states.get(ent_id).state == STATE_OFF + for ent_id in hass.states.async_entity_ids("light") + ) + assert hass.states.get(device_1).state == "not_home" + assert hass.states.get(device_2).state == "home" + assert hass.states.get("person.me").state == "not_home" - # person home switches on - hass.states.async_set(device_1, STATE_HOME) - await hass.async_block_till_done() - await hass.async_block_till_done() + # person home switches on + hass.states.async_set(device_1, STATE_HOME) + await hass.async_block_till_done() + await hass.async_block_till_done() - assert all( - hass.states.get(ent_id).state == light.STATE_ON - for ent_id in hass.states.async_entity_ids("light") - ) - assert hass.states.get(device_1).state == "home" - assert hass.states.get(device_2).state == "home" - assert hass.states.get("person.me").state == "home" + assert all( + hass.states.get(ent_id).state == light.STATE_ON + for ent_id in hass.states.async_entity_ids("light") + ) + assert hass.states.get(device_1).state == "home" + assert hass.states.get(device_2).state == "home" + assert hass.states.get("person.me").state == "home" async def test_initialize_start(hass: HomeAssistant) -> None: From 4859226496f2c5037fbd99340a8d3b6aa4d89538 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 12 Dec 2023 08:30:08 +0100 Subject: [PATCH 055/118] Migrate geonetnz_* tests to use freezegun (#105521) --- .../geonetnz_quakes/test_geo_location.py | 23 +++++++++++-------- .../geonetnz_volcano/test_sensor.py | 9 +++++--- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 561d9aaedeb197..afc6ada75cd280 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -2,6 +2,8 @@ import datetime from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components import geonetnz_quakes from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED @@ -38,7 +40,11 @@ CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} -async def test_setup(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: """Test the general setup of the integration.""" # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry( @@ -64,9 +70,8 @@ async def test_setup(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update" - ) as mock_feed_update: + freezer.move_to(utcnow) + with patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update: mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) await hass.async_block_till_done() @@ -167,17 +172,17 @@ async def test_setup(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> assert len(entity_registry.entities) == 1 -async def test_setup_imperial(hass: HomeAssistant) -> None: +async def test_setup_imperial( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test the setup of the integration using imperial unit system.""" hass.config.units = US_CUSTOMARY_SYSTEM # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 15.5, (38.0, -3.0)) # Patching 'utcnow' to gain more control over the timed update. - utcnow = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update" - ) as mock_feed_update, patch( + freezer.move_to(dt_util.utcnow()) + with patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, patch( "aio_geojson_client.feed.GeoJsonFeed.last_timestamp", create=True ): mock_feed_update.return_value = "OK", [mock_entry_1] diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index a237fb2c314b64..4d11ff0673cfbf 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory from homeassistant.components import geonetnz_volcano from homeassistant.components.geo_location import ATTR_DISTANCE @@ -149,15 +150,17 @@ async def test_setup(hass: HomeAssistant) -> None: ) -async def test_setup_imperial(hass: HomeAssistant) -> None: +async def test_setup_imperial( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test the setup of the integration using imperial unit system.""" hass.config.units = US_CUSTOMARY_SYSTEM # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 1, 15.5, (38.0, -3.0)) # Patching 'utcnow' to gain more control over the timed update. - utcnow = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + freezer.move_to(dt_util.utcnow()) + with patch( "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock ) as mock_feed_update, patch( "aio_geojson_client.feed.GeoJsonFeed.__init__" From e2abd3b8d0e956ffd7d65c8fe19f8c10e417a794 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Dec 2023 21:31:23 -1000 Subject: [PATCH 056/118] Bump bluetooth libraries (#105522) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_manager.py | 1 + tests/components/private_ble_device/test_sensor.py | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 5d54ae6ea8241e..5f8cdbea9398e5 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.17.0", "dbus-fast==2.20.0", - "habluetooth==0.10.0" + "habluetooth==0.11.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1fce2f8092ba96..5d959667a8d9c7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,10 +23,10 @@ dbus-fast==2.20.0 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 -habluetooth==0.10.0 +habluetooth==0.11.1 hass-nabucasa==0.74.0 hassil==1.5.1 -home-assistant-bluetooth==1.10.4 +home-assistant-bluetooth==1.11.0 home-assistant-frontend==20231208.2 home-assistant-intents==2023.12.05 httpx==0.25.0 diff --git a/pyproject.toml b/pyproject.toml index 7b1b025ee242bc..b30e611d4a1303 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.25.0", - "home-assistant-bluetooth==1.10.4", + "home-assistant-bluetooth==1.11.0", "ifaddr==0.2.0", "Jinja2==3.1.2", "lru-dict==1.2.0", diff --git a/requirements.txt b/requirements.txt index 250a0948714e6f..4faf7f8b2c22eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.25.0 -home-assistant-bluetooth==1.10.4 +home-assistant-bluetooth==1.11.0 ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 90b3435979bd2b..b74ee315c32227 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -984,7 +984,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==0.10.0 +habluetooth==0.11.1 # homeassistant.components.cloud hass-nabucasa==0.74.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4568fdd26e988..d95eb332059878 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -783,7 +783,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==0.10.0 +habluetooth==0.11.1 # homeassistant.components.cloud hass-nabucasa==0.74.0 diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 33683977ef028c..ba28d8fa19cf1d 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -897,6 +897,7 @@ def clear_all_devices(self) -> None: """Clear all devices.""" self._discovered_device_advertisement_datas.clear() self._discovered_device_timestamps.clear() + self._previous_service_info.clear() new_info_callback = async_get_advertisement_callback(hass) connector = ( diff --git a/tests/components/private_ble_device/test_sensor.py b/tests/components/private_ble_device/test_sensor.py index a517578990947a..15e205c8c86dfd 100644 --- a/tests/components/private_ble_device/test_sensor.py +++ b/tests/components/private_ble_device/test_sensor.py @@ -94,7 +94,7 @@ async def test_estimated_broadcast_interval( "sensor.private_ble_device_000000_estimated_broadcast_interval" ) assert state - assert state.state == "10" + assert state.state == "10.0" # MAC address changes, the broadcast interval is kept From ac656847cb521d8cb621379f71f6a5b578bbb91a Mon Sep 17 00:00:00 2001 From: Khole <29937485+KJonline@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:38:12 +0000 Subject: [PATCH 057/118] Bump pyhiveapi to v0.5.16 (#105513) Co-authored-by: Khole Jones --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 67da3617b44278..870223f8fe6e6b 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_polling", "loggers": ["apyhiveapi"], - "requirements": ["pyhiveapi==0.5.14"] + "requirements": ["pyhiveapi==0.5.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index b74ee315c32227..b8210059527cda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1788,7 +1788,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.14 +pyhiveapi==0.5.16 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d95eb332059878..0fed6ef512a26a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1353,7 +1353,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.14 +pyhiveapi==0.5.16 # homeassistant.components.homematic pyhomematic==0.1.77 From 6908497c3dade54900643aac43681d5895998989 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Dec 2023 08:44:35 +0100 Subject: [PATCH 058/118] Add minor version to config entries (#105479) --- homeassistant/config_entries.py | 12 +++- homeassistant/data_entry_flow.py | 3 + tests/common.py | 2 + .../airly/snapshots/test_diagnostics.ambr | 1 + .../airnow/snapshots/test_diagnostics.ambr | 1 + .../airvisual/snapshots/test_diagnostics.ambr | 1 + .../snapshots/test_diagnostics.ambr | 1 + .../airzone/snapshots/test_diagnostics.ambr | 1 + .../snapshots/test_diagnostics.ambr | 1 + .../snapshots/test_diagnostics.ambr | 1 + .../axis/snapshots/test_diagnostics.ambr | 1 + .../blink/snapshots/test_diagnostics.ambr | 1 + tests/components/cloud/test_repairs.py | 1 + .../co2signal/snapshots/test_diagnostics.ambr | 1 + .../coinbase/snapshots/test_diagnostics.ambr | 1 + .../components/config/test_config_entries.py | 3 + .../deconz/snapshots/test_diagnostics.ambr | 1 + .../snapshots/test_diagnostics.ambr | 1 + .../snapshots/test_diagnostics.ambr | 1 + .../elgato/snapshots/test_config_flow.ambr | 6 ++ .../snapshots/test_config_flow.ambr | 2 + .../snapshots/test_diagnostics.ambr | 1 + .../esphome/snapshots/test_diagnostics.ambr | 1 + .../forecast_solar/snapshots/test_init.ambr | 1 + .../snapshots/test_config_flow.ambr | 4 ++ .../gios/snapshots/test_diagnostics.ambr | 1 + .../snapshots/test_diagnostics.ambr | 1 + tests/components/guardian/test_diagnostics.py | 1 + tests/components/hassio/test_repairs.py | 6 ++ .../snapshots/test_config_flow.ambr | 8 +++ tests/components/hue/test_services.py | 44 +++++++------ .../iqvia/snapshots/test_diagnostics.ambr | 1 + tests/components/kitchen_sink/test_init.py | 1 + .../kostal_plenticore/test_diagnostics.py | 1 + .../snapshots/test_diagnostics.ambr | 1 + .../netatmo/snapshots/test_diagnostics.ambr | 1 + .../nextdns/snapshots/test_diagnostics.ambr | 1 + tests/components/notion/test_diagnostics.py | 1 + .../onvif/snapshots/test_diagnostics.ambr | 1 + tests/components/openuv/test_diagnostics.py | 1 + .../components/philips_js/test_config_flow.py | 1 + .../pi_hole/snapshots/test_diagnostics.ambr | 1 + tests/components/ps4/test_init.py | 1 + .../components/purpleair/test_diagnostics.py | 1 + .../rainmachine/test_diagnostics.py | 2 + .../recollect_waste/test_diagnostics.py | 1 + .../components/repairs/test_websocket_api.py | 1 + .../ridwell/snapshots/test_diagnostics.ambr | 1 + .../components/samsungtv/test_diagnostics.py | 3 + .../snapshots/test_diagnostics.ambr | 1 + .../components/simplisafe/test_diagnostics.py | 1 + tests/components/subaru/test_config_flow.py | 2 + .../switcher_kis/test_diagnostics.py | 1 + .../snapshots/test_config_flow.ambr | 4 ++ .../twinkly/snapshots/test_diagnostics.ambr | 1 + tests/components/unifi/test_device_tracker.py | 1 + tests/components/unifi/test_diagnostics.py | 1 + tests/components/unifi/test_switch.py | 1 + .../uptime/snapshots/test_config_flow.ambr | 2 + .../vicare/snapshots/test_diagnostics.ambr | 1 + .../watttime/snapshots/test_diagnostics.ambr | 1 + tests/components/webostv/test_diagnostics.py | 1 + .../whois/snapshots/test_config_flow.ambr | 10 +++ .../wyoming/snapshots/test_config_flow.ambr | 6 ++ tests/snapshots/test_config_entries.ambr | 1 + tests/test_bootstrap.py | 1 + tests/test_config_entries.py | 61 ++++++++++++++++--- 67 files changed, 198 insertions(+), 31 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 756b2def58199a..336261c363219a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -205,6 +205,7 @@ class ConfigEntry: __slots__ = ( "entry_id", "version", + "minor_version", "domain", "title", "data", @@ -233,7 +234,9 @@ class ConfigEntry: def __init__( self, + *, version: int, + minor_version: int, domain: str, title: str, data: Mapping[str, Any], @@ -252,6 +255,7 @@ def __init__( # Version of the configuration. self.version = version + self.minor_version = minor_version # Domain the configuration belongs to self.domain = domain @@ -631,7 +635,8 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: while isinstance(handler, functools.partial): handler = handler.func # type: ignore[unreachable] - if self.version == handler.VERSION: + same_major_version = self.version == handler.VERSION + if same_major_version and self.minor_version == handler.MINOR_VERSION: return True if not (integration := self._integration_for_domain): @@ -639,6 +644,8 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: component = integration.get_component() supports_migrate = hasattr(component, "async_migrate_entry") if not supports_migrate: + if same_major_version: + return True _LOGGER.error( "Migration handler not found for entry %s for %s", self.title, @@ -676,6 +683,7 @@ def as_dict(self) -> dict[str, Any]: return { "entry_id": self.entry_id, "version": self.version, + "minor_version": self.minor_version, "domain": self.domain, "title": self.title, "data": dict(self.data), @@ -974,6 +982,7 @@ async def async_finish_flow( entry = ConfigEntry( version=result["version"], + minor_version=result["minor_version"], domain=result["handler"], title=result["title"], data=result["data"], @@ -1196,6 +1205,7 @@ async def async_initialize(self) -> None: config_entry = ConfigEntry( version=entry["version"], + minor_version=entry.get("minor_version", 1), domain=domain, entry_id=entry_id, data=entry["data"], diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e0ea195a3ffbed..b02fcbfcd1f1c0 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -94,6 +94,7 @@ class FlowResult(TypedDict, total=False): handler: Required[str] last_step: bool | None menu_options: list[str] | dict[str, str] + minor_version: int options: Mapping[str, Any] preview: str | None progress_action: str @@ -470,6 +471,7 @@ class FlowHandler: # Set by developer VERSION = 1 + MINOR_VERSION = 1 @property def source(self) -> str | None: @@ -549,6 +551,7 @@ def async_create_entry( """Finish flow.""" flow_result = FlowResult( version=self.VERSION, + minor_version=self.MINOR_VERSION, type=FlowResultType.CREATE_ENTRY, flow_id=self.flow_id, handler=self.handler, diff --git a/tests/common.py b/tests/common.py index 15498019b16c3b..1d0b278a6cb352 100644 --- a/tests/common.py +++ b/tests/common.py @@ -890,6 +890,7 @@ def __init__( domain="test", data=None, version=1, + minor_version=1, entry_id=None, source=config_entries.SOURCE_USER, title="Mock Title", @@ -910,6 +911,7 @@ def __init__( "pref_disable_polling": pref_disable_polling, "options": options, "version": version, + "minor_version": minor_version, "title": title, "unique_id": unique_id, "disabled_by": disabled_by, diff --git a/tests/components/airly/snapshots/test_diagnostics.ambr b/tests/components/airly/snapshots/test_diagnostics.ambr index a224ea07d46649..c22e96a2082e64 100644 --- a/tests/components/airly/snapshots/test_diagnostics.ambr +++ b/tests/components/airly/snapshots/test_diagnostics.ambr @@ -11,6 +11,7 @@ 'disabled_by': None, 'domain': 'airly', 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/airnow/snapshots/test_diagnostics.ambr b/tests/components/airnow/snapshots/test_diagnostics.ambr index 80c6de427ca77e..71fda040c1d1ca 100644 --- a/tests/components/airnow/snapshots/test_diagnostics.ambr +++ b/tests/components/airnow/snapshots/test_diagnostics.ambr @@ -26,6 +26,7 @@ 'disabled_by': None, 'domain': 'airnow', 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'minor_version': 1, 'options': dict({ 'radius': 150, }), diff --git a/tests/components/airvisual/snapshots/test_diagnostics.ambr b/tests/components/airvisual/snapshots/test_diagnostics.ambr index c805c5f9cb7ab0..cb9d25b8790ab0 100644 --- a/tests/components/airvisual/snapshots/test_diagnostics.ambr +++ b/tests/components/airvisual/snapshots/test_diagnostics.ambr @@ -38,6 +38,7 @@ 'disabled_by': None, 'domain': 'airvisual', 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'minor_version': 1, 'options': dict({ 'show_on_map': True, }), diff --git a/tests/components/airvisual_pro/snapshots/test_diagnostics.ambr b/tests/components/airvisual_pro/snapshots/test_diagnostics.ambr index 96cda8e012fb29..be709621e31e0e 100644 --- a/tests/components/airvisual_pro/snapshots/test_diagnostics.ambr +++ b/tests/components/airvisual_pro/snapshots/test_diagnostics.ambr @@ -93,6 +93,7 @@ 'disabled_by': None, 'domain': 'airvisual_pro', 'entry_id': '6a2b3770e53c28dc1eeb2515e906b0ce', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index 9cb6e550711957..8a8573689fa428 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -240,6 +240,7 @@ 'disabled_by': None, 'domain': 'airzone', 'entry_id': '6e7a0798c1734ba81d26ced0e690eaec', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index 594a5e6765a42a..4a7217a08c5e3f 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -93,6 +93,7 @@ 'disabled_by': None, 'domain': 'airzone_cloud', 'entry_id': 'd186e31edb46d64d14b9b2f11f1ebd9f', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/ambient_station/snapshots/test_diagnostics.ambr b/tests/components/ambient_station/snapshots/test_diagnostics.ambr index 4b231660c4b82e..b4aede7948c737 100644 --- a/tests/components/ambient_station/snapshots/test_diagnostics.ambr +++ b/tests/components/ambient_station/snapshots/test_diagnostics.ambr @@ -9,6 +9,7 @@ 'disabled_by': None, 'domain': 'ambient_station', 'entry_id': '382cf7643f016fd48b3fe52163fe8877', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/axis/snapshots/test_diagnostics.ambr b/tests/components/axis/snapshots/test_diagnostics.ambr index 74a1f110c142e1..9960fc9bfd2d23 100644 --- a/tests/components/axis/snapshots/test_diagnostics.ambr +++ b/tests/components/axis/snapshots/test_diagnostics.ambr @@ -41,6 +41,7 @@ 'disabled_by': None, 'domain': 'axis', 'entry_id': '676abe5b73621446e6550a2e86ffe3dd', + 'minor_version': 1, 'options': dict({ 'events': True, }), diff --git a/tests/components/blink/snapshots/test_diagnostics.ambr b/tests/components/blink/snapshots/test_diagnostics.ambr index 7fb13c975487b8..a1c18223c1159e 100644 --- a/tests/components/blink/snapshots/test_diagnostics.ambr +++ b/tests/components/blink/snapshots/test_diagnostics.ambr @@ -38,6 +38,7 @@ }), 'disabled_by': None, 'domain': 'blink', + 'minor_version': 1, 'options': dict({ 'scan_interval': 300, }), diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index f83de408bcced6..8d890a503e146f 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -154,6 +154,7 @@ async def test_legacy_subscription_repair_flow( "handler": DOMAIN, "description": None, "description_placeholders": None, + "minor_version": 1, } assert not issue_registry.async_get_issue( diff --git a/tests/components/co2signal/snapshots/test_diagnostics.ambr b/tests/components/co2signal/snapshots/test_diagnostics.ambr index 53a0f000f28002..645e0bd87e9855 100644 --- a/tests/components/co2signal/snapshots/test_diagnostics.ambr +++ b/tests/components/co2signal/snapshots/test_diagnostics.ambr @@ -9,6 +9,7 @@ 'disabled_by': None, 'domain': 'co2signal', 'entry_id': '904a74160aa6f335526706bee85dfb83', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/coinbase/snapshots/test_diagnostics.ambr b/tests/components/coinbase/snapshots/test_diagnostics.ambr index 38224a9992f698..9079a7682c803c 100644 --- a/tests/components/coinbase/snapshots/test_diagnostics.ambr +++ b/tests/components/coinbase/snapshots/test_diagnostics.ambr @@ -47,6 +47,7 @@ 'disabled_by': None, 'domain': 'coinbase', 'entry_id': '080272b77a4f80c41b94d7cdc86fd826', + 'minor_version': 1, 'options': dict({ 'account_balance_currencies': list([ ]), diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index bfee7551cffe41..414f4eb39f2f39 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -532,6 +532,7 @@ async def async_step_user(self, user_input=None): "description": None, "description_placeholders": None, "options": {}, + "minor_version": 1, } @@ -609,6 +610,7 @@ async def async_step_account(self, user_input=None): "description": None, "description_placeholders": None, "options": {}, + "minor_version": 1, } @@ -942,6 +944,7 @@ async def async_step_finish(self, user_input=None): "version": 1, "description": None, "description_placeholders": None, + "minor_version": 1, } diff --git a/tests/components/deconz/snapshots/test_diagnostics.ambr b/tests/components/deconz/snapshots/test_diagnostics.ambr index bbd96f1751cfcb..911f2e134f2636 100644 --- a/tests/components/deconz/snapshots/test_diagnostics.ambr +++ b/tests/components/deconz/snapshots/test_diagnostics.ambr @@ -12,6 +12,7 @@ 'disabled_by': None, 'domain': 'deconz', 'entry_id': '1', + 'minor_version': 1, 'options': dict({ 'master': True, }), diff --git a/tests/components/devolo_home_control/snapshots/test_diagnostics.ambr b/tests/components/devolo_home_control/snapshots/test_diagnostics.ambr index d2ff64ad59650e..8c069de8f62a8b 100644 --- a/tests/components/devolo_home_control/snapshots/test_diagnostics.ambr +++ b/tests/components/devolo_home_control/snapshots/test_diagnostics.ambr @@ -40,6 +40,7 @@ 'disabled_by': None, 'domain': 'devolo_home_control', 'entry_id': '123456', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/devolo_home_network/snapshots/test_diagnostics.ambr b/tests/components/devolo_home_network/snapshots/test_diagnostics.ambr index 236588b87ada6f..317aaac0116bb7 100644 --- a/tests/components/devolo_home_network/snapshots/test_diagnostics.ambr +++ b/tests/components/devolo_home_network/snapshots/test_diagnostics.ambr @@ -24,6 +24,7 @@ 'disabled_by': None, 'domain': 'devolo_home_network', 'entry_id': '123456', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/elgato/snapshots/test_config_flow.ambr b/tests/components/elgato/snapshots/test_config_flow.ambr index 46180994e61687..39202d383fa026 100644 --- a/tests/components/elgato/snapshots/test_config_flow.ambr +++ b/tests/components/elgato/snapshots/test_config_flow.ambr @@ -14,6 +14,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'elgato', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -25,6 +26,7 @@ 'disabled_by': None, 'domain': 'elgato', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -55,6 +57,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'elgato', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -66,6 +69,7 @@ 'disabled_by': None, 'domain': 'elgato', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -95,6 +99,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'elgato', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -106,6 +111,7 @@ 'disabled_by': None, 'domain': 'elgato', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/energyzero/snapshots/test_config_flow.ambr b/tests/components/energyzero/snapshots/test_config_flow.ambr index 68c46a705d79f2..9b4b3bfc635d38 100644 --- a/tests/components/energyzero/snapshots/test_config_flow.ambr +++ b/tests/components/energyzero/snapshots/test_config_flow.ambr @@ -11,6 +11,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'energyzero', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -19,6 +20,7 @@ 'disabled_by': None, 'domain': 'energyzero', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 098fc4ee37e95c..f0021e1934a895 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -15,6 +15,7 @@ 'disabled_by': None, 'domain': 'enphase_envoy', 'entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/esphome/snapshots/test_diagnostics.ambr b/tests/components/esphome/snapshots/test_diagnostics.ambr index d8de8f06bc638e..0d2f0e60b82d78 100644 --- a/tests/components/esphome/snapshots/test_diagnostics.ambr +++ b/tests/components/esphome/snapshots/test_diagnostics.ambr @@ -12,6 +12,7 @@ 'disabled_by': None, 'domain': 'esphome', 'entry_id': '08d821dc059cf4f645cb024d32c8e708', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/forecast_solar/snapshots/test_init.ambr b/tests/components/forecast_solar/snapshots/test_init.ambr index a009105e2e6b44..43145bcef9e9e5 100644 --- a/tests/components/forecast_solar/snapshots/test_init.ambr +++ b/tests/components/forecast_solar/snapshots/test_init.ambr @@ -8,6 +8,7 @@ 'disabled_by': None, 'domain': 'forecast_solar', 'entry_id': , + 'minor_version': 1, 'options': dict({ 'api_key': 'abcdef12345', 'azimuth': 190, diff --git a/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr index 31925e2d626cac..a2fe4b63cf833d 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr @@ -31,6 +31,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'gardena_bluetooth', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -40,6 +41,7 @@ 'disabled_by': None, 'domain': 'gardena_bluetooth', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -238,6 +240,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'gardena_bluetooth', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -247,6 +250,7 @@ 'disabled_by': None, 'domain': 'gardena_bluetooth', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/gios/snapshots/test_diagnostics.ambr b/tests/components/gios/snapshots/test_diagnostics.ambr index 67691602fcf253..1401b1e22a0821 100644 --- a/tests/components/gios/snapshots/test_diagnostics.ambr +++ b/tests/components/gios/snapshots/test_diagnostics.ambr @@ -9,6 +9,7 @@ 'disabled_by': None, 'domain': 'gios', 'entry_id': '86129426118ae32020417a53712d6eef', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/google_assistant/snapshots/test_diagnostics.ambr b/tests/components/google_assistant/snapshots/test_diagnostics.ambr index dffcddf5de51a8..663979eda7713e 100644 --- a/tests/components/google_assistant/snapshots/test_diagnostics.ambr +++ b/tests/components/google_assistant/snapshots/test_diagnostics.ambr @@ -7,6 +7,7 @@ }), 'disabled_by': None, 'domain': 'google_assistant', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index b58b2ccdba3aa1..ec288461661682 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -23,6 +23,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 1, + "minor_version": 1, "domain": "guardian", "title": REDACTED, "data": { diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 21bf7e5b47a7a4..5dd73a2161538e 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -100,6 +100,7 @@ async def test_supervisor_issue_repair_flow( "handler": "hassio", "description": None, "description_placeholders": None, + "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -195,6 +196,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions( "handler": "hassio", "description": None, "description_placeholders": None, + "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -309,6 +311,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir "handler": "hassio", "description": None, "description_placeholders": None, + "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -389,6 +392,7 @@ async def test_supervisor_issue_repair_flow_skip_confirmation( "handler": "hassio", "description": None, "description_placeholders": None, + "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -488,6 +492,7 @@ async def test_mount_failed_repair_flow( "handler": "hassio", "description": None, "description_placeholders": None, + "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -599,6 +604,7 @@ async def test_supervisor_issue_docker_config_repair_flow( "handler": "hassio", "description": None, "description_placeholders": None, + "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") diff --git a/tests/components/homewizard/snapshots/test_config_flow.ambr b/tests/components/homewizard/snapshots/test_config_flow.ambr index b5b7411532e1dd..663d91539911e5 100644 --- a/tests/components/homewizard/snapshots/test_config_flow.ambr +++ b/tests/components/homewizard/snapshots/test_config_flow.ambr @@ -12,6 +12,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'homewizard', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -21,6 +22,7 @@ 'disabled_by': None, 'domain': 'homewizard', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -52,6 +54,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'homewizard', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -61,6 +64,7 @@ 'disabled_by': None, 'domain': 'homewizard', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -92,6 +96,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'homewizard', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -101,6 +106,7 @@ 'disabled_by': None, 'domain': 'homewizard', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -128,6 +134,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'homewizard', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -137,6 +144,7 @@ 'disabled_by': None, 'domain': 'homewizard', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/hue/test_services.py b/tests/components/hue/test_services.py index 01b349c7361132..ec1c1154d7571f 100644 --- a/tests/components/hue/test_services.py +++ b/tests/components/hue/test_services.py @@ -49,11 +49,12 @@ async def test_hue_activate_scene(hass: HomeAssistant, mock_api_v1) -> None: """Test successful hue_activate_scene.""" config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, - "test", + version=1, + minor_version=1, + domain=hue.DOMAIN, + title="Mock Title", + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + source="test", options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) @@ -85,11 +86,12 @@ async def test_hue_activate_scene(hass: HomeAssistant, mock_api_v1) -> None: async def test_hue_activate_scene_transition(hass: HomeAssistant, mock_api_v1) -> None: """Test successful hue_activate_scene with transition.""" config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, - "test", + version=1, + minor_version=1, + domain=hue.DOMAIN, + title="Mock Title", + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + source="test", options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) @@ -123,11 +125,12 @@ async def test_hue_activate_scene_group_not_found( ) -> None: """Test failed hue_activate_scene due to missing group.""" config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, - "test", + version=1, + minor_version=1, + domain=hue.DOMAIN, + title="Mock Title", + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + source="test", options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) @@ -156,11 +159,12 @@ async def test_hue_activate_scene_scene_not_found( ) -> None: """Test failed hue_activate_scene due to missing scene.""" config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, - "test", + version=1, + minor_version=1, + domain=hue.DOMAIN, + title="Mock Title", + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + source="test", options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) diff --git a/tests/components/iqvia/snapshots/test_diagnostics.ambr b/tests/components/iqvia/snapshots/test_diagnostics.ambr index 49006716fb3c3b..c46a2cc15e39be 100644 --- a/tests/components/iqvia/snapshots/test_diagnostics.ambr +++ b/tests/components/iqvia/snapshots/test_diagnostics.ambr @@ -350,6 +350,7 @@ 'disabled_by': None, 'domain': 'iqvia', 'entry_id': '690ac4b7e99855fc5ee7b987a758d5cb', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py index ebd0f781d22513..71f3a83c701537 100644 --- a/tests/components/kitchen_sink/test_init.py +++ b/tests/components/kitchen_sink/test_init.py @@ -229,6 +229,7 @@ async def test_issues_created( "description_placeholders": None, "flow_id": flow_id, "handler": DOMAIN, + "minor_version": 1, "type": "create_entry", "version": 1, } diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index 87c8c0e26a8820..d509a323e6a47d 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -43,6 +43,7 @@ async def test_entry_diagnostics( "config_entry": { "entry_id": "2ab8dd92a62787ddfe213a67e09406bd", "version": 1, + "minor_version": 1, "domain": "kostal_plenticore", "title": "scb", "data": {"host": "192.168.1.2", "password": REDACTED}, diff --git a/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr b/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr index 30094f97cd3599..9d880746ff9d5d 100644 --- a/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr +++ b/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr @@ -17,6 +17,7 @@ 'disabled_by': None, 'domain': 'lacrosse_view', 'entry_id': 'lacrosse_view_test_entry_id', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/netatmo/snapshots/test_diagnostics.ambr b/tests/components/netatmo/snapshots/test_diagnostics.ambr index bd9005bd389019..cd547481de96c9 100644 --- a/tests/components/netatmo/snapshots/test_diagnostics.ambr +++ b/tests/components/netatmo/snapshots/test_diagnostics.ambr @@ -588,6 +588,7 @@ }), 'disabled_by': None, 'domain': 'netatmo', + 'minor_version': 1, 'options': dict({ 'weather_areas': dict({ 'Home avg': dict({ diff --git a/tests/components/nextdns/snapshots/test_diagnostics.ambr b/tests/components/nextdns/snapshots/test_diagnostics.ambr index 071d14f183b907..5040c6e052e3fc 100644 --- a/tests/components/nextdns/snapshots/test_diagnostics.ambr +++ b/tests/components/nextdns/snapshots/test_diagnostics.ambr @@ -9,6 +9,7 @@ 'disabled_by': None, 'domain': 'nextdns', 'entry_id': 'd9aa37407ddac7b964a99e86312288d6', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index 14a1a0e1768515..07a67cb142950a 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -18,6 +18,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 1, + "minor_version": 1, "domain": DOMAIN, "title": REDACTED, "data": {"username": REDACTED, "password": REDACTED}, diff --git a/tests/components/onvif/snapshots/test_diagnostics.ambr b/tests/components/onvif/snapshots/test_diagnostics.ambr index e10c8791ba92c5..c4f692a4e61102 100644 --- a/tests/components/onvif/snapshots/test_diagnostics.ambr +++ b/tests/components/onvif/snapshots/test_diagnostics.ambr @@ -13,6 +13,7 @@ 'disabled_by': None, 'domain': 'onvif', 'entry_id': '1', + 'minor_version': 1, 'options': dict({ 'enable_webhooks': True, 'extra_arguments': '-pred 1', diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index fa7c7898037367..e7efc4596309cd 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -19,6 +19,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 2, + "minor_version": 1, "domain": "openuv", "title": REDACTED, "data": { diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 603e278d592484..8229f4e8fa9ee7 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -160,6 +160,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) "data": MOCK_CONFIG_PAIRED, "version": 1, "options": {}, + "minor_version": 1, } await hass.async_block_till_done() diff --git a/tests/components/pi_hole/snapshots/test_diagnostics.ambr b/tests/components/pi_hole/snapshots/test_diagnostics.ambr index 69c77acc64aede..865494b5e9feb1 100644 --- a/tests/components/pi_hole/snapshots/test_diagnostics.ambr +++ b/tests/components/pi_hole/snapshots/test_diagnostics.ambr @@ -25,6 +25,7 @@ 'disabled_by': None, 'domain': 'pi_hole', 'entry_id': 'pi_hole_mock_entry', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 3b8dc5e1e247b7..1252348b3e000d 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -44,6 +44,7 @@ MOCK_FLOW_RESULT = { "version": VERSION, + "minor_version": 1, "handler": DOMAIN, "type": data_entry_flow.FlowResultType.CREATE_ENTRY, "title": "test_ps4", diff --git a/tests/components/purpleair/test_diagnostics.py b/tests/components/purpleair/test_diagnostics.py index 35dc515241c0ba..85b078d0765c77 100644 --- a/tests/components/purpleair/test_diagnostics.py +++ b/tests/components/purpleair/test_diagnostics.py @@ -17,6 +17,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 1, + "minor_version": 1, "domain": "purpleair", "title": REDACTED, "data": { diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index 9c3aa6cd7dedc4..47cb32020262a5 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -19,6 +19,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 2, + "minor_version": 1, "domain": "rainmachine", "title": "Mock Title", "data": { @@ -645,6 +646,7 @@ async def test_entry_diagnostics_failed_controller_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 2, + "minor_version": 1, "domain": "rainmachine", "title": "Mock Title", "data": { diff --git a/tests/components/recollect_waste/test_diagnostics.py b/tests/components/recollect_waste/test_diagnostics.py index e905f4a5606540..69ff1596d7ccdb 100644 --- a/tests/components/recollect_waste/test_diagnostics.py +++ b/tests/components/recollect_waste/test_diagnostics.py @@ -19,6 +19,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 2, + "minor_version": 1, "domain": "recollect_waste", "title": REDACTED, "data": {"place_id": REDACTED, "service_id": TEST_SERVICE_ID}, diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 6c9b51a7cf67d1..1f68c9a28d3dc2 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -338,6 +338,7 @@ async def test_fix_issue( "description_placeholders": None, "flow_id": flow_id, "handler": domain, + "minor_version": 1, "type": "create_entry", "version": 1, } diff --git a/tests/components/ridwell/snapshots/test_diagnostics.ambr b/tests/components/ridwell/snapshots/test_diagnostics.ambr index a98374d2941c36..d32b1d3f446089 100644 --- a/tests/components/ridwell/snapshots/test_diagnostics.ambr +++ b/tests/components/ridwell/snapshots/test_diagnostics.ambr @@ -36,6 +36,7 @@ 'disabled_by': None, 'domain': 'ridwell', 'entry_id': '11554ec901379b9cc8f5a6c1d11ce978', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 231880a009b9cb..651b6f27a44c8b 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -41,6 +41,7 @@ async def test_entry_diagnostics( "disabled_by": None, "domain": "samsungtv", "entry_id": "123456", + "minor_version": 1, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -77,6 +78,7 @@ async def test_entry_diagnostics_encrypted( "disabled_by": None, "domain": "samsungtv", "entry_id": "123456", + "minor_version": 1, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -112,6 +114,7 @@ async def test_entry_diagnostics_encrypte_offline( "disabled_by": None, "domain": "samsungtv", "entry_id": "123456", + "minor_version": 1, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, diff --git a/tests/components/screenlogic/snapshots/test_diagnostics.ambr b/tests/components/screenlogic/snapshots/test_diagnostics.ambr index 05320c147e5a79..0efd10fb914381 100644 --- a/tests/components/screenlogic/snapshots/test_diagnostics.ambr +++ b/tests/components/screenlogic/snapshots/test_diagnostics.ambr @@ -9,6 +9,7 @@ 'disabled_by': None, 'domain': 'screenlogic', 'entry_id': 'screenlogictest', + 'minor_version': 1, 'options': dict({ 'scan_interval': 30, }), diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index 3153674ce57635..538165bd769f5f 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -17,6 +17,7 @@ async def test_entry_diagnostics( "entry": { "entry_id": config_entry.entry_id, "version": 1, + "minor_version": 1, "domain": "simplisafe", "title": REDACTED, "data": {"token": REDACTED, "username": REDACTED}, diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index c3df10ed6181d1..7e892d2c99a5a4 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -130,6 +130,7 @@ async def test_user_form_pin_not_required( "version": 1, "data": deepcopy(TEST_CONFIG), "options": {}, + "minor_version": 1, } expected["data"][CONF_PIN] = None @@ -316,6 +317,7 @@ async def test_pin_form_success(hass: HomeAssistant, pin_form) -> None: "version": 1, "data": TEST_CONFIG, "options": {}, + "minor_version": 1, } result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID assert result == expected diff --git a/tests/components/switcher_kis/test_diagnostics.py b/tests/components/switcher_kis/test_diagnostics.py index 0af89cd238c328..f238bceb39ef35 100644 --- a/tests/components/switcher_kis/test_diagnostics.py +++ b/tests/components/switcher_kis/test_diagnostics.py @@ -48,6 +48,7 @@ async def test_diagnostics( "entry": { "entry_id": entry.entry_id, "version": 1, + "minor_version": 1, "domain": "switcher_kis", "title": "Mock Title", "data": {}, diff --git a/tests/components/twentemilieu/snapshots/test_config_flow.ambr b/tests/components/twentemilieu/snapshots/test_config_flow.ambr index 7acb466d997772..00b960620520ec 100644 --- a/tests/components/twentemilieu/snapshots/test_config_flow.ambr +++ b/tests/components/twentemilieu/snapshots/test_config_flow.ambr @@ -15,6 +15,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'twentemilieu', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -27,6 +28,7 @@ 'disabled_by': None, 'domain': 'twentemilieu', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -57,6 +59,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'twentemilieu', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -69,6 +72,7 @@ 'disabled_by': None, 'domain': 'twentemilieu', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/twinkly/snapshots/test_diagnostics.ambr b/tests/components/twinkly/snapshots/test_diagnostics.ambr index 7a7dc2557ef41d..2a10154c3dacf8 100644 --- a/tests/components/twinkly/snapshots/test_diagnostics.ambr +++ b/tests/components/twinkly/snapshots/test_diagnostics.ambr @@ -30,6 +30,7 @@ 'disabled_by': None, 'domain': 'twinkly', 'entry_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 5a12b99d10b602..34d43129a947ab 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -930,6 +930,7 @@ async def test_restoring_client( config_entry = config_entries.ConfigEntry( version=1, + minor_version=1, domain=UNIFI_DOMAIN, title="Mock Title", data=ENTRY_CONFIG, diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 638e79ae64961d..127b9b79c2b729 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -129,6 +129,7 @@ async def test_entry_diagnostics( "disabled_by": None, "domain": "unifi", "entry_id": "1", + "minor_version": 1, "options": { "allow_bandwidth_sensors": True, "allow_uptime_sensors": True, diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 00ebcd0e683bc5..6a9e58b6f760f4 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1628,6 +1628,7 @@ async def test_updating_unique_id( config_entry = config_entries.ConfigEntry( version=1, + minor_version=1, domain=UNIFI_DOMAIN, title="Mock Title", data=ENTRY_CONFIG, diff --git a/tests/components/uptime/snapshots/test_config_flow.ambr b/tests/components/uptime/snapshots/test_config_flow.ambr index ac4b7396839fb8..3e5b492f871a1a 100644 --- a/tests/components/uptime/snapshots/test_config_flow.ambr +++ b/tests/components/uptime/snapshots/test_config_flow.ambr @@ -10,6 +10,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'uptime', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -18,6 +19,7 @@ 'disabled_by': None, 'domain': 'uptime', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/vicare/snapshots/test_diagnostics.ambr b/tests/components/vicare/snapshots/test_diagnostics.ambr index dc1b217948fd79..dfc29d46cc243f 100644 --- a/tests/components/vicare/snapshots/test_diagnostics.ambr +++ b/tests/components/vicare/snapshots/test_diagnostics.ambr @@ -4705,6 +4705,7 @@ 'disabled_by': None, 'domain': 'vicare', 'entry_id': '1234', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/watttime/snapshots/test_diagnostics.ambr b/tests/components/watttime/snapshots/test_diagnostics.ambr index e1cf4a8a42f505..2ed35c19ad17de 100644 --- a/tests/components/watttime/snapshots/test_diagnostics.ambr +++ b/tests/components/watttime/snapshots/test_diagnostics.ambr @@ -19,6 +19,7 @@ }), 'disabled_by': None, 'domain': 'watttime', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/webostv/test_diagnostics.py b/tests/components/webostv/test_diagnostics.py index f0f411fb27898d..b7d1646c6b683e 100644 --- a/tests/components/webostv/test_diagnostics.py +++ b/tests/components/webostv/test_diagnostics.py @@ -44,6 +44,7 @@ async def test_diagnostics( "entry": { "entry_id": entry.entry_id, "version": 1, + "minor_version": 1, "domain": "webostv", "title": "fake_webos", "data": { diff --git a/tests/components/whois/snapshots/test_config_flow.ambr b/tests/components/whois/snapshots/test_config_flow.ambr index 6eec94d42a5235..08f3861dcd249a 100644 --- a/tests/components/whois/snapshots/test_config_flow.ambr +++ b/tests/components/whois/snapshots/test_config_flow.ambr @@ -12,6 +12,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'whois', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -21,6 +22,7 @@ 'disabled_by': None, 'domain': 'whois', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -48,6 +50,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'whois', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -57,6 +60,7 @@ 'disabled_by': None, 'domain': 'whois', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -84,6 +88,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'whois', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -93,6 +98,7 @@ 'disabled_by': None, 'domain': 'whois', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -120,6 +126,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'whois', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -129,6 +136,7 @@ 'disabled_by': None, 'domain': 'whois', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -156,6 +164,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'whois', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -165,6 +174,7 @@ 'disabled_by': None, 'domain': 'whois', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/wyoming/snapshots/test_config_flow.ambr b/tests/components/wyoming/snapshots/test_config_flow.ambr index 99f411027f5e79..a0e0c7c5011ab8 100644 --- a/tests/components/wyoming/snapshots/test_config_flow.ambr +++ b/tests/components/wyoming/snapshots/test_config_flow.ambr @@ -55,6 +55,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'wyoming', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -65,6 +66,7 @@ 'disabled_by': None, 'domain': 'wyoming', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -97,6 +99,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'wyoming', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -107,6 +110,7 @@ 'disabled_by': None, 'domain': 'wyoming', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -139,6 +143,7 @@ 'description_placeholders': None, 'flow_id': , 'handler': 'wyoming', + 'minor_version': 1, 'options': dict({ }), 'result': ConfigEntrySnapshot({ @@ -149,6 +154,7 @@ 'disabled_by': None, 'domain': 'wyoming', 'entry_id': , + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/snapshots/test_config_entries.ambr b/tests/snapshots/test_config_entries.ambr index beaa60cf762898..bfb583ba8db3f8 100644 --- a/tests/snapshots/test_config_entries.ambr +++ b/tests/snapshots/test_config_entries.ambr @@ -6,6 +6,7 @@ 'disabled_by': None, 'domain': 'test', 'entry_id': 'mock-entry', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 42d679d7ce63c6..4c350168d4e90c 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -913,6 +913,7 @@ class MockConfigFlow: """Mock the MQTT config flow.""" VERSION = 1 + MINOR_VERSION = 1 entry = MockConfigEntry(domain="mqtt", data={"broker": "test-broker"}) entry.add_to_hass(hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 40e3b3b4c3c162..e9989b6839ebf3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -134,11 +134,15 @@ async def test_call_setup_entry_without_reload_support(hass: HomeAssistant) -> N assert not entry.supports_unload -async def test_call_async_migrate_entry(hass: HomeAssistant) -> None: +@pytest.mark.parametrize(("major_version", "minor_version"), [(2, 1), (1, 2), (2, 2)]) +async def test_call_async_migrate_entry( + hass: HomeAssistant, major_version: int, minor_version: int +) -> None: """Test we call .async_migrate_entry when version mismatch.""" entry = MockConfigEntry(domain="comp") assert not entry.supports_unload - entry.version = 2 + entry.version = major_version + entry.minor_version = minor_version entry.add_to_hass(hass) mock_migrate_entry = AsyncMock(return_value=True) @@ -164,10 +168,14 @@ async def test_call_async_migrate_entry(hass: HomeAssistant) -> None: assert entry.supports_unload -async def test_call_async_migrate_entry_failure_false(hass: HomeAssistant) -> None: +@pytest.mark.parametrize(("major_version", "minor_version"), [(2, 1), (1, 2), (2, 2)]) +async def test_call_async_migrate_entry_failure_false( + hass: HomeAssistant, major_version: int, minor_version: int +) -> None: """Test migration fails if returns false.""" entry = MockConfigEntry(domain="comp") - entry.version = 2 + entry.version = major_version + entry.minor_version = minor_version entry.add_to_hass(hass) assert not entry.supports_unload @@ -192,10 +200,14 @@ async def test_call_async_migrate_entry_failure_false(hass: HomeAssistant) -> No assert not entry.supports_unload -async def test_call_async_migrate_entry_failure_exception(hass: HomeAssistant) -> None: +@pytest.mark.parametrize(("major_version", "minor_version"), [(2, 1), (1, 2), (2, 2)]) +async def test_call_async_migrate_entry_failure_exception( + hass: HomeAssistant, major_version: int, minor_version: int +) -> None: """Test migration fails if exception raised.""" entry = MockConfigEntry(domain="comp") - entry.version = 2 + entry.version = major_version + entry.minor_version = minor_version entry.add_to_hass(hass) assert not entry.supports_unload @@ -220,10 +232,14 @@ async def test_call_async_migrate_entry_failure_exception(hass: HomeAssistant) - assert not entry.supports_unload -async def test_call_async_migrate_entry_failure_not_bool(hass: HomeAssistant) -> None: +@pytest.mark.parametrize(("major_version", "minor_version"), [(2, 1), (1, 2), (2, 2)]) +async def test_call_async_migrate_entry_failure_not_bool( + hass: HomeAssistant, major_version: int, minor_version: int +) -> None: """Test migration fails if boolean not returned.""" entry = MockConfigEntry(domain="comp") - entry.version = 2 + entry.version = major_version + entry.minor_version = minor_version entry.add_to_hass(hass) assert not entry.supports_unload @@ -248,12 +264,14 @@ async def test_call_async_migrate_entry_failure_not_bool(hass: HomeAssistant) -> assert not entry.supports_unload +@pytest.mark.parametrize(("major_version", "minor_version"), [(2, 1), (2, 2)]) async def test_call_async_migrate_entry_failure_not_supported( - hass: HomeAssistant, + hass: HomeAssistant, major_version: int, minor_version: int ) -> None: """Test migration fails if async_migrate_entry not implemented.""" entry = MockConfigEntry(domain="comp") - entry.version = 2 + entry.version = major_version + entry.minor_version = minor_version entry.add_to_hass(hass) assert not entry.supports_unload @@ -269,6 +287,29 @@ async def test_call_async_migrate_entry_failure_not_supported( assert not entry.supports_unload +@pytest.mark.parametrize(("major_version", "minor_version"), [(1, 2)]) +async def test_call_async_migrate_entry_not_supported_minor_version( + hass: HomeAssistant, major_version: int, minor_version: int +) -> None: + """Test migration without async_migrate_entry and minor version changed.""" + entry = MockConfigEntry(domain="comp") + entry.version = major_version + entry.minor_version = minor_version + entry.add_to_hass(hass) + assert not entry.supports_unload + + mock_setup_entry = AsyncMock(return_value=True) + + mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) + mock_platform(hass, "comp.config_flow", None) + + result = await async_setup_component(hass, "comp", {}) + assert result + assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert not entry.supports_unload + + async def test_remove_entry( hass: HomeAssistant, manager: config_entries.ConfigEntries, From 9d44dc4437e40f8615f58b5d0e226599a721fc73 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 12 Dec 2023 10:41:00 +0100 Subject: [PATCH 059/118] Add Fast.com Device Info (#105528) Co-authored-by: G Johansson --- homeassistant/components/fastdotcom/sensor.py | 9 ++++++++- homeassistant/components/fastdotcom/strings.json | 7 +++++++ tests/components/fastdotcom/test_coordinator.py | 6 +++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index b82b20defb5857..2ca0b2d91686b3 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfDataRate from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -31,12 +32,13 @@ class SpeedtestSensor( ): """Implementation of a Fast.com sensor.""" - _attr_name = "Fast.com Download" + _attr_translation_key = "download" _attr_device_class = SensorDeviceClass.DATA_RATE _attr_native_unit_of_measurement = UnitOfDataRate.MEGABITS_PER_SECOND _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:speedometer" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, entry_id: str, coordinator: FastdotcomDataUpdateCoordindator @@ -44,6 +46,11 @@ def __init__( """Initialize the sensor.""" super().__init__(coordinator) self._attr_unique_id = entry_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry_id)}, + entry_type=DeviceEntryType.SERVICE, + configuration_url="https://www.fast.com", + ) @property def native_value( diff --git a/homeassistant/components/fastdotcom/strings.json b/homeassistant/components/fastdotcom/strings.json index d647250b423923..d274ca8d679f30 100644 --- a/homeassistant/components/fastdotcom/strings.json +++ b/homeassistant/components/fastdotcom/strings.json @@ -9,6 +9,13 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "entity": { + "sensor": { + "download": { + "name": "Download" + } + } + }, "services": { "speedtest": { "name": "Speed test", diff --git a/tests/components/fastdotcom/test_coordinator.py b/tests/components/fastdotcom/test_coordinator.py index 254301950fb6ab..5ee8c76092bb1b 100644 --- a/tests/components/fastdotcom/test_coordinator.py +++ b/tests/components/fastdotcom/test_coordinator.py @@ -28,7 +28,7 @@ async def test_fastdotcom_data_update_coordinator( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.fast_com_download") + state = hass.states.get("sensor.mock_title_download") assert state is not None assert state.state == "5.0" @@ -39,7 +39,7 @@ async def test_fastdotcom_data_update_coordinator( async_fire_time_changed(hass) await hass.async_block_till_done() - state = hass.states.get("sensor.fast_com_download") + state = hass.states.get("sensor.mock_title_download") assert state.state == "10.0" with patch( @@ -50,5 +50,5 @@ async def test_fastdotcom_data_update_coordinator( async_fire_time_changed(hass) await hass.async_block_till_done() - state = hass.states.get("sensor.fast_com_download") + state = hass.states.get("sensor.mock_title_download") assert state.state is STATE_UNAVAILABLE From fb615817b4ccb3a0dcfeaf0384a1ff29633170cd Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 12 Dec 2023 10:55:22 +0100 Subject: [PATCH 060/118] Add Tado error handling to fetching devices (#105546) --- homeassistant/components/tado/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 1cd21634c8e174..c9ed4b34c307f3 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -181,7 +181,12 @@ def update(self): def update_devices(self): """Update the device data from Tado.""" - devices = self.tado.getDevices() + try: + devices = self.tado.getDevices() + except RuntimeError: + _LOGGER.error("Unable to connect to Tado while updating devices") + return + for device in devices: device_short_serial_no = device["shortSerialNo"] _LOGGER.debug("Updating device %s", device_short_serial_no) From 2631fde0f7347d99e88d878ea428833897628565 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 12 Dec 2023 14:40:38 +0100 Subject: [PATCH 061/118] Patch aiohttp server app router freeze in tests (#105555) * Add test for registering a http view late * Patch aiohttp server app router freeze * Correct language --- tests/conftest.py | 2 ++ tests/test_test_fixtures.py | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 777b2073847713..1e70ad48065ce1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -487,6 +487,8 @@ async def go( if isinstance(__param, Application): server_kwargs = server_kwargs or {} server = TestServer(__param, loop=loop, **server_kwargs) + # Registering a view after starting the server should still work. + server.app._router.freeze = lambda: None client = CoalescingClient(server, loop=loop, **kwargs) elif isinstance(__param, BaseTestServer): client = TestClient(__param, loop=loop, **kwargs) diff --git a/tests/test_test_fixtures.py b/tests/test_test_fixtures.py index 1c0fe0a7eaab70..eb2103d42720d2 100644 --- a/tests/test_test_fixtures.py +++ b/tests/test_test_fixtures.py @@ -1,10 +1,16 @@ """Test test fixture configuration.""" +from http import HTTPStatus import socket +from aiohttp import web import pytest import pytest_socket +from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, async_get_hass +from homeassistant.setup import async_setup_component + +from .typing import ClientSessionGenerator def test_sockets_disabled() -> None: @@ -27,3 +33,38 @@ async def test_hass_cv(hass: HomeAssistant) -> None: in the fixture and that async_get_hass() works correctly. """ assert async_get_hass() is hass + + +def register_view(hass: HomeAssistant) -> None: + """Register a view.""" + + class TestView(HomeAssistantView): + """Test view to serve the test.""" + + requires_auth = False + url = "/api/test" + name = "api:test" + + async def get(self, request: web.Request) -> web.Response: + """Return a test result.""" + return self.json({"test": True}) + + hass.http.register_view(TestView()) + + +async def test_aiohttp_client_frozen_router_view( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, +) -> None: + """Test aiohttp_client fixture patches frozen router for views.""" + assert await async_setup_component(hass, "http", {}) + await hass.async_block_till_done() + + # Registering the view after starting the server should still work. + client = await hass_client() + register_view(hass) + + response = await client.get("/api/test") + assert response.status == HTTPStatus.OK + result = await response.json() + assert result["test"] is True From 5c514b6b1944cc52f795fd759880a491cbdf3263 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 12 Dec 2023 14:44:17 +0100 Subject: [PATCH 062/118] Add Suez Water to strict typing (#105559) --- .strict-typing | 1 + homeassistant/components/suez_water/sensor.py | 2 +- mypy.ini | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.strict-typing b/.strict-typing index 6180379f97725a..ee39824f476963 100644 --- a/.strict-typing +++ b/.strict-typing @@ -318,6 +318,7 @@ homeassistant.components.steamist.* homeassistant.components.stookalert.* homeassistant.components.stream.* homeassistant.components.streamlabswater.* +homeassistant.components.suez_water.* homeassistant.components.sun.* homeassistant.components.surepetcare.* homeassistant.components.switch.* diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index fc5b804137d871..7d7540ed1c0a78 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -74,7 +74,7 @@ def __init__(self, client: SuezClient) -> None: self.client = client self._attr_extra_state_attributes = {} - def _fetch_data(self): + def _fetch_data(self) -> None: """Fetch latest data from Suez.""" try: self.client.update() diff --git a/mypy.ini b/mypy.ini index 2a3a5f0fb0fe09..6e67167dacac5b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2942,6 +2942,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.suez_water.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.sun.*] check_untyped_defs = true disallow_incomplete_defs = true From 280637822b21d633a7c3e80b3427f6a78d4180e4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 12 Dec 2023 15:49:01 +0100 Subject: [PATCH 063/118] Use mocked entity platform for lock service tests (#105020) * Use mocked entity platform for lock service tests * Cleanup old mock class * Follow up on code review * Improve mock entity platform * Use entity_id of passed entity instead of constant --- tests/components/lock/conftest.py | 141 +++++++++ tests/components/lock/test_init.py | 484 +++++++++++++++-------------- 2 files changed, 396 insertions(+), 229 deletions(-) create mode 100644 tests/components/lock/conftest.py diff --git a/tests/components/lock/conftest.py b/tests/components/lock/conftest.py new file mode 100644 index 00000000000000..07399a39e92a10 --- /dev/null +++ b/tests/components/lock/conftest.py @@ -0,0 +1,141 @@ +"""Fixtures for the lock entity platform tests.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + LockEntity, + LockEntityFeature, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test" + + +class MockLock(LockEntity): + """Mocked lock entity.""" + + def __init__( + self, + supported_features: LockEntityFeature = LockEntityFeature(0), + code_format: str | None = None, + ) -> None: + """Initialize the lock.""" + self.calls_open = MagicMock() + self.calls_lock = MagicMock() + self.calls_unlock = MagicMock() + self._attr_code_format = code_format + self._attr_supported_features = supported_features + self._attr_has_entity_name = True + self._attr_name = "test_lock" + self._attr_unique_id = "very_unique_lock_id" + super().__init__() + + def lock(self, **kwargs: Any) -> None: + """Mock lock lock calls.""" + self.calls_lock(**kwargs) + + def unlock(self, **kwargs: Any) -> None: + """Mock lock unlock calls.""" + self.calls_unlock(**kwargs) + + def open(self, **kwargs: Any) -> None: + """Mock lock open calls.""" + self.calls_open(**kwargs) + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture(autouse=True) +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, MockFlow): + yield + + +@pytest.fixture +async def code_format() -> str | None: + """Return the code format for the test lock entity.""" + return None + + +@pytest.fixture(name="supported_features") +async def lock_supported_features() -> LockEntityFeature: + """Return the supported features for the test lock entity.""" + return LockEntityFeature.OPEN + + +@pytest.fixture(name="mock_lock_entity") +async def setup_lock_platform_test_entity( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + code_format: str | None, + supported_features: LockEntityFeature, +) -> MagicMock: + """Set up lock entity using an entity platform.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup(config_entry, LOCK_DOMAIN) + return True + + MockPlatform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + # Unnamed sensor without device class -> no name + entity = MockLock( + supported_features=supported_features, + code_format=code_format, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test lock platform via config entry.""" + async_add_entities([entity]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{LOCK_DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + 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(entity.entity_id) + assert state is not None + + return entity diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index 637acc22d05148..d8589ea265ec46 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -2,328 +2,354 @@ from __future__ import annotations from typing import Any -from unittest.mock import MagicMock import pytest from homeassistant.components.lock import ( ATTR_CODE, CONF_DEFAULT_CODE, + DOMAIN, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, STATE_JAMMED, STATE_LOCKED, STATE_LOCKING, STATE_UNLOCKED, STATE_UNLOCKING, - LockEntity, LockEntityFeature, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er -from homeassistant.setup import async_setup_component - -from tests.testing_config.custom_components.test.lock import MockLock - - -class MockLockEntity(LockEntity): - """Mock lock to use in tests.""" - - def __init__( - self, - code_format: str | None = None, - lock_option_default_code: str = "", - supported_features: LockEntityFeature = LockEntityFeature(0), - ) -> None: - """Initialize mock lock entity.""" - self._attr_supported_features = supported_features - self.calls_lock = MagicMock() - self.calls_unlock = MagicMock() - self.calls_open = MagicMock() - if code_format is not None: - self._attr_code_format = code_format - self._lock_option_default_code = lock_option_default_code - - async def async_lock(self, **kwargs: Any) -> None: - """Lock the lock.""" - self.calls_lock(kwargs) - self._attr_is_locking = False - self._attr_is_locked = True - - async def async_unlock(self, **kwargs: Any) -> None: - """Unlock the lock.""" - self.calls_unlock(kwargs) - self._attr_is_unlocking = False - self._attr_is_locked = False - - async def async_open(self, **kwargs: Any) -> None: - """Open the door latch.""" - self.calls_open(kwargs) - - -async def test_lock_default(hass: HomeAssistant) -> None: - """Test lock entity with defaults.""" - lock = MockLockEntity() - lock.hass = hass +from homeassistant.helpers.typing import UNDEFINED, UndefinedType - assert lock.code_format is None - assert lock.state is None +from .conftest import MockLock -async def test_lock_states(hass: HomeAssistant) -> None: - """Test lock entity states.""" +async def help_test_async_lock_service( + hass: HomeAssistant, + entity_id: str, + service: str, + code: str | None | UndefinedType = UNDEFINED, +) -> None: + """Help to lock a test lock.""" + data: dict[str, Any] = {"entity_id": entity_id} + if code is not UNDEFINED: + data[ATTR_CODE] = code - lock = MockLockEntity() - lock.hass = hass + await hass.services.async_call(DOMAIN, service, data, blocking=True) - assert lock.state is None - lock._attr_is_locking = True - assert lock.is_locking - assert lock.state == STATE_LOCKING +async def test_lock_default(hass: HomeAssistant, mock_lock_entity: MockLock) -> None: + """Test lock entity with defaults.""" - await lock.async_handle_lock_service() - assert lock.is_locked - assert lock.state == STATE_LOCKED + assert mock_lock_entity.code_format is None + assert mock_lock_entity.state is None + assert mock_lock_entity.is_jammed is None + assert mock_lock_entity.is_locked is None + assert mock_lock_entity.is_locking is None + assert mock_lock_entity.is_unlocking is None - lock._attr_is_unlocking = True - assert lock.is_unlocking - assert lock.state == STATE_UNLOCKING - await lock.async_handle_unlock_service() - assert not lock.is_locked - assert lock.state == STATE_UNLOCKED +async def test_lock_states(hass: HomeAssistant, mock_lock_entity: MockLock) -> None: + """Test lock entity states.""" - lock._attr_is_jammed = True - assert lock.is_jammed - assert lock.state == STATE_JAMMED - assert not lock.is_locked + assert mock_lock_entity.state is None + mock_lock_entity._attr_is_locking = True + assert mock_lock_entity.is_locking + assert mock_lock_entity.state == STATE_LOCKING -async def test_set_default_code_option( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - enable_custom_integrations: None, -) -> None: - """Test default code stored in the registry.""" + mock_lock_entity._attr_is_locked = True + mock_lock_entity._attr_is_locking = False + assert mock_lock_entity.is_locked + assert mock_lock_entity.state == STATE_LOCKED - entry = entity_registry.async_get_or_create("lock", "test", "very_unique") - await hass.async_block_till_done() + mock_lock_entity._attr_is_unlocking = True + assert mock_lock_entity.is_unlocking + assert mock_lock_entity.state == STATE_UNLOCKING - platform = getattr(hass.components, "test.lock") - platform.init(empty=True) - platform.ENTITIES["lock1"] = platform.MockLock( - name="Test", - code_format=r"^\d{4}$", - supported_features=LockEntityFeature.OPEN, - unique_id="very_unique", - ) + mock_lock_entity._attr_is_locked = False + mock_lock_entity._attr_is_unlocking = False + assert not mock_lock_entity.is_locked + assert mock_lock_entity.state == STATE_UNLOCKED - assert await async_setup_component(hass, "lock", {"lock": {"platform": "test"}}) - await hass.async_block_till_done() + mock_lock_entity._attr_is_jammed = True + assert mock_lock_entity.is_jammed + assert mock_lock_entity.state == STATE_JAMMED + assert not mock_lock_entity.is_locked - entity0: MockLock = platform.ENTITIES["lock1"] + +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(r"^\d{4}$", LockEntityFeature.OPEN)], +) +async def test_set_mock_lock_options( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_lock_entity: MockLock, +) -> None: + """Test mock attributes and default code stored in the registry.""" entity_registry.async_update_entity_options( - entry.entity_id, "lock", {CONF_DEFAULT_CODE: "1234"} + "lock.test_lock", "lock", {CONF_DEFAULT_CODE: "1234"} ) await hass.async_block_till_done() - assert entity0._lock_option_default_code == "1234" + assert mock_lock_entity._lock_option_default_code == "1234" + state = hass.states.get(mock_lock_entity.entity_id) + assert state is not None + assert state.attributes["code_format"] == r"^\d{4}$" + assert state.attributes["supported_features"] == LockEntityFeature.OPEN +@pytest.mark.parametrize("code_format", [r"^\d{4}$"]) async def test_default_code_option_update( hass: HomeAssistant, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_lock_entity: MockLock, ) -> None: """Test default code stored in the registry is updated.""" - entry = entity_registry.async_get_or_create("lock", "test", "very_unique") - await hass.async_block_till_done() - - platform = getattr(hass.components, "test.lock") - platform.init(empty=True) - - # Pre-register entities - entry = entity_registry.async_get_or_create("lock", "test", "very_unique") - entity_registry.async_update_entity_options( - entry.entity_id, - "lock", - { - "default_code": "5432", - }, - ) - platform.ENTITIES["lock1"] = platform.MockLock( - name="Test", - code_format=r"^\d{4}$", - supported_features=LockEntityFeature.OPEN, - unique_id="very_unique", - ) - - assert await async_setup_component(hass, "lock", {"lock": {"platform": "test"}}) - await hass.async_block_till_done() - - entity0: MockLock = platform.ENTITIES["lock1"] - assert entity0._lock_option_default_code == "5432" + assert mock_lock_entity._lock_option_default_code == "" entity_registry.async_update_entity_options( - entry.entity_id, "lock", {CONF_DEFAULT_CODE: "1234"} + "lock.test_lock", "lock", {CONF_DEFAULT_CODE: "4321"} ) await hass.async_block_till_done() - assert entity0._lock_option_default_code == "1234" + assert mock_lock_entity._lock_option_default_code == "4321" -async def test_lock_open_with_code(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(r"^\d{4}$", LockEntityFeature.OPEN)], +) +async def test_lock_open_with_code( + hass: HomeAssistant, mock_lock_entity: MockLock +) -> None: """Test lock entity with open service.""" - lock = MockLockEntity( - code_format=r"^\d{4}$", supported_features=LockEntityFeature.OPEN - ) - lock.hass = hass - - assert lock.state_attributes == {"code_format": r"^\d{4}$"} + state = hass.states.get(mock_lock_entity.entity_id) + assert state.attributes["code_format"] == r"^\d{4}$" with pytest.raises(ValueError): - await lock.async_handle_open_service() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN + ) with pytest.raises(ValueError): - await lock.async_handle_open_service(code="") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN, code="" + ) with pytest.raises(ValueError): - await lock.async_handle_open_service(code="HELLO") - await lock.async_handle_open_service(code="1234") - assert lock.calls_open.call_count == 1 + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN, code="HELLO" + ) + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN, code="1234" + ) + assert mock_lock_entity.calls_open.call_count == 1 + mock_lock_entity.calls_open.assert_called_with(code="1234") -async def test_lock_lock_with_code(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(r"^\d{4}$", LockEntityFeature.OPEN)], +) +async def test_lock_lock_with_code( + hass: HomeAssistant, mock_lock_entity: MockLock +) -> None: """Test lock entity with open service.""" - lock = MockLockEntity(code_format=r"^\d{4}$") - lock.hass = hass + state = hass.states.get(mock_lock_entity.entity_id) + assert state.attributes["code_format"] == r"^\d{4}$" - await lock.async_handle_unlock_service(code="1234") - assert not lock.is_locked + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="1234" + ) + mock_lock_entity.calls_unlock.assert_called_with(code="1234") + assert mock_lock_entity.calls_lock.call_count == 0 with pytest.raises(ValueError): - await lock.async_handle_lock_service() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK + ) with pytest.raises(ValueError): - await lock.async_handle_lock_service(code="") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="" + ) with pytest.raises(ValueError): - await lock.async_handle_lock_service(code="HELLO") - await lock.async_handle_lock_service(code="1234") - assert lock.is_locked + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="HELLO" + ) + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="1234" + ) + assert mock_lock_entity.calls_lock.call_count == 1 + mock_lock_entity.calls_lock.assert_called_with(code="1234") -async def test_lock_unlock_with_code(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(r"^\d{4}$", LockEntityFeature.OPEN)], +) +async def test_lock_unlock_with_code( + hass: HomeAssistant, mock_lock_entity: MockLock +) -> None: """Test unlock entity with open service.""" - lock = MockLockEntity(code_format=r"^\d{4}$") - lock.hass = hass + state = hass.states.get(mock_lock_entity.entity_id) + assert state.attributes["code_format"] == r"^\d{4}$" - await lock.async_handle_lock_service(code="1234") - assert lock.is_locked + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="1234" + ) + mock_lock_entity.calls_lock.assert_called_with(code="1234") + assert mock_lock_entity.calls_unlock.call_count == 0 with pytest.raises(ValueError): - await lock.async_handle_unlock_service() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK + ) with pytest.raises(ValueError): - await lock.async_handle_unlock_service(code="") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="" + ) with pytest.raises(ValueError): - await lock.async_handle_unlock_service(code="HELLO") - await lock.async_handle_unlock_service(code="1234") - assert not lock.is_locked + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="HELLO" + ) + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="1234" + ) + assert mock_lock_entity.calls_unlock.call_count == 1 + mock_lock_entity.calls_unlock.assert_called_with(code="1234") -async def test_lock_with_illegal_code(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(r"^\d{4}$", LockEntityFeature.OPEN)], +) +async def test_lock_with_illegal_code( + hass: HomeAssistant, mock_lock_entity: MockLock +) -> None: """Test lock entity with default code that does not match the code format.""" - lock = MockLockEntity( - code_format=r"^\d{4}$", - supported_features=LockEntityFeature.OPEN, - ) - lock.hass = hass with pytest.raises(ValueError): - await lock.async_handle_open_service(code="123456") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN, code="123456" + ) with pytest.raises(ValueError): - await lock.async_handle_lock_service(code="123456") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="123456" + ) with pytest.raises(ValueError): - await lock.async_handle_unlock_service(code="123456") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="123456" + ) -async def test_lock_with_no_code(hass: HomeAssistant) -> None: - """Test lock entity with default code that does not match the code format.""" - lock = MockLockEntity( - supported_features=LockEntityFeature.OPEN, +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(None, LockEntityFeature.OPEN)], +) +async def test_lock_with_no_code( + hass: HomeAssistant, mock_lock_entity: MockLock +) -> None: + """Test lock entity without code.""" + await help_test_async_lock_service(hass, mock_lock_entity.entity_id, SERVICE_OPEN) + mock_lock_entity.calls_open.assert_called_with() + await help_test_async_lock_service(hass, mock_lock_entity.entity_id, SERVICE_LOCK) + mock_lock_entity.calls_lock.assert_called_with() + await help_test_async_lock_service(hass, mock_lock_entity.entity_id, SERVICE_UNLOCK) + mock_lock_entity.calls_unlock.assert_called_with() + + mock_lock_entity.calls_open.reset_mock() + mock_lock_entity.calls_lock.reset_mock() + mock_lock_entity.calls_unlock.reset_mock() + + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN, code="" + ) + mock_lock_entity.calls_open.assert_called_with() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="" ) - lock.hass = hass + mock_lock_entity.calls_lock.assert_called_with() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="" + ) + mock_lock_entity.calls_unlock.assert_called_with() - await lock.async_handle_open_service() - lock.calls_open.assert_called_with({}) - await lock.async_handle_lock_service() - lock.calls_lock.assert_called_with({}) - await lock.async_handle_unlock_service() - lock.calls_unlock.assert_called_with({}) - await lock.async_handle_open_service(code="") - lock.calls_open.assert_called_with({}) - await lock.async_handle_lock_service(code="") - lock.calls_lock.assert_called_with({}) - await lock.async_handle_unlock_service(code="") - lock.calls_unlock.assert_called_with({}) +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(r"^\d{4}$", LockEntityFeature.OPEN)], +) +async def test_lock_with_default_code( + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_lock_entity: MockLock +) -> None: + """Test lock entity with default code.""" + entity_registry.async_update_entity_options( + "lock.test_lock", "lock", {CONF_DEFAULT_CODE: "1234"} + ) + await hass.async_block_till_done() + assert mock_lock_entity.state_attributes == {"code_format": r"^\d{4}$"} + assert mock_lock_entity._lock_option_default_code == "1234" -async def test_lock_with_default_code(hass: HomeAssistant) -> None: - """Test lock entity with default code.""" - lock = MockLockEntity( - code_format=r"^\d{4}$", - supported_features=LockEntityFeature.OPEN, - lock_option_default_code="1234", + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN, code="1234" ) - lock.hass = hass - - assert lock.state_attributes == {"code_format": r"^\d{4}$"} - assert lock._lock_option_default_code == "1234" - - await lock.async_handle_open_service() - lock.calls_open.assert_called_with({ATTR_CODE: "1234"}) - await lock.async_handle_lock_service() - lock.calls_lock.assert_called_with({ATTR_CODE: "1234"}) - await lock.async_handle_unlock_service() - lock.calls_unlock.assert_called_with({ATTR_CODE: "1234"}) - - await lock.async_handle_open_service(code="") - lock.calls_open.assert_called_with({ATTR_CODE: "1234"}) - await lock.async_handle_lock_service(code="") - lock.calls_lock.assert_called_with({ATTR_CODE: "1234"}) - await lock.async_handle_unlock_service(code="") - lock.calls_unlock.assert_called_with({ATTR_CODE: "1234"}) - - -async def test_lock_with_provided_and_default_code(hass: HomeAssistant) -> None: - """Test lock entity with provided code when default code is set.""" - lock = MockLockEntity( - code_format=r"^\d{4}$", - supported_features=LockEntityFeature.OPEN, - lock_option_default_code="1234", + mock_lock_entity.calls_open.assert_called_with(code="1234") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="1234" ) - lock.hass = hass + mock_lock_entity.calls_lock.assert_called_with(code="1234") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="1234" + ) + mock_lock_entity.calls_unlock.assert_called_with(code="1234") + + mock_lock_entity.calls_open.reset_mock() + mock_lock_entity.calls_lock.reset_mock() + mock_lock_entity.calls_unlock.reset_mock() - await lock.async_handle_open_service(code="4321") - lock.calls_open.assert_called_with({ATTR_CODE: "4321"}) - await lock.async_handle_lock_service(code="4321") - lock.calls_lock.assert_called_with({ATTR_CODE: "4321"}) - await lock.async_handle_unlock_service(code="4321") - lock.calls_unlock.assert_called_with({ATTR_CODE: "4321"}) + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN, code="" + ) + mock_lock_entity.calls_open.assert_called_with(code="1234") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK, code="" + ) + mock_lock_entity.calls_lock.assert_called_with(code="1234") + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK, code="" + ) + mock_lock_entity.calls_unlock.assert_called_with(code="1234") -async def test_lock_with_illegal_default_code(hass: HomeAssistant) -> None: - """Test lock entity with default code that does not match the code format.""" - lock = MockLockEntity( - code_format=r"^\d{4}$", - supported_features=LockEntityFeature.OPEN, - lock_option_default_code="123456", +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(r"^\d{4}$", LockEntityFeature.OPEN)], +) +async def test_lock_with_illegal_default_code( + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_lock_entity: MockLock +) -> None: + """Test lock entity with illegal default code.""" + entity_registry.async_update_entity_options( + "lock.test_lock", "lock", {CONF_DEFAULT_CODE: "123456"} ) - lock.hass = hass + await hass.async_block_till_done() - assert lock.state_attributes == {"code_format": r"^\d{4}$"} - assert lock._lock_option_default_code == "123456" + assert mock_lock_entity.state_attributes == {"code_format": r"^\d{4}$"} + assert mock_lock_entity._lock_option_default_code == "" with pytest.raises(ValueError): - await lock.async_handle_open_service() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_OPEN + ) with pytest.raises(ValueError): - await lock.async_handle_lock_service() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_LOCK + ) with pytest.raises(ValueError): - await lock.async_handle_unlock_service() + await help_test_async_lock_service( + hass, mock_lock_entity.entity_id, SERVICE_UNLOCK + ) From 64c3cfca17c8f94af3072356c9178ed478c2f6fb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 12 Dec 2023 17:27:41 +0100 Subject: [PATCH 064/118] Add Airvisual pro to strict typing (#105568) --- .strict-typing | 1 + mypy.ini | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.strict-typing b/.strict-typing index ee39824f476963..ab4cc944ea1e98 100644 --- a/.strict-typing +++ b/.strict-typing @@ -48,6 +48,7 @@ homeassistant.components.aftership.* homeassistant.components.air_quality.* homeassistant.components.airly.* homeassistant.components.airvisual.* +homeassistant.components.airvisual_pro.* homeassistant.components.airzone.* homeassistant.components.airzone_cloud.* homeassistant.components.aladdin_connect.* diff --git a/mypy.ini b/mypy.ini index 6e67167dacac5b..3e882d15812fcc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -240,6 +240,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.airvisual_pro.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.airzone.*] check_untyped_defs = true disallow_incomplete_defs = true From ec1cde77f635361a84de0ae70233adc74269423e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 07:54:33 -1000 Subject: [PATCH 065/118] Add support for Happy Eyeballs to homekit_controller (#105454) --- homeassistant/components/homekit_controller/config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 088747d39ffc65..08444555aca54d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -257,6 +257,11 @@ async def async_step_zeroconf( ) updated_ip_port = { "AccessoryIP": discovery_info.host, + "AccessoryIPs": [ + str(ip_addr) + for ip_addr in discovery_info.ip_addresses + if not ip_addr.is_link_local and not ip_addr.is_unspecified + ], "AccessoryPort": discovery_info.port, } # If the device is already paired and known to us we should monitor c# From c7a95d565417fa0e031657575fcc60d1a32d6541 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 07:55:12 -1000 Subject: [PATCH 066/118] Bump dbus-fast to 2.21.0 (#105536) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 5f8cdbea9398e5..5abec24b6d1d79 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,7 +19,7 @@ "bluetooth-adapters==0.16.1", "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.17.0", - "dbus-fast==2.20.0", + "dbus-fast==2.21.0", "habluetooth==0.11.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5d959667a8d9c7..53ed955f79138b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ bluetooth-data-tools==1.17.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.7 -dbus-fast==2.20.0 +dbus-fast==2.21.0 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index b8210059527cda..b7ae74413fe77d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ datadog==0.15.0 datapoint==0.9.8;python_version<'3.12' # homeassistant.components.bluetooth -dbus-fast==2.20.0 +dbus-fast==2.21.0 # homeassistant.components.debugpy debugpy==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fed6ef512a26a..af90b1d3caf8e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -539,7 +539,7 @@ datadog==0.15.0 datapoint==0.9.8;python_version<'3.12' # homeassistant.components.bluetooth -dbus-fast==2.20.0 +dbus-fast==2.21.0 # homeassistant.components.debugpy debugpy==1.8.0 From f58af0d71780d7de75443968a93cf8f697319f7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 08:07:20 -1000 Subject: [PATCH 067/118] Bump aiohomekit to 3.1.0 (#105584) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 91fd199e17c0b0..e6ef6d58df63da 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.0.9"], + "requirements": ["aiohomekit==3.1.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index b7ae74413fe77d..5022fd8e7a34e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.0.9 +aiohomekit==3.1.0 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af90b1d3caf8e3..41ec51327dc2ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.0.9 +aiohomekit==3.1.0 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 From 54d314d1d03b2a8652b87fdc54b450daffcfcacf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 08:41:50 -1000 Subject: [PATCH 068/118] Bump aioesphomeapi to 20.0.0 (#105586) changelog: https://github.com/esphome/aioesphomeapi/compare/v19.3.1...v20.0.0 - Add happy eyeballs support (RFC 8305) (#789) Note that nothing much happens yet on the HA side since we only pass one IP in so its always going to fallback at this point --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e0b47f09d95f92..eac721a446266c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==19.3.0", + "aioesphomeapi==20.0.0", "bluetooth-data-tools==1.17.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5022fd8e7a34e8..642068e15e7267 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -239,7 +239,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==19.3.0 +aioesphomeapi==20.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41ec51327dc2ea..b6e7bb801d8e07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -218,7 +218,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==19.3.0 +aioesphomeapi==20.0.0 # homeassistant.components.flo aioflo==2021.11.0 From 4ad16b56f2c20f253926ddf5543ee47d22ce8f65 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 12 Dec 2023 20:43:09 +0100 Subject: [PATCH 069/118] Fix setup Fast.com (#105580) * Fix setup fastdotcom * Add if --- homeassistant/components/fastdotcom/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index e872c3f501d1a9..06c5fc7036abf2 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -31,15 +31,16 @@ ) -async def async_setup_platform(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Fast.com component. (deprecated).""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) ) - ) return True From d33aa6b8e7184c47bac6a1cdf295b9e003d79dab Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 12 Dec 2023 20:51:32 +0100 Subject: [PATCH 070/118] Migrate homematicip_cloud tests to use freezegun (#105592) --- .../homematicip_cloud/test_button.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/components/homematicip_cloud/test_button.py b/tests/components/homematicip_cloud/test_button.py index c4b83692267415..5135c0ec48aba2 100644 --- a/tests/components/homematicip_cloud/test_button.py +++ b/tests/components/homematicip_cloud/test_button.py @@ -1,5 +1,6 @@ """Tests for HomematicIP Cloud button.""" -from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS @@ -11,7 +12,7 @@ async def test_hmip_garage_door_controller_button( - hass: HomeAssistant, default_mock_hap_factory + hass: HomeAssistant, freezer: FrozenDateTimeFactory, default_mock_hap_factory ) -> None: """Test HomematicipGarageDoorControllerButton.""" entity_id = "button.garagentor" @@ -28,13 +29,13 @@ async def test_hmip_garage_door_controller_button( assert state.state == STATE_UNKNOWN now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.utcnow", return_value=now): - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) + freezer.move_to(now) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state From 32147dbdd9d7455af988e25aaff6c32049118364 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 12 Dec 2023 20:52:59 +0100 Subject: [PATCH 071/118] Bump PyTado to 0.17.0 (#105573) --- homeassistant/components/tado/__init__.py | 39 ++++++++++----------- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index c9ed4b34c307f3..7faf918f8da2bc 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -164,11 +164,10 @@ def fallback(self): def setup(self): """Connect to Tado and fetch the zones.""" self.tado = Tado(self._username, self._password) - self.tado.setDebugging(True) # Load zones and devices - self.zones = self.tado.getZones() - self.devices = self.tado.getDevices() - tado_home = self.tado.getMe()["homes"][0] + self.zones = self.tado.get_zones() + self.devices = self.tado.get_devices() + tado_home = self.tado.get_me()["homes"][0] self.home_id = tado_home["id"] self.home_name = tado_home["name"] @@ -182,7 +181,7 @@ def update(self): def update_devices(self): """Update the device data from Tado.""" try: - devices = self.tado.getDevices() + devices = self.tado.get_devices() except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating devices") return @@ -195,7 +194,7 @@ def update_devices(self): INSIDE_TEMPERATURE_MEASUREMENT in device["characteristics"]["capabilities"] ): - device[TEMP_OFFSET] = self.tado.getDeviceInfo( + device[TEMP_OFFSET] = self.tado.get_device_info( device_short_serial_no, TEMP_OFFSET ) except RuntimeError: @@ -223,7 +222,7 @@ def update_devices(self): def update_zones(self): """Update the zone data from Tado.""" try: - zone_states = self.tado.getZoneStates()["zoneStates"] + zone_states = self.tado.get_zone_states()["zoneStates"] except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating zones") return @@ -235,7 +234,7 @@ def update_zone(self, zone_id): """Update the internal data from Tado.""" _LOGGER.debug("Updating zone %s", zone_id) try: - data = self.tado.getZoneState(zone_id) + data = self.tado.get_zone_state(zone_id) except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id) return @@ -256,8 +255,8 @@ def update_zone(self, zone_id): def update_home(self): """Update the home data from Tado.""" try: - self.data["weather"] = self.tado.getWeather() - self.data["geofence"] = self.tado.getHomeState() + self.data["weather"] = self.tado.get_weather() + self.data["geofence"] = self.tado.get_home_state() dispatcher_send( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"), @@ -270,15 +269,15 @@ def update_home(self): def get_capabilities(self, zone_id): """Return the capabilities of the devices.""" - return self.tado.getCapabilities(zone_id) + return self.tado.get_capabilities(zone_id) def get_auto_geofencing_supported(self): """Return whether the Tado Home supports auto geofencing.""" - return self.tado.getAutoGeofencingSupported() + return self.tado.get_auto_geofencing_supported() def reset_zone_overlay(self, zone_id): """Reset the zone back to the default operation.""" - self.tado.resetZoneOverlay(zone_id) + self.tado.reset_zone_overlay(zone_id) self.update_zone(zone_id) def set_presence( @@ -287,11 +286,11 @@ def set_presence( ): """Set the presence to home, away or auto.""" if presence == PRESET_AWAY: - self.tado.setAway() + self.tado.set_away() elif presence == PRESET_HOME: - self.tado.setHome() + self.tado.set_home() elif presence == PRESET_AUTO: - self.tado.setAuto() + self.tado.set_auto() # Update everything when changing modes self.update_zones() @@ -325,7 +324,7 @@ def set_zone_overlay( ) try: - self.tado.setZoneOverlay( + self.tado.set_zone_overlay( zone_id, overlay_mode, temperature, @@ -333,7 +332,7 @@ def set_zone_overlay( device_type, "ON", mode, - fanSpeed=fan_speed, + fan_speed=fan_speed, swing=swing, ) @@ -345,7 +344,7 @@ def set_zone_overlay( def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" try: - self.tado.setZoneOverlay( + self.tado.set_zone_overlay( zone_id, overlay_mode, None, None, device_type, "OFF" ) except RequestException as exc: @@ -356,6 +355,6 @@ def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): def set_temperature_offset(self, device_id, offset): """Set temperature offset of device.""" try: - self.tado.setTempOffset(device_id, offset) + self.tado.set_temp_offset(device_id, offset) except RequestException as exc: _LOGGER.error("Could not set temperature offset: %s", exc) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 62f7a377239efb..4c6a3eac2c5493 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.15.0"] + "requirements": ["python-tado==0.17.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 642068e15e7267..573d51cb43aae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2222,7 +2222,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.15.0 +python-tado==0.17.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6e7bb801d8e07..6e61b196514cd8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1670,7 +1670,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.15.0 +python-tado==0.17.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 From 09b07c071bb1b95123c002f64087b246687ad8bd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 12 Dec 2023 20:53:36 +0100 Subject: [PATCH 072/118] Add Apprise to strict typing (#105575) --- .strict-typing | 1 + homeassistant/components/apprise/notify.py | 5 +++-- mypy.ini | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index ab4cc944ea1e98..ce1491b33eb5eb 100644 --- a/.strict-typing +++ b/.strict-typing @@ -63,6 +63,7 @@ homeassistant.components.analytics.* homeassistant.components.anova.* homeassistant.components.anthemav.* homeassistant.components.apcupsd.* +homeassistant.components.apprise.* homeassistant.components.aqualogic.* homeassistant.components.aseko_pool_live.* homeassistant.components.assist_pipeline.* diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index b215f93aeb1c11..e4b350c4da854a 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import apprise import voluptuous as vol @@ -61,11 +62,11 @@ def get_service( class AppriseNotificationService(BaseNotificationService): """Implement the notification service for Apprise.""" - def __init__(self, a_obj): + def __init__(self, a_obj: apprise.Apprise) -> None: """Initialize the service.""" self.apprise = a_obj - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a specified target. If no target/tags are specified, then services are notified as is diff --git a/mypy.ini b/mypy.ini index 3e882d15812fcc..df774c3a167fbe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -390,6 +390,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.apprise.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aqualogic.*] check_untyped_defs = true disallow_incomplete_defs = true From 84bffcd2e141d76055d27ce633739a3dc4ef38aa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 12 Dec 2023 20:54:00 +0100 Subject: [PATCH 073/118] Add Aranet to strict typing (#105577) --- .strict-typing | 1 + mypy.ini | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.strict-typing b/.strict-typing index ce1491b33eb5eb..f8361ba82dda3c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -65,6 +65,7 @@ homeassistant.components.anthemav.* homeassistant.components.apcupsd.* homeassistant.components.apprise.* homeassistant.components.aqualogic.* +homeassistant.components.aranet.* homeassistant.components.aseko_pool_live.* homeassistant.components.assist_pipeline.* homeassistant.components.asuswrt.* diff --git a/mypy.ini b/mypy.ini index df774c3a167fbe..ae0d18562ee7f5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -410,6 +410,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.aranet.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aseko_pool_live.*] check_untyped_defs = true disallow_incomplete_defs = true From 0e5d72a501dabb9b3c4209a98efab9ead488c932 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 12 Dec 2023 20:54:35 +0100 Subject: [PATCH 074/118] Add Android IP webcam to strict typing (#105570) --- .strict-typing | 1 + mypy.ini | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.strict-typing b/.strict-typing index f8361ba82dda3c..e2a630f01799f2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -60,6 +60,7 @@ homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* homeassistant.components.analytics.* +homeassistant.components.android_ip_webcam.* homeassistant.components.anova.* homeassistant.components.anthemav.* homeassistant.components.apcupsd.* diff --git a/mypy.ini b/mypy.ini index ae0d18562ee7f5..94ff0c1e46d50e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -360,6 +360,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.android_ip_webcam.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.anova.*] check_untyped_defs = true disallow_incomplete_defs = true From 8bd265c3aedd1f02fd066b48c240f9a25eeb816c Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 12 Dec 2023 21:18:12 +0100 Subject: [PATCH 075/118] Add Fastdotcom service (#105553) * Add service for manual control * Proper naming * Removing old translation * Reverting back service * Removig services.yaml * Putting back in service * Putting back in service description and yaml * Proper naming * Adding create_issue * Feedback fixes * Fix deprecation date in strings * Update homeassistant/components/fastdotcom/__init__.py * Update homeassistant/components/fastdotcom/strings.json --------- Co-authored-by: G Johansson --- .../components/fastdotcom/__init__.py | 19 ++++++++++++++++++- .../components/fastdotcom/strings.json | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 06c5fc7036abf2..165d81edd0bf22 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -7,7 +7,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CoreState, Event, HomeAssistant +from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall +from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -52,6 +53,20 @@ async def _request_refresh(event: Event) -> None: """Request a refresh.""" await coordinator.async_request_refresh() + async def _request_refresh_service(call: ServiceCall) -> None: + """Request a refresh via the service.""" + ir.async_create_issue( + hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.7.0", + is_fixable=True, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) + await coordinator.async_request_refresh() + if hass.state == CoreState.running: await coordinator.async_config_entry_first_refresh() else: @@ -59,6 +74,8 @@ async def _request_refresh(event: Event) -> None: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _request_refresh) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.services.async_register(DOMAIN, "speedtest", _request_refresh_service) + await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS, diff --git a/homeassistant/components/fastdotcom/strings.json b/homeassistant/components/fastdotcom/strings.json index d274ca8d679f30..61a1f6867479e6 100644 --- a/homeassistant/components/fastdotcom/strings.json +++ b/homeassistant/components/fastdotcom/strings.json @@ -21,5 +21,18 @@ "name": "Speed test", "description": "Immediately executes a speed test with Fast.com." } + }, + "issues": { + "service_deprecation": { + "title": "Fast.com speedtest service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::fastdotcom::issues::service_deprecation::title%]", + "description": "Use `homeassistant.update_entity` instead to update the data.\n\nPlease replace this service and adjust your automations and scripts and select **submit** to fix this issue." + } + } + } + } } } From 5bd0833f49c7f223a503b124f6e326b6cb1e8d17 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Dec 2023 21:19:41 +0100 Subject: [PATCH 076/118] Improve FrozenOrThawed (#105541) --- homeassistant/util/frozen_dataclass_compat.py | 51 +++++-------- tests/helpers/snapshots/test_entity.ambr | 74 ++++++++++++++++++- tests/helpers/test_entity.py | 43 +++++++++++ 3 files changed, 135 insertions(+), 33 deletions(-) diff --git a/homeassistant/util/frozen_dataclass_compat.py b/homeassistant/util/frozen_dataclass_compat.py index e62e0a34cf141f..58faedeea6fc91 100644 --- a/homeassistant/util/frozen_dataclass_compat.py +++ b/homeassistant/util/frozen_dataclass_compat.py @@ -59,7 +59,7 @@ def _make_dataclass(cls, name: str, bases: tuple[type, ...], kw_only: bool) -> N for base in bases: dataclass_bases.append(getattr(base, "_dataclass", base)) cls._dataclass = dataclasses.make_dataclass( - f"{name}_dataclass", class_fields, bases=tuple(dataclass_bases), frozen=True + name, class_fields, bases=tuple(dataclass_bases), frozen=True ) def __new__( @@ -87,15 +87,17 @@ def __init__( class will be a real dataclass, i.e. it's decorated with @dataclass. """ if not namespace["_FrozenOrThawed__frozen_or_thawed"]: - parent = cls.__mro__[1] # This class is a real dataclass, optionally inject the parent's annotations - if dataclasses.is_dataclass(parent) or not hasattr(parent, "_dataclass"): - # Rely on dataclass inheritance + if all(dataclasses.is_dataclass(base) for base in bases): + # All direct parents are dataclasses, rely on dataclass inheritance return - # Parent is not a dataclass, inject its annotations - cls.__annotations__ = ( - parent._dataclass.__annotations__ | cls.__annotations__ - ) + # Parent is not a dataclass, inject all parents' annotations + annotations: dict = {} + for parent in cls.__mro__[::-1]: + if parent is object: + continue + annotations |= parent.__annotations__ + cls.__annotations__ = annotations return # First try without setting the kw_only flag, and if that fails, try setting it @@ -104,30 +106,15 @@ class will be a real dataclass, i.e. it's decorated with @dataclass. except TypeError: cls._make_dataclass(name, bases, True) - def __delattr__(self: object, name: str) -> None: - """Delete an attribute. + def __new__(*args: Any, **kwargs: Any) -> object: + """Create a new instance. - If self is a real dataclass, this is called if the dataclass is not frozen. - If self is not a real dataclass, forward to cls._dataclass.__delattr. + The function has no named arguments to avoid name collisions with dataclass + field names. """ - if dataclasses.is_dataclass(self): - return object.__delattr__(self, name) - return self._dataclass.__delattr__(self, name) # type: ignore[attr-defined, no-any-return] - - def __setattr__(self: object, name: str, value: Any) -> None: - """Set an attribute. + cls, *_args = args + if dataclasses.is_dataclass(cls): + return object.__new__(cls) + return cls._dataclass(*_args, **kwargs) - If self is a real dataclass, this is called if the dataclass is not frozen. - If self is not a real dataclass, forward to cls._dataclass.__setattr__. - """ - if dataclasses.is_dataclass(self): - return object.__setattr__(self, name, value) - return self._dataclass.__setattr__(self, name, value) # type: ignore[attr-defined, no-any-return] - - # Set generated dunder methods from the dataclass - # MyPy doesn't understand what's happening, so we ignore it - cls.__delattr__ = __delattr__ # type: ignore[assignment, method-assign] - cls.__eq__ = cls._dataclass.__eq__ # type: ignore[method-assign] - cls.__init__ = cls._dataclass.__init__ # type: ignore[misc] - cls.__repr__ = cls._dataclass.__repr__ # type: ignore[method-assign] - cls.__setattr__ = __setattr__ # type: ignore[assignment, method-assign] + cls.__new__ = __new__ # type: ignore[method-assign] diff --git a/tests/helpers/snapshots/test_entity.ambr b/tests/helpers/snapshots/test_entity.ambr index 3b04286b62f703..7f146fa049403e 100644 --- a/tests/helpers/snapshots/test_entity.ambr +++ b/tests/helpers/snapshots/test_entity.ambr @@ -1,6 +1,18 @@ # serializer version: 1 # name: test_entity_description_as_dataclass - EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=, translation_key=None, unit_of_measurement=None) + dict({ + 'device_class': 'test', + 'entity_category': None, + 'entity_registry_enabled_default': True, + 'entity_registry_visible_default': True, + 'force_update': False, + 'has_entity_name': False, + 'icon': None, + 'key': 'blah', + 'name': , + 'translation_key': None, + 'unit_of_measurement': None, + }) # --- # name: test_entity_description_as_dataclass.1 "EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=, translation_key=None, unit_of_measurement=None)" @@ -43,3 +55,63 @@ # name: test_extending_entity_description.3 "test_extending_entity_description..ThawedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')" # --- +# name: test_extending_entity_description.4 + dict({ + 'device_class': None, + 'entity_category': None, + 'entity_registry_enabled_default': True, + 'entity_registry_visible_default': True, + 'extension': 'ext', + 'extra': 'foo', + 'force_update': False, + 'has_entity_name': False, + 'icon': None, + 'key': 'blah', + 'name': 'name', + 'translation_key': None, + 'unit_of_measurement': None, + }) +# --- +# name: test_extending_entity_description.5 + "test_extending_entity_description..MyExtendedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extension='ext', extra='foo')" +# --- +# name: test_extending_entity_description.6 + dict({ + 'device_class': None, + 'entity_category': None, + 'entity_registry_enabled_default': True, + 'entity_registry_visible_default': True, + 'extra': 'foo', + 'force_update': False, + 'has_entity_name': False, + 'icon': None, + 'key': 'blah', + 'mixin': 'mixin', + 'name': 'name', + 'translation_key': None, + 'unit_of_measurement': None, + }) +# --- +# name: test_extending_entity_description.7 + "test_extending_entity_description..ComplexEntityDescription1(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')" +# --- +# name: test_extending_entity_description.8 + dict({ + 'device_class': None, + 'entity_category': None, + 'entity_registry_enabled_default': True, + 'entity_registry_visible_default': True, + 'extra': 'foo', + 'force_update': False, + 'has_entity_name': False, + 'icon': None, + 'key': 'blah', + 'mixin': 'mixin', + 'name': 'name', + 'translation_key': None, + 'unit_of_measurement': None, + }) +# --- +# name: test_extending_entity_description.9 + "test_extending_entity_description..ComplexEntityDescription2(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')" +# --- diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 66ba9f947c9302..5a706b73b49591 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1669,6 +1669,7 @@ def test_entity_description_as_dataclass(snapshot: SnapshotAssertion): with pytest.raises(dataclasses.FrozenInstanceError): delattr(obj, "name") + assert dataclasses.is_dataclass(obj) assert obj == snapshot assert obj == entity.EntityDescription("blah", device_class="test") assert repr(obj) == snapshot @@ -1706,3 +1707,45 @@ class ThawedEntityDescription(entity.EntityDescription): assert obj.name == "mutate" delattr(obj, "key") assert not hasattr(obj, "key") + + # Try multiple levels of FrozenOrThawed + class ExtendedEntityDescription(entity.EntityDescription, frozen_or_thawed=True): + extension: str = None + + @dataclasses.dataclass(frozen=True) + class MyExtendedEntityDescription(ExtendedEntityDescription): + extra: str = None + + obj = MyExtendedEntityDescription("blah", extension="ext", extra="foo", name="name") + assert obj == snapshot + assert obj == MyExtendedEntityDescription( + "blah", extension="ext", extra="foo", name="name" + ) + assert repr(obj) == snapshot + + # Try multiple direct parents + @dataclasses.dataclass(frozen=True) + class MyMixin: + mixin: str = None + + @dataclasses.dataclass(frozen=True, kw_only=True) + class ComplexEntityDescription1(MyMixin, entity.EntityDescription): + extra: str = None + + obj = ComplexEntityDescription1(key="blah", extra="foo", mixin="mixin", name="name") + assert obj == snapshot + assert obj == ComplexEntityDescription1( + key="blah", extra="foo", mixin="mixin", name="name" + ) + assert repr(obj) == snapshot + + @dataclasses.dataclass(frozen=True, kw_only=True) + class ComplexEntityDescription2(entity.EntityDescription, MyMixin): + extra: str = None + + obj = ComplexEntityDescription2(key="blah", extra="foo", mixin="mixin", name="name") + assert obj == snapshot + assert obj == ComplexEntityDescription2( + key="blah", extra="foo", mixin="mixin", name="name" + ) + assert repr(obj) == snapshot From f002a6a73225284874063c221de1f0e537e9d4e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 10:28:43 -1000 Subject: [PATCH 077/118] Refactor all Bluetooth scanners to inherit from BaseHaRemoteScanner (#105523) --- .../components/bluetooth/__init__.py | 3 +- .../components/bluetooth/base_scanner.py | 95 ------------------- homeassistant/components/bluetooth/manager.py | 41 +++++++- .../components/esphome/bluetooth/__init__.py | 2 +- .../components/esphome/bluetooth/scanner.py | 7 +- .../components/ruuvi_gateway/bluetooth.py | 7 +- .../components/shelly/bluetooth/__init__.py | 4 +- .../components/shelly/bluetooth/scanner.py | 7 +- tests/components/bluetooth/test_api.py | 6 +- .../components/bluetooth/test_base_scanner.py | 22 ++--- .../components/bluetooth/test_diagnostics.py | 9 +- tests/components/bluetooth/test_manager.py | 10 +- tests/components/bluetooth/test_models.py | 19 ++-- tests/components/bluetooth/test_wrappers.py | 13 +-- .../esphome/bluetooth/test_client.py | 2 +- tests/components/shelly/test_diagnostics.py | 1 - 16 files changed, 83 insertions(+), 165 deletions(-) delete mode 100644 homeassistant/components/bluetooth/base_scanner.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 4a53347e826ae9..c4434f8695f3cf 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -23,6 +23,7 @@ ) from bluetooth_data_tools import monotonic_time_coarse as MONOTONIC_TIME from habluetooth import ( + BaseHaRemoteScanner, BaseHaScanner, BluetoothScannerDevice, BluetoothScanningMode, @@ -69,7 +70,6 @@ async_set_fallback_availability_interval, async_track_unavailable, ) -from .base_scanner import HomeAssistantRemoteScanner from .const import ( BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, @@ -116,6 +116,7 @@ "BluetoothCallback", "BluetoothScannerDevice", "HaBluetoothConnector", + "BaseHaRemoteScanner", "SOURCE_LOCAL", "FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS", "MONOTONIC_TIME", diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py deleted file mode 100644 index b8e1e909ad2880..00000000000000 --- a/homeassistant/components/bluetooth/base_scanner.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Base classes for HA Bluetooth scanners for bluetooth.""" -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -from bluetooth_adapters import DiscoveredDeviceAdvertisementData -from habluetooth import BaseHaRemoteScanner, HaBluetoothConnector -from home_assistant_bluetooth import BluetoothServiceInfoBleak - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - callback as hass_callback, -) - -from . import models - - -class HomeAssistantRemoteScanner(BaseHaRemoteScanner): - """Home Assistant remote BLE scanner. - - This is the only object that should know about - the hass object. - """ - - __slots__ = ( - "hass", - "_storage", - "_cancel_stop", - ) - - def __init__( - self, - hass: HomeAssistant, - scanner_id: str, - name: str, - new_info_callback: Callable[[BluetoothServiceInfoBleak], None], - connector: HaBluetoothConnector | None, - connectable: bool, - ) -> None: - """Initialize the scanner.""" - self.hass = hass - assert models.MANAGER is not None - self._storage = models.MANAGER.storage - self._cancel_stop: CALLBACK_TYPE | None = None - super().__init__(scanner_id, name, new_info_callback, connector, connectable) - - @hass_callback - def async_setup(self) -> CALLBACK_TYPE: - """Set up the scanner.""" - super().async_setup() - if history := self._storage.async_get_advertisement_history(self.source): - self._discovered_device_advertisement_datas = ( - history.discovered_device_advertisement_datas - ) - self._discovered_device_timestamps = history.discovered_device_timestamps - # Expire anything that is too old - self._async_expire_devices() - - self._cancel_stop = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, self._async_save_history - ) - return self._unsetup - - @hass_callback - def _unsetup(self) -> None: - super()._unsetup() - self._async_save_history() - if self._cancel_stop: - self._cancel_stop() - self._cancel_stop = None - - @hass_callback - def _async_save_history(self, event: Event | None = None) -> None: - """Save the history.""" - self._storage.async_set_advertisement_history( - self.source, - DiscoveredDeviceAdvertisementData( - self.connectable, - self._expire_seconds, - self._discovered_device_advertisement_datas, - self._discovered_device_timestamps, - ), - ) - - async def async_diagnostics(self) -> dict[str, Any]: - """Return diagnostic information about the scanner.""" - diag = await super().async_diagnostics() - diag["storage"] = self._storage.async_get_advertisement_history_as_dict( - self.source - ) - return diag diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 848460455ca736..5508f58c82ba0c 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -2,12 +2,13 @@ from __future__ import annotations from collections.abc import Callable, Iterable +from functools import partial import itertools import logging from bleak_retry_connector import BleakSlotManager from bluetooth_adapters import BluetoothAdapters -from habluetooth import BluetoothManager +from habluetooth import BaseHaRemoteScanner, BaseHaScanner, BluetoothManager from homeassistant import config_entries from homeassistant.const import EVENT_LOGGING_CHANGED @@ -189,7 +190,45 @@ def _async_remove_callback() -> None: def async_stop(self) -> None: """Stop the Bluetooth integration at shutdown.""" _LOGGER.debug("Stopping bluetooth manager") + self._async_save_scanner_histories() super().async_stop() if self._cancel_logging_listener: self._cancel_logging_listener() self._cancel_logging_listener = None + + def _async_save_scanner_histories(self) -> None: + """Save the scanner histories.""" + for scanner in itertools.chain( + self._connectable_scanners, self._non_connectable_scanners + ): + self._async_save_scanner_history(scanner) + + def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None: + """Save the scanner history.""" + if isinstance(scanner, BaseHaRemoteScanner): + self.storage.async_set_advertisement_history( + scanner.source, scanner.serialize_discovered_devices() + ) + + def _async_unregister_scanner( + self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE + ) -> None: + """Unregister a scanner.""" + unregister() + self._async_save_scanner_history(scanner) + + def async_register_scanner( + self, + scanner: BaseHaScanner, + connectable: bool, + connection_slots: int | None = None, + ) -> CALLBACK_TYPE: + """Register a scanner.""" + if isinstance(scanner, BaseHaRemoteScanner): + if history := self.storage.async_get_advertisement_history(scanner.source): + scanner.restore_discovered_devices(history) + + unregister = super().async_register_scanner( + scanner, connectable, connection_slots + ) + return partial(self._async_unregister_scanner, scanner, unregister) diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 6936afac71421f..0fe28730fce2fa 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -99,7 +99,7 @@ async def async_connect_scanner( ), ) scanner = ESPHomeScanner( - hass, source, entry.title, new_info_callback, connector, connectable + source, entry.title, new_info_callback, connector, connectable ) client_data.scanner = scanner coros: list[Coroutine[Any, Any, CALLBACK_TYPE]] = [] diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index b4fb12210d3436..a54e7af59a62ed 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -7,14 +7,11 @@ parse_advertisement_data_tuple, ) -from homeassistant.components.bluetooth import ( - MONOTONIC_TIME, - HomeAssistantRemoteScanner, -) +from homeassistant.components.bluetooth import MONOTONIC_TIME, BaseHaRemoteScanner from homeassistant.core import callback -class ESPHomeScanner(HomeAssistantRemoteScanner): +class ESPHomeScanner(BaseHaRemoteScanner): """Scanner for esphome.""" __slots__ = () diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py index 8a154bca019d40..2d9bf8c6644767 100644 --- a/homeassistant/components/ruuvi_gateway/bluetooth.py +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -10,7 +10,7 @@ from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, MONOTONIC_TIME, - HomeAssistantRemoteScanner, + BaseHaRemoteScanner, async_get_advertisement_callback, async_register_scanner, ) @@ -22,12 +22,11 @@ _LOGGER = logging.getLogger(__name__) -class RuuviGatewayScanner(HomeAssistantRemoteScanner): +class RuuviGatewayScanner(BaseHaRemoteScanner): """Scanner for Ruuvi Gateway.""" def __init__( self, - hass: HomeAssistant, scanner_id: str, name: str, new_info_callback: Callable[[BluetoothServiceInfoBleak], None], @@ -36,7 +35,6 @@ def __init__( ) -> None: """Initialize the scanner, using the given update coordinator as data source.""" super().__init__( - hass, scanner_id, name, new_info_callback, @@ -87,7 +85,6 @@ def async_connect_scanner( source, ) scanner = RuuviGatewayScanner( - hass=hass, scanner_id=source, name=entry.title, new_info_callback=async_get_advertisement_callback(hass), diff --git a/homeassistant/components/shelly/bluetooth/__init__.py b/homeassistant/components/shelly/bluetooth/__init__.py index 429fae1a9a1939..007900a5cdc9f2 100644 --- a/homeassistant/components/shelly/bluetooth/__init__.py +++ b/homeassistant/components/shelly/bluetooth/__init__.py @@ -43,9 +43,7 @@ async def async_connect_scanner( source=source, can_connect=lambda: False, ) - scanner = ShellyBLEScanner( - hass, source, entry.title, new_info_callback, connector, False - ) + scanner = ShellyBLEScanner(source, entry.title, new_info_callback, connector, False) unload_callbacks = [ async_register_scanner(hass, scanner, False), scanner.async_setup(), diff --git a/homeassistant/components/shelly/bluetooth/scanner.py b/homeassistant/components/shelly/bluetooth/scanner.py index 3ada1ce55f5b46..7c0dc3c792ad98 100644 --- a/homeassistant/components/shelly/bluetooth/scanner.py +++ b/homeassistant/components/shelly/bluetooth/scanner.py @@ -6,16 +6,13 @@ from aioshelly.ble import parse_ble_scan_result_event from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT, BLE_SCAN_RESULT_VERSION -from homeassistant.components.bluetooth import ( - MONOTONIC_TIME, - HomeAssistantRemoteScanner, -) +from homeassistant.components.bluetooth import MONOTONIC_TIME, BaseHaRemoteScanner from homeassistant.core import callback from ..const import LOGGER -class ShellyBLEScanner(HomeAssistantRemoteScanner): +class ShellyBLEScanner(BaseHaRemoteScanner): """Scanner for shelly.""" @callback diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index 30e9554f2afb57..aee15f7874e37c 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -7,9 +7,9 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( MONOTONIC_TIME, + BaseHaRemoteScanner, BaseHaScanner, HaBluetoothConnector, - HomeAssistantRemoteScanner, async_scanner_by_source, async_scanner_devices_by_address, ) @@ -46,7 +46,7 @@ async def test_async_scanner_devices_by_address_connectable( """Test getting scanner devices by address with connectable devices.""" manager = _get_manager() - class FakeInjectableScanner(HomeAssistantRemoteScanner): + class FakeInjectableScanner(BaseHaRemoteScanner): def inject_advertisement( self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: @@ -68,7 +68,7 @@ def inject_advertisement( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) scanner = FakeInjectableScanner( - hass, "esp32", "esp32", new_info_callback, connector, False + "esp32", "esp32", new_info_callback, connector, False ) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 2e2be0e7963dcd..c94e3c874e098f 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -14,8 +14,8 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( MONOTONIC_TIME, + BaseHaRemoteScanner, HaBluetoothConnector, - HomeAssistantRemoteScanner, storage, ) from homeassistant.components.bluetooth.const import ( @@ -41,7 +41,7 @@ from tests.common import async_fire_time_changed, load_fixture -class FakeScanner(HomeAssistantRemoteScanner): +class FakeScanner(BaseHaRemoteScanner): """Fake scanner.""" def inject_advertisement( @@ -115,7 +115,7 @@ async def test_remote_scanner( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True) + scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, True) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -182,7 +182,7 @@ async def test_remote_scanner_expires_connectable( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True) + scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, True) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -237,7 +237,7 @@ async def test_remote_scanner_expires_non_connectable( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -312,7 +312,7 @@ async def test_base_scanner_connecting_behavior( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -363,8 +363,7 @@ async def test_restore_history_remote_adapter( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = HomeAssistantRemoteScanner( - hass, + scanner = BaseHaRemoteScanner( "atom-bluetooth-proxy-ceaac4", "atom-bluetooth-proxy-ceaac4", lambda adv: None, @@ -379,8 +378,7 @@ async def test_restore_history_remote_adapter( cancel() unsetup() - scanner = HomeAssistantRemoteScanner( - hass, + scanner = BaseHaRemoteScanner( "atom-bluetooth-proxy-ceaac4", "atom-bluetooth-proxy-ceaac4", lambda adv: None, @@ -419,7 +417,7 @@ async def test_device_with_ten_minute_advertising_interval( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -511,7 +509,7 @@ async def test_scanner_stops_responding( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index a69c26a16ea84b..8d87d5ef396c00 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -8,8 +8,8 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( MONOTONIC_TIME, + BaseHaRemoteScanner, HaBluetoothConnector, - HomeAssistantRemoteScanner, ) from homeassistant.core import HomeAssistant @@ -423,7 +423,7 @@ async def test_diagnostics_remote_adapter( local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): def inject_advertisement( self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: @@ -458,9 +458,7 @@ def inject_advertisement( connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner( - hass, "esp32", "esp32", new_info_callback, connector, False - ) + scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -631,7 +629,6 @@ def inject_advertisement( "scanning": True, "source": "esp32", "start_time": ANY, - "storage": None, "time_since_last_device_detection": {"44:44:33:11:23:45": ANY}, "type": "FakeScanner", }, diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index ba28d8fa19cf1d..63201f790fef04 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -13,12 +13,12 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( MONOTONIC_TIME, + BaseHaRemoteScanner, BluetoothChange, BluetoothScanningMode, BluetoothServiceInfo, BluetoothServiceInfoBleak, HaBluetoothConnector, - HomeAssistantRemoteScanner, async_ble_device_from_address, async_get_advertisement_callback, async_get_fallback_availability_interval, @@ -703,7 +703,7 @@ def _fake_subscriber( BluetoothScanningMode.ACTIVE, ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): def inject_advertisement( self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: @@ -725,7 +725,6 @@ def inject_advertisement( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) connectable_scanner = FakeScanner( - hass, "connectable", "connectable", new_info_callback, @@ -749,7 +748,6 @@ def inject_advertisement( ) not_connectable_scanner = FakeScanner( - hass, "not_connectable", "not_connectable", new_info_callback, @@ -800,7 +798,6 @@ def _unavailable_callback(service_info: BluetoothServiceInfoBleak) -> None: cancel_unavailable() connectable_scanner_2 = FakeScanner( - hass, "connectable", "connectable", new_info_callback, @@ -876,7 +873,7 @@ def _fake_subscriber( BluetoothScanningMode.ACTIVE, ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): def inject_advertisement( self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: @@ -904,7 +901,6 @@ def clear_all_devices(self) -> None: HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) non_connectable_scanner = FakeScanner( - hass, "connectable", "connectable", new_info_callback, diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 7499f312cef939..c0423aca357018 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -11,9 +11,9 @@ import pytest from homeassistant.components.bluetooth import ( + BaseHaRemoteScanner, BaseHaScanner, HaBluetoothConnector, - HomeAssistantRemoteScanner, ) from homeassistant.core import HomeAssistant @@ -154,7 +154,7 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected( local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}, rssi=-100 ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" @@ -182,7 +182,6 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: MockBleakClient, "esp32_has_connection_slot", lambda: True ) scanner = FakeScanner( - hass, "esp32_has_connection_slot", "esp32_has_connection_slot", lambda info: None, @@ -267,7 +266,7 @@ async def test_ble_device_with_proxy_client_out_of_connections( local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" @@ -292,7 +291,7 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: return None connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: False) - scanner = FakeScanner(hass, "esp32", "esp32", lambda info: None, connector, True) + scanner = FakeScanner("esp32", "esp32", lambda info: None, connector, True) cancel = manager.async_register_scanner(scanner, True) inject_advertisement_with_source( hass, switchbot_proxy_device_no_connection_slot, switchbot_adv, "esp32" @@ -332,7 +331,7 @@ async def test_ble_device_with_proxy_clear_cache( local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" @@ -357,7 +356,7 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: return None connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: True) - scanner = FakeScanner(hass, "esp32", "esp32", lambda info: None, connector, True) + scanner = FakeScanner("esp32", "esp32", lambda info: None, connector, True) cancel = manager.async_register_scanner(scanner, True) inject_advertisement_with_source( hass, switchbot_proxy_device_with_connection_slot, switchbot_adv, "esp32" @@ -435,7 +434,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab "esp32_no_connection_slot", ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" @@ -463,7 +462,6 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: MockBleakClient, "esp32_has_connection_slot", lambda: True ) scanner = FakeScanner( - hass, "esp32_has_connection_slot", "esp32_has_connection_slot", lambda info: None, @@ -549,7 +547,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab "esp32_no_connection_slot", ) - class FakeScanner(HomeAssistantRemoteScanner): + class FakeScanner(BaseHaRemoteScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" @@ -577,7 +575,6 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: MockBleakClient, "esp32_has_connection_slot", lambda: True ) scanner = FakeScanner( - hass, "esp32_has_connection_slot", "esp32_has_connection_slot", lambda info: None, diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index 1d294d90d7675f..6ebba080f6a677 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -17,10 +17,10 @@ from homeassistant.components.bluetooth import ( MONOTONIC_TIME, + BaseHaRemoteScanner, BluetoothServiceInfoBleak, HaBluetoothConnector, HomeAssistantBluetoothManager, - HomeAssistantRemoteScanner, async_get_advertisement_callback, ) from homeassistant.core import HomeAssistant @@ -36,12 +36,11 @@ def mock_shutdown(manager: HomeAssistantBluetoothManager) -> None: manager.shutdown = False -class FakeScanner(HomeAssistantRemoteScanner): +class FakeScanner(BaseHaRemoteScanner): """Fake scanner.""" def __init__( self, - hass: HomeAssistant, scanner_id: str, name: str, new_info_callback: Callable[[BluetoothServiceInfoBleak], None], @@ -49,9 +48,7 @@ def __init__( connectable: bool, ) -> None: """Initialize the scanner.""" - super().__init__( - hass, scanner_id, name, new_info_callback, connector, connectable - ) + super().__init__(scanner_id, name, new_info_callback, connector, connectable) self._details: dict[str, str | HaBluetoothConnector] = {} def __repr__(self) -> str: @@ -187,10 +184,10 @@ def _generate_scanners_with_fake_devices(hass): new_info_callback = async_get_advertisement_callback(hass) scanner_hci0 = FakeScanner( - hass, "00:00:00:00:00:01", "hci0", new_info_callback, None, True + "00:00:00:00:00:01", "hci0", new_info_callback, None, True ) scanner_hci1 = FakeScanner( - hass, "00:00:00:00:00:02", "hci1", new_info_callback, None, True + "00:00:00:00:00:02", "hci1", new_info_callback, None, True ) for device, adv_data in hci0_device_advs.values(): diff --git a/tests/components/esphome/bluetooth/test_client.py b/tests/components/esphome/bluetooth/test_client.py index 7ed1403041d4d9..d74766023d71a9 100644 --- a/tests/components/esphome/bluetooth/test_client.py +++ b/tests/components/esphome/bluetooth/test_client.py @@ -44,7 +44,7 @@ async def client_data_fixture( api_version=APIVersion(1, 9), title=ESP_NAME, scanner=ESPHomeScanner( - hass, ESP_MAC_ADDRESS, ESP_NAME, lambda info: None, connector, True + ESP_MAC_ADDRESS, ESP_NAME, lambda info: None, connector, True ), ) diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index 13126db0a0ec77..3a9b548757b473 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -130,7 +130,6 @@ async def test_rpc_config_entry_diagnostics( "scanning": True, "start_time": ANY, "source": "12:34:56:78:9A:BC", - "storage": None, "time_since_last_device_detection": {"AA:BB:CC:DD:EE:FF": ANY}, "type": "ShellyBLEScanner", } From 283ff4fadaaf5e46737f29841573922a3ff41940 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 12 Dec 2023 21:29:18 +0100 Subject: [PATCH 078/118] Add Adax to strict typing (#105562) --- .strict-typing | 1 + homeassistant/components/adax/__init__.py | 2 +- homeassistant/components/adax/climate.py | 2 +- homeassistant/components/adax/config_flow.py | 8 ++++++-- mypy.ini | 10 ++++++++++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.strict-typing b/.strict-typing index e2a630f01799f2..4ee01b15d1a11c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -43,6 +43,7 @@ homeassistant.components.abode.* homeassistant.components.accuweather.* homeassistant.components.acer_projector.* homeassistant.components.actiontec.* +homeassistant.components.adax.* homeassistant.components.adguard.* homeassistant.components.aftership.* homeassistant.components.air_quality.* diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py index 511fb746216bba..cf60d40631c1ed 100644 --- a/homeassistant/components/adax/__init__.py +++ b/homeassistant/components/adax/__init__.py @@ -24,7 +24,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # convert title and unique_id to string if config_entry.version == 1: if isinstance(config_entry.unique_id, int): - hass.config_entries.async_update_entry( + hass.config_entries.async_update_entry( # type: ignore[unreachable] config_entry, unique_id=str(config_entry.unique_id), title=str(config_entry.title), diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 7587bfc0799cac..34812f9e449b79 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -137,7 +137,7 @@ class LocalAdaxDevice(ClimateEntity): _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS - def __init__(self, adax_data_handler, unique_id): + def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None: """Initialize the heater.""" self._adax_data_handler = adax_data_handler self._attr_unique_id = unique_id diff --git a/homeassistant/components/adax/config_flow.py b/homeassistant/components/adax/config_flow.py index b9e8ef1abcac15..b614c968d488ba 100644 --- a/homeassistant/components/adax/config_flow.py +++ b/homeassistant/components/adax/config_flow.py @@ -36,7 +36,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" data_schema = vol.Schema( { @@ -59,7 +61,9 @@ async def async_step_user(self, user_input=None): return await self.async_step_local() return await self.async_step_cloud() - async def async_step_local(self, user_input=None): + async def async_step_local( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the local step.""" data_schema = vol.Schema( {vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str} diff --git a/mypy.ini b/mypy.ini index 94ff0c1e46d50e..cf590b5391884f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -190,6 +190,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.adax.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.adguard.*] check_untyped_defs = true disallow_incomplete_defs = true From d144d6c9abaa836b5cf97ed1ff323bd6755f79dd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Dec 2023 21:40:11 +0100 Subject: [PATCH 079/118] Mark more entities secondary on Fully Kiosk Browser (#105595) --- homeassistant/components/fully_kiosk/button.py | 3 +++ homeassistant/components/fully_kiosk/number.py | 1 + 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/fully_kiosk/button.py b/homeassistant/components/fully_kiosk/button.py index 9f4d60e9574803..b16265ed467bb9 100644 --- a/homeassistant/components/fully_kiosk/button.py +++ b/homeassistant/components/fully_kiosk/button.py @@ -54,16 +54,19 @@ class FullyButtonEntityDescription( FullyButtonEntityDescription( key="toForeground", translation_key="to_foreground", + entity_category=EntityCategory.CONFIG, press_action=lambda fully: fully.toForeground(), ), FullyButtonEntityDescription( key="toBackground", translation_key="to_background", + entity_category=EntityCategory.CONFIG, press_action=lambda fully: fully.toBackground(), ), FullyButtonEntityDescription( key="loadStartUrl", translation_key="load_start_url", + entity_category=EntityCategory.CONFIG, press_action=lambda fully: fully.loadStartUrl(), ), ) diff --git a/homeassistant/components/fully_kiosk/number.py b/homeassistant/components/fully_kiosk/number.py index 298a58e2a112ad..4203a64074d33b 100644 --- a/homeassistant/components/fully_kiosk/number.py +++ b/homeassistant/components/fully_kiosk/number.py @@ -46,6 +46,7 @@ native_max_value=255, native_step=1, native_min_value=0, + entity_category=EntityCategory.CONFIG, ), ) From 77283704a54b6aa8d0456d7217b07c52d73970c7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 12 Dec 2023 22:36:11 +0100 Subject: [PATCH 080/118] Bump `brother` library, use `pysnmp-lextudio` with SNMP integration (#105591) --- homeassistant/components/brother/__init__.py | 15 +-- .../components/brother/manifest.json | 2 +- homeassistant/components/brother/utils.py | 8 +- .../components/snmp/device_tracker.py | 13 +-- homeassistant/components/snmp/manifest.json | 2 +- homeassistant/components/snmp/sensor.py | 34 +++---- homeassistant/components/snmp/switch.py | 91 +++++++++---------- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/brother/__init__.py | 5 +- tests/components/brother/conftest.py | 4 - tests/components/snmp/conftest.py | 5 - 12 files changed, 69 insertions(+), 118 deletions(-) delete mode 100644 tests/components/snmp/conftest.py diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 0f8f94c73c4c30..27ac97a27dc8c6 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -4,23 +4,18 @@ from asyncio import timeout from datetime import timedelta import logging -import sys -from typing import Any + +from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP from .utils import get_snmp_engine -if sys.version_info < (3, 12): - from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError -else: - BrotherSensors = Any - PLATFORMS = [Platform.SENSOR] SCAN_INTERVAL = timedelta(seconds=30) @@ -30,10 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Brother from a config entry.""" - if sys.version_info >= (3, 12): - raise HomeAssistantError( - "Brother Printer is not supported on Python 3.12. Please use Python 3.11." - ) host = entry.data[CONF_HOST] printer_type = entry.data[CONF_TYPE] diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index cba44b68c6ac44..06b8574dbb47c5 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"], "quality_scale": "platinum", - "requirements": ["brother==2.3.0"], + "requirements": ["brother==3.0.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/brother/utils.py b/homeassistant/components/brother/utils.py index cd472b9b754691..47b7ae31a67695 100644 --- a/homeassistant/components/brother/utils.py +++ b/homeassistant/components/brother/utils.py @@ -2,7 +2,9 @@ from __future__ import annotations import logging -import sys + +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio.cmdgen import lcd from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -10,10 +12,6 @@ from .const import DOMAIN, SNMP -if sys.version_info < (3, 12): - import pysnmp.hlapi.asyncio as hlapi - from pysnmp.hlapi.asyncio.cmdgen import lcd - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index 7ca31bae61899d..696b079fd5e5f0 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -3,8 +3,9 @@ import binascii import logging -import sys +from pysnmp.entity import config as cfg +from pysnmp.entity.rfc3413.oneliner import cmdgen import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -14,7 +15,6 @@ ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -26,11 +26,6 @@ DEFAULT_COMMUNITY, ) -if sys.version_info < (3, 12): - from pysnmp.entity import config as cfg - from pysnmp.entity.rfc3413.oneliner import cmdgen - - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( @@ -46,10 +41,6 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> SnmpScanner | None: """Validate the configuration and return an SNMP scanner.""" - if sys.version_info >= (3, 12): - raise HomeAssistantError( - "SNMP is not supported on Python 3.12. Please use Python 3.11." - ) scanner = SnmpScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index 324a1e493661c3..2756b97157c2e0 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], - "requirements": ["pysnmplib==5.0.21"] + "requirements": ["pysnmp-lextudio==5.0.31"] } diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 58cd12d611f465..a5915183ad0c94 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -3,8 +3,20 @@ from datetime import timedelta import logging -import sys +from pysnmp.error import PySnmpError +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + Udp6TransportTarget, + UdpTransportTarget, + UsmUserData, + getCmd, +) import voluptuous as vol from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA @@ -21,7 +33,6 @@ STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template @@ -56,21 +67,6 @@ SNMP_VERSIONS, ) -if sys.version_info < (3, 12): - from pysnmp.error import PySnmpError - import pysnmp.hlapi.asyncio as hlapi - from pysnmp.hlapi.asyncio import ( - CommunityData, - ContextData, - ObjectIdentity, - ObjectType, - SnmpEngine, - Udp6TransportTarget, - UdpTransportTarget, - UsmUserData, - getCmd, - ) - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) @@ -115,10 +111,6 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the SNMP sensor.""" - if sys.version_info >= (3, 12): - raise HomeAssistantError( - "SNMP is not supported on Python 3.12. Please use Python 3.11." - ) host = config.get(CONF_HOST) port = config.get(CONF_PORT) community = config.get(CONF_COMMUNITY) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index e94c6991601155..d0fe393d55083c 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -2,9 +2,34 @@ from __future__ import annotations import logging -import sys from typing import Any +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + UdpTransportTarget, + UsmUserData, + getCmd, + setCmd, +) +from pysnmp.proto.rfc1902 import ( + Counter32, + Counter64, + Gauge32, + Integer, + Integer32, + IpAddress, + Null, + ObjectIdentifier, + OctetString, + Opaque, + TimeTicks, + Unsigned32, +) import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -17,7 +42,6 @@ CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -43,34 +67,6 @@ SNMP_VERSIONS, ) -if sys.version_info < (3, 12): - import pysnmp.hlapi.asyncio as hlapi - from pysnmp.hlapi.asyncio import ( - CommunityData, - ContextData, - ObjectIdentity, - ObjectType, - SnmpEngine, - UdpTransportTarget, - UsmUserData, - getCmd, - setCmd, - ) - from pysnmp.proto.rfc1902 import ( - Counter32, - Counter64, - Gauge32, - Integer, - Integer32, - IpAddress, - Null, - ObjectIdentifier, - OctetString, - Opaque, - TimeTicks, - Unsigned32, - ) - _LOGGER = logging.getLogger(__name__) CONF_COMMAND_OID = "command_oid" @@ -81,22 +77,21 @@ DEFAULT_PAYLOAD_OFF = 0 DEFAULT_PAYLOAD_ON = 1 -if sys.version_info < (3, 12): - MAP_SNMP_VARTYPES = { - "Counter32": Counter32, - "Counter64": Counter64, - "Gauge32": Gauge32, - "Integer32": Integer32, - "Integer": Integer, - "IpAddress": IpAddress, - "Null": Null, - # some work todo to support tuple ObjectIdentifier, this just supports str - "ObjectIdentifier": ObjectIdentifier, - "OctetString": OctetString, - "Opaque": Opaque, - "TimeTicks": TimeTicks, - "Unsigned32": Unsigned32, - } +MAP_SNMP_VARTYPES = { + "Counter32": Counter32, + "Counter64": Counter64, + "Gauge32": Gauge32, + "Integer32": Integer32, + "Integer": Integer, + "IpAddress": IpAddress, + "Null": Null, + # some work todo to support tuple ObjectIdentifier, this just supports str + "ObjectIdentifier": ObjectIdentifier, + "OctetString": OctetString, + "Opaque": Opaque, + "TimeTicks": TimeTicks, + "Unsigned32": Unsigned32, +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -132,10 +127,6 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the SNMP switch.""" - if sys.version_info >= (3, 12): - raise HomeAssistantError( - "SNMP is not supported on Python 3.12. Please use Python 3.11." - ) name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/requirements_all.txt b/requirements_all.txt index 573d51cb43aae0..010d6bbc799541 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -583,7 +583,7 @@ boto3==1.28.17 broadlink==0.18.3 # homeassistant.components.brother -brother==2.3.0 +brother==3.0.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -2089,7 +2089,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmplib==5.0.21 +pysnmp-lextudio==5.0.31 # homeassistant.components.snooz pysnooz==0.8.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e61b196514cd8..daabb5e343cb88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -491,7 +491,7 @@ boschshcpy==0.2.75 broadlink==0.18.3 # homeassistant.components.brother -brother==2.3.0 +brother==3.0.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -1588,7 +1588,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmplib==5.0.21 +pysnmp-lextudio==5.0.31 # homeassistant.components.snooz pysnooz==0.8.6 diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index 3176fa7fc28fd1..8e24c2d8058f50 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -1,16 +1,13 @@ """Tests for Brother Printer integration.""" import json -import sys from unittest.mock import patch +from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture -if sys.version_info < (3, 12): - from homeassistant.components.brother.const import DOMAIN - async def init_integration( hass: HomeAssistant, skip_setup: bool = False diff --git a/tests/components/brother/conftest.py b/tests/components/brother/conftest.py index 558b3b8ac3eb8a..9e81cce9d123cb 100644 --- a/tests/components/brother/conftest.py +++ b/tests/components/brother/conftest.py @@ -1,13 +1,9 @@ """Test fixtures for brother.""" from collections.abc import Generator -import sys from unittest.mock import AsyncMock, patch import pytest -if sys.version_info >= (3, 12): - collect_ignore_glob = ["test_*.py"] - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: diff --git a/tests/components/snmp/conftest.py b/tests/components/snmp/conftest.py deleted file mode 100644 index 05a518ad7f3bb2..00000000000000 --- a/tests/components/snmp/conftest.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Skip test collection for Python 3.12.""" -import sys - -if sys.version_info >= (3, 12): - collect_ignore_glob = ["test_*.py"] From 98b1bc9bed72cca6abdcc967ed30e56175c99067 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 12:51:18 -1000 Subject: [PATCH 081/118] Bump aioesphomeapi to 20.1.0 (#105602) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index eac721a446266c..0a22b3a4b59cae 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==20.0.0", + "aioesphomeapi==20.1.0", "bluetooth-data-tools==1.17.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 010d6bbc799541..a5876e4df4be6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -239,7 +239,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==20.0.0 +aioesphomeapi==20.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index daabb5e343cb88..9ca69fb466d46a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -218,7 +218,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==20.0.0 +aioesphomeapi==20.1.0 # homeassistant.components.flo aioflo==2021.11.0 From a595cd7141a5179f4e49eb6d1bf251a273fdb19c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:52:15 -0500 Subject: [PATCH 082/118] Add sensor platform to A. O. Smith integration (#105604) * Add sensor platform to A. O. Smith integration * Fix typo * Remove unnecessary mixin * Simplify async_setup_entry --- homeassistant/components/aosmith/__init__.py | 2 +- homeassistant/components/aosmith/const.py | 6 ++ homeassistant/components/aosmith/sensor.py | 75 +++++++++++++++++++ homeassistant/components/aosmith/strings.json | 12 +++ .../aosmith/snapshots/test_sensor.ambr | 20 +++++ tests/components/aosmith/test_sensor.py | 27 +++++++ 6 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/aosmith/sensor.py create mode 100644 tests/components/aosmith/snapshots/test_sensor.ambr create mode 100644 tests/components/aosmith/test_sensor.py diff --git a/homeassistant/components/aosmith/__init__.py b/homeassistant/components/aosmith/__init__.py index af780e012aef34..cac746e189ecb0 100644 --- a/homeassistant/components/aosmith/__init__.py +++ b/homeassistant/components/aosmith/__init__.py @@ -13,7 +13,7 @@ from .const import DOMAIN from .coordinator import AOSmithCoordinator -PLATFORMS: list[Platform] = [Platform.WATER_HEATER] +PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WATER_HEATER] @dataclass diff --git a/homeassistant/components/aosmith/const.py b/homeassistant/components/aosmith/const.py index 06794582258865..e79b993182ebf5 100644 --- a/homeassistant/components/aosmith/const.py +++ b/homeassistant/components/aosmith/const.py @@ -14,3 +14,9 @@ # Update interval to be used while a mode or setpoint change is in progress. FAST_INTERVAL = timedelta(seconds=1) + +HOT_WATER_STATUS_MAP = { + "LOW": "low", + "MEDIUM": "medium", + "HIGH": "high", +} diff --git a/homeassistant/components/aosmith/sensor.py b/homeassistant/components/aosmith/sensor.py new file mode 100644 index 00000000000000..c9bd9f1321e2f3 --- /dev/null +++ b/homeassistant/components/aosmith/sensor.py @@ -0,0 +1,75 @@ +"""The sensor platform for the A. O. Smith integration.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AOSmithData +from .const import DOMAIN, HOT_WATER_STATUS_MAP +from .coordinator import AOSmithCoordinator +from .entity import AOSmithEntity + + +@dataclass(kw_only=True) +class AOSmithSensorEntityDescription(SensorEntityDescription): + """Define sensor entity description class.""" + + value_fn: Callable[[dict[str, Any]], str | int | None] + + +ENTITY_DESCRIPTIONS: tuple[AOSmithSensorEntityDescription, ...] = ( + AOSmithSensorEntityDescription( + key="hot_water_availability", + translation_key="hot_water_availability", + icon="mdi:water-thermometer", + device_class=SensorDeviceClass.ENUM, + options=["low", "medium", "high"], + value_fn=lambda device: HOT_WATER_STATUS_MAP.get( + device.get("data", {}).get("hotWaterStatus") + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up A. O. Smith sensor platform.""" + data: AOSmithData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + AOSmithSensorEntity(data.coordinator, description, junction_id) + for description in ENTITY_DESCRIPTIONS + for junction_id in data.coordinator.data + ) + + +class AOSmithSensorEntity(AOSmithEntity, SensorEntity): + """The sensor entity for the A. O. Smith integration.""" + + entity_description: AOSmithSensorEntityDescription + + def __init__( + self, + coordinator: AOSmithCoordinator, + description: AOSmithSensorEntityDescription, + junction_id: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator, junction_id) + self.entity_description = description + self._attr_unique_id = f"{description.key}_{junction_id}" + + @property + def native_value(self) -> str | int | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.device) diff --git a/homeassistant/components/aosmith/strings.json b/homeassistant/components/aosmith/strings.json index 26de264bab9f03..0f1fcfc1744656 100644 --- a/homeassistant/components/aosmith/strings.json +++ b/homeassistant/components/aosmith/strings.json @@ -24,5 +24,17 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "hot_water_availability": { + "name": "Hot water availability", + "state": { + "low": "Low", + "medium": "Medium", + "high": "High" + } + } + } } } diff --git a/tests/components/aosmith/snapshots/test_sensor.ambr b/tests/components/aosmith/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..8499a98c8e553d --- /dev/null +++ b/tests/components/aosmith/snapshots/test_sensor.ambr @@ -0,0 +1,20 @@ +# serializer version: 1 +# name: test_state + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'My water heater Hot water availability', + 'icon': 'mdi:water-thermometer', + 'options': list([ + 'low', + 'medium', + 'high', + ]), + }), + 'context': , + 'entity_id': 'sensor.my_water_heater_hot_water_availability', + 'last_changed': , + 'last_updated': , + 'state': 'low', + }) +# --- diff --git a/tests/components/aosmith/test_sensor.py b/tests/components/aosmith/test_sensor.py new file mode 100644 index 00000000000000..99626b09e8361c --- /dev/null +++ b/tests/components/aosmith/test_sensor.py @@ -0,0 +1,27 @@ +"""Tests for the sensor platform of the A. O. Smith integration.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, +) -> None: + """Test the setup of the sensor entity.""" + entry = entity_registry.async_get("sensor.my_water_heater_hot_water_availability") + assert entry + assert entry.unique_id == "hot_water_availability_junctionId" + + +async def test_state( + hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion +) -> None: + """Test the state of the sensor entity.""" + state = hass.states.get("sensor.my_water_heater_hot_water_availability") + assert state == snapshot From 22f0e09b8c2d92fdbdf926c92c37900b4e4f648d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 15:20:05 -1000 Subject: [PATCH 083/118] Bump aioesphomeapi to 21.0.0 (#105609) --- homeassistant/components/esphome/bluetooth/scanner.py | 6 +++--- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index a54e7af59a62ed..ecbfeb4124c9f9 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -1,7 +1,7 @@ """Bluetooth scanner for esphome.""" from __future__ import annotations -from aioesphomeapi import BluetoothLEAdvertisement, BluetoothLERawAdvertisement +from aioesphomeapi import BluetoothLEAdvertisement, BluetoothLERawAdvertisementsResponse from bluetooth_data_tools import ( int_to_bluetooth_address, parse_advertisement_data_tuple, @@ -34,11 +34,11 @@ def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: @callback def async_on_raw_advertisements( - self, advertisements: list[BluetoothLERawAdvertisement] + self, raw: BluetoothLERawAdvertisementsResponse ) -> None: """Call the registered callback.""" now = MONOTONIC_TIME() - for adv in advertisements: + for adv in raw.advertisements: self._async_on_advertisement( int_to_bluetooth_address(adv.address), adv.rssi, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 0a22b3a4b59cae..a7712de14fa791 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==20.1.0", + "aioesphomeapi==21.0.0", "bluetooth-data-tools==1.17.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index a5876e4df4be6c..ff852542d0db37 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -239,7 +239,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==20.1.0 +aioesphomeapi==21.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ca69fb466d46a..bb14d282a21fac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -218,7 +218,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==20.1.0 +aioesphomeapi==21.0.0 # homeassistant.components.flo aioflo==2021.11.0 From 431a44ab673c8caffa6ad91bb986a1ec7f04b796 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 12 Dec 2023 21:54:15 -0600 Subject: [PATCH 084/118] Add name slot to HassClimateGetTemperature intent (#105585) --- homeassistant/components/climate/intent.py | 16 +++++++++++++++- tests/components/climate/test_intent.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index 23cc3d5bcd206e..4152fb5ee2d50f 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -21,7 +21,7 @@ class GetTemperatureIntent(intent.IntentHandler): """Handle GetTemperature intents.""" intent_type = INTENT_GET_TEMPERATURE - slot_schema = {vol.Optional("area"): str} + slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" @@ -49,6 +49,20 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse if climate_state is None: raise intent.IntentHandleError(f"No climate entity in area {area_name}") + climate_entity = component.get_entity(climate_state.entity_id) + elif "name" in slots: + # Filter by name + entity_name = slots["name"]["value"] + + for maybe_climate in intent.async_match_states( + hass, name=entity_name, domains=[DOMAIN] + ): + climate_state = maybe_climate + break + + if climate_state is None: + raise intent.IntentHandleError(f"No climate entity named {entity_name}") + climate_entity = component.get_entity(climate_state.entity_id) else: # First entity diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index eaf7029d303b57..6473eca1b883b4 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -153,7 +153,7 @@ async def test_get_temperature( state = response.matched_states[0] assert state.attributes["current_temperature"] == 10.0 - # Select by area instead (climate_2) + # Select by area (climate_2) response = await intent.async_handle( hass, "test", @@ -166,6 +166,19 @@ async def test_get_temperature( state = response.matched_states[0] assert state.attributes["current_temperature"] == 22.0 + # Select by name (climate_2) + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": "Climate 2"}}, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + state = response.matched_states[0] + assert state.attributes["current_temperature"] == 22.0 + async def test_get_temperature_no_entities( hass: HomeAssistant, From a73e86a74160a71354c433acb1c104480dd15e41 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 12 Dec 2023 23:21:16 -0600 Subject: [PATCH 085/118] Skip TTS events entirely with empty text (#105617) --- .../components/assist_pipeline/pipeline.py | 62 ++++++++++--------- .../snapshots/test_websocket.ambr | 28 +++++++-- .../assist_pipeline/test_websocket.py | 11 ++-- 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index ed9029d1c2c1df..26d599da836ab2 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -369,6 +369,7 @@ class PipelineStage(StrEnum): STT = "stt" INTENT = "intent" TTS = "tts" + END = "end" PIPELINE_STAGE_ORDER = [ @@ -1024,35 +1025,32 @@ async def text_to_speech(self, tts_input: str) -> None: ) ) - if tts_input := tts_input.strip(): - try: - # Synthesize audio and get URL - tts_media_id = tts_generate_media_source_id( - self.hass, - tts_input, - engine=self.tts_engine, - language=self.pipeline.tts_language, - options=self.tts_options, - ) - tts_media = await media_source.async_resolve_media( - self.hass, - tts_media_id, - None, - ) - except Exception as src_error: - _LOGGER.exception("Unexpected error during text-to-speech") - raise TextToSpeechError( - code="tts-failed", - message="Unexpected error during text-to-speech", - ) from src_error - - _LOGGER.debug("TTS result %s", tts_media) - tts_output = { - "media_id": tts_media_id, - **asdict(tts_media), - } - else: - tts_output = {} + try: + # Synthesize audio and get URL + tts_media_id = tts_generate_media_source_id( + self.hass, + tts_input, + engine=self.tts_engine, + language=self.pipeline.tts_language, + options=self.tts_options, + ) + tts_media = await media_source.async_resolve_media( + self.hass, + tts_media_id, + None, + ) + except Exception as src_error: + _LOGGER.exception("Unexpected error during text-to-speech") + raise TextToSpeechError( + code="tts-failed", + message="Unexpected error during text-to-speech", + ) from src_error + + _LOGGER.debug("TTS result %s", tts_media) + tts_output = { + "media_id": tts_media_id, + **asdict(tts_media), + } self.process_event( PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output}) @@ -1345,7 +1343,11 @@ async def buffer_then_audio_stream() -> ( self.conversation_id, self.device_id, ) - current_stage = PipelineStage.TTS + if tts_input.strip(): + current_stage = PipelineStage.TTS + else: + # Skip TTS + current_stage = PipelineStage.END if self.run.end_stage != PipelineStage.INTENT: # text-to-speech diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 072b1ff730ace8..c165675a6ff93a 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -662,15 +662,33 @@ # --- # name: test_pipeline_empty_tts_output.1 dict({ - 'engine': 'test', - 'language': 'en-US', - 'tts_input': '', - 'voice': 'james_earl_jones', + 'conversation_id': None, + 'device_id': None, + 'engine': 'homeassistant', + 'intent_input': 'never mind', + 'language': 'en', }) # --- # name: test_pipeline_empty_tts_output.2 dict({ - 'tts_output': dict({ + 'intent_output': dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + }), + }), }), }) # --- diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 0e2a3ad538c754..458320a9a90c14 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -2467,10 +2467,10 @@ async def test_pipeline_empty_tts_output( await client.send_json_auto_id( { "type": "assist_pipeline/run", - "start_stage": "tts", + "start_stage": "intent", "end_stage": "tts", "input": { - "text": "", + "text": "never mind", }, } ) @@ -2486,16 +2486,15 @@ async def test_pipeline_empty_tts_output( assert msg["event"]["data"] == snapshot events.append(msg["event"]) - # text-to-speech + # intent msg = await client.receive_json() - assert msg["event"]["type"] == "tts-start" + assert msg["event"]["type"] == "intent-start" assert msg["event"]["data"] == snapshot events.append(msg["event"]) msg = await client.receive_json() - assert msg["event"]["type"] == "tts-end" + assert msg["event"]["type"] == "intent-end" assert msg["event"]["data"] == snapshot - assert not msg["event"]["data"]["tts_output"] events.append(msg["event"]) # run end From 9e9b5184337e2a8383b61bcf21a6ccbcc681617f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 07:38:42 +0100 Subject: [PATCH 086/118] Bump github/codeql-action from 2.22.9 to 2.22.10 (#105620) --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c9e6bb8fcc88e3..74cb3826a6ca08 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,11 +29,11 @@ jobs: uses: actions/checkout@v4.1.1 - name: Initialize CodeQL - uses: github/codeql-action/init@v2.22.9 + uses: github/codeql-action/init@v2.22.10 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2.22.9 + uses: github/codeql-action/analyze@v2.22.10 with: category: "/language:python" From 66d24b38aac6acbdf812e4ec9414b594f8b6452f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 13 Dec 2023 08:18:50 +0100 Subject: [PATCH 087/118] Add diagnostics platform to BraviaTV (#105603) * Add diagnostics platform * Add test * Improve test * Use consts * Fix test * Patch methods * Patch methods --- .../components/braviatv/diagnostics.py | 28 ++++++++ .../braviatv/snapshots/test_diagnostics.ambr | 37 ++++++++++ tests/components/braviatv/test_diagnostics.py | 72 +++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 homeassistant/components/braviatv/diagnostics.py create mode 100644 tests/components/braviatv/snapshots/test_diagnostics.ambr create mode 100644 tests/components/braviatv/test_diagnostics.py diff --git a/homeassistant/components/braviatv/diagnostics.py b/homeassistant/components/braviatv/diagnostics.py new file mode 100644 index 00000000000000..f1822b545e9bcc --- /dev/null +++ b/homeassistant/components/braviatv/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for BraviaTV.""" +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MAC, CONF_PIN +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import BraviaTVCoordinator + +TO_REDACT = {CONF_MAC, CONF_PIN, "macAddr"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: BraviaTVCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + device_info = await coordinator.client.get_system_info() + + diagnostics_data = { + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "device_info": async_redact_data(device_info, TO_REDACT), + } + + return diagnostics_data diff --git a/tests/components/braviatv/snapshots/test_diagnostics.ambr b/tests/components/braviatv/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000000..2fd515b24e579a --- /dev/null +++ b/tests/components/braviatv/snapshots/test_diagnostics.ambr @@ -0,0 +1,37 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': 'localhost', + 'mac': '**REDACTED**', + 'pin': '**REDACTED**', + 'use_psk': True, + }), + 'disabled_by': None, + 'domain': 'braviatv', + 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': 'very_unique_string', + 'version': 1, + }), + 'device_info': dict({ + 'area': 'POL', + 'cid': 'very_unique_string', + 'generation': '5.2.0', + 'language': 'pol', + 'macAddr': '**REDACTED**', + 'model': 'TV-Model', + 'name': 'BRAVIA', + 'product': 'TV', + 'region': 'XEU', + 'serial': 'serial_number', + }), + }) +# --- diff --git a/tests/components/braviatv/test_diagnostics.py b/tests/components/braviatv/test_diagnostics.py new file mode 100644 index 00000000000000..d0974774e7bcd1 --- /dev/null +++ b/tests/components/braviatv/test_diagnostics.py @@ -0,0 +1,72 @@ +"""Test the BraviaTV diagnostics.""" +from unittest.mock import patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.braviatv.const import CONF_USE_PSK, DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + +BRAVIA_SYSTEM_INFO = { + "product": "TV", + "region": "XEU", + "language": "pol", + "model": "TV-Model", + "serial": "serial_number", + "macAddr": "AA:BB:CC:DD:EE:FF", + "name": "BRAVIA", + "generation": "5.2.0", + "area": "POL", + "cid": "very_unique_string", +} +INPUTS = [ + { + "uri": "extInput:hdmi?port=1", + "title": "HDMI 1", + "connection": False, + "label": "", + "icon": "meta:hdmi", + } +] + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "localhost", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_USE_PSK: True, + CONF_PIN: "12345qwerty", + }, + unique_id="very_unique_string", + entry_id="3bd2acb0e4f0476d40865546d0d91921", + ) + + config_entry.add_to_hass(hass) + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.pair" + ), patch("pybravia.BraviaClient.set_wol_mode"), patch( + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO + ), patch("pybravia.BraviaClient.get_power_status", return_value="active"), patch( + "pybravia.BraviaClient.get_external_status", return_value=INPUTS + ), patch("pybravia.BraviaClient.get_volume_info", return_value={}), patch( + "pybravia.BraviaClient.get_playing_info", return_value={} + ), patch("pybravia.BraviaClient.get_app_list", return_value=[]), patch( + "pybravia.BraviaClient.get_content_list_all", return_value=[] + ): + assert await async_setup_component(hass, DOMAIN, {}) + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + + assert result == snapshot From aaccf190134a448bfeef62c0c8d53fdb73167b94 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 13 Dec 2023 02:09:22 -0600 Subject: [PATCH 088/118] Rename "satellite enabled" to "mute" (#105619) --- homeassistant/components/wyoming/devices.py | 28 ++++++------ homeassistant/components/wyoming/satellite.py | 44 +++++++++---------- homeassistant/components/wyoming/strings.json | 4 +- homeassistant/components/wyoming/switch.py | 18 ++++---- tests/components/wyoming/test_devices.py | 20 ++++----- tests/components/wyoming/test_satellite.py | 26 +++++------ tests/components/wyoming/test_switch.py | 28 ++++++------ 7 files changed, 83 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/wyoming/devices.py b/homeassistant/components/wyoming/devices.py index bd7252bcf6ba41..6865669fbf05eb 100644 --- a/homeassistant/components/wyoming/devices.py +++ b/homeassistant/components/wyoming/devices.py @@ -17,14 +17,14 @@ class SatelliteDevice: satellite_id: str device_id: str is_active: bool = False - is_enabled: bool = True + is_muted: bool = False pipeline_name: str | None = None noise_suppression_level: int = 0 auto_gain: int = 0 volume_multiplier: float = 1.0 _is_active_listener: Callable[[], None] | None = None - _is_enabled_listener: Callable[[], None] | None = None + _is_muted_listener: Callable[[], None] | None = None _pipeline_listener: Callable[[], None] | None = None _audio_settings_listener: Callable[[], None] | None = None @@ -37,12 +37,12 @@ def set_is_active(self, active: bool) -> None: self._is_active_listener() @callback - def set_is_enabled(self, enabled: bool) -> None: - """Set enabled state.""" - if enabled != self.is_enabled: - self.is_enabled = enabled - if self._is_enabled_listener is not None: - self._is_enabled_listener() + def set_is_muted(self, muted: bool) -> None: + """Set muted state.""" + if muted != self.is_muted: + self.is_muted = muted + if self._is_muted_listener is not None: + self._is_muted_listener() @callback def set_pipeline_name(self, pipeline_name: str) -> None: @@ -82,9 +82,9 @@ def set_is_active_listener(self, is_active_listener: Callable[[], None]) -> None self._is_active_listener = is_active_listener @callback - def set_is_enabled_listener(self, is_enabled_listener: Callable[[], None]) -> None: - """Listen for updates to is_enabled.""" - self._is_enabled_listener = is_enabled_listener + def set_is_muted_listener(self, is_muted_listener: Callable[[], None]) -> None: + """Listen for updates to muted status.""" + self._is_muted_listener = is_muted_listener @callback def set_pipeline_listener(self, pipeline_listener: Callable[[], None]) -> None: @@ -105,11 +105,11 @@ def get_assist_in_progress_entity_id(self, hass: HomeAssistant) -> str | None: "binary_sensor", DOMAIN, f"{self.satellite_id}-assist_in_progress" ) - def get_satellite_enabled_entity_id(self, hass: HomeAssistant) -> str | None: - """Return entity id for satellite enabled switch.""" + def get_muted_entity_id(self, hass: HomeAssistant) -> str | None: + """Return entity id for satellite muted switch.""" ent_reg = er.async_get(hass) return ent_reg.async_get_entity_id( - "switch", DOMAIN, f"{self.satellite_id}-satellite_enabled" + "switch", DOMAIN, f"{self.satellite_id}-mute" ) def get_pipeline_entity_id(self, hass: HomeAssistant) -> str | None: diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 2c93b762015bd1..78f57ff4b01ada 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -49,7 +49,6 @@ def __init__( self.hass = hass self.service = service self.device = device - self.is_enabled = True self.is_running = True self._client: AsyncTcpClient | None = None @@ -57,9 +56,9 @@ def __init__( self._is_pipeline_running = False self._audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue() self._pipeline_id: str | None = None - self._enabled_changed_event = asyncio.Event() + self._muted_changed_event = asyncio.Event() - self.device.set_is_enabled_listener(self._enabled_changed) + self.device.set_is_muted_listener(self._muted_changed) self.device.set_pipeline_listener(self._pipeline_changed) self.device.set_audio_settings_listener(self._audio_settings_changed) @@ -70,11 +69,11 @@ async def run(self) -> None: try: while self.is_running: try: - # Check if satellite has been disabled - while not self.device.is_enabled: - await self.on_disabled() + # Check if satellite has been muted + while self.device.is_muted: + await self.on_muted() if not self.is_running: - # Satellite was stopped while waiting to be enabled + # Satellite was stopped while waiting to be unmuted return # Connect and run pipeline loop @@ -93,8 +92,8 @@ def stop(self) -> None: """Signal satellite task to stop running.""" self.is_running = False - # Unblock waiting for enabled - self._enabled_changed_event.set() + # Unblock waiting for unmuted + self._muted_changed_event.set() async def on_restart(self) -> None: """Block until pipeline loop will be restarted.""" @@ -112,9 +111,9 @@ async def on_reconnect(self) -> None: ) await asyncio.sleep(_RECONNECT_SECONDS) - async def on_disabled(self) -> None: - """Block until device may be enabled again.""" - await self._enabled_changed_event.wait() + async def on_muted(self) -> None: + """Block until device may be unmated again.""" + await self._muted_changed_event.wait() async def on_stopped(self) -> None: """Run when run() has fully stopped.""" @@ -122,15 +121,14 @@ async def on_stopped(self) -> None: # ------------------------------------------------------------------------- - def _enabled_changed(self) -> None: - """Run when device enabled status changes.""" - - if not self.device.is_enabled: + def _muted_changed(self) -> None: + """Run when device muted status changes.""" + if self.device.is_muted: # Cancel any running pipeline self._audio_queue.put_nowait(None) - self._enabled_changed_event.set() - self._enabled_changed_event.clear() + self._muted_changed_event.set() + self._muted_changed_event.clear() def _pipeline_changed(self) -> None: """Run when device pipeline changes.""" @@ -148,7 +146,7 @@ async def _run_once(self) -> None: """Run pipelines until an error occurs.""" self.device.set_is_active(False) - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): try: await self._connect() break @@ -158,7 +156,7 @@ async def _run_once(self) -> None: assert self._client is not None _LOGGER.debug("Connected to satellite") - if (not self.is_running) or (not self.is_enabled): + if (not self.is_running) or self.device.is_muted: # Run was cancelled or satellite was disabled during connection return @@ -167,7 +165,7 @@ async def _run_once(self) -> None: # Wait until we get RunPipeline event run_pipeline: RunPipeline | None = None - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): run_event = await self._client.read_event() if run_event is None: raise ConnectionResetError("Satellite disconnected") @@ -181,7 +179,7 @@ async def _run_once(self) -> None: assert run_pipeline is not None _LOGGER.debug("Received run information: %s", run_pipeline) - if (not self.is_running) or (not self.is_enabled): + if (not self.is_running) or self.device.is_muted: # Run was cancelled or satellite was disabled while waiting for # RunPipeline event. return @@ -196,7 +194,7 @@ async def _run_once(self) -> None: raise ValueError(f"Invalid end stage: {end_stage}") # Each loop is a pipeline run - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): # Use select to get pipeline each time in case it's changed pipeline_id = pipeline_select.get_chosen_pipeline( self.hass, diff --git a/homeassistant/components/wyoming/strings.json b/homeassistant/components/wyoming/strings.json index 7b6be68aeb20c1..f2768e45eb8aff 100644 --- a/homeassistant/components/wyoming/strings.json +++ b/homeassistant/components/wyoming/strings.json @@ -49,8 +49,8 @@ } }, "switch": { - "satellite_enabled": { - "name": "Satellite enabled" + "mute": { + "name": "Mute" } }, "number": { diff --git a/homeassistant/components/wyoming/switch.py b/homeassistant/components/wyoming/switch.py index 2bc43122588915..7366a52efab6dc 100644 --- a/homeassistant/components/wyoming/switch.py +++ b/homeassistant/components/wyoming/switch.py @@ -29,17 +29,17 @@ async def async_setup_entry( # Setup is only forwarded for satellites assert item.satellite is not None - async_add_entities([WyomingSatelliteEnabledSwitch(item.satellite.device)]) + async_add_entities([WyomingSatelliteMuteSwitch(item.satellite.device)]) -class WyomingSatelliteEnabledSwitch( +class WyomingSatelliteMuteSwitch( WyomingSatelliteEntity, restore_state.RestoreEntity, SwitchEntity ): - """Entity to represent if satellite is enabled.""" + """Entity to represent if satellite is muted.""" entity_description = SwitchEntityDescription( - key="satellite_enabled", - translation_key="satellite_enabled", + key="mute", + translation_key="mute", entity_category=EntityCategory.CONFIG, ) @@ -49,17 +49,17 @@ async def async_added_to_hass(self) -> None: state = await self.async_get_last_state() - # Default to on - self._attr_is_on = (state is None) or (state.state == STATE_ON) + # Default to off + self._attr_is_on = (state is not None) and (state.state == STATE_ON) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on.""" self._attr_is_on = True self.async_write_ha_state() - self._device.set_is_enabled(True) + self._device.set_is_muted(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" self._attr_is_on = False self.async_write_ha_state() - self._device.set_is_enabled(False) + self._device.set_is_muted(False) diff --git a/tests/components/wyoming/test_devices.py b/tests/components/wyoming/test_devices.py index 549f76f20f1c26..0273a7da275261 100644 --- a/tests/components/wyoming/test_devices.py +++ b/tests/components/wyoming/test_devices.py @@ -5,7 +5,7 @@ from homeassistant.components.wyoming import DOMAIN from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -34,11 +34,11 @@ async def test_device_registry_info( assert assist_in_progress_state is not None assert assist_in_progress_state.state == STATE_OFF - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id - satellite_enabled_state = hass.states.get(satellite_enabled_id) - assert satellite_enabled_state is not None - assert satellite_enabled_state.state == STATE_ON + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id + muted_state = hass.states.get(muted_id) + assert muted_state is not None + assert muted_state.state == STATE_OFF pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass) assert pipeline_entity_id @@ -59,9 +59,9 @@ async def test_remove_device_registry_entry( assert assist_in_progress_id assert hass.states.get(assist_in_progress_id) is not None - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id - assert hass.states.get(satellite_enabled_id) is not None + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id + assert hass.states.get(muted_id) is not None pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass) assert pipeline_entity_id @@ -74,5 +74,5 @@ async def test_remove_device_registry_entry( # Everything should be gone assert hass.states.get(assist_in_progress_id) is None - assert hass.states.get(satellite_enabled_id) is None + assert hass.states.get(muted_id) is None assert hass.states.get(pipeline_entity_id) is None diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 83e4d98d971046..07a6aa8925e19f 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -196,7 +196,7 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: await mock_client.detect_event.wait() assert not device.is_active - assert device.is_enabled + assert not device.is_muted # Wake word is detected event_callback( @@ -312,36 +312,36 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_satellite_disabled(hass: HomeAssistant) -> None: - """Test callback for a satellite that has been disabled.""" - on_disabled_event = asyncio.Event() +async def test_satellite_muted(hass: HomeAssistant) -> None: + """Test callback for a satellite that has been muted.""" + on_muted_event = asyncio.Event() original_make_satellite = wyoming._make_satellite - def make_disabled_satellite( + def make_muted_satellite( hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService ): satellite = original_make_satellite(hass, config_entry, service) - satellite.device.set_is_enabled(False) + satellite.device.set_is_muted(True) return satellite - async def on_disabled(self): - self.device.set_is_enabled(True) - on_disabled_event.set() + async def on_muted(self): + self.device.set_is_muted(False) + on_muted_event.set() with patch( "homeassistant.components.wyoming.data.load_wyoming_info", return_value=SATELLITE_INFO, ), patch( - "homeassistant.components.wyoming._make_satellite", make_disabled_satellite + "homeassistant.components.wyoming._make_satellite", make_muted_satellite ), patch( - "homeassistant.components.wyoming.satellite.WyomingSatellite.on_disabled", - on_disabled, + "homeassistant.components.wyoming.satellite.WyomingSatellite.on_muted", + on_muted, ): await setup_config_entry(hass) async with asyncio.timeout(1): - await on_disabled_event.wait() + await on_muted_event.wait() async def test_satellite_restart(hass: HomeAssistant) -> None: diff --git a/tests/components/wyoming/test_switch.py b/tests/components/wyoming/test_switch.py index a39b7087f6d545..6246ba950030c8 100644 --- a/tests/components/wyoming/test_switch.py +++ b/tests/components/wyoming/test_switch.py @@ -7,35 +7,35 @@ from . import reload_satellite -async def test_satellite_enabled( +async def test_muted( hass: HomeAssistant, satellite_config_entry: ConfigEntry, satellite_device: SatelliteDevice, ) -> None: - """Test satellite enabled.""" - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id + """Test satellite muted.""" + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id - state = hass.states.get(satellite_enabled_id) + state = hass.states.get(muted_id) assert state is not None - assert state.state == STATE_ON - assert satellite_device.is_enabled + assert state.state == STATE_OFF + assert not satellite_device.is_muted await hass.services.async_call( "switch", - "turn_off", - {"entity_id": satellite_enabled_id}, + "turn_on", + {"entity_id": muted_id}, blocking=True, ) - state = hass.states.get(satellite_enabled_id) + state = hass.states.get(muted_id) assert state is not None - assert state.state == STATE_OFF - assert not satellite_device.is_enabled + assert state.state == STATE_ON + assert satellite_device.is_muted # test restore satellite_device = await reload_satellite(hass, satellite_config_entry.entry_id) - state = hass.states.get(satellite_enabled_id) + state = hass.states.get(muted_id) assert state is not None - assert state.state == STATE_OFF + assert state.state == STATE_ON From 5dbd0dede1556be103e0588ddafa27c7829500d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 22:17:48 -1000 Subject: [PATCH 089/118] Refactor Bluetooth scanners to avoid the need to pass a callback (#105607) --- .../components/bluetooth/__init__.py | 4 ++-- .../components/bluetooth/manifest.json | 2 +- .../components/esphome/bluetooth/__init__.py | 6 +----- .../components/ruuvi_gateway/bluetooth.py | 7 ------- .../components/shelly/bluetooth/__init__.py | 4 +--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_api.py | 13 ++++++++---- .../components/bluetooth/test_base_scanner.py | 20 ++++++------------- .../components/bluetooth/test_diagnostics.py | 3 +-- tests/components/bluetooth/test_manager.py | 7 ------- tests/components/bluetooth/test_models.py | 7 ++----- tests/components/bluetooth/test_wrappers.py | 15 +++----------- .../esphome/bluetooth/test_client.py | 4 +--- 15 files changed, 30 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index c4434f8695f3cf..234712bddafa9c 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -106,6 +106,7 @@ "async_scanner_by_source", "async_scanner_count", "async_scanner_devices_by_address", + "async_get_advertisement_callback", "BaseHaScanner", "HomeAssistantRemoteScanner", "BluetoothCallbackMatcher", @@ -287,9 +288,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE - new_info_callback = async_get_advertisement_callback(hass) manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER] - scanner = HaScanner(mode, adapter, address, new_info_callback) + scanner = HaScanner(mode, adapter, address) try: scanner.async_setup() except RuntimeError as err: diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 5abec24b6d1d79..a4c96c91727b47 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.17.0", "dbus-fast==2.21.0", - "habluetooth==0.11.1" + "habluetooth==1.0.0" ] } diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 0fe28730fce2fa..e7dd0697987263 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -11,7 +11,6 @@ from homeassistant.components.bluetooth import ( HaBluetoothConnector, - async_get_advertisement_callback, async_register_scanner, ) from homeassistant.config_entries import ConfigEntry @@ -63,7 +62,6 @@ async def async_connect_scanner( """Connect scanner.""" assert entry.unique_id is not None source = str(entry.unique_id) - new_info_callback = async_get_advertisement_callback(hass) device_info = entry_data.device_info assert device_info is not None feature_flags = device_info.bluetooth_proxy_feature_flags_compat( @@ -98,9 +96,7 @@ async def async_connect_scanner( partial(_async_can_connect, entry_data, bluetooth_device, source) ), ) - scanner = ESPHomeScanner( - source, entry.title, new_info_callback, connector, connectable - ) + scanner = ESPHomeScanner(source, entry.title, connector, connectable) client_data.scanner = scanner coros: list[Coroutine[Any, Any, CALLBACK_TYPE]] = [] # These calls all return a callback that can be used to unsubscribe diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py index 2d9bf8c6644767..d3cf1e8137969f 100644 --- a/homeassistant/components/ruuvi_gateway/bluetooth.py +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -1,17 +1,13 @@ """Bluetooth support for Ruuvi Gateway.""" from __future__ import annotations -from collections.abc import Callable import logging import time -from home_assistant_bluetooth import BluetoothServiceInfoBleak - from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, MONOTONIC_TIME, BaseHaRemoteScanner, - async_get_advertisement_callback, async_register_scanner, ) from homeassistant.config_entries import ConfigEntry @@ -29,7 +25,6 @@ def __init__( self, scanner_id: str, name: str, - new_info_callback: Callable[[BluetoothServiceInfoBleak], None], *, coordinator: RuuviGatewayUpdateCoordinator, ) -> None: @@ -37,7 +32,6 @@ def __init__( super().__init__( scanner_id, name, - new_info_callback, connector=None, connectable=False, ) @@ -87,7 +81,6 @@ def async_connect_scanner( scanner = RuuviGatewayScanner( scanner_id=source, name=entry.title, - new_info_callback=async_get_advertisement_callback(hass), coordinator=coordinator, ) unload_callbacks = [ diff --git a/homeassistant/components/shelly/bluetooth/__init__.py b/homeassistant/components/shelly/bluetooth/__init__.py index 007900a5cdc9f2..92c630323bab28 100644 --- a/homeassistant/components/shelly/bluetooth/__init__.py +++ b/homeassistant/components/shelly/bluetooth/__init__.py @@ -14,7 +14,6 @@ from homeassistant.components.bluetooth import ( HaBluetoothConnector, - async_get_advertisement_callback, async_register_scanner, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback @@ -36,14 +35,13 @@ async def async_connect_scanner( device = coordinator.device entry = coordinator.entry source = format_mac(coordinator.mac).upper() - new_info_callback = async_get_advertisement_callback(hass) connector = HaBluetoothConnector( # no active connections to shelly yet client=None, # type: ignore[arg-type] source=source, can_connect=lambda: False, ) - scanner = ShellyBLEScanner(source, entry.title, new_info_callback, connector, False) + scanner = ShellyBLEScanner(source, entry.title, connector, False) unload_callbacks = [ async_register_scanner(hass, scanner, False), scanner.async_setup(), diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 53ed955f79138b..1832c61712e8af 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ dbus-fast==2.21.0 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 -habluetooth==0.11.1 +habluetooth==1.0.0 hass-nabucasa==0.74.0 hassil==1.5.1 home-assistant-bluetooth==1.11.0 diff --git a/requirements_all.txt b/requirements_all.txt index ff852542d0db37..2fdbe18fa278f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -984,7 +984,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==0.11.1 +habluetooth==1.0.0 # homeassistant.components.cloud hass-nabucasa==0.74.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb14d282a21fac..148d0597d8aa19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -783,7 +783,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==0.11.1 +habluetooth==1.0.0 # homeassistant.components.cloud hass-nabucasa==0.74.0 diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index aee15f7874e37c..732fce4c8e2ee3 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -40,6 +40,14 @@ async def test_monotonic_time() -> None: assert MONOTONIC_TIME() == pytest.approx(time.monotonic(), abs=0.1) +async def test_async_get_advertisement_callback( + hass: HomeAssistant, enable_bluetooth: None +) -> None: + """Test getting advertisement callback.""" + callback = bluetooth.async_get_advertisement_callback(hass) + assert callback is not None + + async def test_async_scanner_devices_by_address_connectable( hass: HomeAssistant, enable_bluetooth: None ) -> None: @@ -63,13 +71,10 @@ def inject_advertisement( MONOTONIC_TIME(), ) - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeInjectableScanner( - "esp32", "esp32", new_info_callback, connector, False - ) + scanner = FakeInjectableScanner("esp32", "esp32", connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) switchbot_device = generate_ble_device( diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index c94e3c874e098f..4f60fc9ef9b399 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -111,11 +111,10 @@ async def test_remote_scanner( rssi=-100, ) - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, True) + scanner = FakeScanner("esp32", "esp32", connector, True) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -178,11 +177,10 @@ async def test_remote_scanner_expires_connectable( rssi=-100, ) - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, True) + scanner = FakeScanner("esp32", "esp32", connector, True) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -233,11 +231,10 @@ async def test_remote_scanner_expires_non_connectable( rssi=-100, ) - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -308,11 +305,10 @@ async def test_base_scanner_connecting_behavior( rssi=-100, ) - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -366,7 +362,6 @@ async def test_restore_history_remote_adapter( scanner = BaseHaRemoteScanner( "atom-bluetooth-proxy-ceaac4", "atom-bluetooth-proxy-ceaac4", - lambda adv: None, connector, True, ) @@ -381,7 +376,6 @@ async def test_restore_history_remote_adapter( scanner = BaseHaRemoteScanner( "atom-bluetooth-proxy-ceaac4", "atom-bluetooth-proxy-ceaac4", - lambda adv: None, connector, True, ) @@ -413,11 +407,10 @@ async def test_device_with_ten_minute_advertising_interval( rssi=-100, ) - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) @@ -505,11 +498,10 @@ async def test_scanner_stops_responding( """Test we mark a scanner are not scanning when it stops responding.""" manager = _get_manager() - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 8d87d5ef396c00..f70c301dcfe72c 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -454,11 +454,10 @@ def inject_advertisement( assert await hass.config_entries.async_setup(entry1.entry_id) await hass.async_block_till_done() - new_info_callback = manager.scanner_adv_received connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) - scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False) + scanner = FakeScanner("esp32", "esp32", connector, False) unsetup = scanner.async_setup() cancel = manager.async_register_scanner(scanner, True) diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 63201f790fef04..2a470feacfa54b 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -20,7 +20,6 @@ BluetoothServiceInfoBleak, HaBluetoothConnector, async_ble_device_from_address, - async_get_advertisement_callback, async_get_fallback_availability_interval, async_get_learned_advertising_interval, async_scanner_count, @@ -720,14 +719,12 @@ def inject_advertisement( MONOTONIC_TIME(), ) - new_info_callback = async_get_advertisement_callback(hass) connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) connectable_scanner = FakeScanner( "connectable", "connectable", - new_info_callback, connector, True, ) @@ -750,7 +747,6 @@ def inject_advertisement( not_connectable_scanner = FakeScanner( "not_connectable", "not_connectable", - new_info_callback, connector, False, ) @@ -800,7 +796,6 @@ def _unavailable_callback(service_info: BluetoothServiceInfoBleak) -> None: connectable_scanner_2 = FakeScanner( "connectable", "connectable", - new_info_callback, connector, True, ) @@ -896,14 +891,12 @@ def clear_all_devices(self) -> None: self._discovered_device_timestamps.clear() self._previous_service_info.clear() - new_info_callback = async_get_advertisement_callback(hass) connector = ( HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), ) non_connectable_scanner = FakeScanner( "connectable", "connectable", - new_info_callback, connector, False, ) diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index c0423aca357018..6e8181b5a222bc 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -184,7 +184,6 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: scanner = FakeScanner( "esp32_has_connection_slot", "esp32_has_connection_slot", - lambda info: None, connector, True, ) @@ -291,7 +290,7 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: return None connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: False) - scanner = FakeScanner("esp32", "esp32", lambda info: None, connector, True) + scanner = FakeScanner("esp32", "esp32", connector, True) cancel = manager.async_register_scanner(scanner, True) inject_advertisement_with_source( hass, switchbot_proxy_device_no_connection_slot, switchbot_adv, "esp32" @@ -356,7 +355,7 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: return None connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: True) - scanner = FakeScanner("esp32", "esp32", lambda info: None, connector, True) + scanner = FakeScanner("esp32", "esp32", connector, True) cancel = manager.async_register_scanner(scanner, True) inject_advertisement_with_source( hass, switchbot_proxy_device_with_connection_slot, switchbot_adv, "esp32" @@ -464,7 +463,6 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: scanner = FakeScanner( "esp32_has_connection_slot", "esp32_has_connection_slot", - lambda info: None, connector, True, ) @@ -577,7 +575,6 @@ async def async_get_device_by_address(self, address: str) -> BLEDevice | None: scanner = FakeScanner( "esp32_has_connection_slot", "esp32_has_connection_slot", - lambda info: None, connector, True, ) diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index 6ebba080f6a677..78ec5bd16ac590 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -1,7 +1,6 @@ """Tests for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable from contextlib import contextmanager from unittest.mock import patch @@ -18,10 +17,8 @@ from homeassistant.components.bluetooth import ( MONOTONIC_TIME, BaseHaRemoteScanner, - BluetoothServiceInfoBleak, HaBluetoothConnector, HomeAssistantBluetoothManager, - async_get_advertisement_callback, ) from homeassistant.core import HomeAssistant @@ -43,12 +40,11 @@ def __init__( self, scanner_id: str, name: str, - new_info_callback: Callable[[BluetoothServiceInfoBleak], None], connector: None, connectable: bool, ) -> None: """Initialize the scanner.""" - super().__init__(scanner_id, name, new_info_callback, connector, connectable) + super().__init__(scanner_id, name, connector, connectable) self._details: dict[str, str | HaBluetoothConnector] = {} def __repr__(self) -> str: @@ -182,13 +178,8 @@ def _generate_scanners_with_fake_devices(hass): ) hci1_device_advs[device.address] = (device, adv_data) - new_info_callback = async_get_advertisement_callback(hass) - scanner_hci0 = FakeScanner( - "00:00:00:00:00:01", "hci0", new_info_callback, None, True - ) - scanner_hci1 = FakeScanner( - "00:00:00:00:00:02", "hci1", new_info_callback, None, True - ) + scanner_hci0 = FakeScanner("00:00:00:00:00:01", "hci0", None, True) + scanner_hci1 = FakeScanner("00:00:00:00:00:02", "hci1", None, True) for device, adv_data in hci0_device_advs.values(): scanner_hci0.inject_advertisement(device, adv_data) diff --git a/tests/components/esphome/bluetooth/test_client.py b/tests/components/esphome/bluetooth/test_client.py index d74766023d71a9..e770c75cf0343c 100644 --- a/tests/components/esphome/bluetooth/test_client.py +++ b/tests/components/esphome/bluetooth/test_client.py @@ -43,9 +43,7 @@ async def client_data_fixture( ), api_version=APIVersion(1, 9), title=ESP_NAME, - scanner=ESPHomeScanner( - ESP_MAC_ADDRESS, ESP_NAME, lambda info: None, connector, True - ), + scanner=ESPHomeScanner(ESP_MAC_ADDRESS, ESP_NAME, connector, True), ) From c318445a761d9b96c832cf094e2363a7a01d570d Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:22:10 +0100 Subject: [PATCH 090/118] Write Enphase Envoy data to log when in debug mode (#105456) --- homeassistant/components/enphase_envoy/coordinator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 75f2ef3928974d..02a9d2f249191b 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -144,7 +144,10 @@ async def _async_update_data(self) -> dict[str, Any]: if not self._setup_complete: await self._async_setup_and_authenticate() self._async_mark_setup_complete() - return (await envoy.update()).raw + # dump all received data in debug mode to assist troubleshooting + envoy_data = await envoy.update() + _LOGGER.debug("Envoy data: %s", envoy_data) + return envoy_data.raw except INVALID_AUTH_ERRORS as err: if self._setup_complete and tries == 0: # token likely expired or firmware changed, try to re-authenticate From 22c3847c0e5117c34e73b89659981c922b96d5cc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 10:13:34 +0100 Subject: [PATCH 091/118] Allow inheriting `FrozenOrThawed` with custom init (#105624) --- homeassistant/util/frozen_dataclass_compat.py | 1 + tests/helpers/snapshots/test_entity.ambr | 18 ++++++++++++++++++ tests/helpers/test_entity.py | 12 ++++++++++++ 3 files changed, 31 insertions(+) diff --git a/homeassistant/util/frozen_dataclass_compat.py b/homeassistant/util/frozen_dataclass_compat.py index 58faedeea6fc91..456fc4f1570dad 100644 --- a/homeassistant/util/frozen_dataclass_compat.py +++ b/homeassistant/util/frozen_dataclass_compat.py @@ -117,4 +117,5 @@ def __new__(*args: Any, **kwargs: Any) -> object: return object.__new__(cls) return cls._dataclass(*_args, **kwargs) + cls.__init__ = cls._dataclass.__init__ # type: ignore[misc] cls.__new__ = __new__ # type: ignore[method-assign] diff --git a/tests/helpers/snapshots/test_entity.ambr b/tests/helpers/snapshots/test_entity.ambr index 7f146fa049403e..1031134d2ada3f 100644 --- a/tests/helpers/snapshots/test_entity.ambr +++ b/tests/helpers/snapshots/test_entity.ambr @@ -36,6 +36,24 @@ # name: test_extending_entity_description.1 "test_extending_entity_description..FrozenEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')" # --- +# name: test_extending_entity_description.10 + dict({ + 'device_class': None, + 'entity_category': None, + 'entity_registry_enabled_default': True, + 'entity_registry_visible_default': True, + 'force_update': False, + 'has_entity_name': False, + 'icon': None, + 'key': 'blah', + 'name': 'name', + 'translation_key': None, + 'unit_of_measurement': None, + }) +# --- +# name: test_extending_entity_description.11 + "test_extending_entity_description..CustomInitEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None)" +# --- # name: test_extending_entity_description.2 dict({ 'device_class': None, diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 5a706b73b49591..76577daf8a6cb4 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1749,3 +1749,15 @@ class ComplexEntityDescription2(entity.EntityDescription, MyMixin): key="blah", extra="foo", mixin="mixin", name="name" ) assert repr(obj) == snapshot + + # Try inheriting with custom init + @dataclasses.dataclass + class CustomInitEntityDescription(entity.EntityDescription): + def __init__(self, extra, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.extra: str = extra + + obj = CustomInitEntityDescription(key="blah", extra="foo", name="name") + assert obj == snapshot + assert obj == CustomInitEntityDescription(key="blah", extra="foo", name="name") + assert repr(obj) == snapshot From a91dfc79547e12c795efab1007305e1166f2c380 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 10:24:34 +0100 Subject: [PATCH 092/118] Fix entity descriptions in philips_js (#105625) --- homeassistant/components/philips_js/binary_sensor.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/philips_js/binary_sensor.py b/homeassistant/components/philips_js/binary_sensor.py index 1e6c1241aea746..ec93f0ab87e950 100644 --- a/homeassistant/components/philips_js/binary_sensor.py +++ b/homeassistant/components/philips_js/binary_sensor.py @@ -18,14 +18,11 @@ from .entity import PhilipsJsEntity -@dataclass +@dataclass(kw_only=True) class PhilipsTVBinarySensorEntityDescription(BinarySensorEntityDescription): """A entity description for Philips TV binary sensor.""" - def __init__(self, recording_value, *args, **kwargs) -> None: - """Set up a binary sensor entity description and add additional attributes.""" - super().__init__(*args, **kwargs) - self.recording_value: str = recording_value + recording_value: str DESCRIPTIONS = ( From 06f81251fbf29e53d3e26e2bf234d799816ac070 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 10:41:35 +0100 Subject: [PATCH 093/118] Reduce code duplication in Suez config flow (#105558) --- .../components/suez_water/config_flow.py | 71 +------------------ homeassistant/components/suez_water/sensor.py | 47 +++++++++--- .../components/suez_water/test_config_flow.py | 18 +---- 3 files changed, 44 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/suez_water/config_flow.py b/homeassistant/components/suez_water/config_flow.py index 1dd79c017e01ea..ba288c90e34058 100644 --- a/homeassistant/components/suez_water/config_flow.py +++ b/homeassistant/components/suez_water/config_flow.py @@ -10,16 +10,13 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_COUNTER_ID, DOMAIN _LOGGER = logging.getLogger(__name__) -ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=suez_water"} STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): str, @@ -81,80 +78,16 @@ async def async_step_user( async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Import the yaml config.""" await self.async_set_unique_id(user_input[CONF_USERNAME]) - try: - self._abort_if_unique_id_configured() - except AbortFlow as err: - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Suez Water", - }, - ) - raise err + self._abort_if_unique_id_configured() try: await self.hass.async_add_executor_job(validate_input, user_input) except CannotConnect: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_import_issue_cannot_connect", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_cannot_connect", - translation_placeholders=ISSUE_PLACEHOLDER, - ) return self.async_abort(reason="cannot_connect") except InvalidAuth: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_import_issue_invalid_auth", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_invalid_auth", - translation_placeholders=ISSUE_PLACEHOLDER, - ) return self.async_abort(reason="invalid_auth") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_import_issue_unknown", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_unknown", - translation_placeholders=ISSUE_PLACEHOLDER, - ) return self.async_abort(reason="unknown") - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Suez Water", - }, - ) return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 7d7540ed1c0a78..4602df27748b98 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -15,14 +15,17 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfVolume -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_COUNTER_ID, DOMAIN _LOGGER = logging.getLogger(__name__) +ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=suez_water"} SCAN_INTERVAL = timedelta(hours=12) @@ -35,20 +38,48 @@ ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the sensor platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + if ( + result["type"] == FlowResultType.CREATE_ENTRY + or result["reason"] == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Suez Water", + }, + ) + else: + async_create_issue( + hass, DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, + f"deprecated_yaml_import_issue_${result['reason']}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_placeholders=ISSUE_PLACEHOLDER, ) - ) async def async_setup_entry( diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index 265598e5c645de..c18b8a927e9275 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -8,7 +8,6 @@ from homeassistant.components.suez_water.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import issue_registry as ir from tests.common import MockConfigEntry @@ -138,9 +137,7 @@ async def test_form_error( assert len(mock_setup_entry.mock_calls) == 1 -async def test_import( - hass: HomeAssistant, mock_setup_entry: AsyncMock, issue_registry: ir.IssueRegistry -) -> None: +async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test import flow.""" with patch("homeassistant.components.suez_water.config_flow.SuezClient"): result = await hass.config_entries.flow.async_init( @@ -153,7 +150,6 @@ async def test_import( assert result["result"].unique_id == "test-username" assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 - assert len(issue_registry.issues) == 1 @pytest.mark.parametrize( @@ -164,7 +160,6 @@ async def test_import_error( mock_setup_entry: AsyncMock, exception: Exception, reason: str, - issue_registry: ir.IssueRegistry, ) -> None: """Test we handle errors while importing.""" @@ -178,12 +173,9 @@ async def test_import_error( assert result["type"] == FlowResultType.ABORT assert result["reason"] == reason - assert len(issue_registry.issues) == 1 -async def test_importing_invalid_auth( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: +async def test_importing_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth when importing.""" with patch( @@ -199,12 +191,9 @@ async def test_importing_invalid_auth( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_auth" - assert len(issue_registry.issues) == 1 -async def test_import_already_configured( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: +async def test_import_already_configured(hass: HomeAssistant) -> None: """Test we abort import when entry is already configured.""" entry = MockConfigEntry( @@ -220,4 +209,3 @@ async def test_import_already_configured( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert len(issue_registry.issues) == 1 From 22b2c588ebb5ef858aa100196fd8dc798abf2bfa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 11:23:38 +0100 Subject: [PATCH 094/118] Use issue registry fixture (#105633) --- tests/components/cloud/test_repairs.py | 12 +++++------- tests/components/harmony/test_switch.py | 2 +- tests/components/shelly/test_climate.py | 7 ++++--- tests/components/shelly/test_coordinator.py | 7 ++++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index 8d890a503e146f..0e662c30ee77f5 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -21,10 +21,9 @@ async def test_do_not_create_repair_issues_at_startup_if_not_logged_in( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, ) -> None: """Test that we create repair issue at startup if we are logged in.""" - issue_registry: ir.IssueRegistry = ir.async_get(hass) - with patch("homeassistant.components.cloud.Cloud.is_logged_in", False): await mock_cloud(hass) @@ -40,9 +39,9 @@ async def test_create_repair_issues_at_startup_if_logged_in( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_auth: Generator[None, AsyncMock, None], + issue_registry: ir.IssueRegistry, ): """Test that we create repair issue at startup if we are logged in.""" - issue_registry: ir.IssueRegistry = ir.async_get(hass) aioclient_mock.get( "https://accounts.nabucasa.com/payments/subscription_info", json={"provider": "legacy"}, @@ -61,9 +60,9 @@ async def test_create_repair_issues_at_startup_if_logged_in( async def test_legacy_subscription_delete_issue_if_no_longer_legacy( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, ) -> None: """Test that we delete the legacy subscription issue if no longer legacy.""" - issue_registry: ir.IssueRegistry = ir.async_get(hass) cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) assert issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" @@ -80,9 +79,9 @@ async def test_legacy_subscription_repair_flow( aioclient_mock: AiohttpClientMocker, mock_auth: Generator[None, AsyncMock, None], hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, ): """Test desired flow of the fix flow for legacy subscription.""" - issue_registry: ir.IssueRegistry = ir.async_get(hass) aioclient_mock.get( "https://accounts.nabucasa.com/payments/subscription_info", json={"provider": None}, @@ -167,6 +166,7 @@ async def test_legacy_subscription_repair_flow_timeout( hass_client: ClientSessionGenerator, mock_auth: Generator[None, AsyncMock, None], aioclient_mock: AiohttpClientMocker, + issue_registry: ir.IssueRegistry, ): """Test timeout flow of the fix flow for legacy subscription.""" aioclient_mock.post( @@ -174,8 +174,6 @@ async def test_legacy_subscription_repair_flow_timeout( status=403, ) - issue_registry: ir.IssueRegistry = ir.async_get(hass) - cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) repair_issue = issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" diff --git a/tests/components/harmony/test_switch.py b/tests/components/harmony/test_switch.py index 59e5a7c7fc8010..f843ab4decad79 100644 --- a/tests/components/harmony/test_switch.py +++ b/tests/components/harmony/test_switch.py @@ -146,6 +146,7 @@ async def test_create_issue( hass: HomeAssistant, mock_write_config, entity_registry_enabled_by_default: None, + issue_registry: ir.IssueRegistry, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" assert await async_setup_component( @@ -186,7 +187,6 @@ async def test_create_issue( assert automations_with_entity(hass, ENTITY_WATCH_TV)[0] == "automation.test" assert scripts_with_entity(hass, ENTITY_WATCH_TV)[0] == "script.test" - issue_registry: ir.IssueRegistry = ir.async_get(hass) assert issue_registry.async_get_issue(DOMAIN, "deprecated_switches") assert issue_registry.async_get_issue( diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index fe518b8509c3bb..f52b542b389711 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -503,11 +503,12 @@ async def test_block_restored_climate_auth_error( async def test_device_not_calibrated( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, + mock_block_device, + monkeypatch, + issue_registry: ir.IssueRegistry, ) -> None: """Test to create an issue when the device is not calibrated.""" - issue_registry: ir.IssueRegistry = ir.async_get(hass) - await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index e73168c6b20107..27aa8710621062 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -250,11 +250,12 @@ async def test_block_sleeping_device_no_periodic_updates( async def test_block_device_push_updates_failure( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, + mock_block_device, + monkeypatch, + issue_registry: ir.IssueRegistry, ) -> None: """Test block device with push updates failure.""" - issue_registry: ir.IssueRegistry = ir.async_get(hass) - await init_integration(hass, 1) # Updates with COAP_REPLAY type should create an issue From 0548f9f3425e65449ff2d0eabe76d99b83bc715b Mon Sep 17 00:00:00 2001 From: mletenay Date: Wed, 13 Dec 2023 12:35:53 +0100 Subject: [PATCH 095/118] Add diagnostics download to goodwe integration (#102928) * Add diagnostics download to goodwe integration * Revert change not related to test * Use MagicMock for mock inverter * Use spec with mock --- .../components/goodwe/diagnostics.py | 35 +++++++++++++++++++ tests/components/goodwe/conftest.py | 25 +++++++++++++ .../goodwe/snapshots/test_diagnostics.ambr | 33 +++++++++++++++++ tests/components/goodwe/test_diagnostics.py | 34 ++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 homeassistant/components/goodwe/diagnostics.py create mode 100644 tests/components/goodwe/conftest.py create mode 100644 tests/components/goodwe/snapshots/test_diagnostics.ambr create mode 100644 tests/components/goodwe/test_diagnostics.py diff --git a/homeassistant/components/goodwe/diagnostics.py b/homeassistant/components/goodwe/diagnostics.py new file mode 100644 index 00000000000000..285036c0254628 --- /dev/null +++ b/homeassistant/components/goodwe/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for Goodwe.""" +from __future__ import annotations + +from typing import Any + +from goodwe import Inverter + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, KEY_INVERTER + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + inverter: Inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] + + diagnostics_data = { + "config_entry": config_entry.as_dict(), + "inverter": { + "model_name": inverter.model_name, + "rated_power": inverter.rated_power, + "firmware": inverter.firmware, + "arm_firmware": inverter.arm_firmware, + "dsp1_version": inverter.dsp1_version, + "dsp2_version": inverter.dsp2_version, + "dsp_svn_version": inverter.dsp_svn_version, + "arm_version": inverter.arm_version, + "arm_svn_version": inverter.arm_svn_version, + }, + } + + return diagnostics_data diff --git a/tests/components/goodwe/conftest.py b/tests/components/goodwe/conftest.py new file mode 100644 index 00000000000000..cabb0f6ea10464 --- /dev/null +++ b/tests/components/goodwe/conftest.py @@ -0,0 +1,25 @@ +"""Fixtures for the Aladdin Connect integration tests.""" +from unittest.mock import AsyncMock, MagicMock + +from goodwe import Inverter +import pytest + + +@pytest.fixture(name="mock_inverter") +def fixture_mock_inverter(): + """Set up inverter fixture.""" + mock_inverter = MagicMock(spec=Inverter) + mock_inverter.serial_number = "dummy_serial_nr" + mock_inverter.arm_version = 1 + mock_inverter.arm_svn_version = 2 + mock_inverter.arm_firmware = "dummy.arm.version" + mock_inverter.firmware = "dummy.fw.version" + mock_inverter.model_name = "MOCK" + mock_inverter.rated_power = 10000 + mock_inverter.dsp1_version = 3 + mock_inverter.dsp2_version = 4 + mock_inverter.dsp_svn_version = 5 + + mock_inverter.read_runtime_data = AsyncMock(return_value={}) + + return mock_inverter diff --git a/tests/components/goodwe/snapshots/test_diagnostics.ambr b/tests/components/goodwe/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000000..f259e020cd54d9 --- /dev/null +++ b/tests/components/goodwe/snapshots/test_diagnostics.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': 'localhost', + 'model_family': 'ET', + }), + 'disabled_by': None, + 'domain': 'goodwe', + 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 1, + }), + 'inverter': dict({ + 'arm_firmware': 'dummy.arm.version', + 'arm_svn_version': 2, + 'arm_version': 1, + 'dsp1_version': 3, + 'dsp2_version': 4, + 'dsp_svn_version': 5, + 'firmware': 'dummy.fw.version', + 'model_name': 'MOCK', + 'rated_power': 10000, + }), + }) +# --- diff --git a/tests/components/goodwe/test_diagnostics.py b/tests/components/goodwe/test_diagnostics.py new file mode 100644 index 00000000000000..edda2ed2cb79a1 --- /dev/null +++ b/tests/components/goodwe/test_diagnostics.py @@ -0,0 +1,34 @@ +"""Test the CO2Signal diagnostics.""" +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.goodwe import CONF_MODEL_FAMILY, DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + mock_inverter: MagicMock, +) -> None: + """Test config entry diagnostics.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "localhost", CONF_MODEL_FAMILY: "ET"}, + entry_id="3bd2acb0e4f0476d40865546d0d91921", + ) + config_entry.add_to_hass(hass) + with patch("homeassistant.components.goodwe.connect", return_value=mock_inverter): + assert await async_setup_component(hass, DOMAIN, {}) + + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert result == snapshot From 5bb233998e87ffa0acebdea0d39a6f8a715d614e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 13 Dec 2023 13:53:22 +0100 Subject: [PATCH 096/118] Improve cloud http api tests (#105610) * Improve cloud http api tests * Add comments to the cloud fixture * Fix docstring --- tests/components/cloud/conftest.py | 116 +++- tests/components/cloud/test_http_api.py | 714 ++++++++++++++++-------- 2 files changed, 594 insertions(+), 236 deletions(-) diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 221267c59fb0a4..0de43c80e875dc 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,14 +1,124 @@ """Fixtures for cloud tests.""" -from unittest.mock import patch - +from collections.abc import AsyncGenerator +from typing import Any +from unittest.mock import DEFAULT, MagicMock, PropertyMock, patch + +from hass_nabucasa import Cloud +from hass_nabucasa.auth import CognitoAuth +from hass_nabucasa.cloudhooks import Cloudhooks +from hass_nabucasa.const import DEFAULT_SERVERS, DEFAULT_VALUES, STATE_CONNECTED +from hass_nabucasa.google_report_state import GoogleReportState +from hass_nabucasa.iot import CloudIoT +from hass_nabucasa.remote import RemoteUI +from hass_nabucasa.voice import Voice import jwt import pytest -from homeassistant.components.cloud import const, prefs +from homeassistant.components.cloud import CloudClient, const, prefs from . import mock_cloud, mock_cloud_prefs +@pytest.fixture(name="cloud") +async def cloud_fixture() -> AsyncGenerator[MagicMock, None]: + """Mock the cloud object. + + See the real hass_nabucasa.Cloud class for how to configure the mock. + """ + with patch( + "homeassistant.components.cloud.Cloud", autospec=True + ) as mock_cloud_class: + mock_cloud = mock_cloud_class.return_value + + # Attributes set in the constructor without parameters. + # We spec the mocks with the real classes + # and set constructor attributes or mock properties as needed. + mock_cloud.google_report_state = MagicMock(spec=GoogleReportState) + mock_cloud.cloudhooks = MagicMock(spec=Cloudhooks) + mock_cloud.remote = MagicMock( + spec=RemoteUI, + certificate=None, + certificate_status=None, + instance_domain=None, + is_connected=False, + ) + mock_cloud.auth = MagicMock(spec=CognitoAuth) + mock_cloud.iot = MagicMock( + spec=CloudIoT, last_disconnect_reason=None, state=STATE_CONNECTED + ) + mock_cloud.voice = MagicMock(spec=Voice) + mock_cloud.started = None + + def set_up_mock_cloud( + cloud_client: CloudClient, mode: str, **kwargs: Any + ) -> DEFAULT: + """Set up mock cloud with a mock constructor.""" + + # Attributes set in the constructor with parameters. + cloud_client.cloud = mock_cloud + mock_cloud.client = cloud_client + default_values = DEFAULT_VALUES[mode] + servers = { + f"{name}_server": server + for name, server in DEFAULT_SERVERS[mode].items() + } + mock_cloud.configure_mock(**default_values, **servers, **kwargs) + mock_cloud.mode = mode + + # Properties that we mock as attributes from the constructor. + mock_cloud.websession = cloud_client.websession + + return DEFAULT + + mock_cloud_class.side_effect = set_up_mock_cloud + + # Attributes that we mock with default values. + + mock_cloud.id_token = jwt.encode( + { + "email": "hello@home-assistant.io", + "custom:sub-exp": "2018-01-03", + "cognito:username": "abcdefghjkl", + }, + "test", + ) + mock_cloud.access_token = "test_access_token" + mock_cloud.refresh_token = "test_refresh_token" + + # Properties that we keep as properties. + + def mock_is_logged_in() -> bool: + """Mock is logged in.""" + return mock_cloud.id_token is not None + + is_logged_in = PropertyMock(side_effect=mock_is_logged_in) + type(mock_cloud).is_logged_in = is_logged_in + + def mock_claims() -> dict[str, Any]: + """Mock claims.""" + return Cloud._decode_claims(mock_cloud.id_token) + + claims = PropertyMock(side_effect=mock_claims) + type(mock_cloud).claims = claims + + # Properties that we mock as attributes. + mock_cloud.subscription_expired = False + + # Methods that we mock with a custom side effect. + + async def mock_login(email: str, password: str) -> None: + """Mock login. + + When called, it should call the on_start callback. + """ + on_start_callback = mock_cloud.register_on_start.call_args[0][0] + await on_start_callback() + + mock_cloud.login.side_effect = mock_login + + yield mock_cloud + + @pytest.fixture(autouse=True) def mock_tts_cache_dir_autouse(mock_tts_cache_dir): """Mock the TTS cache dir with empty dir.""" diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index fc6861f2b49c8a..15acc27593118e 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,19 +1,21 @@ """Tests for the HTTP API for the cloud component.""" import asyncio +from copy import deepcopy from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp +from aiohttp.test_utils import TestClient from hass_nabucasa import thingtalk, voice from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED -from jose import jwt import pytest from homeassistant.components.alexa import errors as alexa_errors from homeassistant.components.alexa.entities import LightCapabilities -from homeassistant.components.cloud.const import DOMAIN +from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY +from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.components.homeassistant import exposed_entities from homeassistant.core import HomeAssistant, State @@ -21,39 +23,67 @@ from homeassistant.setup import async_setup_component from homeassistant.util.location import LocationInfo -from . import mock_cloud, mock_cloud_prefs - from tests.components.google_assistant import MockConfig from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator, WebSocketGenerator -SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/payments/subscription_info" - +PIPELINE_DATA_LEGACY = { + "items": [ + { + "conversation_engine": "homeassistant", + "conversation_language": "language_1", + "id": "12345", + "language": "language_1", + "name": "Home Assistant Cloud", + "stt_engine": "cloud", + "stt_language": "language_1", + "tts_engine": "cloud", + "tts_language": "language_1", + "tts_voice": "Arnold Schwarzenegger", + "wake_word_entity": None, + "wake_word_id": None, + }, + ], + "preferred_item": "12345", +} -@pytest.fixture(name="mock_cloud_login") -def mock_cloud_login_fixture(hass, setup_api): - """Mock cloud is logged in.""" - hass.data[DOMAIN].id_token = jwt.encode( +PIPELINE_DATA_OTHER = { + "items": [ { - "email": "hello@home-assistant.io", - "custom:sub-exp": "2018-01-03", - "cognito:username": "abcdefghjkl", + "conversation_engine": "other", + "conversation_language": "language_1", + "id": "12345", + "language": "language_1", + "name": "Home Assistant", + "stt_engine": "stt.other", + "stt_language": "language_1", + "tts_engine": "other", + "tts_language": "language_1", + "tts_voice": "Arnold Schwarzenegger", + "wake_word_entity": None, + "wake_word_id": None, }, - "test", - ) + ], + "preferred_item": "12345", +} + +SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/payments/subscription_info" -@pytest.fixture(autouse=True, name="setup_api") -def setup_api_fixture(hass, aioclient_mock): - """Initialize HTTP API.""" - hass.loop.run_until_complete( - mock_cloud( - hass, - { +@pytest.fixture(name="setup_cloud") +async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None: + """Fixture that sets up cloud.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { "mode": "development", "cognito_client_id": "cognito_client_id", "user_pool_id": "user_pool_id", "region": "region", + "alexa_server": "alexa-api.nabucasa.com", "relayer_server": "relayer", "accounts_server": "api-test.hass.io", "google_actions": {"filter": {"include_domains": "light"}}, @@ -61,27 +91,24 @@ def setup_api_fixture(hass, aioclient_mock): "filter": {"include_entities": ["light.kitchen", "switch.ac"]} }, }, - ) + }, ) - return mock_cloud_prefs(hass) + await hass.async_block_till_done() + on_start_callback = cloud.register_on_start.call_args[0][0] + await on_start_callback() @pytest.fixture(name="cloud_client") -def cloud_client_fixture(hass, hass_client): +async def cloud_client_fixture( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> TestClient: """Fixture that can fetch from the cloud client.""" - with patch("hass_nabucasa.Cloud._write_user_info"): - yield hass.loop.run_until_complete(hass_client()) - - -@pytest.fixture(name="mock_cognito") -def mock_cognito_fixture(): - """Mock warrant.""" - with patch("hass_nabucasa.auth.CognitoAuth._cognito") as mock_cog: - yield mock_cog() + return await hass_client() async def test_google_actions_sync( - mock_cognito, mock_cloud_login, cloud_client + setup_cloud: None, + cloud_client: TestClient, ) -> None: """Test syncing Google Actions.""" with patch( @@ -90,11 +117,12 @@ async def test_google_actions_sync( ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == HTTPStatus.OK - assert len(mock_request_sync.mock_calls) == 1 + assert mock_request_sync.call_count == 1 async def test_google_actions_sync_fails( - mock_cognito, mock_cloud_login, cloud_client + setup_cloud: None, + cloud_client: TestClient, ) -> None: """Test syncing Google Actions gone bad.""" with patch( @@ -103,26 +131,32 @@ async def test_google_actions_sync_fails( ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == HTTPStatus.INTERNAL_SERVER_ERROR - assert len(mock_request_sync.mock_calls) == 1 + assert mock_request_sync.call_count == 1 -async def test_login_view(hass: HomeAssistant, cloud_client) -> None: +@pytest.mark.parametrize("pipeline_data", [PIPELINE_DATA_LEGACY]) +async def test_login_view_existing_pipeline( + hass: HomeAssistant, + cloud: MagicMock, + hass_client: ClientSessionGenerator, + hass_storage: dict[str, Any], + pipeline_data: dict[str, Any], +) -> None: """Test logging in when an assist pipeline is available.""" - hass.data["cloud"] = MagicMock(login=AsyncMock()) - await async_setup_component(hass, "stt", {}) - await async_setup_component(hass, "tts", {}) + hass_storage[STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": STORAGE_KEY, + "data": deepcopy(pipeline_data), + } + + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + + cloud_client = await hass_client() with patch( - "homeassistant.components.cloud.http_api.assist_pipeline.async_get_pipelines", - return_value=[ - Mock( - conversation_engine="homeassistant", - id="12345", - stt_engine=DOMAIN, - tts_engine=DOMAIN, - ) - ], - ), patch( "homeassistant.components.cloud.http_api.assist_pipeline.async_create_default_pipeline", ) as create_pipeline_mock: req = await cloud_client.post( @@ -135,11 +169,25 @@ async def test_login_view(hass: HomeAssistant, cloud_client) -> None: create_pipeline_mock.assert_not_awaited() -async def test_login_view_create_pipeline(hass: HomeAssistant, cloud_client) -> None: - """Test logging in when no assist pipeline is available.""" - hass.data["cloud"] = MagicMock(login=AsyncMock()) - await async_setup_component(hass, "stt", {}) - await async_setup_component(hass, "tts", {}) +async def test_login_view_create_pipeline( + hass: HomeAssistant, + cloud: MagicMock, + hass_client: ClientSessionGenerator, + hass_storage: dict[str, Any], +) -> None: + """Test logging in when no existing cloud assist pipeline is available.""" + hass_storage[STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": STORAGE_KEY, + "data": deepcopy(PIPELINE_DATA_OTHER), + } + + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + + cloud_client = await hass_client() with patch( "homeassistant.components.cloud.http_api.assist_pipeline.async_create_default_pipeline", @@ -156,12 +204,24 @@ async def test_login_view_create_pipeline(hass: HomeAssistant, cloud_client) -> async def test_login_view_create_pipeline_fail( - hass: HomeAssistant, cloud_client + hass: HomeAssistant, + cloud: MagicMock, + hass_client: ClientSessionGenerator, + hass_storage: dict[str, Any], ) -> None: """Test logging in when no assist pipeline is available.""" - hass.data["cloud"] = MagicMock(login=AsyncMock()) - await async_setup_component(hass, "stt", {}) - await async_setup_component(hass, "tts", {}) + hass_storage[STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": STORAGE_KEY, + "data": deepcopy(PIPELINE_DATA_OTHER), + } + + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + + cloud_client = await hass_client() with patch( "homeassistant.components.cloud.http_api.assist_pipeline.async_create_default_pipeline", @@ -177,96 +237,143 @@ async def test_login_view_create_pipeline_fail( create_pipeline_mock.assert_awaited_once_with(hass, "cloud", "cloud") -async def test_login_view_random_exception(cloud_client) -> None: - """Try logging in with invalid JSON.""" - with patch("hass_nabucasa.Cloud.login", side_effect=ValueError("Boom")): - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) +async def test_login_view_random_exception( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Try logging in with random exception.""" + cloud.login.side_effect = ValueError("Boom") + + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) + assert req.status == HTTPStatus.BAD_GATEWAY resp = await req.json() assert resp == {"code": "valueerror", "message": "Unexpected error: Boom"} -async def test_login_view_invalid_json(cloud_client) -> None: +async def test_login_view_invalid_json( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Try logging in with invalid JSON.""" - with patch("hass_nabucasa.auth.CognitoAuth.async_login") as mock_login: - req = await cloud_client.post("/api/cloud/login", data="Not JSON") + mock_login = cloud.login + + req = await cloud_client.post("/api/cloud/login", data="Not JSON") + assert req.status == HTTPStatus.BAD_REQUEST - assert len(mock_login.mock_calls) == 0 + assert mock_login.call_count == 0 -async def test_login_view_invalid_schema(cloud_client) -> None: +async def test_login_view_invalid_schema( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Try logging in with invalid schema.""" - with patch("hass_nabucasa.auth.CognitoAuth.async_login") as mock_login: - req = await cloud_client.post("/api/cloud/login", json={"invalid": "schema"}) + mock_login = cloud.login + + req = await cloud_client.post("/api/cloud/login", json={"invalid": "schema"}) + assert req.status == HTTPStatus.BAD_REQUEST - assert len(mock_login.mock_calls) == 0 + assert mock_login.call_count == 0 -async def test_login_view_request_timeout(cloud_client) -> None: +async def test_login_view_request_timeout( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test request timeout while trying to log in.""" - with patch( - "hass_nabucasa.auth.CognitoAuth.async_login", side_effect=asyncio.TimeoutError - ): - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) + cloud.login.side_effect = asyncio.TimeoutError + + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) assert req.status == HTTPStatus.BAD_GATEWAY -async def test_login_view_invalid_credentials(cloud_client) -> None: +async def test_login_view_invalid_credentials( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test logging in with invalid credentials.""" - with patch( - "hass_nabucasa.auth.CognitoAuth.async_login", side_effect=Unauthenticated - ): - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) + cloud.login.side_effect = Unauthenticated + + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) assert req.status == HTTPStatus.UNAUTHORIZED -async def test_login_view_unknown_error(cloud_client) -> None: +async def test_login_view_unknown_error( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test unknown error while logging in.""" - with patch("hass_nabucasa.auth.CognitoAuth.async_login", side_effect=UnknownError): - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) + cloud.login.side_effect = UnknownError + + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) assert req.status == HTTPStatus.BAD_GATEWAY -async def test_logout_view(hass: HomeAssistant, cloud_client) -> None: +async def test_logout_view( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test logging out.""" - cloud = hass.data["cloud"] = MagicMock() - cloud.logout = AsyncMock(return_value=None) req = await cloud_client.post("/api/cloud/logout") + assert req.status == HTTPStatus.OK data = await req.json() assert data == {"message": "ok"} - assert len(cloud.logout.mock_calls) == 1 + assert cloud.logout.call_count == 1 -async def test_logout_view_request_timeout(hass: HomeAssistant, cloud_client) -> None: +async def test_logout_view_request_timeout( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test timeout while logging out.""" - cloud = hass.data["cloud"] = MagicMock() cloud.logout.side_effect = asyncio.TimeoutError + req = await cloud_client.post("/api/cloud/logout") + assert req.status == HTTPStatus.BAD_GATEWAY -async def test_logout_view_unknown_error(hass: HomeAssistant, cloud_client) -> None: +async def test_logout_view_unknown_error( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test unknown error while logging out.""" - cloud = hass.data["cloud"] = MagicMock() cloud.logout.side_effect = UnknownError + req = await cloud_client.post("/api/cloud/logout") + assert req.status == HTTPStatus.BAD_GATEWAY -async def test_register_view_no_location(mock_cognito, cloud_client) -> None: +async def test_register_view_no_location( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test register without location.""" + mock_cognito = cloud.auth with patch( "homeassistant.components.cloud.http_api.async_detect_location_info", return_value=None, @@ -275,17 +382,23 @@ async def test_register_view_no_location(mock_cognito, cloud_client) -> None: "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"}, ) + assert req.status == HTTPStatus.OK - assert len(mock_cognito.register.mock_calls) == 1 - call = mock_cognito.register.mock_calls[0] + assert mock_cognito.async_register.call_count == 1 + call = mock_cognito.async_register.mock_calls[0] result_email, result_pass = call.args assert result_email == "hello@bla.com" assert result_pass == "falcon42" assert call.kwargs["client_metadata"] is None -async def test_register_view_with_location(mock_cognito, cloud_client) -> None: +async def test_register_view_with_location( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: """Test register with location.""" + mock_cognito = cloud.auth with patch( "homeassistant.components.cloud.http_api.async_detect_location_info", return_value=LocationInfo( @@ -308,9 +421,10 @@ async def test_register_view_with_location(mock_cognito, cloud_client) -> None: "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"}, ) + assert req.status == HTTPStatus.OK - assert len(mock_cognito.register.mock_calls) == 1 - call = mock_cognito.register.mock_calls[0] + assert mock_cognito.async_register.call_count == 1 + call = mock_cognito.async_register.mock_calls[0] result_email, result_pass = call.args assert result_email == "hello@bla.com" assert result_pass == "falcon42" @@ -321,124 +435,201 @@ async def test_register_view_with_location(mock_cognito, cloud_client) -> None: } -async def test_register_view_bad_data(mock_cognito, cloud_client) -> None: - """Test logging out.""" +async def test_register_view_bad_data( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test register bad data.""" + mock_cognito = cloud.auth + req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "not_password": "falcon"} ) + assert req.status == HTTPStatus.BAD_REQUEST - assert len(mock_cognito.logout.mock_calls) == 0 + assert mock_cognito.async_register.call_count == 0 -async def test_register_view_request_timeout(mock_cognito, cloud_client) -> None: - """Test timeout while logging out.""" - mock_cognito.register.side_effect = asyncio.TimeoutError +async def test_register_view_request_timeout( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test timeout while registering.""" + cloud.auth.async_register.side_effect = asyncio.TimeoutError + req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} ) + assert req.status == HTTPStatus.BAD_GATEWAY -async def test_register_view_unknown_error(mock_cognito, cloud_client) -> None: - """Test unknown error while logging out.""" - mock_cognito.register.side_effect = UnknownError +async def test_register_view_unknown_error( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test unknown error while registering.""" + cloud.auth.async_register.side_effect = UnknownError + req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} ) + assert req.status == HTTPStatus.BAD_GATEWAY -async def test_forgot_password_view(mock_cognito, cloud_client) -> None: - """Test logging out.""" +async def test_forgot_password_view( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test forgot password.""" + mock_cognito = cloud.auth + req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) + assert req.status == HTTPStatus.OK - assert len(mock_cognito.initiate_forgot_password.mock_calls) == 1 + assert mock_cognito.async_forgot_password.call_count == 1 -async def test_forgot_password_view_bad_data(mock_cognito, cloud_client) -> None: - """Test logging out.""" +async def test_forgot_password_view_bad_data( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test forgot password bad data.""" + mock_cognito = cloud.auth + req = await cloud_client.post( "/api/cloud/forgot_password", json={"not_email": "hello@bla.com"} ) + assert req.status == HTTPStatus.BAD_REQUEST - assert len(mock_cognito.initiate_forgot_password.mock_calls) == 0 + assert mock_cognito.async_forgot_password.call_count == 0 -async def test_forgot_password_view_request_timeout(mock_cognito, cloud_client) -> None: - """Test timeout while logging out.""" - mock_cognito.initiate_forgot_password.side_effect = asyncio.TimeoutError +async def test_forgot_password_view_request_timeout( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test timeout while forgot password.""" + cloud.auth.async_forgot_password.side_effect = asyncio.TimeoutError + req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) + assert req.status == HTTPStatus.BAD_GATEWAY -async def test_forgot_password_view_unknown_error(mock_cognito, cloud_client) -> None: - """Test unknown error while logging out.""" - mock_cognito.initiate_forgot_password.side_effect = UnknownError +async def test_forgot_password_view_unknown_error( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test unknown error while forgot password.""" + cloud.auth.async_forgot_password.side_effect = UnknownError + req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) + assert req.status == HTTPStatus.BAD_GATEWAY -async def test_forgot_password_view_aiohttp_error(mock_cognito, cloud_client) -> None: - """Test unknown error while logging out.""" - mock_cognito.initiate_forgot_password.side_effect = aiohttp.ClientResponseError( +async def test_forgot_password_view_aiohttp_error( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test unknown error while forgot password.""" + cloud.auth.async_forgot_password.side_effect = aiohttp.ClientResponseError( Mock(), Mock() ) + req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) + assert req.status == HTTPStatus.INTERNAL_SERVER_ERROR -async def test_resend_confirm_view(mock_cognito, cloud_client) -> None: - """Test logging out.""" +async def test_resend_confirm_view( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test resend confirm.""" + mock_cognito = cloud.auth + req = await cloud_client.post( "/api/cloud/resend_confirm", json={"email": "hello@bla.com"} ) + assert req.status == HTTPStatus.OK - assert len(mock_cognito.client.resend_confirmation_code.mock_calls) == 1 + assert mock_cognito.async_resend_email_confirm.call_count == 1 -async def test_resend_confirm_view_bad_data(mock_cognito, cloud_client) -> None: - """Test logging out.""" +async def test_resend_confirm_view_bad_data( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test resend confirm bad data.""" + mock_cognito = cloud.auth + req = await cloud_client.post( "/api/cloud/resend_confirm", json={"not_email": "hello@bla.com"} ) + assert req.status == HTTPStatus.BAD_REQUEST - assert len(mock_cognito.client.resend_confirmation_code.mock_calls) == 0 + assert mock_cognito.async_resend_email_confirm.call_count == 0 -async def test_resend_confirm_view_request_timeout(mock_cognito, cloud_client) -> None: - """Test timeout while logging out.""" - mock_cognito.client.resend_confirmation_code.side_effect = asyncio.TimeoutError +async def test_resend_confirm_view_request_timeout( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test timeout while resend confirm.""" + cloud.auth.async_resend_email_confirm.side_effect = asyncio.TimeoutError + req = await cloud_client.post( "/api/cloud/resend_confirm", json={"email": "hello@bla.com"} ) + assert req.status == HTTPStatus.BAD_GATEWAY -async def test_resend_confirm_view_unknown_error(mock_cognito, cloud_client) -> None: - """Test unknown error while logging out.""" - mock_cognito.client.resend_confirmation_code.side_effect = UnknownError +async def test_resend_confirm_view_unknown_error( + cloud: MagicMock, + setup_cloud: None, + cloud_client: TestClient, +) -> None: + """Test unknown error while resend confirm.""" + cloud.auth.async_resend_email_confirm.side_effect = UnknownError + req = await cloud_client.post( "/api/cloud/resend_confirm", json={"email": "hello@bla.com"} ) + assert req.status == HTTPStatus.BAD_GATEWAY async def test_websocket_status( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - mock_cloud_fixture, - mock_cloud_login, + cloud: MagicMock, + setup_cloud: None, ) -> None: """Test querying the status.""" - hass.data[DOMAIN].iot.state = STATE_CONNECTED + cloud.iot.state = STATE_CONNECTED client = await hass_ws_client(hass) with patch.dict( @@ -452,6 +643,7 @@ async def test_websocket_status( ): await client.send_json({"id": 5, "type": "cloud/status"}) response = await client.receive_json() + assert response["result"] == { "logged_in": True, "email": "hello@home-assistant.io", @@ -462,8 +654,8 @@ async def test_websocket_status( "cloudhooks": {}, "google_enabled": True, "google_secure_devices_pin": None, - "google_default_expose": None, - "alexa_default_expose": None, + "google_default_expose": DEFAULT_EXPOSED_DOMAINS, + "alexa_default_expose": DEFAULT_EXPOSED_DOMAINS, "alexa_report_state": True, "google_report_state": True, "remote_enabled": False, @@ -493,17 +685,23 @@ async def test_websocket_status( "remote_certificate_status": None, "remote_certificate": None, "http_use_ssl": False, - "active_subscription": False, + "active_subscription": True, } async def test_websocket_status_not_logged_in( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, ) -> None: - """Test querying the status.""" + """Test querying the status not logged in.""" + cloud.id_token = None client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "cloud/status"}) response = await client.receive_json() + assert response["result"] == { "logged_in": False, "cloud": "disconnected", @@ -515,30 +713,32 @@ async def test_websocket_subscription_info( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, - mock_auth, - mock_cloud_login, + cloud: MagicMock, + setup_cloud: None, ) -> None: - """Test querying the status and connecting because valid account.""" + """Test subscription info and connecting because valid account.""" aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={"provider": "stripe"}) client = await hass_ws_client(hass) + mock_renew = cloud.auth.async_renew_access_token + + await client.send_json({"id": 5, "type": "cloud/subscription"}) + response = await client.receive_json() - with patch("hass_nabucasa.auth.CognitoAuth.async_renew_access_token") as mock_renew: - await client.send_json({"id": 5, "type": "cloud/subscription"}) - response = await client.receive_json() assert response["result"] == {"provider": "stripe"} - assert len(mock_renew.mock_calls) == 1 + assert mock_renew.call_count == 1 async def test_websocket_subscription_fail( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, - mock_auth, - mock_cloud_login, + cloud: MagicMock, + setup_cloud: None, ) -> None: - """Test querying the status.""" + """Test subscription info fail.""" aioclient_mock.get(SUBSCRIPTION_INFO_URL, status=HTTPStatus.INTERNAL_SERVER_ERROR) client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "cloud/subscription"}) response = await client.receive_json() @@ -547,10 +747,15 @@ async def test_websocket_subscription_fail( async def test_websocket_subscription_not_logged_in( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, ) -> None: - """Test querying the status.""" + """Test subscription info not logged in.""" + cloud.id_token = None client = await hass_ws_client(hass) + with patch( "hass_nabucasa.cloud_api.async_subscription_info", return_value={"return": "value"}, @@ -565,15 +770,16 @@ async def test_websocket_subscription_not_logged_in( async def test_websocket_update_preferences( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - aioclient_mock: AiohttpClientMocker, - setup_api, - mock_cloud_login, + cloud: MagicMock, + setup_cloud: None, ) -> None: """Test updating preference.""" - assert setup_api.google_enabled - assert setup_api.alexa_enabled - assert setup_api.google_secure_devices_pin is None + assert cloud.client.prefs.google_enabled + assert cloud.client.prefs.alexa_enabled + assert cloud.client.prefs.google_secure_devices_pin is None + client = await hass_ws_client(hass) + await client.send_json( { "id": 5, @@ -587,18 +793,16 @@ async def test_websocket_update_preferences( response = await client.receive_json() assert response["success"] - assert not setup_api.google_enabled - assert not setup_api.alexa_enabled - assert setup_api.google_secure_devices_pin == "1234" - assert setup_api.tts_default_voice == ("en-GB", "male") + assert not cloud.client.prefs.google_enabled + assert not cloud.client.prefs.alexa_enabled + assert cloud.client.prefs.google_secure_devices_pin == "1234" + assert cloud.client.prefs.tts_default_voice == ("en-GB", "male") async def test_websocket_update_preferences_alexa_report_state( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - aioclient_mock: AiohttpClientMocker, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test updating alexa_report_state sets alexa authorized.""" client = await hass_ws_client(hass) @@ -612,10 +816,12 @@ async def test_websocket_update_preferences_alexa_report_state( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" ) as set_authorized_mock: set_authorized_mock.assert_not_called() + await client.send_json( {"id": 5, "type": "cloud/update_prefs", "alexa_report_state": True} ) response = await client.receive_json() + set_authorized_mock.assert_called_once_with(True) assert response["success"] @@ -624,9 +830,7 @@ async def test_websocket_update_preferences_alexa_report_state( async def test_websocket_update_preferences_require_relink( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - aioclient_mock: AiohttpClientMocker, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test updating preference requires relink.""" client = await hass_ws_client(hass) @@ -641,10 +845,12 @@ async def test_websocket_update_preferences_require_relink( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" ) as set_authorized_mock: set_authorized_mock.assert_not_called() + await client.send_json( {"id": 5, "type": "cloud/update_prefs", "alexa_report_state": True} ) response = await client.receive_json() + set_authorized_mock.assert_called_once_with(False) assert not response["success"] @@ -654,9 +860,7 @@ async def test_websocket_update_preferences_require_relink( async def test_websocket_update_preferences_no_token( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - aioclient_mock: AiohttpClientMocker, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test updating preference no token available.""" client = await hass_ws_client(hass) @@ -671,10 +875,12 @@ async def test_websocket_update_preferences_no_token( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" ) as set_authorized_mock: set_authorized_mock.assert_not_called() + await client.send_json( {"id": 5, "type": "cloud/update_prefs", "alexa_report_state": True} ) response = await client.receive_json() + set_authorized_mock.assert_called_once_with(False) assert not response["success"] @@ -682,69 +888,79 @@ async def test_websocket_update_preferences_no_token( async def test_enabling_webhook( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, ) -> None: """Test we call right code to enable webhooks.""" client = await hass_ws_client(hass) - with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", return_value={} - ) as mock_enable: - await client.send_json( - {"id": 5, "type": "cloud/cloudhook/create", "webhook_id": "mock-webhook-id"} - ) - response = await client.receive_json() - assert response["success"] + mock_enable = cloud.cloudhooks.async_create + mock_enable.return_value = {} - assert len(mock_enable.mock_calls) == 1 + await client.send_json( + {"id": 5, "type": "cloud/cloudhook/create", "webhook_id": "mock-webhook-id"} + ) + response = await client.receive_json() + + assert response["success"] + assert mock_enable.call_count == 1 assert mock_enable.mock_calls[0][1][0] == "mock-webhook-id" async def test_disabling_webhook( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, ) -> None: """Test we call right code to disable webhooks.""" client = await hass_ws_client(hass) - with patch("hass_nabucasa.cloudhooks.Cloudhooks.async_delete") as mock_disable: - await client.send_json( - {"id": 5, "type": "cloud/cloudhook/delete", "webhook_id": "mock-webhook-id"} - ) - response = await client.receive_json() - assert response["success"] + mock_disable = cloud.cloudhooks.async_delete + + await client.send_json( + {"id": 5, "type": "cloud/cloudhook/delete", "webhook_id": "mock-webhook-id"} + ) + response = await client.receive_json() - assert len(mock_disable.mock_calls) == 1 + assert response["success"] + assert mock_disable.call_count == 1 assert mock_disable.mock_calls[0][1][0] == "mock-webhook-id" async def test_enabling_remote( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, ) -> None: """Test we call right code to enable remote UI.""" client = await hass_ws_client(hass) - cloud = hass.data[DOMAIN] + mock_connect = cloud.remote.connect + assert not cloud.client.remote_autostart + + await client.send_json({"id": 5, "type": "cloud/remote/connect"}) + response = await client.receive_json() - with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: - await client.send_json({"id": 5, "type": "cloud/remote/connect"}) - response = await client.receive_json() assert response["success"] assert cloud.client.remote_autostart + assert mock_connect.call_count == 1 - assert len(mock_connect.mock_calls) == 1 + mock_disconnect = cloud.remote.disconnect + + await client.send_json({"id": 6, "type": "cloud/remote/disconnect"}) + response = await client.receive_json() - with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect: - await client.send_json({"id": 6, "type": "cloud/remote/disconnect"}) - response = await client.receive_json() assert response["success"] assert not cloud.client.remote_autostart - - assert len(mock_disconnect.mock_calls) == 1 + assert mock_disconnect.call_count == 1 async def test_list_google_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test that we can list Google entities.""" client = await hass_ws_client(hass) @@ -762,6 +978,7 @@ async def test_list_google_entities( ): await client.send_json_auto_id({"type": "cloud/google_assistant/entities"}) response = await client.receive_json() + assert response["success"] assert len(response["result"]) == 2 assert response["result"][0] == { @@ -789,6 +1006,7 @@ async def test_list_google_entities( ): await client.send_json_auto_id({"type": "cloud/google_assistant/entities"}) response = await client.receive_json() + assert response["success"] assert len(response["result"]) == 2 assert response["result"][0] == { @@ -807,8 +1025,7 @@ async def test_get_google_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test that we can get a Google entity.""" client = await hass_ws_client(hass) @@ -818,6 +1035,7 @@ async def test_get_google_entity( {"type": "cloud/google_assistant/entities/get", "entity_id": "light.kitchen"} ) response = await client.receive_json() + assert not response["success"] assert response["error"] == { "code": "not_found", @@ -829,10 +1047,12 @@ async def test_get_google_entity( "group", "test", "unique", suggested_object_id="all_locks" ) hass.states.async_set("group.all_locks", "bla") + await client.send_json_auto_id( {"type": "cloud/google_assistant/entities/get", "entity_id": "group.all_locks"} ) response = await client.receive_json() + assert not response["success"] assert response["error"] == { "code": "not_supported", @@ -849,6 +1069,7 @@ async def test_get_google_entity( {"type": "cloud/google_assistant/entities/get", "entity_id": "light.kitchen"} ) response = await client.receive_json() + assert response["success"] assert response["result"] == { "disable_2fa": None, @@ -861,6 +1082,7 @@ async def test_get_google_entity( {"type": "cloud/google_assistant/entities/get", "entity_id": "cover.garage"} ) response = await client.receive_json() + assert response["success"] assert response["result"] == { "disable_2fa": None, @@ -878,12 +1100,14 @@ async def test_get_google_entity( } ) response = await client.receive_json() + assert response["success"] await client.send_json_auto_id( {"type": "cloud/google_assistant/entities/get", "entity_id": "cover.garage"} ) response = await client.receive_json() + assert response["success"] assert response["result"] == { "disable_2fa": True, @@ -895,13 +1119,12 @@ async def test_get_google_entity( async def test_update_google_entity( hass: HomeAssistant, - entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test that we can update config of a Google entity.""" client = await hass_ws_client(hass) + await client.send_json_auto_id( { "type": "cloud/google_assistant/entities/update", @@ -910,6 +1133,7 @@ async def test_update_google_entity( } ) response = await client.receive_json() + assert response["success"] await client.send_json_auto_id( @@ -921,8 +1145,8 @@ async def test_update_google_entity( } ) response = await client.receive_json() - assert response["success"] + assert response["success"] assert exposed_entities.async_get_entity_settings(hass, "light.kitchen") == { "cloud.google_assistant": {"disable_2fa": False, "should_expose": False} } @@ -932,8 +1156,7 @@ async def test_list_alexa_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test that we can list Alexa entities.""" client = await hass_ws_client(hass) @@ -946,6 +1169,7 @@ async def test_list_alexa_entities( ): await client.send_json_auto_id({"id": 5, "type": "cloud/alexa/entities"}) response = await client.receive_json() + assert response["success"] assert len(response["result"]) == 1 assert response["result"][0] == { @@ -965,6 +1189,7 @@ async def test_list_alexa_entities( ): await client.send_json_auto_id({"type": "cloud/alexa/entities"}) response = await client.receive_json() + assert response["success"] assert len(response["result"]) == 1 assert response["result"][0] == { @@ -978,8 +1203,7 @@ async def test_get_alexa_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test that we can get an Alexa entity.""" client = await hass_ws_client(hass) @@ -989,6 +1213,7 @@ async def test_get_alexa_entity( {"type": "cloud/alexa/entities/get", "entity_id": "light.kitchen"} ) response = await client.receive_json() + assert response["success"] assert response["result"] is None @@ -997,6 +1222,7 @@ async def test_get_alexa_entity( {"type": "cloud/alexa/entities/get", "entity_id": "sensor.temperature"} ) response = await client.receive_json() + assert not response["success"] assert response["error"] == { "code": "not_supported", @@ -1008,10 +1234,12 @@ async def test_get_alexa_entity( "group", "test", "unique", suggested_object_id="all_locks" ) hass.states.async_set("group.all_locks", "bla") + await client.send_json_auto_id( {"type": "cloud/alexa/entities/get", "entity_id": "group.all_locks"} ) response = await client.receive_json() + assert not response["success"] assert response["error"] == { "code": "not_supported", @@ -1029,6 +1257,7 @@ async def test_get_alexa_entity( {"type": "cloud/alexa/entities/get", "entity_id": "light.kitchen"} ) response = await client.receive_json() + assert response["success"] assert response["result"] is None @@ -1036,6 +1265,7 @@ async def test_get_alexa_entity( {"type": "cloud/alexa/entities/get", "entity_id": "water_heater.basement"} ) response = await client.receive_json() + assert not response["success"] assert response["error"] == { "code": "not_supported", @@ -1047,14 +1277,14 @@ async def test_update_alexa_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - setup_api, - mock_cloud_login, + setup_cloud: None, ) -> None: """Test that we can update config of an Alexa entity.""" entry = entity_registry.async_get_or_create( "light", "test", "unique", suggested_object_id="kitchen" ) client = await hass_ws_client(hass) + await client.send_json_auto_id( { "type": "homeassistant/expose_entity", @@ -1072,10 +1302,13 @@ async def test_update_alexa_entity( async def test_sync_alexa_entities_timeout( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_cloud: None, ) -> None: """Test that timeout syncing Alexa entities.""" client = await hass_ws_client(hass) + with patch( ( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" @@ -1091,10 +1324,13 @@ async def test_sync_alexa_entities_timeout( async def test_sync_alexa_entities_no_token( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_cloud: None, ) -> None: """Test sync Alexa entities when we have no token.""" client = await hass_ws_client(hass) + with patch( ( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" @@ -1110,10 +1346,13 @@ async def test_sync_alexa_entities_no_token( async def test_enable_alexa_state_report_fail( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_cloud: None, ) -> None: """Test enable Alexa entities state reporting when no token available.""" client = await hass_ws_client(hass) + with patch( ( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" @@ -1129,7 +1368,9 @@ async def test_enable_alexa_state_report_fail( async def test_thingtalk_convert( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_cloud: None, ) -> None: """Test that we can convert a query.""" client = await hass_ws_client(hass) @@ -1148,7 +1389,9 @@ async def test_thingtalk_convert( async def test_thingtalk_convert_timeout( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_cloud: None, ) -> None: """Test that we can convert a query.""" client = await hass_ws_client(hass) @@ -1167,7 +1410,9 @@ async def test_thingtalk_convert_timeout( async def test_thingtalk_convert_internal( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_cloud: None, ) -> None: """Test that we can convert a query.""" client = await hass_ws_client(hass) @@ -1187,7 +1432,9 @@ async def test_thingtalk_convert_internal( async def test_tts_info( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_cloud: None, ) -> None: """Test that we can get TTS info.""" # Verify the format is as expected @@ -1223,6 +1470,7 @@ async def test_tts_info( ) async def test_api_calls_require_admin( hass: HomeAssistant, + setup_cloud: None, hass_client: ClientSessionGenerator, hass_read_only_access_token: str, endpoint: str, From 02853a62f0822bb55c92d80c48ebe8c396a54f71 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 13 Dec 2023 14:21:33 +0100 Subject: [PATCH 097/118] Clean cloud client fixture from cloud http api tests (#105649) --- tests/components/cloud/test_http_api.py | 84 +++++++++++++++---------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 15acc27593118e..cc6fb4a12192c2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp -from aiohttp.test_utils import TestClient from hass_nabucasa import thingtalk, voice from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED @@ -98,19 +97,12 @@ async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None: await on_start_callback() -@pytest.fixture(name="cloud_client") -async def cloud_client_fixture( - hass: HomeAssistant, hass_client: ClientSessionGenerator -) -> TestClient: - """Fixture that can fetch from the cloud client.""" - return await hass_client() - - async def test_google_actions_sync( setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test syncing Google Actions.""" + cloud_client = await hass_client() with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", return_value=Mock(status=200), @@ -122,9 +114,10 @@ async def test_google_actions_sync( async def test_google_actions_sync_fails( setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test syncing Google Actions gone bad.""" + cloud_client = await hass_client() with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", return_value=Mock(status=HTTPStatus.INTERNAL_SERVER_ERROR), @@ -240,9 +233,10 @@ async def test_login_view_create_pipeline_fail( async def test_login_view_random_exception( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Try logging in with random exception.""" + cloud_client = await hass_client() cloud.login.side_effect = ValueError("Boom") req = await cloud_client.post( @@ -257,9 +251,10 @@ async def test_login_view_random_exception( async def test_login_view_invalid_json( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Try logging in with invalid JSON.""" + cloud_client = await hass_client() mock_login = cloud.login req = await cloud_client.post("/api/cloud/login", data="Not JSON") @@ -271,9 +266,10 @@ async def test_login_view_invalid_json( async def test_login_view_invalid_schema( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Try logging in with invalid schema.""" + cloud_client = await hass_client() mock_login = cloud.login req = await cloud_client.post("/api/cloud/login", json={"invalid": "schema"}) @@ -285,9 +281,10 @@ async def test_login_view_invalid_schema( async def test_login_view_request_timeout( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test request timeout while trying to log in.""" + cloud_client = await hass_client() cloud.login.side_effect = asyncio.TimeoutError req = await cloud_client.post( @@ -300,9 +297,10 @@ async def test_login_view_request_timeout( async def test_login_view_invalid_credentials( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test logging in with invalid credentials.""" + cloud_client = await hass_client() cloud.login.side_effect = Unauthenticated req = await cloud_client.post( @@ -315,9 +313,10 @@ async def test_login_view_invalid_credentials( async def test_login_view_unknown_error( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test unknown error while logging in.""" + cloud_client = await hass_client() cloud.login.side_effect = UnknownError req = await cloud_client.post( @@ -330,9 +329,10 @@ async def test_login_view_unknown_error( async def test_logout_view( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test logging out.""" + cloud_client = await hass_client() req = await cloud_client.post("/api/cloud/logout") assert req.status == HTTPStatus.OK @@ -344,9 +344,10 @@ async def test_logout_view( async def test_logout_view_request_timeout( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test timeout while logging out.""" + cloud_client = await hass_client() cloud.logout.side_effect = asyncio.TimeoutError req = await cloud_client.post("/api/cloud/logout") @@ -357,9 +358,10 @@ async def test_logout_view_request_timeout( async def test_logout_view_unknown_error( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test unknown error while logging out.""" + cloud_client = await hass_client() cloud.logout.side_effect = UnknownError req = await cloud_client.post("/api/cloud/logout") @@ -370,9 +372,10 @@ async def test_logout_view_unknown_error( async def test_register_view_no_location( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test register without location.""" + cloud_client = await hass_client() mock_cognito = cloud.auth with patch( "homeassistant.components.cloud.http_api.async_detect_location_info", @@ -395,9 +398,10 @@ async def test_register_view_no_location( async def test_register_view_with_location( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test register with location.""" + cloud_client = await hass_client() mock_cognito = cloud.auth with patch( "homeassistant.components.cloud.http_api.async_detect_location_info", @@ -438,9 +442,10 @@ async def test_register_view_with_location( async def test_register_view_bad_data( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test register bad data.""" + cloud_client = await hass_client() mock_cognito = cloud.auth req = await cloud_client.post( @@ -454,9 +459,10 @@ async def test_register_view_bad_data( async def test_register_view_request_timeout( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test timeout while registering.""" + cloud_client = await hass_client() cloud.auth.async_register.side_effect = asyncio.TimeoutError req = await cloud_client.post( @@ -469,9 +475,10 @@ async def test_register_view_request_timeout( async def test_register_view_unknown_error( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test unknown error while registering.""" + cloud_client = await hass_client() cloud.auth.async_register.side_effect = UnknownError req = await cloud_client.post( @@ -484,9 +491,10 @@ async def test_register_view_unknown_error( async def test_forgot_password_view( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test forgot password.""" + cloud_client = await hass_client() mock_cognito = cloud.auth req = await cloud_client.post( @@ -500,9 +508,10 @@ async def test_forgot_password_view( async def test_forgot_password_view_bad_data( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test forgot password bad data.""" + cloud_client = await hass_client() mock_cognito = cloud.auth req = await cloud_client.post( @@ -516,9 +525,10 @@ async def test_forgot_password_view_bad_data( async def test_forgot_password_view_request_timeout( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test timeout while forgot password.""" + cloud_client = await hass_client() cloud.auth.async_forgot_password.side_effect = asyncio.TimeoutError req = await cloud_client.post( @@ -531,9 +541,10 @@ async def test_forgot_password_view_request_timeout( async def test_forgot_password_view_unknown_error( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test unknown error while forgot password.""" + cloud_client = await hass_client() cloud.auth.async_forgot_password.side_effect = UnknownError req = await cloud_client.post( @@ -546,9 +557,10 @@ async def test_forgot_password_view_unknown_error( async def test_forgot_password_view_aiohttp_error( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test unknown error while forgot password.""" + cloud_client = await hass_client() cloud.auth.async_forgot_password.side_effect = aiohttp.ClientResponseError( Mock(), Mock() ) @@ -563,9 +575,10 @@ async def test_forgot_password_view_aiohttp_error( async def test_resend_confirm_view( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test resend confirm.""" + cloud_client = await hass_client() mock_cognito = cloud.auth req = await cloud_client.post( @@ -579,9 +592,10 @@ async def test_resend_confirm_view( async def test_resend_confirm_view_bad_data( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test resend confirm bad data.""" + cloud_client = await hass_client() mock_cognito = cloud.auth req = await cloud_client.post( @@ -595,9 +609,10 @@ async def test_resend_confirm_view_bad_data( async def test_resend_confirm_view_request_timeout( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test timeout while resend confirm.""" + cloud_client = await hass_client() cloud.auth.async_resend_email_confirm.side_effect = asyncio.TimeoutError req = await cloud_client.post( @@ -610,9 +625,10 @@ async def test_resend_confirm_view_request_timeout( async def test_resend_confirm_view_unknown_error( cloud: MagicMock, setup_cloud: None, - cloud_client: TestClient, + hass_client: ClientSessionGenerator, ) -> None: """Test unknown error while resend confirm.""" + cloud_client = await hass_client() cloud.auth.async_resend_email_confirm.side_effect = UnknownError req = await cloud_client.post( From ac53b78a0c208feefc4626462ed132e6ef15897c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 14:21:44 +0100 Subject: [PATCH 098/118] Deduplicate constants A-D (#105638) --- homeassistant/components/airvisual/__init__.py | 2 +- homeassistant/components/airvisual/config_flow.py | 2 +- homeassistant/components/airvisual/const.py | 1 - homeassistant/components/airvisual/diagnostics.py | 3 ++- homeassistant/components/airvisual/sensor.py | 3 ++- homeassistant/components/amberelectric/const.py | 1 - homeassistant/components/blink/const.py | 1 - homeassistant/components/braviatv/config_flow.py | 3 +-- homeassistant/components/braviatv/const.py | 1 - homeassistant/components/braviatv/coordinator.py | 3 +-- homeassistant/components/elkm1/__init__.py | 2 +- homeassistant/components/elkm1/const.py | 1 - homeassistant/components/github/config_flow.py | 10 ++-------- homeassistant/components/github/const.py | 1 - homeassistant/components/github/diagnostics.py | 3 ++- homeassistant/components/homewizard/const.py | 1 - homeassistant/components/hue/bridge.py | 4 ++-- homeassistant/components/hue/config_flow.py | 3 +-- homeassistant/components/hue/const.py | 1 - homeassistant/components/hue/migration.py | 4 ++-- homeassistant/components/mysensors/config_flow.py | 2 +- homeassistant/components/mysensors/const.py | 1 - homeassistant/components/mysensors/device.py | 9 +++++++-- homeassistant/components/mysensors/gateway.py | 3 +-- homeassistant/components/prosegur/__init__.py | 4 ++-- homeassistant/components/prosegur/config_flow.py | 4 ++-- homeassistant/components/prosegur/const.py | 1 - homeassistant/components/samsungtv/bridge.py | 2 +- homeassistant/components/samsungtv/const.py | 1 - homeassistant/components/subaru/__init__.py | 9 +++++++-- homeassistant/components/subaru/config_flow.py | 10 ++++++++-- homeassistant/components/subaru/const.py | 1 - homeassistant/components/tplink/const.py | 1 - homeassistant/components/workday/__init__.py | 4 ++-- homeassistant/components/workday/binary_sensor.py | 3 +-- homeassistant/components/workday/config_flow.py | 3 +-- homeassistant/components/workday/const.py | 1 - homeassistant/components/xiaomi_miio/__init__.py | 3 +-- homeassistant/components/xiaomi_miio/air_quality.py | 3 +-- homeassistant/components/xiaomi_miio/binary_sensor.py | 3 +-- homeassistant/components/xiaomi_miio/config_flow.py | 3 +-- homeassistant/components/xiaomi_miio/const.py | 1 - homeassistant/components/xiaomi_miio/fan.py | 3 +-- homeassistant/components/xiaomi_miio/humidifier.py | 3 +-- homeassistant/components/xiaomi_miio/light.py | 9 +++++++-- homeassistant/components/xiaomi_miio/number.py | 2 +- homeassistant/components/xiaomi_miio/select.py | 3 +-- homeassistant/components/xiaomi_miio/sensor.py | 2 +- homeassistant/components/xiaomi_miio/switch.py | 2 +- homeassistant/components/xiaomi_miio/vacuum.py | 2 +- tests/components/braviatv/test_config_flow.py | 3 +-- tests/components/github/conftest.py | 7 ++----- tests/components/github/test_config_flow.py | 2 +- tests/components/mysensors/conftest.py | 2 +- tests/components/mysensors/test_config_flow.py | 2 +- tests/components/subaru/conftest.py | 9 +++++++-- tests/components/workday/test_config_flow.py | 3 +-- tests/components/xiaomi_miio/test_button.py | 2 +- tests/components/xiaomi_miio/test_config_flow.py | 10 +++++----- tests/components/xiaomi_miio/test_select.py | 2 +- tests/components/xiaomi_miio/test_vacuum.py | 2 +- 61 files changed, 91 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index e07400f27640e8..1d5babee6d72f0 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -19,6 +19,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, + CONF_COUNTRY, CONF_IP_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE, @@ -44,7 +45,6 @@ from .const import ( CONF_CITY, - CONF_COUNTRY, CONF_GEOGRAPHIES, CONF_INTEGRATION_TYPE, DOMAIN, diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 893726fc0223d0..23a26e2cca6c8e 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, + CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_SHOW_ON_MAP, @@ -35,7 +36,6 @@ from . import async_get_geography_id from .const import ( CONF_CITY, - CONF_COUNTRY, CONF_INTEGRATION_TYPE, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY_COORDS, diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py index 8e2c08eb8967d0..0afa7d32d41752 100644 --- a/homeassistant/components/airvisual/const.py +++ b/homeassistant/components/airvisual/const.py @@ -9,6 +9,5 @@ INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro" CONF_CITY = "city" -CONF_COUNTRY = "country" CONF_GEOGRAPHIES = "geographies" CONF_INTEGRATION_TYPE = "integration_type" diff --git a/homeassistant/components/airvisual/diagnostics.py b/homeassistant/components/airvisual/diagnostics.py index c273dbe7a55a89..05e716367bb01f 100644 --- a/homeassistant/components/airvisual/diagnostics.py +++ b/homeassistant/components/airvisual/diagnostics.py @@ -7,6 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, + CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_STATE, @@ -15,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_CITY, CONF_COUNTRY, DOMAIN +from .const import CONF_CITY, DOMAIN CONF_COORDINATES = "coordinates" CONF_TITLE = "title" diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 1f0c5aa1baa97c..ab80e154903e7f 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -15,6 +15,7 @@ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_SHOW_ON_MAP, @@ -25,7 +26,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import AirVisualEntity -from .const import CONF_CITY, CONF_COUNTRY, DOMAIN +from .const import CONF_CITY, DOMAIN ATTR_CITY = "city" ATTR_COUNTRY = "country" diff --git a/homeassistant/components/amberelectric/const.py b/homeassistant/components/amberelectric/const.py index f3cda887150c2a..5f92e5a9117365 100644 --- a/homeassistant/components/amberelectric/const.py +++ b/homeassistant/components/amberelectric/const.py @@ -4,7 +4,6 @@ from homeassistant.const import Platform DOMAIN = "amberelectric" -CONF_API_TOKEN = "api_token" CONF_SITE_NAME = "site_name" CONF_SITE_ID = "site_id" CONF_SITE_NMI = "site_nmi" diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index 64b05e1ba27a49..d394b5c00087e1 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -7,7 +7,6 @@ CONF_MIGRATE = "migrate" CONF_CAMERA = "camera" CONF_ALARM_CONTROL_PANEL = "alarm_control_panel" -CONF_DEVICE_ID = "device_id" DEFAULT_BRAND = "Blink" DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" DEFAULT_SCAN_INTERVAL = 300 diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 3fb6e6b3b4027a..fd72203b249703 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -12,7 +12,7 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN +from homeassistant.const import CONF_CLIENT_ID, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import instance_id from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -22,7 +22,6 @@ ATTR_CID, ATTR_MAC, ATTR_MODEL, - CONF_CLIENT_ID, CONF_NICKNAME, CONF_USE_PSK, DOMAIN, diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 34b621802f9eba..aff02aa9e8baf3 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -9,7 +9,6 @@ ATTR_MANUFACTURER: Final = "Sony" ATTR_MODEL: Final = "model" -CONF_CLIENT_ID: Final = "client_id" CONF_NICKNAME: Final = "nickname" CONF_USE_PSK: Final = "use_psk" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 20b30d1dd11ee1..43f911cd3a29ea 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -19,14 +19,13 @@ ) from homeassistant.components.media_player import MediaType -from homeassistant.const import CONF_PIN +from homeassistant.const import CONF_CLIENT_ID, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - CONF_CLIENT_ID, CONF_NICKNAME, CONF_USE_PSK, DOMAIN, diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index b78157588e82e2..b633e1ae62091f 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -17,6 +17,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_CONNECTIONS, + CONF_ENABLED, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, @@ -46,7 +47,6 @@ CONF_AREA, CONF_AUTO_CONFIGURE, CONF_COUNTER, - CONF_ENABLED, CONF_KEYPAD, CONF_OUTPUT, CONF_PLC, diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index a2bb5744c11784..9e952c7ee0b299 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -14,7 +14,6 @@ CONF_AUTO_CONFIGURE = "auto_configure" CONF_AREA = "area" CONF_COUNTER = "counter" -CONF_ENABLED = "enabled" CONF_KEYPAD = "keypad" CONF_OUTPUT = "output" CONF_PLC = "plc" diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 9afbf80297c7a6..5e223483e2e9b4 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -15,6 +15,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import ( @@ -23,14 +24,7 @@ ) import homeassistant.helpers.config_validation as cv -from .const import ( - CLIENT_ID, - CONF_ACCESS_TOKEN, - CONF_REPOSITORIES, - DEFAULT_REPOSITORIES, - DOMAIN, - LOGGER, -) +from .const import CLIENT_ID, CONF_REPOSITORIES, DEFAULT_REPOSITORIES, DOMAIN, LOGGER async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]: diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index a186f4684b37ad..d01656ee8ae967 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -13,7 +13,6 @@ DEFAULT_REPOSITORIES = ["home-assistant/core", "esphome/esphome"] FALLBACK_UPDATE_INTERVAL = timedelta(hours=1, minutes=30) -CONF_ACCESS_TOKEN = "access_token" CONF_REPOSITORIES = "repositories" diff --git a/homeassistant/components/github/diagnostics.py b/homeassistant/components/github/diagnostics.py index c2546d636b8281..1562649734462c 100644 --- a/homeassistant/components/github/diagnostics.py +++ b/homeassistant/components/github/diagnostics.py @@ -6,13 +6,14 @@ from aiogithubapi import GitHubAPI, GitHubException from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import ( SERVER_SOFTWARE, async_get_clientsession, ) -from .const import CONF_ACCESS_TOKEN, DOMAIN +from .const import DOMAIN from .coordinator import GitHubDataUpdateCoordinator diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index d4692ee8bf07f3..daeed9d3505c7c 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -17,7 +17,6 @@ # Platform config. CONF_API_ENABLED = "api_enabled" CONF_DATA = "data" -CONF_DEVICE = "device" CONF_PATH = "path" CONF_PRODUCT_NAME = "product_name" CONF_PRODUCT_TYPE = "product_type" diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 04bd63e5b1f995..c5ceebec3f83e4 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -13,11 +13,11 @@ from homeassistant import core from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform +from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST, Platform from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import aiohttp_client -from .const import CONF_API_VERSION, DOMAIN +from .const import DOMAIN from .v1.sensor_base import SensorManager from .v2.device import async_setup_devices from .v2.hue_event import async_setup_hue_events diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0957329abb0b83..7262dea39ef750 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import ( @@ -26,7 +26,6 @@ from .const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, - CONF_API_VERSION, CONF_IGNORE_AVAILABILITY, DEFAULT_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_UNREACHABLE, diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 38c2587bc1ab6d..5033aaa427abbf 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -7,7 +7,6 @@ DOMAIN = "hue" -CONF_API_VERSION = "api_version" CONF_IGNORE_AVAILABILITY = "ignore_availability" CONF_SUBTYPE = "subtype" diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 035da145cc0f72..f4bf6366d61a9d 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_USERNAME +from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST, CONF_USERNAME from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import ( async_entries_for_config_entry as devices_for_config_entries, @@ -23,7 +23,7 @@ async_get as async_get_entity_registry, ) -from .const import CONF_API_VERSION, DOMAIN +from .const import DOMAIN LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 8011bfcb1552f1..fdf056c6c06909 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -18,6 +18,7 @@ valid_subscribe_topic, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector @@ -25,7 +26,6 @@ from .const import ( CONF_BAUD_RATE, - CONF_DEVICE, CONF_GATEWAY_TYPE, CONF_GATEWAY_TYPE_MQTT, CONF_GATEWAY_TYPE_SERIAL, diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index a5c82c32b558a9..0a4b4c090eff05 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -11,7 +11,6 @@ ATTR_NODE_ID: Final = "node_id" CONF_BAUD_RATE: Final = "baud_rate" -CONF_DEVICE: Final = "device" CONF_PERSISTENCE_FILE: Final = "persistence_file" CONF_RETAIN: Final = "retain" CONF_TCP_PORT: Final = "tcp_port" diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 6d7decf14f417d..c70ef1f89ed2a1 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -8,7 +8,13 @@ from mysensors import BaseAsyncGateway, Sensor from mysensors.sensor import ChildSensor -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, Platform +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + CONF_DEVICE, + STATE_OFF, + STATE_ON, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import DeviceInfo @@ -17,7 +23,6 @@ from .const import ( CHILD_CALLBACK, - CONF_DEVICE, DOMAIN, NODE_CALLBACK, PLATFORM_TYPES, diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 590ad41d6a277c..0818d68de2bac1 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -18,14 +18,13 @@ ReceivePayloadType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( CONF_BAUD_RATE, - CONF_DEVICE, CONF_GATEWAY_TYPE, CONF_GATEWAY_TYPE_MQTT, CONF_GATEWAY_TYPE_SERIAL, diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index 9f594fc6dae7c2..fd79a091e39ad8 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -4,12 +4,12 @@ from pyprosegur.auth import Auth from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from .const import CONF_COUNTRY, DOMAIN +from .const import DOMAIN PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA] diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index ac2b704b012ef0..c28245a09ff06f 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -9,11 +9,11 @@ from homeassistant import config_entries, core, exceptions from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, selector -from .const import CONF_CONTRACT, CONF_COUNTRY, DOMAIN +from .const import CONF_CONTRACT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/prosegur/const.py b/homeassistant/components/prosegur/const.py index ea823e760621e6..495bec5d4ca560 100644 --- a/homeassistant/components/prosegur/const.py +++ b/homeassistant/components/prosegur/const.py @@ -2,7 +2,6 @@ DOMAIN = "prosegur" -CONF_COUNTRY = "country" CONF_CONTRACT = "contract" SERVICE_REQUEST_IMAGE = "request_image" diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 03a9c35c9ba281..f2767ce693ec8c 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -34,6 +34,7 @@ from websockets.exceptions import ConnectionClosedError, WebSocketException from homeassistant.const import ( + CONF_DESCRIPTION, CONF_HOST, CONF_ID, CONF_METHOD, @@ -50,7 +51,6 @@ from homeassistant.util import dt as dt_util from .const import ( - CONF_DESCRIPTION, CONF_SESSION_ID, ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index 6699d26243bb1f..6c657145d7a63b 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -11,7 +11,6 @@ VALUE_CONF_NAME = "HomeAssistant" VALUE_CONF_ID = "ha.component.samsung" -CONF_DESCRIPTION = "description" CONF_MANUFACTURER = "manufacturer" CONF_SSDP_RENDERING_CONTROL_LOCATION = "ssdp_rendering_control_location" CONF_SSDP_MAIN_TV_AGENT_LOCATION = "ssdp_main_tv_agent_location" diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 091a281defca91..8a22391284fd68 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -6,7 +6,13 @@ from subarulink import Controller as SubaruAPI, InvalidCredentials, SubaruException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.const import ( + CONF_COUNTRY, + CONF_DEVICE_ID, + CONF_PASSWORD, + CONF_PIN, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -14,7 +20,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - CONF_COUNTRY, CONF_UPDATE_ENABLED, COORDINATOR_NAME, DOMAIN, diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 6d1d5015ed3f8c..b21feab784374e 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -15,12 +15,18 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.const import ( + CONF_COUNTRY, + CONF_DEVICE_ID, + CONF_PASSWORD, + CONF_PIN, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN +from .const import CONF_UPDATE_ENABLED, DOMAIN _LOGGER = logging.getLogger(__name__) CONF_CONTACT_METHOD = "contact_method" diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index 9c94ed353617b4..ab76c363f7eb8f 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -7,7 +7,6 @@ FETCH_INTERVAL = 300 UPDATE_INTERVAL = 7200 CONF_UPDATE_ENABLED = "update_enabled" -CONF_COUNTRY = "country" # entry fields ENTRY_CONTROLLER = "controller" diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index b1cd323a36a6f5..22b5741fceb615 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -13,7 +13,6 @@ ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DIMMER: Final = "dimmer" -CONF_DISCOVERY: Final = "discovery" CONF_LIGHT: Final = "light" CONF_STRIP: Final = "strip" CONF_SWITCH: Final = "switch" diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index 455f5d4618a189..3000570731b579 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -4,12 +4,12 @@ from holidays import HolidayBase, country_holidays, list_supported_countries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from .const import CONF_COUNTRY, CONF_PROVINCE, DOMAIN, PLATFORMS +from .const import CONF_PROVINCE, DOMAIN, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 2d1030c6b92fb0..e2369baade59f8 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE, CONF_NAME +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -26,7 +26,6 @@ from .const import ( ALLOWED_DAYS, CONF_ADD_HOLIDAYS, - CONF_COUNTRY, CONF_EXCLUDES, CONF_OFFSET, CONF_PROVINCE, diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 9ae319772768cf..859d3710ca4a5e 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -11,7 +11,7 @@ ConfigFlow, OptionsFlowWithConfigEntry, ) -from homeassistant.const import CONF_LANGUAGE, CONF_NAME +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError @@ -33,7 +33,6 @@ from .const import ( ALLOWED_DAYS, CONF_ADD_HOLIDAYS, - CONF_COUNTRY, CONF_EXCLUDES, CONF_OFFSET, CONF_PROVINCE, diff --git a/homeassistant/components/workday/const.py b/homeassistant/components/workday/const.py index 20905fb9892080..ad9375830ddcf5 100644 --- a/homeassistant/components/workday/const.py +++ b/homeassistant/components/workday/const.py @@ -12,7 +12,6 @@ DOMAIN = "workday" PLATFORMS = [Platform.BINARY_SENSOR] -CONF_COUNTRY = "country" CONF_PROVINCE = "province" CONF_WORKDAYS = "workdays" CONF_EXCLUDES = "excludes" diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 3c316fd3f47d50..716d4a04fa7b6c 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -35,7 +35,7 @@ from miio.gateway.gateway import GatewayException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN, Platform +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -43,7 +43,6 @@ from .const import ( ATTR_AVAILABLE, - CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 30fcaa5152a84c..f9248ba5ff3cce 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -6,12 +6,11 @@ from homeassistant.components.air_quality import AirQualityEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, MODEL_AIRQUALITYMONITOR_B1, MODEL_AIRQUALITYMONITOR_CGDN1, diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 051ac2ab77896e..130b5ebd922db7 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -11,13 +11,12 @@ BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODEL, EntityCategory +from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VacuumCoordinatorDataAttributes from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, KEY_COORDINATOR, diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 70e6fb5c0b6bed..2a4deffb1616de 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -13,7 +13,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac @@ -23,7 +23,6 @@ CONF_CLOUD_PASSWORD, CONF_CLOUD_SUBDEVICES, CONF_CLOUD_USERNAME, - CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, CONF_MAC, diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 6621e41e7aa7cf..376c23c10d433b 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -18,7 +18,6 @@ # Config flow CONF_FLOW_TYPE = "config_flow_device" CONF_GATEWAY = "gateway" -CONF_DEVICE = "device" CONF_MAC = "mac" CONF_CLOUD_USERNAME = "cloud_username" CONF_CLOUD_PASSWORD = "cloud_password" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 9be019ed72449d..3038342621054c 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -30,7 +30,7 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_MODEL +from homeassistant.const import ATTR_ENTITY_ID, CONF_DEVICE, CONF_MODEL from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -40,7 +40,6 @@ ) from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, FEATURE_FLAGS_AIRFRESH, diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 0438b606efd9c1..f2660bef68a73b 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -20,13 +20,12 @@ HumidifierEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODE, CONF_MODEL +from homeassistant.const import ATTR_MODE, CONF_DEVICE, CONF_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import percentage_to_ranged_value from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, KEY_COORDINATOR, diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 1fc032b5c3665f..8d198ae2a8f705 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -33,7 +33,13 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_MODEL, CONF_TOKEN +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE, + CONF_HOST, + CONF_MODEL, + CONF_TOKEN, +) from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo @@ -41,7 +47,6 @@ from homeassistant.util import color, dt as dt_util from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index a8346caa8941ab..1062b2d42b0279 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -13,6 +13,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_DEVICE, CONF_MODEL, DEGREE, REVOLUTIONS_PER_MINUTE, @@ -25,7 +26,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, FEATURE_FLAGS_AIRFRESH, diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 74ce36ca57a69d..f6123ad0f0c5fa 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -29,12 +29,11 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODEL, EntityCategory +from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, KEY_COORDINATOR, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 17d60e1a9527e6..200a67e5f54da5 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -28,6 +28,7 @@ ATTR_TEMPERATURE, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, + CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN, @@ -48,7 +49,6 @@ from . import VacuumCoordinatorDataAttributes from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 9bba9f6112314b..7de6192e7366b8 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -21,6 +21,7 @@ ATTR_ENTITY_ID, ATTR_MODE, ATTR_TEMPERATURE, + CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN, @@ -31,7 +32,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 34a7b949646878..73e2e54b62f5e0 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -19,6 +19,7 @@ VacuumEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,7 +28,6 @@ from . import VacuumCoordinatorData from .const import ( - CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, KEY_COORDINATOR, diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 1ac1fcd4bea78d..0f1d08792fab92 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -12,14 +12,13 @@ from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.braviatv.const import ( - CONF_CLIENT_ID, CONF_NICKNAME, CONF_USE_PSK, DOMAIN, NICKNAME_PREFIX, ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.const import CONF_CLIENT_ID, CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.helpers import instance_id diff --git a/tests/components/github/conftest.py b/tests/components/github/conftest.py index 04b53da6b91a5e..b0b6f243fa0a4a 100644 --- a/tests/components/github/conftest.py +++ b/tests/components/github/conftest.py @@ -4,11 +4,8 @@ import pytest -from homeassistant.components.github.const import ( - CONF_ACCESS_TOKEN, - CONF_REPOSITORIES, - DOMAIN, -) +from homeassistant.components.github.const import CONF_REPOSITORIES, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from .common import MOCK_ACCESS_TOKEN, TEST_REPOSITORY, setup_github_integration diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index ad3be582a5dc38..a86e1d134aa115 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -6,11 +6,11 @@ from homeassistant import config_entries from homeassistant.components.github.config_flow import get_repositories from homeassistant.components.github.const import ( - CONF_ACCESS_TOKEN, CONF_REPOSITORIES, DEFAULT_REPOSITORIES, DOMAIN, ) +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 64fbb61aac3337..6df50f04ae2836 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -16,12 +16,12 @@ from homeassistant.components.mysensors.config_flow import DEFAULT_BAUD_RATE from homeassistant.components.mysensors.const import ( CONF_BAUD_RATE, - CONF_DEVICE, CONF_GATEWAY_TYPE, CONF_GATEWAY_TYPE_SERIAL, CONF_VERSION, DOMAIN, ) +from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index dc24a48edd416d..bff13d1604f440 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -9,7 +9,6 @@ from homeassistant import config_entries from homeassistant.components.mysensors.const import ( CONF_BAUD_RATE, - CONF_DEVICE, CONF_GATEWAY_TYPE, CONF_GATEWAY_TYPE_MQTT, CONF_GATEWAY_TYPE_SERIAL, @@ -23,6 +22,7 @@ DOMAIN, ConfGatewayType, ) +from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 8bed67cb15f0b1..4927525d896e8b 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -8,7 +8,6 @@ from homeassistant import config_entries from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN from homeassistant.components.subaru.const import ( - CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN, FETCH_INTERVAL, @@ -22,7 +21,13 @@ VEHICLE_NAME, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.const import ( + CONF_COUNTRY, + CONF_DEVICE_ID, + CONF_PASSWORD, + CONF_PIN, + CONF_USERNAME, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index 57a7046546e41c..fb0d78365e8c0a 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -9,7 +9,6 @@ from homeassistant import config_entries from homeassistant.components.workday.const import ( CONF_ADD_HOLIDAYS, - CONF_COUNTRY, CONF_EXCLUDES, CONF_OFFSET, CONF_REMOVE_HOLIDAYS, @@ -19,7 +18,7 @@ DEFAULT_WORKDAYS, DOMAIN, ) -from homeassistant.const import CONF_LANGUAGE, CONF_NAME +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.util.dt import UTC diff --git a/tests/components/xiaomi_miio/test_button.py b/tests/components/xiaomi_miio/test_button.py index 995c5ae034cb0f..d00b2ec5853311 100644 --- a/tests/components/xiaomi_miio/test_button.py +++ b/tests/components/xiaomi_miio/test_button.py @@ -5,7 +5,6 @@ from homeassistant.components.button import DOMAIN, SERVICE_PRESS from homeassistant.components.xiaomi_miio.const import ( - CONF_DEVICE, CONF_FLOW_TYPE, CONF_MAC, DOMAIN as XIAOMI_DOMAIN, @@ -13,6 +12,7 @@ ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN, diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index a436908b44ff96..0fe8c3d247c95e 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import const -from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant from . import TEST_MAC @@ -685,7 +685,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) assert result["type"] == "create_entry" assert result["title"] == overwrite_model assert result["data"] == { - const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_FLOW_TYPE: CONF_DEVICE, const.CONF_CLOUD_USERNAME: None, const.CONF_CLOUD_PASSWORD: None, const.CONF_CLOUD_COUNTRY: None, @@ -729,7 +729,7 @@ async def config_flow_device_success(hass, model_to_test): assert result["type"] == "create_entry" assert result["title"] == model_to_test assert result["data"] == { - const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_FLOW_TYPE: CONF_DEVICE, const.CONF_CLOUD_USERNAME: None, const.CONF_CLOUD_PASSWORD: None, const.CONF_CLOUD_COUNTRY: None, @@ -775,7 +775,7 @@ async def config_flow_generic_roborock(hass): assert result["type"] == "create_entry" assert result["title"] == dummy_model assert result["data"] == { - const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_FLOW_TYPE: CONF_DEVICE, const.CONF_CLOUD_USERNAME: None, const.CONF_CLOUD_PASSWORD: None, const.CONF_CLOUD_COUNTRY: None, @@ -829,7 +829,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): assert result["type"] == "create_entry" assert result["title"] == model_to_test assert result["data"] == { - const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_FLOW_TYPE: CONF_DEVICE, const.CONF_CLOUD_USERNAME: None, const.CONF_CLOUD_PASSWORD: None, const.CONF_CLOUD_COUNTRY: None, diff --git a/tests/components/xiaomi_miio/test_select.py b/tests/components/xiaomi_miio/test_select.py index 48b8216bffc6c0..04cb6ee6ea79f3 100644 --- a/tests/components/xiaomi_miio/test_select.py +++ b/tests/components/xiaomi_miio/test_select.py @@ -17,7 +17,6 @@ ) from homeassistant.components.xiaomi_miio import UPDATE_INTERVAL from homeassistant.components.xiaomi_miio.const import ( - CONF_DEVICE, CONF_FLOW_TYPE, CONF_MAC, DOMAIN as XIAOMI_DOMAIN, @@ -25,6 +24,7 @@ ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN, diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 422a52b44ac082..e1f2233c5bc445 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -23,7 +23,6 @@ STATE_ERROR, ) from homeassistant.components.xiaomi_miio.const import ( - CONF_DEVICE, CONF_FLOW_TYPE, CONF_MAC, DOMAIN as XIAOMI_DOMAIN, @@ -32,6 +31,7 @@ from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_ERROR, ATTR_TIMERS, + CONF_DEVICE, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, From 61a99c911c03c59a721206b9e1428ed200ba4f6d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 13 Dec 2023 14:33:59 +0100 Subject: [PATCH 099/118] Migrate demo test to use freezegun (#105644) --- tests/components/demo/test_button.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/components/demo/test_button.py b/tests/components/demo/test_button.py index bcaddab433b8d5..6049de12570e75 100644 --- a/tests/components/demo/test_button.py +++ b/tests/components/demo/test_button.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.button import DOMAIN, SERVICE_PRESS @@ -37,20 +38,20 @@ def test_setup_params(hass: HomeAssistant) -> None: assert state.state == STATE_UNKNOWN -async def test_press(hass: HomeAssistant) -> None: +async def test_press(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test pressing the button.""" state = hass.states.get(ENTITY_PUSH) assert state assert state.state == STATE_UNKNOWN now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.utcnow", return_value=now): - await hass.services.async_call( - DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: ENTITY_PUSH}, - blocking=True, - ) + freezer.move_to(now) + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ENTITY_PUSH}, + blocking=True, + ) state = hass.states.get(ENTITY_PUSH) assert state From dff7725c1f76a786aa9f044d895da2414500c812 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 14:52:44 +0100 Subject: [PATCH 100/118] Fix goodwe tests (#105653) --- tests/components/goodwe/snapshots/test_diagnostics.ambr | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/goodwe/snapshots/test_diagnostics.ambr b/tests/components/goodwe/snapshots/test_diagnostics.ambr index f259e020cd54d9..4097848a34a255 100644 --- a/tests/components/goodwe/snapshots/test_diagnostics.ambr +++ b/tests/components/goodwe/snapshots/test_diagnostics.ambr @@ -9,6 +9,7 @@ 'disabled_by': None, 'domain': 'goodwe', 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'minor_version': 1, 'options': dict({ }), 'pref_disable_new_entities': False, From abac68f158ad488d358daeeb17afba12752df971 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 15:20:29 +0100 Subject: [PATCH 101/118] Avoid mutating entity descriptions in efergy (#105626) --- homeassistant/components/efergy/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 6fc6eed40f6906..809f1c531da798 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -1,6 +1,7 @@ """Support for Efergy sensors.""" from __future__ import annotations +import dataclasses from re import sub from typing import cast @@ -121,7 +122,10 @@ async def async_setup_entry( ) ) else: - description.entity_registry_enabled_default = len(api.sids) > 1 + description = dataclasses.replace( + description, + entity_registry_enabled_default=len(api.sids) > 1, + ) for sid in api.sids: sensors.append( EfergySensor( From 7ab003c746c1a889f2072d71ad5e0b068a6c2fba Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 15:22:29 +0100 Subject: [PATCH 102/118] Avoid mutating entity descriptions in lidarr (#105628) --- homeassistant/components/lidarr/sensor.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lidarr/sensor.py b/homeassistant/components/lidarr/sensor.py index 1a2930c8051709..552bc35768fb59 100644 --- a/homeassistant/components/lidarr/sensor.py +++ b/homeassistant/components/lidarr/sensor.py @@ -2,8 +2,7 @@ from __future__ import annotations from collections.abc import Callable -from copy import deepcopy -from dataclasses import dataclass +import dataclasses from typing import Any, Generic from aiopyarr import LidarrQueue, LidarrQueueItem, LidarrRootFolder @@ -40,21 +39,23 @@ def get_modified_description( description: LidarrSensorEntityDescription[T], mount: LidarrRootFolder ) -> tuple[LidarrSensorEntityDescription[T], str]: """Return modified description and folder name.""" - desc = deepcopy(description) name = mount.path.rsplit("/")[-1].rsplit("\\")[-1] - desc.key = f"{description.key}_{name}" - desc.name = f"{description.name} {name}".capitalize() + desc = dataclasses.replace( + description, + key=f"{description.key}_{name}", + name=f"{description.name} {name}".capitalize(), + ) return desc, name -@dataclass +@dataclasses.dataclass class LidarrSensorEntityDescriptionMixIn(Generic[T]): """Mixin for required keys.""" value_fn: Callable[[T, str], str | int] -@dataclass +@dataclasses.dataclass class LidarrSensorEntityDescription( SensorEntityDescription, LidarrSensorEntityDescriptionMixIn[T], Generic[T] ): From 2d59eba4c74dad310e358d93719a551b8bb07b0c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 15:23:38 +0100 Subject: [PATCH 103/118] Avoid mutating entity descriptions in airthings_ble (#105627) --- homeassistant/components/airthings_ble/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index aaeb91cf30bc1a..c4797713bb8a5a 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -1,6 +1,7 @@ """Support for airthings ble sensors.""" from __future__ import annotations +import dataclasses import logging from airthings_ble import AirthingsDevice @@ -167,10 +168,13 @@ async def async_setup_entry( # we need to change some units sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy() if not is_metric: - for val in sensors_mapping.values(): + for key, val in sensors_mapping.items(): if val.native_unit_of_measurement is not VOLUME_BECQUEREL: continue - val.native_unit_of_measurement = VOLUME_PICOCURIE + sensors_mapping[key] = dataclasses.replace( + val, + native_unit_of_measurement=VOLUME_PICOCURIE, + ) entities = [] _LOGGER.debug("got sensors: %s", coordinator.data.sensors) From e475829ce63d1a0c921c94c5e073dba411942d54 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:24:26 -0500 Subject: [PATCH 104/118] Reload ZHA integration on any error, not just recoverable ones (#105659) --- homeassistant/components/zha/__init__.py | 79 ++++++++++------------ homeassistant/components/zha/core/const.py | 3 - tests/components/zha/conftest.py | 2 +- tests/components/zha/test_repairs.py | 4 +- 4 files changed, 36 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 340e0db40a696b..1eb3369c1bef9c 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -37,8 +37,6 @@ DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, - STARTUP_FAILURE_DELAY_S, - STARTUP_RETRIES, RadioType, ) from .core.device import get_device_automation_triggers @@ -161,49 +159,40 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("Trigger cache: %s", zha_data.device_trigger_cache) - # Retry setup a few times before giving up to deal with missing serial ports in VMs - for attempt in range(STARTUP_RETRIES): - try: - zha_gateway = await ZHAGateway.async_from_config( - hass=hass, - config=zha_data.yaml_config, - config_entry=config_entry, - ) - break - except NetworkSettingsInconsistent as exc: - await warn_on_inconsistent_network_settings( - hass, - config_entry=config_entry, - old_state=exc.old_state, - new_state=exc.new_state, - ) - raise ConfigEntryError( - "Network settings do not match most recent backup" - ) from exc - except TransientConnectionError as exc: - raise ConfigEntryNotReady from exc - except Exception as exc: - _LOGGER.debug( - "Couldn't start coordinator (attempt %s of %s)", - attempt + 1, - STARTUP_RETRIES, - exc_info=exc, - ) - - if attempt < STARTUP_RETRIES - 1: - await asyncio.sleep(STARTUP_FAILURE_DELAY_S) - continue - - if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp: - try: - # Ignore all exceptions during probing, they shouldn't halt setup - await warn_on_wrong_silabs_firmware( - hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - ) - except AlreadyRunningEZSP as ezsp_exc: - raise ConfigEntryNotReady from ezsp_exc - - raise + try: + zha_gateway = await ZHAGateway.async_from_config( + hass=hass, + config=zha_data.yaml_config, + config_entry=config_entry, + ) + except NetworkSettingsInconsistent as exc: + await warn_on_inconsistent_network_settings( + hass, + config_entry=config_entry, + old_state=exc.old_state, + new_state=exc.new_state, + ) + raise ConfigEntryError( + "Network settings do not match most recent backup" + ) from exc + except TransientConnectionError as exc: + raise ConfigEntryNotReady from exc + except Exception as exc: + _LOGGER.debug("Failed to set up ZHA", exc_info=exc) + device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + + if ( + not device_path.startswith("socket://") + and RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp + ): + try: + # Ignore all exceptions during probing, they shouldn't halt setup + if await warn_on_wrong_silabs_firmware(hass, device_path): + raise ConfigEntryError("Incorrect firmware installed") from exc + except AlreadyRunningEZSP as ezsp_exc: + raise ConfigEntryNotReady from ezsp_exc + + raise ConfigEntryNotReady from exc repairs.async_delete_blocking_issues(hass) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index f89ed8d9a52913..ecbd347a6211c3 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -409,9 +409,6 @@ class Strobe(t.enum8): Strobe = 0x01 -STARTUP_FAILURE_DELAY_S = 3 -STARTUP_RETRIES = 3 - EZSP_OVERWRITE_EUI64 = ( "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" ) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 1b3a536007ad55..55405d0a51c812 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -46,7 +46,7 @@ def disable_request_retry_delay(): with patch( "homeassistant.components.zha.core.cluster_handlers.RETRYABLE_REQUEST_DECORATOR", zigpy.util.retryable_request(tries=3, delay=0), - ), patch("homeassistant.components.zha.STARTUP_FAILURE_DELAY_S", 0.01): + ): yield diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index d168e2e57b12a3..0efff5ecb526f1 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -95,7 +95,6 @@ def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER -@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) @pytest.mark.parametrize( ("detected_hardware", "expected_learn_more_url"), [ @@ -176,7 +175,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state == ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -189,7 +188,6 @@ async def test_multipan_firmware_no_repair_on_probe_failure( assert issue is None -@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) async def test_multipan_firmware_retry_on_probe_ezsp( hass: HomeAssistant, config_entry: MockConfigEntry, From 816a37f9fc3b324fdb1edf7db9145a64a258afe6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 16:48:46 +0100 Subject: [PATCH 105/118] Fix timing issue in Withings (#105203) --- homeassistant/components/withings/__init__.py | 87 +++++++++++-------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 701f7f444cfd39..f42fb7a57b98d7 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -192,52 +192,67 @@ async def _refresh_token() -> str: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = withings_data + register_lock = asyncio.Lock() + webhooks_registered = False + async def unregister_webhook( _: Any, ) -> None: - LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID]) - webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await async_unsubscribe_webhooks(client) - for coordinator in withings_data.coordinators: - coordinator.webhook_subscription_listener(False) + nonlocal webhooks_registered + async with register_lock: + LOGGER.debug( + "Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID] + ) + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + await async_unsubscribe_webhooks(client) + for coordinator in withings_data.coordinators: + coordinator.webhook_subscription_listener(False) + webhooks_registered = False async def register_webhook( _: Any, ) -> None: - if cloud.async_active_subscription(hass): - webhook_url = await _async_cloudhook_generate_url(hass, entry) - else: - webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) - url = URL(webhook_url) - if url.scheme != "https" or url.port != 443: - LOGGER.warning( - "Webhook not registered - " - "https and port 443 is required to register the webhook" + nonlocal webhooks_registered + async with register_lock: + if webhooks_registered: + return + if cloud.async_active_subscription(hass): + webhook_url = await _async_cloudhook_generate_url(hass, entry) + else: + webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) + url = URL(webhook_url) + if url.scheme != "https" or url.port != 443: + LOGGER.warning( + "Webhook not registered - " + "https and port 443 is required to register the webhook" + ) + return + + webhook_name = "Withings" + if entry.title != DEFAULT_TITLE: + webhook_name = f"{DEFAULT_TITLE} {entry.title}" + + webhook_register( + hass, + DOMAIN, + webhook_name, + entry.data[CONF_WEBHOOK_ID], + get_webhook_handler(withings_data), + allowed_methods=[METH_POST], ) - return - - webhook_name = "Withings" - if entry.title != DEFAULT_TITLE: - webhook_name = f"{DEFAULT_TITLE} {entry.title}" - - webhook_register( - hass, - DOMAIN, - webhook_name, - entry.data[CONF_WEBHOOK_ID], - get_webhook_handler(withings_data), - allowed_methods=[METH_POST], - ) - - await async_subscribe_webhooks(client, webhook_url) - for coordinator in withings_data.coordinators: - coordinator.webhook_subscription_listener(True) - LOGGER.debug("Register Withings webhook: %s", webhook_url) - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) - ) + LOGGER.debug("Registered Withings webhook at hass: %s", webhook_url) + + await async_subscribe_webhooks(client, webhook_url) + for coordinator in withings_data.coordinators: + coordinator.webhook_subscription_listener(True) + LOGGER.debug("Registered Withings webhook at Withings: %s", webhook_url) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) + webhooks_registered = True async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: + LOGGER.debug("Cloudconnection state changed to %s", state) if state is cloud.CloudConnectionState.CLOUD_CONNECTED: await register_webhook(None) From e4453ace881b68fafc65c8adb6821ae265c73303 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 16:50:46 +0100 Subject: [PATCH 106/118] Add country code constant (#105640) --- homeassistant/components/buienradar/camera.py | 14 +++++--------- homeassistant/components/buienradar/config_flow.py | 7 ++++--- homeassistant/components/buienradar/const.py | 1 - homeassistant/components/co2signal/config_flow.py | 9 +++++++-- homeassistant/components/co2signal/const.py | 1 - homeassistant/components/co2signal/helpers.py | 4 +--- homeassistant/components/co2signal/util.py | 4 +--- homeassistant/components/picnic/__init__.py | 4 ++-- homeassistant/components/picnic/config_flow.py | 9 +++++++-- homeassistant/components/picnic/const.py | 1 - homeassistant/components/tuya/__init__.py | 2 +- homeassistant/components/tuya/config_flow.py | 2 +- homeassistant/components/tuya/const.py | 1 - homeassistant/components/tuya/diagnostics.py | 10 ++-------- homeassistant/const.py | 1 + tests/components/buienradar/test_camera.py | 6 +++--- tests/components/picnic/test_config_flow.py | 4 ++-- tests/components/picnic/test_sensor.py | 3 ++- tests/components/tuya/test_config_flow.py | 2 +- 19 files changed, 40 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 439921928d65e1..1963041bccad37 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -10,19 +10,13 @@ from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import ( - CONF_COUNTRY, - CONF_DELTA, - DEFAULT_COUNTRY, - DEFAULT_DELTA, - DEFAULT_DIMENSION, -) +from .const import CONF_DELTA, DEFAULT_COUNTRY, DEFAULT_DELTA, DEFAULT_DIMENSION _LOGGER = logging.getLogger(__name__) @@ -40,7 +34,9 @@ async def async_setup_entry( config = entry.data options = entry.options - country = options.get(CONF_COUNTRY, config.get(CONF_COUNTRY, DEFAULT_COUNTRY)) + country = options.get( + CONF_COUNTRY_CODE, config.get(CONF_COUNTRY_CODE, DEFAULT_COUNTRY) + ) delta = options.get(CONF_DELTA, config.get(CONF_DELTA, DEFAULT_DELTA)) diff --git a/homeassistant/components/buienradar/config_flow.py b/homeassistant/components/buienradar/config_flow.py index 4a81a774b4fb75..1e77693f7fba11 100644 --- a/homeassistant/components/buienradar/config_flow.py +++ b/homeassistant/components/buienradar/config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector @@ -20,7 +20,6 @@ ) from .const import ( - CONF_COUNTRY, CONF_DELTA, CONF_TIMEFRAME, DEFAULT_COUNTRY, @@ -32,7 +31,9 @@ OPTIONS_SCHEMA = vol.Schema( { - vol.Optional(CONF_COUNTRY, default=DEFAULT_COUNTRY): selector.CountrySelector( + vol.Optional( + CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY + ): selector.CountrySelector( selector.CountrySelectorConfig(countries=SUPPORTED_COUNTRY_CODES) ), vol.Optional(CONF_DELTA, default=DEFAULT_DELTA): selector.NumberSelector( diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py index 718812c5c731fc..c82970ed318837 100644 --- a/homeassistant/components/buienradar/const.py +++ b/homeassistant/components/buienradar/const.py @@ -8,7 +8,6 @@ DEFAULT_DELTA = 600 CONF_DELTA = "delta" -CONF_COUNTRY = "country_code" CONF_TIMEFRAME = "timeframe" SUPPORTED_COUNTRY_CODES = ["NL", "BE"] diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index 234c1c01392a4f..dfa1e25d7d8907 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -10,7 +10,12 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import ( + CONF_API_KEY, + CONF_COUNTRY_CODE, + CONF_LATITUDE, + CONF_LONGITUDE, +) from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -20,7 +25,7 @@ SelectSelectorMode, ) -from .const import CONF_COUNTRY_CODE, DOMAIN +from .const import DOMAIN from .helpers import fetch_latest_carbon_intensity from .util import get_extra_name diff --git a/homeassistant/components/co2signal/const.py b/homeassistant/components/co2signal/const.py index 1e0cbfe0f11275..b025c655ce6cbc 100644 --- a/homeassistant/components/co2signal/const.py +++ b/homeassistant/components/co2signal/const.py @@ -2,5 +2,4 @@ DOMAIN = "co2signal" -CONF_COUNTRY_CODE = "country_code" ATTRIBUTION = "Data provided by Electricity Maps" diff --git a/homeassistant/components/co2signal/helpers.py b/homeassistant/components/co2signal/helpers.py index 43579c162e293a..937b72a357cecd 100644 --- a/homeassistant/components/co2signal/helpers.py +++ b/homeassistant/components/co2signal/helpers.py @@ -5,11 +5,9 @@ from aioelectricitymaps import ElectricityMaps from aioelectricitymaps.models import CarbonIntensityResponse -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from .const import CONF_COUNTRY_CODE - async def fetch_latest_carbon_intensity( hass: HomeAssistant, diff --git a/homeassistant/components/co2signal/util.py b/homeassistant/components/co2signal/util.py index af0bec34904cf4..68403b4803e7cc 100644 --- a/homeassistant/components/co2signal/util.py +++ b/homeassistant/components/co2signal/util.py @@ -3,9 +3,7 @@ from collections.abc import Mapping -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE - -from .const import CONF_COUNTRY_CODE +from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE def get_extra_name(config: Mapping) -> str | None: diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index 6826d8940abb35..d2f023af79f4c8 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -3,10 +3,10 @@ from python_picnic_api import PicnicAPI from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY_CODE, Platform from homeassistant.core import HomeAssistant -from .const import CONF_API, CONF_COORDINATOR, CONF_COUNTRY_CODE, DOMAIN +from .const import CONF_API, CONF_COORDINATOR, DOMAIN from .coordinator import PicnicUpdateCoordinator from .services import async_register_services diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py index 65ae201482acb5..b02c0a74bfce55 100644 --- a/homeassistant/components/picnic/config_flow.py +++ b/homeassistant/components/picnic/config_flow.py @@ -12,10 +12,15 @@ from homeassistant import config_entries, core, exceptions from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_COUNTRY_CODE, + CONF_PASSWORD, + CONF_USERNAME, +) from homeassistant.data_entry_flow import FlowResult -from .const import CONF_COUNTRY_CODE, COUNTRY_CODES, DOMAIN +from .const import COUNTRY_CODES, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 7e983321f3d618..a2543c177e4e12 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -5,7 +5,6 @@ CONF_API = "api" CONF_COORDINATOR = "coordinator" -CONF_COUNTRY_CODE = "country_code" SERVICE_ADD_PRODUCT_TO_CART = "add_product" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 276d21f3821947..d0ae13c09b7038 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -15,6 +15,7 @@ ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_COUNTRY_CODE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -25,7 +26,6 @@ CONF_ACCESS_SECRET, CONF_APP_TYPE, CONF_AUTH_TYPE, - CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, CONF_USERNAME, diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index bf2c54a6158b45..eb490791f7e1e6 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -7,13 +7,13 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import CONF_COUNTRY_CODE from .const import ( CONF_ACCESS_ID, CONF_ACCESS_SECRET, CONF_APP_TYPE, CONF_AUTH_TYPE, - CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, CONF_USERNAME, diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 19faa76a191e48..56dbbc4fa40428 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -38,7 +38,6 @@ CONF_ACCESS_SECRET = "access_secret" CONF_USERNAME = "username" CONF_PASSWORD = "password" -CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" TUYA_DISCOVERY_NEW = "tuya_discovery_new" diff --git a/homeassistant/components/tuya/diagnostics.py b/homeassistant/components/tuya/diagnostics.py index 454416970eaa5d..adac97174b9117 100644 --- a/homeassistant/components/tuya/diagnostics.py +++ b/homeassistant/components/tuya/diagnostics.py @@ -9,20 +9,14 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_COUNTRY_CODE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.util import dt as dt_util from . import HomeAssistantTuyaData -from .const import ( - CONF_APP_TYPE, - CONF_AUTH_TYPE, - CONF_COUNTRY_CODE, - CONF_ENDPOINT, - DOMAIN, - DPCode, -) +from .const import CONF_APP_TYPE, CONF_AUTH_TYPE, CONF_ENDPOINT, DOMAIN, DPCode async def async_get_config_entry_diagnostics( diff --git a/homeassistant/const.py b/homeassistant/const.py index 8da1c251b4ef6f..df68e3ab05abf3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -129,6 +129,7 @@ class Platform(StrEnum): CONF_CONTINUE_ON_TIMEOUT: Final = "continue_on_timeout" CONF_COUNT: Final = "count" CONF_COUNTRY: Final = "country" +CONF_COUNTRY_CODE: Final = "country_code" CONF_COVERS: Final = "covers" CONF_CURRENCY: Final = "currency" CONF_CUSTOMIZE: Final = "customize" diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index 027fde853c1907..f048f8d69a778d 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -6,8 +6,8 @@ from aiohttp.client_exceptions import ClientResponseError -from homeassistant.components.buienradar.const import CONF_COUNTRY, CONF_DELTA, DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.components.buienradar.const import CONF_DELTA, DOMAIN +from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util @@ -144,7 +144,7 @@ async def test_belgium_country( aioclient_mock.get(radar_map_url(country_code="BE"), text="hello world") data = copy.deepcopy(TEST_CFG_DATA) - data[CONF_COUNTRY] = "BE" + data[CONF_COUNTRY_CODE] = "BE" mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=data) diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index a649240bd21958..d90551b01df1a7 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -6,8 +6,8 @@ import requests from homeassistant import config_entries, data_entry_flow -from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, DOMAIN -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.components.picnic.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY_CODE from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index cae10320fb9be4..fb1fbe9f009fe2 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -9,11 +9,12 @@ from homeassistant import config_entries from homeassistant.components.picnic import const -from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, DOMAIN +from homeassistant.components.picnic.const import DOMAIN from homeassistant.components.picnic.sensor import SENSOR_TYPES from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( CONF_ACCESS_TOKEN, + CONF_COUNTRY_CODE, CURRENCY_EURO, STATE_UNAVAILABLE, STATE_UNKNOWN, diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 0630114da9035f..9505e1ef423fb7 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -13,7 +13,6 @@ CONF_ACCESS_SECRET, CONF_APP_TYPE, CONF_AUTH_TYPE, - CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, CONF_USERNAME, @@ -22,6 +21,7 @@ TUYA_COUNTRIES, TUYA_SMART_APP, ) +from homeassistant.const import CONF_COUNTRY_CODE from homeassistant.core import HomeAssistant MOCK_SMART_HOME_PROJECT_TYPE = 0 From bbfffbb47ec273351595c53c147c07981ad3a1ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 16:57:22 +0100 Subject: [PATCH 107/118] Avoid mutating entity descriptions in melcloud (#105629) --- homeassistant/components/melcloud/sensor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index ca02d15db01c74..1cb8930049d6ba 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass +import dataclasses from typing import Any from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW @@ -23,7 +23,7 @@ from .const import DOMAIN -@dataclass +@dataclasses.dataclass class MelcloudRequiredKeysMixin: """Mixin for required keys.""" @@ -31,7 +31,7 @@ class MelcloudRequiredKeysMixin: enabled: Callable[[Any], bool] -@dataclass +@dataclasses.dataclass class MelcloudSensorEntityDescription( SensorEntityDescription, MelcloudRequiredKeysMixin ): @@ -203,7 +203,10 @@ def __init__( ) -> None: """Initialize the sensor.""" if zone.zone_index != 1: - description.key = f"{description.key}-zone-{zone.zone_index}" + description = dataclasses.replace( + description, + key=f"{description.key}-zone-{zone.zone_index}", + ) super().__init__(api, description) self._attr_device_info = api.zone_device_info(zone) From a82410d5e991a41661664c125b1d6a39a2393022 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 17:05:37 +0100 Subject: [PATCH 108/118] Deduplicate constants E-Z (#105657) --- .../components/airthings/__init__.py | 4 +-- .../components/airthings/config_flow.py | 3 ++- homeassistant/components/airthings/const.py | 1 - .../components/anthemav/config_flow.py | 10 ++----- homeassistant/components/anthemav/const.py | 2 +- .../components/anthemav/media_player.py | 4 +-- homeassistant/components/dsmr/config_flow.py | 3 +-- homeassistant/components/dsmr/const.py | 1 - homeassistant/components/dsmr/sensor.py | 2 +- .../components/environment_canada/__init__.py | 4 +-- .../environment_canada/config_flow.py | 4 +-- .../components/environment_canada/const.py | 1 - .../components/eufylife_ble/__init__.py | 4 +-- .../components/eufylife_ble/config_flow.py | 4 +-- .../components/eufylife_ble/const.py | 2 -- .../components/flux_led/config_flow.py | 6 ++--- homeassistant/components/flux_led/const.py | 1 - homeassistant/components/flux_led/light.py | 2 +- .../components/frontier_silicon/__init__.py | 4 +-- .../frontier_silicon/config_flow.py | 3 +-- .../components/frontier_silicon/const.py | 1 - .../google_travel_time/config_flow.py | 3 +-- .../components/google_travel_time/const.py | 1 - .../components/homewizard/config_flow.py | 3 +-- homeassistant/components/homewizard/const.py | 1 - homeassistant/components/influxdb/__init__.py | 16 ++++++------ homeassistant/components/influxdb/const.py | 1 - homeassistant/components/influxdb/sensor.py | 2 +- homeassistant/components/knx/button.py | 10 ++----- homeassistant/components/knx/const.py | 1 - homeassistant/components/knx/schema.py | 2 +- homeassistant/components/knx/select.py | 2 +- homeassistant/components/lcn/const.py | 1 - homeassistant/components/lcn/helpers.py | 2 +- .../components/livisi/config_flow.py | 3 ++- homeassistant/components/livisi/const.py | 2 -- .../components/livisi/coordinator.py | 3 +-- homeassistant/components/nextbus/__init__.py | 4 +-- .../components/nextbus/config_flow.py | 4 +-- homeassistant/components/nextbus/const.py | 1 - homeassistant/components/nextbus/sensor.py | 4 +-- .../components/nextdns/config_flow.py | 4 +-- homeassistant/components/nextdns/const.py | 1 - .../components/openweathermap/__init__.py | 2 +- .../components/openweathermap/config_flow.py | 2 +- .../components/openweathermap/const.py | 1 - .../components/purpleair/__init__.py | 9 +++++-- .../components/purpleair/config_flow.py | 9 +++++-- homeassistant/components/purpleair/const.py | 1 - homeassistant/components/rachio/__init__.py | 4 +-- homeassistant/components/rachio/const.py | 1 - homeassistant/components/rachio/webhooks.py | 3 +-- .../components/reolink/config_flow.py | 10 +++++-- homeassistant/components/reolink/const.py | 1 - homeassistant/components/reolink/host.py | 10 +++++-- .../components/totalconnect/const.py | 1 - .../trafikverket_camera/__init__.py | 4 +-- .../trafikverket_camera/config_flow.py | 4 +-- .../components/trafikverket_camera/const.py | 1 - homeassistant/components/tuya/__init__.py | 4 +-- homeassistant/components/tuya/config_flow.py | 4 +-- homeassistant/components/tuya/const.py | 2 -- homeassistant/components/twinkly/__init__.py | 4 +-- .../components/twinkly/config_flow.py | 4 +-- homeassistant/components/twinkly/const.py | 5 ---- homeassistant/components/twinkly/light.py | 11 +++++--- .../components/watttime/config_flow.py | 2 +- homeassistant/components/watttime/const.py | 1 - homeassistant/components/watttime/sensor.py | 15 ++++++----- .../components/xiaomi_miio/config_flow.py | 3 +-- homeassistant/components/xiaomi_miio/const.py | 1 - .../components/xiaomi_miio/device.py | 4 +-- homeassistant/components/youtube/const.py | 1 - .../components/airthings/test_config_flow.py | 3 ++- tests/components/anthemav/conftest.py | 4 +-- .../environment_canada/test_config_flow.py | 8 ++---- .../environment_canada/test_diagnostics.py | 8 ++---- tests/components/flux_led/test_light.py | 2 +- tests/components/frontier_silicon/conftest.py | 7 ++--- .../google_travel_time/test_config_flow.py | 3 +-- tests/components/knx/test_button.py | 9 ++----- tests/components/knx/test_select.py | 3 +-- tests/components/livisi/__init__.py | 2 +- tests/components/nextbus/test_config_flow.py | 9 ++----- tests/components/nextbus/test_sensor.py | 9 ++----- tests/components/nextdns/test_config_flow.py | 8 ++---- .../openweathermap/test_config_flow.py | 2 +- tests/components/reolink/conftest.py | 10 +++++-- tests/components/reolink/test_config_flow.py | 26 ++++++++++++------- tests/components/reolink/test_media_source.py | 3 ++- .../trafikverket_camera/__init__.py | 3 +-- .../trafikverket_camera/test_config_flow.py | 4 +-- tests/components/tuya/test_config_flow.py | 4 +-- tests/components/twinkly/__init__.py | 2 +- tests/components/twinkly/test_config_flow.py | 9 ++----- tests/components/twinkly/test_init.py | 9 ++----- tests/components/twinkly/test_light.py | 9 ++----- tests/components/watttime/test_config_flow.py | 2 +- tests/components/xiaomi_miio/test_button.py | 2 +- .../xiaomi_miio/test_config_flow.py | 26 +++++++++---------- tests/components/xiaomi_miio/test_select.py | 2 +- tests/components/xiaomi_miio/test_vacuum.py | 2 +- 102 files changed, 193 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 423e890a855aed..d596c1db757b5e 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -7,12 +7,12 @@ from airthings import Airthings, AirthingsError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_ID, CONF_SECRET, DOMAIN +from .const import CONF_SECRET, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airthings/config_flow.py b/homeassistant/components/airthings/config_flow.py index f07f7164f2bc8c..62f66213a0f2f7 100644 --- a/homeassistant/components/airthings/config_flow.py +++ b/homeassistant/components/airthings/config_flow.py @@ -8,10 +8,11 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import CONF_ID from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_ID, CONF_SECRET, DOMAIN +from .const import CONF_SECRET, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airthings/const.py b/homeassistant/components/airthings/const.py index 70de549141b32c..5f846fbb31dd62 100644 --- a/homeassistant/components/airthings/const.py +++ b/homeassistant/components/airthings/const.py @@ -2,5 +2,4 @@ DOMAIN = "airthings" -CONF_ID = "id" CONF_SECRET = "secret" diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py index e75c67cb2c521a..892c40cde0e60d 100644 --- a/homeassistant/components/anthemav/config_flow.py +++ b/homeassistant/components/anthemav/config_flow.py @@ -10,18 +10,12 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_PORT from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac -from .const import ( - CONF_MODEL, - DEFAULT_NAME, - DEFAULT_PORT, - DEVICE_TIMEOUT_SECONDS, - DOMAIN, -) +from .const import DEFAULT_NAME, DEFAULT_PORT, DEVICE_TIMEOUT_SECONDS, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py index 2b1ff753fba6b3..7cf586fb05d845 100644 --- a/homeassistant/components/anthemav/const.py +++ b/homeassistant/components/anthemav/const.py @@ -1,6 +1,6 @@ """Constants for the Anthem A/V Receivers integration.""" ANTHEMAV_UPDATE_SIGNAL = "anthemav_update" -CONF_MODEL = "model" + DEFAULT_NAME = "Anthem AV" DEFAULT_PORT = 14999 DOMAIN = "anthemav" diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 91f8536d348f46..c13e6389bfc2d9 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -13,13 +13,13 @@ MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC +from homeassistant.const import CONF_MAC, CONF_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ANTHEMAV_UPDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER +from .const import ANTHEMAV_UPDATE_SIGNAL, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 86a7bee9ef1b3d..376b4d100fc97c 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -19,13 +19,12 @@ from homeassistant import config_entries, core, exceptions from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_DSMR_VERSION, - CONF_PROTOCOL, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, CONF_TIME_BETWEEN_UPDATE, diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 4ac59372debaed..9504929c5a9a3d 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -11,7 +11,6 @@ PLATFORMS = [Platform.SENSOR] CONF_DSMR_VERSION = "dsmr_version" -CONF_PROTOCOL = "protocol" CONF_TIME_BETWEEN_UPDATE = "time_between_update" CONF_SERIAL_ID = "serial_id" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 6aadcd63d44e99..9c511ef9191303 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, + CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP, EntityCategory, UnitOfEnergy, @@ -46,7 +47,6 @@ from .const import ( CONF_DSMR_VERSION, - CONF_PROTOCOL, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, CONF_TIME_BETWEEN_UPDATE, diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 64a4b7dad20cab..14fb3e8e54c8fb 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -6,13 +6,13 @@ from env_canada import ECAirQuality, ECRadar, ECWeather, ec_exc from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform +from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN +from .const import CONF_STATION, DOMAIN DEFAULT_RADAR_UPDATE_INTERVAL = timedelta(minutes=5) DEFAULT_WEATHER_UPDATE_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index 07b6eac0da0350..f4b9ee792c3964 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -7,10 +7,10 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv -from .const import CONF_LANGUAGE, CONF_STATION, CONF_TITLE, DOMAIN +from .const import CONF_STATION, CONF_TITLE, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/environment_canada/const.py b/homeassistant/components/environment_canada/const.py index 16f7dc1cf9902c..f1f6db2e0df9bb 100644 --- a/homeassistant/components/environment_canada/const.py +++ b/homeassistant/components/environment_canada/const.py @@ -2,7 +2,6 @@ ATTR_OBSERVATION_TIME = "observation_time" ATTR_STATION = "station" -CONF_LANGUAGE = "language" CONF_STATION = "station" CONF_TITLE = "title" DOMAIN = "environment_canada" diff --git a/homeassistant/components/eufylife_ble/__init__.py b/homeassistant/components/eufylife_ble/__init__.py index 49370c2efcf84b..f407e86a289d81 100644 --- a/homeassistant/components/eufylife_ble/__init__.py +++ b/homeassistant/components/eufylife_ble/__init__.py @@ -6,10 +6,10 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import CONF_MODEL, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant, callback -from .const import CONF_MODEL, DOMAIN +from .const import DOMAIN from .models import EufyLifeData PLATFORMS: list[Platform] = [Platform.SENSOR] diff --git a/homeassistant/components/eufylife_ble/config_flow.py b/homeassistant/components/eufylife_ble/config_flow.py index 9e1ff4af7a8a95..e3a1a301f2548c 100644 --- a/homeassistant/components/eufylife_ble/config_flow.py +++ b/homeassistant/components/eufylife_ble/config_flow.py @@ -11,10 +11,10 @@ async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_MODEL from homeassistant.data_entry_flow import FlowResult -from .const import CONF_MODEL, DOMAIN +from .const import DOMAIN class EufyLifeConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/eufylife_ble/const.py b/homeassistant/components/eufylife_ble/const.py index dac0afc910956e..e6beb34aaff98a 100644 --- a/homeassistant/components/eufylife_ble/const.py +++ b/homeassistant/components/eufylife_ble/const.py @@ -1,5 +1,3 @@ """Constants for the EufyLife integration.""" DOMAIN = "eufylife_ble" - -CONF_MODEL = "model" diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 11e045bec703d1..9094006c791e8e 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import contextlib -from typing import Any, Final, cast +from typing import Any, cast from flux_led.const import ( ATTR_ID, @@ -17,7 +17,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr @@ -47,8 +47,6 @@ ) from .util import format_as_flux_mac, mac_matches_by_one -CONF_DEVICE: Final = "device" - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Magic Home Integration.""" diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index db545aa1e68248..8b42f5f2e0dddf 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -65,7 +65,6 @@ CONF_COLORS: Final = "colors" CONF_SPEED_PCT: Final = "speed_pct" CONF_TRANSITION: Final = "transition" -CONF_EFFECT: Final = "effect" EFFECT_SPEED_SUPPORT_MODES: Final = {ColorMode.RGB, ColorMode.RGBW, ColorMode.RGBWW} diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index d880d517f1ab6c..1232cb41031c94 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -22,6 +22,7 @@ LightEntity, LightEntityFeature, ) +from homeassistant.const import CONF_EFFECT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -37,7 +38,6 @@ CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, - CONF_EFFECT, CONF_SPEED_PCT, CONF_TRANSITION, DEFAULT_EFFECT_SPEED, diff --git a/homeassistant/components/frontier_silicon/__init__.py b/homeassistant/components/frontier_silicon/__init__.py index 62f2623d05ea71..f1e0ad48d30a04 100644 --- a/homeassistant/components/frontier_silicon/__init__.py +++ b/homeassistant/components/frontier_silicon/__init__.py @@ -6,11 +6,11 @@ from afsapi import AFSAPI, ConnectionError as FSConnectionError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_PIN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import CONF_PIN, CONF_WEBFSAPI_URL, DOMAIN +from .const import CONF_WEBFSAPI_URL, DOMAIN PLATFORMS = [Platform.MEDIA_PLAYER] diff --git a/homeassistant/components/frontier_silicon/config_flow.py b/homeassistant/components/frontier_silicon/config_flow.py index 2274b1cdb4499c..470be7d9b26044 100644 --- a/homeassistant/components/frontier_silicon/config_flow.py +++ b/homeassistant/components/frontier_silicon/config_flow.py @@ -16,11 +16,10 @@ from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT from homeassistant.data_entry_flow import FlowResult from .const import ( - CONF_PIN, CONF_WEBFSAPI_URL, DEFAULT_PIN, DEFAULT_PORT, diff --git a/homeassistant/components/frontier_silicon/const.py b/homeassistant/components/frontier_silicon/const.py index 34201fe8f4a316..94f4e09a35a859 100644 --- a/homeassistant/components/frontier_silicon/const.py +++ b/homeassistant/components/frontier_silicon/const.py @@ -2,7 +2,6 @@ DOMAIN = "frontier_silicon" CONF_WEBFSAPI_URL = "webfsapi_url" -CONF_PIN = "pin" SSDP_ST = "urn:schemas-frontier-silicon-com:undok:fsapi:1" SSDP_ATTR_SPEAKER_NAME = "SPEAKER-NAME" diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 83e144f6bbdc0b..ec8187d91af1c7 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv @@ -23,7 +23,6 @@ CONF_AVOID, CONF_DEPARTURE_TIME, CONF_DESTINATION, - CONF_LANGUAGE, CONF_ORIGIN, CONF_TIME, CONF_TIME_TYPE, diff --git a/homeassistant/components/google_travel_time/const.py b/homeassistant/components/google_travel_time/const.py index 0535e295b93a0e..041858d948fff6 100644 --- a/homeassistant/components/google_travel_time/const.py +++ b/homeassistant/components/google_travel_time/const.py @@ -7,7 +7,6 @@ CONF_OPTIONS = "options" CONF_ORIGIN = "origin" CONF_TRAVEL_MODE = "travel_mode" -CONF_LANGUAGE = "language" CONF_AVOID = "avoid" CONF_UNITS = "units" CONF_ARRIVAL_TIME = "arrival_time" diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index b24b49da96598c..bf425fe5c412cb 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -12,13 +12,12 @@ from homeassistant.components import onboarding, zeroconf from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_API_ENABLED, - CONF_PATH, CONF_PRODUCT_NAME, CONF_PRODUCT_TYPE, CONF_SERIAL, diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index daeed9d3505c7c..f1a1bee256831a 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -17,7 +17,6 @@ # Platform config. CONF_API_ENABLED = "api_enabled" CONF_DATA = "data" -CONF_PATH = "path" CONF_PRODUCT_NAME = "product_name" CONF_PRODUCT_TYPE = "product_type" CONF_SERIAL = "serial" diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index f879ab37e8fcc6..24c80dc1d54c19 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -22,9 +22,17 @@ from homeassistant.const import ( CONF_DOMAIN, CONF_ENTITY_ID, + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_PORT, + CONF_SSL, CONF_TIMEOUT, + CONF_TOKEN, CONF_UNIT_OF_MEASUREMENT, CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, @@ -56,23 +64,15 @@ CONF_COMPONENT_CONFIG_GLOB, CONF_DB_NAME, CONF_DEFAULT_MEASUREMENT, - CONF_HOST, CONF_IGNORE_ATTRIBUTES, CONF_MEASUREMENT_ATTR, CONF_ORG, CONF_OVERRIDE_MEASUREMENT, - CONF_PASSWORD, - CONF_PATH, - CONF_PORT, CONF_PRECISION, CONF_RETRY_COUNT, - CONF_SSL, CONF_SSL_CA_CERT, CONF_TAGS, CONF_TAGS_ATTRIBUTES, - CONF_TOKEN, - CONF_USERNAME, - CONF_VERIFY_SSL, CONNECTION_ERROR, DEFAULT_API_VERSION, DEFAULT_HOST_V2, diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py index f3b0b66df54797..5ffd70fe992a3f 100644 --- a/homeassistant/components/influxdb/const.py +++ b/homeassistant/components/influxdb/const.py @@ -33,7 +33,6 @@ CONF_PRECISION = "precision" CONF_SSL_CA_CERT = "ssl_ca_cert" -CONF_LANGUAGE = "language" CONF_QUERIES = "queries" CONF_QUERIES_FLUX = "queries_flux" CONF_GROUP_FUNCTION = "group_function" diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index b4f643e876f18f..a46ec5812074af 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -13,6 +13,7 @@ ) from homeassistant.const import ( CONF_API_VERSION, + CONF_LANGUAGE, CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -35,7 +36,6 @@ CONF_FIELD, CONF_GROUP_FUNCTION, CONF_IMPORTS, - CONF_LANGUAGE, CONF_MEASUREMENT_NAME, CONF_QUERIES, CONF_QUERIES_FLUX, diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index 274ced801464b5..94b5b51e401d09 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -6,18 +6,12 @@ from homeassistant import config_entries from homeassistant.components.button import ButtonEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_PAYLOAD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_PAYLOAD, - CONF_PAYLOAD_LENGTH, - DATA_KNX_CONFIG, - DOMAIN, - KNX_ADDRESS, -) +from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 519d5d0742d2b0..3d1e3c62a34cc7 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -68,7 +68,6 @@ CONF_KNX_SECURE_DEVICE_AUTHENTICATION: Final = "device_authentication" -CONF_PAYLOAD: Final = "payload" CONF_PAYLOAD_LENGTH: Final = "payload_length" CONF_RESET_AFTER: Final = "reset_after" CONF_RESPOND_TO_READ: Final = "respond_to_read" diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 8240fbaf3c13f7..c7bcd90538f0d1 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -37,6 +37,7 @@ CONF_EVENT, CONF_MODE, CONF_NAME, + CONF_PAYLOAD, CONF_TYPE, Platform, ) @@ -46,7 +47,6 @@ from .const import ( CONF_INVERT, CONF_KNX_EXPOSE, - CONF_PAYLOAD, CONF_PAYLOAD_LENGTH, CONF_RESET_AFTER, CONF_RESPOND_TO_READ, diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index 5baa068eaa6468..2852917e0219d0 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_ENTITY_CATEGORY, CONF_NAME, + CONF_PAYLOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, Platform, @@ -19,7 +20,6 @@ from homeassistant.helpers.typing import ConfigType from .const import ( - CONF_PAYLOAD, CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index bb97658b880d94..e8da5b3907380b 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -21,7 +21,6 @@ CONF_HARDWARE_SERIAL = "hardware_serial" CONF_SOFTWARE_SERIAL = "software_serial" CONF_HARDWARE_TYPE = "hardware_type" -CONF_RESOURCE = "resource" CONF_DOMAIN_DATA = "domain_data" CONF_CONNECTIONS = "connections" diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index e190b25eded2b1..64a789f3a34b05 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -24,6 +24,7 @@ CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_RESOURCE, CONF_SENSORS, CONF_SOURCE, CONF_SWITCHES, @@ -42,7 +43,6 @@ CONF_HARDWARE_SERIAL, CONF_HARDWARE_TYPE, CONF_OUTPUT, - CONF_RESOURCE, CONF_SCENES, CONF_SK_NUM_TRIES, CONF_SOFTWARE_SERIAL, diff --git a/homeassistant/components/livisi/config_flow.py b/homeassistant/components/livisi/config_flow.py index 16cccaacfd113f..c8685eb2390000 100644 --- a/homeassistant/components/livisi/config_flow.py +++ b/homeassistant/components/livisi/config_flow.py @@ -9,10 +9,11 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from .const import CONF_HOST, CONF_PASSWORD, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER class LivisiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/livisi/const.py b/homeassistant/components/livisi/const.py index f6435298f1e3bc..2769e6030eebd7 100644 --- a/homeassistant/components/livisi/const.py +++ b/homeassistant/components/livisi/const.py @@ -5,8 +5,6 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "livisi" -CONF_HOST = "host" -CONF_PASSWORD: Final = "password" AVATAR = "Avatar" AVATAR_PORT: Final = 9090 CLASSIC_PORT: Final = 8080 diff --git a/homeassistant/components/livisi/coordinator.py b/homeassistant/components/livisi/coordinator.py index 56e928307c1734..17a3b1828d0cbe 100644 --- a/homeassistant/components/livisi/coordinator.py +++ b/homeassistant/components/livisi/coordinator.py @@ -9,6 +9,7 @@ from aiolivisi.errors import TokenExpiredException from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -17,8 +18,6 @@ AVATAR, AVATAR_PORT, CLASSIC_PORT, - CONF_HOST, - CONF_PASSWORD, DEVICE_POLLING_DELAY, LIVISI_REACHABILITY_CHANGE, LIVISI_STATE_CHANGE, diff --git a/homeassistant/components/nextbus/__init__.py b/homeassistant/components/nextbus/__init__.py index e1f4dcc284056b..e8c0bc224febc3 100644 --- a/homeassistant/components/nextbus/__init__.py +++ b/homeassistant/components/nextbus/__init__.py @@ -1,10 +1,10 @@ """NextBus platform.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_STOP, Platform from homeassistant.core import HomeAssistant -from .const import CONF_AGENCY, CONF_ROUTE, CONF_STOP, DOMAIN +from .const import CONF_AGENCY, CONF_ROUTE, DOMAIN from .coordinator import NextBusDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/nextbus/config_flow.py b/homeassistant/components/nextbus/config_flow.py index 84417a29c8d09c..a4045ada372914 100644 --- a/homeassistant/components/nextbus/config_flow.py +++ b/homeassistant/components/nextbus/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_STOP from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( SelectOptionDict, @@ -15,7 +15,7 @@ SelectSelectorMode, ) -from .const import CONF_AGENCY, CONF_ROUTE, CONF_STOP, DOMAIN +from .const import CONF_AGENCY, CONF_ROUTE, DOMAIN from .util import listify _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nextbus/const.py b/homeassistant/components/nextbus/const.py index 9d9d0a5262fdd3..0a2eabf57b38af 100644 --- a/homeassistant/components/nextbus/const.py +++ b/homeassistant/components/nextbus/const.py @@ -3,4 +3,3 @@ CONF_AGENCY = "agency" CONF_ROUTE = "route" -CONF_STOP = "stop" diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 6ef647f98adcec..f62bf07eeefa43 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -13,7 +13,7 @@ SensorEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_STOP from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -22,7 +22,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp -from .const import CONF_AGENCY, CONF_ROUTE, CONF_STOP, DOMAIN +from .const import CONF_AGENCY, CONF_ROUTE, DOMAIN from .coordinator import NextBusDataUpdateCoordinator from .util import listify, maybe_first diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index 3985644a478cf3..c502f788a86778 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -9,11 +9,11 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_PROFILE_ID, CONF_PROFILE_NAME, DOMAIN +from .const import CONF_PROFILE_ID, DOMAIN class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py index 8cac556c87c0d4..031dd1c5814e59 100644 --- a/homeassistant/components/nextdns/const.py +++ b/homeassistant/components/nextdns/const.py @@ -10,7 +10,6 @@ ATTR_STATUS = "status" CONF_PROFILE_ID = "profile_id" -CONF_PROFILE_NAME = "profile_name" UPDATE_INTERVAL_CONNECTION = timedelta(minutes=5) UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index d462e34cd846ad..cfe28e2eacc60b 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, + CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -18,7 +19,6 @@ from homeassistant.core import HomeAssistant from .const import ( - CONF_LANGUAGE, CONFIG_FLOW_VERSION, DOMAIN, ENTRY_NAME, diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index c418231946f418..799be35fb42061 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, + CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -17,7 +18,6 @@ import homeassistant.helpers.config_validation as cv from .const import ( - CONF_LANGUAGE, CONFIG_FLOW_VERSION, DEFAULT_FORECAST_MODE, DEFAULT_LANGUAGE, diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 1420b1170ca422..d7deab21743fe4 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -24,7 +24,6 @@ DEFAULT_LANGUAGE = "en" ATTRIBUTION = "Data provided by OpenWeatherMap" MANUFACTURER = "OpenWeather" -CONF_LANGUAGE = "language" CONFIG_FLOW_VERSION = 2 ENTRY_NAME = "name" ENTRY_WEATHER_COORDINATOR = "weather_coordinator" diff --git a/homeassistant/components/purpleair/__init__.py b/homeassistant/components/purpleair/__init__.py index 6b998f6879e096..f52d0799d356c3 100644 --- a/homeassistant/components/purpleair/__init__.py +++ b/homeassistant/components/purpleair/__init__.py @@ -7,12 +7,17 @@ from aiopurpleair.models.sensors import SensorModel from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, Platform +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_SHOW_ON_MAP, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_SHOW_ON_MAP, DOMAIN +from .const import DOMAIN from .coordinator import PurpleAirDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 3daa6f96fdfbc6..e2b43726dc47ae 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -14,7 +14,12 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_SHOW_ON_MAP, +) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import ( @@ -35,7 +40,7 @@ ) from homeassistant.helpers.typing import EventType -from .const import CONF_SENSOR_INDICES, CONF_SHOW_ON_MAP, DOMAIN, LOGGER +from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER CONF_DISTANCE = "distance" CONF_NEARBY_SENSOR_OPTIONS = "nearby_sensor_options" diff --git a/homeassistant/components/purpleair/const.py b/homeassistant/components/purpleair/const.py index e3ea7807a21f00..60f51a9e7ddf9c 100644 --- a/homeassistant/components/purpleair/const.py +++ b/homeassistant/components/purpleair/const.py @@ -7,4 +7,3 @@ CONF_READ_KEY = "read_key" CONF_SENSOR_INDICES = "sensor_indices" -CONF_SHOW_ON_MAP = "show_on_map" diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 8f9c9395adefc4..e47004f5fb7c48 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -7,12 +7,12 @@ from homeassistant.components import cloud from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.const import CONF_API_KEY, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, CONF_WEBHOOK_ID, DOMAIN +from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, DOMAIN from .device import RachioPerson from .webhooks import ( async_get_or_create_registered_webhook_id_and_url, diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 92a57505a7ccef..dad044e50491c8 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -65,7 +65,6 @@ SIGNAL_RACHIO_ZONE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_zone" SIGNAL_RACHIO_SCHEDULE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_schedule" -CONF_WEBHOOK_ID = "webhook_id" CONF_CLOUDHOOK_URL = "cloudhook_url" # Webhook callbacks diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index 5c2fbe5965f954..298b9c03701721 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -5,13 +5,12 @@ from homeassistant.components import cloud, webhook from homeassistant.config_entries import ConfigEntry -from homeassistant.const import URL_API +from homeassistant.const import CONF_WEBHOOK_ID, URL_API from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_CLOUDHOOK_URL, - CONF_WEBHOOK_ID, DOMAIN, KEY_EXTERNAL_ID, KEY_TYPE, diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index a27c84b9593f9f..fc9b717f89b86a 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -10,13 +10,19 @@ from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import format_mac -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN +from .const import CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkException, ReolinkWebhookException, UserNotAdmin from .host import ReolinkHost from .util import is_connected diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index 2a35a0f723d83c..8aa01bfac417f0 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -3,4 +3,3 @@ DOMAIN = "reolink" CONF_USE_HTTPS = "use_https" -CONF_PROTOCOL = "protocol" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index dfc7780693213f..77aeffd541219e 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -13,7 +13,13 @@ from reolink_aio.exceptions import NotSupportedError, ReolinkError, SubscriptionError from homeassistant.components import webhook -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import format_mac @@ -21,7 +27,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import NoURLAvailableError, get_url -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN +from .const import CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin DEFAULT_TIMEOUT = 30 diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py index 5012a303b69bc5..1e98adaaa70a71 100644 --- a/homeassistant/components/totalconnect/const.py +++ b/homeassistant/components/totalconnect/const.py @@ -2,7 +2,6 @@ DOMAIN = "totalconnect" CONF_USERCODES = "usercodes" -CONF_LOCATION = "location" AUTO_BYPASS = "auto_bypass_low_battery" # Most TotalConnect alarms will work passing '-1' as usercode diff --git a/homeassistant/components/trafikverket_camera/__init__.py b/homeassistant/components/trafikverket_camera/__init__.py index 3ac3ce35882468..f0f758272f764a 100644 --- a/homeassistant/components/trafikverket_camera/__init__.py +++ b/homeassistant/components/trafikverket_camera/__init__.py @@ -6,12 +6,12 @@ from pytrafikverket.trafikverket_camera import TrafikverketCamera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_ID +from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_LOCATION from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_LOCATION, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS from .coordinator import TVDataUpdateCoordinator CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py index 7572855b7d44cc..104a6a470dc10a 100644 --- a/homeassistant/components/trafikverket_camera/config_flow.py +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -14,12 +14,12 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_ID +from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_LOCATION from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import CONF_LOCATION, DOMAIN +from .const import DOMAIN class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/trafikverket_camera/const.py b/homeassistant/components/trafikverket_camera/const.py index ff40d1bbc919e5..728ba9f7bd5f50 100644 --- a/homeassistant/components/trafikverket_camera/const.py +++ b/homeassistant/components/trafikverket_camera/const.py @@ -2,7 +2,6 @@ from homeassistant.const import Platform DOMAIN = "trafikverket_camera" -CONF_LOCATION = "location" PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.SENSOR] ATTRIBUTION = "Data provided by Trafikverket" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index d0ae13c09b7038..ee084b77ef1de8 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -15,7 +15,7 @@ ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_COUNTRY_CODE +from homeassistant.const import CONF_COUNTRY_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -27,8 +27,6 @@ CONF_APP_TYPE, CONF_AUTH_TYPE, CONF_ENDPOINT, - CONF_PASSWORD, - CONF_USERNAME, DOMAIN, LOGGER, PLATFORMS, diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index eb490791f7e1e6..f933ac8451986e 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_COUNTRY_CODE +from homeassistant.const import CONF_COUNTRY_CODE, CONF_PASSWORD, CONF_USERNAME from .const import ( CONF_ACCESS_ID, @@ -15,8 +15,6 @@ CONF_APP_TYPE, CONF_AUTH_TYPE, CONF_ENDPOINT, - CONF_PASSWORD, - CONF_USERNAME, DOMAIN, LOGGER, SMARTLIFE_APP, diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 56dbbc4fa40428..4cdca8f39048af 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -36,8 +36,6 @@ CONF_ENDPOINT = "endpoint" CONF_ACCESS_ID = "access_id" CONF_ACCESS_SECRET = "access_secret" -CONF_USERNAME = "username" -CONF_PASSWORD = "password" CONF_APP_TYPE = "tuya_app_type" TUYA_DISCOVERY_NEW = "tuya_discovery_new" diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index 897bfaf4e20e71..d57a56f489b92e 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -6,12 +6,12 @@ from ttls.client import Twinkly from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_SW_VERSION, Platform +from homeassistant.const import ATTR_SW_VERSION, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ATTR_VERSION, CONF_HOST, DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN +from .const import ATTR_VERSION, DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN PLATFORMS = [Platform.LIGHT] diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py index eab44dba59105f..e37e0fd6170202 100644 --- a/homeassistant/components/twinkly/config_flow.py +++ b/homeassistant/components/twinkly/config_flow.py @@ -11,10 +11,10 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp -from homeassistant.const import CONF_HOST, CONF_MODEL +from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_ID, CONF_NAME, DEV_ID, DEV_MODEL, DEV_NAME, DOMAIN +from .const import DEV_ID, DEV_MODEL, DEV_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/twinkly/const.py b/homeassistant/components/twinkly/const.py index 2158e4aae07c5b..f33024ed156b6c 100644 --- a/homeassistant/components/twinkly/const.py +++ b/homeassistant/components/twinkly/const.py @@ -2,11 +2,6 @@ DOMAIN = "twinkly" -# Keys of the config entry -CONF_ID = "id" -CONF_HOST = "host" -CONF_NAME = "name" - # Strongly named HA attributes keys ATTR_HOST = "host" ATTR_VERSION = "version" diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 6d0b31b06ed388..c43019360886c6 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -19,16 +19,19 @@ LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_SW_VERSION, CONF_MODEL +from homeassistant.const import ( + ATTR_SW_VERSION, + CONF_HOST, + CONF_ID, + CONF_MODEL, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - CONF_HOST, - CONF_ID, - CONF_NAME, DATA_CLIENT, DATA_DEVICE_INFO, DEV_LED_PROFILE, diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index 4f4206da6ec6ff..12601c0af8333d 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -14,6 +14,7 @@ CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD, + CONF_SHOW_ON_MAP, CONF_USERNAME, ) from homeassistant.core import callback @@ -23,7 +24,6 @@ from .const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, - CONF_SHOW_ON_MAP, DOMAIN, LOGGER, ) diff --git a/homeassistant/components/watttime/const.py b/homeassistant/components/watttime/const.py index 5bb8cb50d40e5c..ce2731e783290d 100644 --- a/homeassistant/components/watttime/const.py +++ b/homeassistant/components/watttime/const.py @@ -7,4 +7,3 @@ CONF_BALANCING_AUTHORITY = "balancing_authority" CONF_BALANCING_AUTHORITY_ABBREV = "balancing_authority_abbreviation" -CONF_SHOW_ON_MAP = "show_on_map" diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 2a0e21ecf4cb7f..ca5b0d06fa23d2 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -10,7 +10,13 @@ SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, PERCENTAGE, UnitOfMass +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_SHOW_ON_MAP, + PERCENTAGE, + UnitOfMass, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -20,12 +26,7 @@ DataUpdateCoordinator, ) -from .const import ( - CONF_BALANCING_AUTHORITY, - CONF_BALANCING_AUTHORITY_ABBREV, - CONF_SHOW_ON_MAP, - DOMAIN, -) +from .const import CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, DOMAIN ATTR_BALANCING_AUTHORITY = "balancing_authority" diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 2a4deffb1616de..02e88c6b14ecdf 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -13,7 +13,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac @@ -25,7 +25,6 @@ CONF_CLOUD_USERNAME, CONF_FLOW_TYPE, CONF_GATEWAY, - CONF_MAC, CONF_MANUAL, DEFAULT_CLOUD_COUNTRY, DOMAIN, diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 376c23c10d433b..ef9668dbee4efd 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -18,7 +18,6 @@ # Config flow CONF_FLOW_TYPE = "config_flow_device" CONF_GATEWAY = "gateway" -CONF_MAC = "mac" CONF_CLOUD_USERNAME = "cloud_username" CONF_CLOUD_PASSWORD = "cloud_password" CONF_CLOUD_COUNTRY = "cloud_country" diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index da860c7045e417..0c87f74a7e6c25 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -8,7 +8,7 @@ from construct.core import ChecksumError from miio import Device, DeviceException -from homeassistant.const import ATTR_CONNECTIONS, CONF_MODEL +from homeassistant.const import ATTR_CONNECTIONS, CONF_MAC, CONF_MODEL from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -17,7 +17,7 @@ DataUpdateCoordinator, ) -from .const import CONF_MAC, DOMAIN, AuthException, SetupException +from .const import DOMAIN, AuthException, SetupException _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/youtube/const.py b/homeassistant/components/youtube/const.py index 7404cd046652b5..63c4480c007dfa 100644 --- a/homeassistant/components/youtube/const.py +++ b/homeassistant/components/youtube/const.py @@ -7,7 +7,6 @@ CHANNEL_CREATION_HELP_URL = "https://support.google.com/youtube/answer/1646861" CONF_CHANNELS = "channels" -CONF_ID = "id" CONF_UPLOAD_PLAYLIST = "upload_playlist_id" COORDINATOR = "coordinator" AUTH = "auth" diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 3a0c852535ea88..3228b3c7229fc5 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -4,7 +4,8 @@ import airthings from homeassistant import config_entries -from homeassistant.components.airthings.const import CONF_ID, CONF_SECRET, DOMAIN +from homeassistant.components.airthings.const import CONF_SECRET, DOMAIN +from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index 7797f08872fe80..4c1abdd3c9b809 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -4,8 +4,8 @@ import pytest -from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT +from homeassistant.components.anthemav.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_PORT from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index e3058697f3ec99..b745ac02693814 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -6,12 +6,8 @@ import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.environment_canada.const import ( - CONF_LANGUAGE, - CONF_STATION, - DOMAIN, -) -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.components.environment_canada.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index 3eedb7a0ddb9b2..fb1597e36223df 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -5,12 +5,8 @@ from syrupy import SnapshotAssertion -from homeassistant.components.environment_canada.const import ( - CONF_LANGUAGE, - CONF_STATION, - DOMAIN, -) -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.components.environment_canada.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 974a029d14306f..6ddb9e1687fcb1 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -23,7 +23,6 @@ CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, - CONF_EFFECT, CONF_SPEED_PCT, CONF_TRANSITION, CONF_WHITE_CHANNEL_TYPE, @@ -55,6 +54,7 @@ ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_EFFECT, CONF_HOST, CONF_MODE, CONF_NAME, diff --git a/tests/components/frontier_silicon/conftest.py b/tests/components/frontier_silicon/conftest.py index 40a6df853106ed..1def9b160b22f8 100644 --- a/tests/components/frontier_silicon/conftest.py +++ b/tests/components/frontier_silicon/conftest.py @@ -4,11 +4,8 @@ import pytest -from homeassistant.components.frontier_silicon.const import ( - CONF_PIN, - CONF_WEBFSAPI_URL, - DOMAIN, -) +from homeassistant.components.frontier_silicon.const import CONF_WEBFSAPI_URL, DOMAIN +from homeassistant.const import CONF_PIN from tests.common import MockConfigEntry diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 15132baf25a6a9..9e575389e72d7b 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -8,7 +8,6 @@ CONF_AVOID, CONF_DEPARTURE_TIME, CONF_DESTINATION, - CONF_LANGUAGE, CONF_ORIGIN, CONF_TIME, CONF_TIME_TYPE, @@ -21,7 +20,7 @@ DOMAIN, UNITS_IMPERIAL, ) -from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant from .const import MOCK_CONFIG diff --git a/tests/components/knx/test_button.py b/tests/components/knx/test_button.py index a905e66fe5deab..3dedea7d8d440e 100644 --- a/tests/components/knx/test_button.py +++ b/tests/components/knx/test_button.py @@ -4,14 +4,9 @@ import pytest -from homeassistant.components.knx.const import ( - CONF_PAYLOAD, - CONF_PAYLOAD_LENGTH, - DOMAIN, - KNX_ADDRESS, -) +from homeassistant.components.knx.const import CONF_PAYLOAD_LENGTH, DOMAIN, KNX_ADDRESS from homeassistant.components.knx.schema import ButtonSchema -from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util diff --git a/tests/components/knx/test_select.py b/tests/components/knx/test_select.py index 1c89338920ea46..f113a83f7a0730 100644 --- a/tests/components/knx/test_select.py +++ b/tests/components/knx/test_select.py @@ -2,7 +2,6 @@ import pytest from homeassistant.components.knx.const import ( - CONF_PAYLOAD, CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -10,7 +9,7 @@ KNX_ADDRESS, ) from homeassistant.components.knx.schema import SelectSchema -from homeassistant.const import CONF_NAME, STATE_UNKNOWN +from homeassistant.const import CONF_NAME, CONF_PAYLOAD, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from .conftest import KNXTestKit diff --git a/tests/components/livisi/__init__.py b/tests/components/livisi/__init__.py index 3d28d1db70801f..48a7e21ad8d1e2 100644 --- a/tests/components/livisi/__init__.py +++ b/tests/components/livisi/__init__.py @@ -1,7 +1,7 @@ """Tests for the LIVISI Smart Home integration.""" from unittest.mock import patch -from homeassistant.components.livisi.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_PASSWORD VALID_CONFIG = { CONF_HOST: "1.1.1.1", diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index 9f427757183595..0b67f817eb2cff 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -5,13 +5,8 @@ import pytest from homeassistant import config_entries, setup -from homeassistant.components.nextbus.const import ( - CONF_AGENCY, - CONF_ROUTE, - CONF_STOP, - DOMAIN, -) -from homeassistant.const import CONF_NAME +from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN +from homeassistant.const import CONF_NAME, CONF_STOP from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index a4d04997e15997..92da27783bc259 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -8,15 +8,10 @@ import pytest from homeassistant.components import sensor -from homeassistant.components.nextbus.const import ( - CONF_AGENCY, - CONF_ROUTE, - CONF_STOP, - DOMAIN, -) +from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN from homeassistant.components.nextbus.coordinator import NextBusDataUpdateCoordinator from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_STOP from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.update_coordinator import UpdateFailed diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index b5d718b61aa790..a27898629ad0f8 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -6,13 +6,9 @@ import pytest from homeassistant import data_entry_flow -from homeassistant.components.nextdns.const import ( - CONF_PROFILE_ID, - CONF_PROFILE_NAME, - DOMAIN, -) +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME from homeassistant.core import HomeAssistant from . import PROFILES, init_integration diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 2bd62936fe5e22..87f76817044ca9 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -5,7 +5,6 @@ from homeassistant import data_entry_flow from homeassistant.components.openweathermap.const import ( - CONF_LANGUAGE, DEFAULT_FORECAST_MODE, DEFAULT_LANGUAGE, DOMAIN, @@ -13,6 +12,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import ( CONF_API_KEY, + CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 464d4120c65782..3f81a30f898e33 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -6,7 +6,13 @@ from homeassistant.components.reolink import const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac @@ -121,7 +127,7 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry: const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 9b449d4b851754..dd9949a5dce574 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -14,7 +14,13 @@ from homeassistant.components.reolink.exceptions import ReolinkWebhookException from homeassistant.components.reolink.host import DEFAULT_TIMEOUT from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac from homeassistant.util.dt import utcnow @@ -68,7 +74,7 @@ async def test_config_flow_manual_success( const.CONF_USE_HTTPS: TEST_USE_HTTPS, } assert result["options"] == { - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, } @@ -195,7 +201,7 @@ async def test_config_flow_errors( const.CONF_USE_HTTPS: TEST_USE_HTTPS, } assert result["options"] == { - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, } @@ -212,7 +218,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: "rtsp", + CONF_PROTOCOL: "rtsp", }, title=TEST_NVR_NAME, ) @@ -228,12 +234,12 @@ async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={const.CONF_PROTOCOL: "rtmp"}, + user_input={CONF_PROTOCOL: "rtmp"}, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { - const.CONF_PROTOCOL: "rtmp", + CONF_PROTOCOL: "rtmp", } @@ -252,7 +258,7 @@ async def test_change_connection_settings( const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) @@ -295,7 +301,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) @@ -376,7 +382,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No const.CONF_USE_HTTPS: TEST_USE_HTTPS, } assert result["options"] == { - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, } @@ -435,7 +441,7 @@ async def test_dhcp_ip_update( const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 7fe3570564a92e..ddb664634194f8 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -21,6 +21,7 @@ CONF_HOST, CONF_PASSWORD, CONF_PORT, + CONF_PROTOCOL, CONF_USERNAME, Platform, ) @@ -271,7 +272,7 @@ async def test_browsing_not_loaded( const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME2, ) diff --git a/tests/components/trafikverket_camera/__init__.py b/tests/components/trafikverket_camera/__init__.py index a9aa3ad70d10b0..dd23c7bce7ed2f 100644 --- a/tests/components/trafikverket_camera/__init__.py +++ b/tests/components/trafikverket_camera/__init__.py @@ -1,8 +1,7 @@ """Tests for the Trafikverket Camera integration.""" from __future__ import annotations -from homeassistant.components.trafikverket_camera.const import CONF_LOCATION -from homeassistant.const import CONF_API_KEY, CONF_ID +from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_LOCATION ENTRY_CONFIG = { CONF_API_KEY: "1234567890", diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index 305066832e5f0c..ca1d8554c4a3f4 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -13,8 +13,8 @@ from pytrafikverket.trafikverket_camera import CameraInfo from homeassistant import config_entries -from homeassistant.components.trafikverket_camera.const import CONF_LOCATION, DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_ID +from homeassistant.components.trafikverket_camera.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_LOCATION from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 9505e1ef423fb7..f8345683d4ac6e 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -14,14 +14,12 @@ CONF_APP_TYPE, CONF_AUTH_TYPE, CONF_ENDPOINT, - CONF_PASSWORD, - CONF_USERNAME, DOMAIN, SMARTLIFE_APP, TUYA_COUNTRIES, TUYA_SMART_APP, ) -from homeassistant.const import CONF_COUNTRY_CODE +from homeassistant.const import CONF_COUNTRY_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant MOCK_SMART_HOME_PROJECT_TYPE = 0 diff --git a/tests/components/twinkly/__init__.py b/tests/components/twinkly/__init__.py index bd51ac5d7cde86..4b1411e9223d7f 100644 --- a/tests/components/twinkly/__init__.py +++ b/tests/components/twinkly/__init__.py @@ -1,4 +1,4 @@ -"""Constants and mock for the twkinly component tests.""" +"""Constants and mock for the twinkly component tests.""" from aiohttp.client_exceptions import ClientConnectionError diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index 2d335c69923142..a65a2a2d96383f 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -3,13 +3,8 @@ from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.components.twinkly.const import ( - CONF_HOST, - CONF_ID, - CONF_NAME, - DOMAIN as TWINKLY_DOMAIN, -) -from homeassistant.const import CONF_MODEL +from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN +from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant from . import TEST_MODEL, TEST_NAME, ClientMock diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index f2049f9b513899..33f24a31d8f062 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -3,14 +3,9 @@ from unittest.mock import patch from uuid import uuid4 -from homeassistant.components.twinkly.const import ( - CONF_HOST, - CONF_ID, - CONF_NAME, - DOMAIN as TWINKLY_DOMAIN, -) +from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_MODEL +from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant from . import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index bcb40f22d08a27..e3b8b499c8ed6c 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -4,13 +4,8 @@ from unittest.mock import patch from homeassistant.components.light import ATTR_BRIGHTNESS, LightEntityFeature -from homeassistant.components.twinkly.const import ( - CONF_HOST, - CONF_ID, - CONF_NAME, - DOMAIN as TWINKLY_DOMAIN, -) -from homeassistant.const import CONF_MODEL +from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN +from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index dbe1e5444d7e2b..ce9284924f543d 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -12,13 +12,13 @@ from homeassistant.components.watttime.const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, - CONF_SHOW_ON_MAP, DOMAIN, ) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD, + CONF_SHOW_ON_MAP, CONF_USERNAME, ) from homeassistant.core import HomeAssistant diff --git a/tests/components/xiaomi_miio/test_button.py b/tests/components/xiaomi_miio/test_button.py index d00b2ec5853311..552b302aafe912 100644 --- a/tests/components/xiaomi_miio/test_button.py +++ b/tests/components/xiaomi_miio/test_button.py @@ -6,7 +6,6 @@ from homeassistant.components.button import DOMAIN, SERVICE_PRESS from homeassistant.components.xiaomi_miio.const import ( CONF_FLOW_TYPE, - CONF_MAC, DOMAIN as XIAOMI_DOMAIN, MODELS_VACUUM, ) @@ -14,6 +13,7 @@ ATTR_ENTITY_ID, CONF_DEVICE, CONF_HOST, + CONF_MAC, CONF_MODEL, CONF_TOKEN, Platform, diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 0fe8c3d247c95e..b36924764fef44 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import const -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant from . import TEST_MAC @@ -172,7 +172,7 @@ async def test_config_flow_gateway_success(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, } @@ -205,7 +205,7 @@ async def test_config_flow_gateway_cloud_success(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, } @@ -251,7 +251,7 @@ async def test_config_flow_gateway_cloud_multiple_success(hass: HomeAssistant) - CONF_HOST: TEST_HOST2, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC2, + CONF_MAC: TEST_MAC2, } @@ -460,7 +460,7 @@ async def test_zeroconf_gateway_success(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, } @@ -692,7 +692,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: overwrite_model, - const.CONF_MAC: None, + CONF_MAC: None, } @@ -736,7 +736,7 @@ async def config_flow_device_success(hass, model_to_test): CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: model_to_test, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, } @@ -782,7 +782,7 @@ async def config_flow_generic_roborock(hass): CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: dummy_model, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, } @@ -836,7 +836,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: model_to_test, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, } @@ -879,7 +879,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, }, title=TEST_NAME, ) @@ -919,7 +919,7 @@ async def test_options_flow_incomplete(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, }, title=TEST_NAME, ) @@ -957,7 +957,7 @@ async def test_reauth(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, }, title=TEST_NAME, ) @@ -1005,5 +1005,5 @@ async def test_reauth(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, CONF_MODEL: TEST_MODEL, - const.CONF_MAC: TEST_MAC, + CONF_MAC: TEST_MAC, } diff --git a/tests/components/xiaomi_miio/test_select.py b/tests/components/xiaomi_miio/test_select.py index 04cb6ee6ea79f3..a999f0e7c9a5b3 100644 --- a/tests/components/xiaomi_miio/test_select.py +++ b/tests/components/xiaomi_miio/test_select.py @@ -18,7 +18,6 @@ from homeassistant.components.xiaomi_miio import UPDATE_INTERVAL from homeassistant.components.xiaomi_miio.const import ( CONF_FLOW_TYPE, - CONF_MAC, DOMAIN as XIAOMI_DOMAIN, MODEL_AIRFRESH_T2017, ) @@ -26,6 +25,7 @@ ATTR_ENTITY_ID, CONF_DEVICE, CONF_HOST, + CONF_MAC, CONF_MODEL, CONF_TOKEN, Platform, diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index e1f2233c5bc445..9e823035dd9694 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -24,7 +24,6 @@ ) from homeassistant.components.xiaomi_miio.const import ( CONF_FLOW_TYPE, - CONF_MAC, DOMAIN as XIAOMI_DOMAIN, MODELS_VACUUM, ) @@ -44,6 +43,7 @@ ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_HOST, + CONF_MAC, CONF_MODEL, CONF_TOKEN, STATE_UNAVAILABLE, From 4e9b9add29d6936b3f056e3f0830090a65472703 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:06:46 -0500 Subject: [PATCH 109/118] Bump ZHA dependencies (#105661) --- homeassistant/components/zha/core/const.py | 1 - homeassistant/components/zha/core/gateway.py | 10 ----- homeassistant/components/zha/manifest.json | 8 ++-- homeassistant/components/zha/radio_manager.py | 2 - requirements_all.txt | 8 ++-- requirements_test_all.txt | 8 ++-- tests/components/zha/test_gateway.py | 45 +------------------ 7 files changed, 13 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index ecbd347a6211c3..7e591a596e5258 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -139,7 +139,6 @@ CONF_ENABLE_QUIRKS = "enable_quirks" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" -CONF_USE_THREAD = "use_thread" CONF_ZIGPY = "zigpy_config" CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 5c038a2d7f8573..6c461ac45c3f81 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -46,7 +46,6 @@ ATTR_SIGNATURE, ATTR_TYPE, CONF_RADIO_TYPE, - CONF_USE_THREAD, CONF_ZIGPY, DEBUG_COMP_BELLOWS, DEBUG_COMP_ZHA, @@ -158,15 +157,6 @@ def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: if CONF_NWK_VALIDATE_SETTINGS not in app_config: app_config[CONF_NWK_VALIDATE_SETTINGS] = True - # The bellows UART thread sometimes propagates a cancellation into the main Core - # event loop, when a connection to a TCP coordinator fails in a specific way - if ( - CONF_USE_THREAD not in app_config - and radio_type is RadioType.ezsp - and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://") - ): - app_config[CONF_USE_THREAD] = False - # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4c8a58a12cf6d6..fe58ff044cddd3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,13 +21,13 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.37.1", + "bellows==0.37.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.107", - "zigpy-deconz==0.22.0", - "zigpy==0.60.0", - "zigpy-xbee==0.20.0", + "zigpy-deconz==0.22.2", + "zigpy==0.60.1", + "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.0", "universal-silabs-flasher==0.0.15", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index d3ca03de8d89c4..92a90e0e13a410 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -10,7 +10,6 @@ import os from typing import Any, Self -from bellows.config import CONF_USE_THREAD import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups @@ -175,7 +174,6 @@ async def connect_zigpy_app(self) -> ControllerApplication: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings app_config[CONF_NWK_BACKUP_ENABLED] = False - app_config[CONF_USE_THREAD] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( diff --git a/requirements_all.txt b/requirements_all.txt index 2fdbe18fa278f8..2b937eb1ac3a01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.37.1 +bellows==0.37.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2847,10 +2847,10 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.22.0 +zigpy-deconz==0.22.2 # homeassistant.components.zha -zigpy-xbee==0.20.0 +zigpy-xbee==0.20.1 # homeassistant.components.zha zigpy-zigate==0.12.0 @@ -2859,7 +2859,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.60.0 +zigpy==0.60.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 148d0597d8aa19..f706adb50cd2c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.37.1 +bellows==0.37.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2139,10 +2139,10 @@ zeversolar==0.3.1 zha-quirks==0.0.107 # homeassistant.components.zha -zigpy-deconz==0.22.0 +zigpy-deconz==0.22.2 # homeassistant.components.zha -zigpy-xbee==0.20.0 +zigpy-xbee==0.20.1 # homeassistant.components.zha zigpy-zigate==0.12.0 @@ -2151,7 +2151,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.60.0 +zigpy==0.60.1 # homeassistant.components.zwave_js zwave-js-server-python==0.54.0 diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 4f5209207046ee..1d9042daa4aa6b 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,9 +1,8 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest -from zigpy.application import ControllerApplication import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting @@ -223,48 +222,6 @@ async def test_gateway_create_group_with_id( assert zha_group.group_id == 0x1234 -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", - MagicMock(), -) -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", - MagicMock(), -) -@pytest.mark.parametrize( - ("device_path", "thread_state", "config_override"), - [ - ("/dev/ttyUSB0", True, {}), - ("socket://192.168.1.123:9999", False, {}), - ("socket://192.168.1.123:9999", True, {"use_thread": True}), - ], -) -async def test_gateway_initialize_bellows_thread( - device_path: str, - thread_state: bool, - config_override: dict, - hass: HomeAssistant, - zigpy_app_controller: ControllerApplication, - config_entry: MockConfigEntry, -) -> None: - """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" - config_entry.data = dict(config_entry.data) - config_entry.data["device"]["path"] = device_path - config_entry.add_to_hass(hass) - - zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ) as mock_new: - await zha_gateway.async_initialize() - - mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state - - await zha_gateway.shutdown() - - @pytest.mark.parametrize( ("device_path", "config_override", "expected_channel"), [ From 4f9f54892923f558f3d495216ad50be912beddf2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 17:26:34 +0100 Subject: [PATCH 110/118] Add volume_step property to MediaPlayerEntity (#105574) * Add volume_step property to MediaPlayerEntity * Improve tests * Address review comments --- .../components/media_player/__init__.py | 22 ++++++++- .../media_player/test_async_helpers.py | 45 +++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 50365f90f1f09c..2ca47b97275dbc 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -454,6 +454,7 @@ class MediaPlayerEntityDescription(EntityDescription): """A class that describes media player entities.""" device_class: MediaPlayerDeviceClass | None = None + volume_step: float | None = None class MediaPlayerEntity(Entity): @@ -505,6 +506,7 @@ class MediaPlayerEntity(Entity): _attr_state: MediaPlayerState | None = None _attr_supported_features: MediaPlayerEntityFeature = MediaPlayerEntityFeature(0) _attr_volume_level: float | None = None + _attr_volume_step: float # Implement these for your media player @property @@ -533,6 +535,18 @@ def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" return self._attr_volume_level + @property + def volume_step(self) -> float: + """Return the step to be used by the volume_up and volume_down services.""" + if hasattr(self, "_attr_volume_step"): + return self._attr_volume_step + if ( + hasattr(self, "entity_description") + and (volume_step := self.entity_description.volume_step) is not None + ): + return volume_step + return 0.1 + @property def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" @@ -956,7 +970,9 @@ async def async_volume_up(self) -> None: and self.volume_level < 1 and self.supported_features & MediaPlayerEntityFeature.VOLUME_SET ): - await self.async_set_volume_level(min(1, self.volume_level + 0.1)) + await self.async_set_volume_level( + min(1, self.volume_level + self.volume_step) + ) async def async_volume_down(self) -> None: """Turn volume down for media player. @@ -972,7 +988,9 @@ async def async_volume_down(self) -> None: and self.volume_level > 0 and self.supported_features & MediaPlayerEntityFeature.VOLUME_SET ): - await self.async_set_volume_level(max(0, self.volume_level - 0.1)) + await self.async_set_volume_level( + max(0, self.volume_level - self.volume_step) + ) async def async_media_play_pause(self) -> None: """Play or pause the media player.""" diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index cf71b52c046cf4..a24c9cc76b23a9 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -10,6 +10,7 @@ STATE_PLAYING, STATE_STANDBY, ) +from homeassistant.core import HomeAssistant class ExtendedMediaPlayer(mp.MediaPlayerEntity): @@ -148,28 +149,64 @@ def standby(self): self._state = STATE_STANDBY +class AttrMediaPlayer(SimpleMediaPlayer): + """Media player setting properties via _attr_*.""" + + _attr_volume_step = 0.2 + + +class DescrMediaPlayer(SimpleMediaPlayer): + """Media player setting properties via entity description.""" + + entity_description = mp.MediaPlayerEntityDescription(key="test", volume_step=0.3) + + @pytest.fixture(params=[ExtendedMediaPlayer, SimpleMediaPlayer]) def player(hass, request): """Return a media player.""" return request.param(hass) -async def test_volume_up(player) -> None: +@pytest.mark.parametrize( + ("player_class", "volume_step"), + [ + (ExtendedMediaPlayer, 0.1), + (SimpleMediaPlayer, 0.1), + (AttrMediaPlayer, 0.2), + (DescrMediaPlayer, 0.3), + ], +) +async def test_volume_up( + hass: HomeAssistant, player_class: type[mp.MediaPlayerEntity], volume_step: float +) -> None: """Test the volume_up and set volume methods.""" + player = player_class(hass) assert player.volume_level == 0 await player.async_set_volume_level(0.5) assert player.volume_level == 0.5 await player.async_volume_up() - assert player.volume_level == 0.6 + assert player.volume_level == 0.5 + volume_step -async def test_volume_down(player) -> None: +@pytest.mark.parametrize( + ("player_class", "volume_step"), + [ + (ExtendedMediaPlayer, 0.1), + (SimpleMediaPlayer, 0.1), + (AttrMediaPlayer, 0.2), + (DescrMediaPlayer, 0.3), + ], +) +async def test_volume_down( + hass: HomeAssistant, player_class: type[mp.MediaPlayerEntity], volume_step: float +) -> None: """Test the volume_down and set volume methods.""" + player = player_class(hass) assert player.volume_level == 0 await player.async_set_volume_level(0.5) assert player.volume_level == 0.5 await player.async_volume_down() - assert player.volume_level == 0.4 + assert player.volume_level == 0.5 - volume_step async def test_media_play_pause(player) -> None: From dd5a48996ae621754064c90956ffb60739c1f302 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 17:27:26 +0100 Subject: [PATCH 111/118] Keep capabilities up to date in the entity registry (#101748) * Keep capabilities up to date in the entity registry * Warn if entities update their capabilities very often * Fix updating of device class * Stop tracking capability updates once flooding is logged * Only sync registry if state changed * Improve test * Revert "Only sync registry if state changed" This reverts commit 1c52571596c06444df234d4b088242b494b630f2. * Avoid calculating device class twice * Address review comments * Revert using dataclass * Fix unintended revert * Add helper method --- homeassistant/components/group/__init__.py | 3 +- .../components/group/media_player.py | 3 +- .../components/template/template_entity.py | 9 +- homeassistant/helpers/entity.py | 97 ++++++++++- tests/helpers/test_entity.py | 160 +++++++++++++++++- 5 files changed, 257 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index ae246041db9cd5..a2a61b3016a7df 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -509,7 +509,8 @@ def async_state_changed_listener( self.async_update_supported_features( event.data["entity_id"], event.data["new_state"] ) - preview_callback(*self._async_generate_attributes()) + calculated_state = self._async_calculate_state() + preview_callback(calculated_state.state, calculated_state.attributes) async_state_changed_listener(None) return async_track_state_change_event( diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index bc238519cfa90b..b85fbf32a0d188 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -236,7 +236,8 @@ def async_state_changed_listener( ) -> None: """Handle child updates.""" self.async_update_group_state() - preview_callback(*self._async_generate_attributes()) + calculated_state = self._async_calculate_state() + preview_callback(calculated_state.state, calculated_state.attributes) async_state_changed_listener(None) return async_track_state_change_event( diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 8c3554c067e601..f9c61850e58d70 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -430,14 +430,17 @@ def _handle_results( return try: - state, attrs = self._async_generate_attributes() - validate_state(state) + calculated_state = self._async_calculate_state() + validate_state(calculated_state.state) except Exception as err: # pylint: disable=broad-exception-caught self._preview_callback(None, None, None, str(err)) else: assert self._template_result_info self._preview_callback( - state, attrs, self._template_result_info.listeners, None + calculated_state.state, + calculated_state.attributes, + self._template_result_info.listeners, + None, ) @callback diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index dad0e2e00f357d..cc709f4c7548b7 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,6 +3,7 @@ from abc import ABC import asyncio +from collections import deque from collections.abc import Coroutine, Iterable, Mapping, MutableMapping import dataclasses from datetime import timedelta @@ -75,6 +76,9 @@ # epsilon to make the string representation readable FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1 +# How many times per hour we allow capabilities to be updated before logging a warning +CAPABILITIES_UPDATE_LIMIT = 100 + @callback def async_setup(hass: HomeAssistant) -> None: @@ -237,6 +241,22 @@ class EntityDescription(metaclass=FrozenOrThawed, frozen_or_thawed=True): unit_of_measurement: str | None = None +@dataclasses.dataclass(frozen=True, slots=True) +class CalculatedState: + """Container with state and attributes. + + Returned by Entity._async_calculate_state. + """ + + state: str + # The union of all attributes, after overriding with entity registry settings + attributes: dict[str, Any] + # Capability attributes returned by the capability_attributes property + capability_attributes: Mapping[str, Any] | None + # Attributes which may be overridden by the entity registry + shadowed_attributes: Mapping[str, Any] + + class Entity(ABC): """An abstract class for Home Assistant entities.""" @@ -311,6 +331,8 @@ class Entity(ABC): # and removes the need for constant None checks or asserts. _state_info: StateInfo = None # type: ignore[assignment] + __capabilities_updated_at: deque[float] + __capabilities_updated_at_reported: bool = False __remove_event: asyncio.Event | None = None # Entity Properties @@ -775,12 +797,29 @@ def _friendly_name_internal(self) -> str | None: return f"{device_name} {name}" if device_name else name @callback - def _async_generate_attributes(self) -> tuple[str, dict[str, Any]]: + def _async_calculate_state(self) -> CalculatedState: """Calculate state string and attribute mapping.""" + return CalculatedState(*self.__async_calculate_state()) + + def __async_calculate_state( + self, + ) -> tuple[str, dict[str, Any], Mapping[str, Any] | None, Mapping[str, Any]]: + """Calculate state string and attribute mapping. + + Returns a tuple (state, attr, capability_attr, shadowed_attr). + state - the stringified state + attr - the attribute dictionary + capability_attr - a mapping with capability attributes + shadowed_attr - a mapping with attributes which may be overridden + + This method is called when writing the state to avoid the overhead of creating + a dataclass object. + """ entry = self.registry_entry - attr = self.capability_attributes - attr = dict(attr) if attr else {} + capability_attr = self.capability_attributes + attr = dict(capability_attr) if capability_attr else {} + shadowed_attr = {} available = self.available # only call self.available once per update cycle state = self._stringify_state(available) @@ -797,26 +836,30 @@ def _async_generate_attributes(self) -> tuple[str, dict[str, Any]]: if (attribution := self.attribution) is not None: attr[ATTR_ATTRIBUTION] = attribution + shadowed_attr[ATTR_DEVICE_CLASS] = self.device_class if ( - device_class := (entry and entry.device_class) or self.device_class + device_class := (entry and entry.device_class) + or shadowed_attr[ATTR_DEVICE_CLASS] ) is not None: attr[ATTR_DEVICE_CLASS] = str(device_class) if (entity_picture := self.entity_picture) is not None: attr[ATTR_ENTITY_PICTURE] = entity_picture - if (icon := (entry and entry.icon) or self.icon) is not None: + shadowed_attr[ATTR_ICON] = self.icon + if (icon := (entry and entry.icon) or shadowed_attr[ATTR_ICON]) is not None: attr[ATTR_ICON] = icon + shadowed_attr[ATTR_FRIENDLY_NAME] = self._friendly_name_internal() if ( - name := (entry and entry.name) or self._friendly_name_internal() + name := (entry and entry.name) or shadowed_attr[ATTR_FRIENDLY_NAME] ) is not None: attr[ATTR_FRIENDLY_NAME] = name if (supported_features := self.supported_features) is not None: attr[ATTR_SUPPORTED_FEATURES] = supported_features - return (state, attr) + return (state, attr, capability_attr, shadowed_attr) @callback def _async_write_ha_state(self) -> None: @@ -842,9 +885,45 @@ def _async_write_ha_state(self) -> None: return start = timer() - state, attr = self._async_generate_attributes() + state, attr, capabilities, shadowed_attr = self.__async_calculate_state() end = timer() + if entry: + # Make sure capabilities in the entity registry are up to date. Capabilities + # include capability attributes, device class and supported features + original_device_class: str | None = shadowed_attr[ATTR_DEVICE_CLASS] + supported_features: int = attr.get(ATTR_SUPPORTED_FEATURES) or 0 + if ( + capabilities != entry.capabilities + or original_device_class != entry.original_device_class + or supported_features != entry.supported_features + ): + if not self.__capabilities_updated_at_reported: + time_now = hass.loop.time() + capabilities_updated_at = self.__capabilities_updated_at + capabilities_updated_at.append(time_now) + while time_now - capabilities_updated_at[0] > 3600: + capabilities_updated_at.popleft() + if len(capabilities_updated_at) > CAPABILITIES_UPDATE_LIMIT: + self.__capabilities_updated_at_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + ( + "Entity %s (%s) is updating its capabilities too often," + " please %s" + ), + entity_id, + type(self), + report_issue, + ) + entity_registry = er.async_get(self.hass) + self.registry_entry = entity_registry.async_update_entity( + self.entity_id, + capabilities=capabilities, + original_device_class=original_device_class, + supported_features=supported_features, + ) + if end - start > 0.4 and not self._slow_reported: self._slow_reported = True report_issue = self._suggest_report_issue() @@ -1118,6 +1197,8 @@ async def async_internal_added_to_hass(self) -> None: ) self._async_subscribe_device_updates() + self.__capabilities_updated_at = deque(maxlen=CAPABILITIES_UPDATE_LIMIT + 1) + async def async_internal_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass. diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 76577daf8a6cb4..e9d0906970ac8c 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import MagicMock, PropertyMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol @@ -1412,8 +1413,8 @@ def state(self): """Return the state.""" raise ValueError("Boom") - entity = MyEntity(entity_id="test.test", available=False) - assert str(entity) == "" + my_entity = MyEntity(entity_id="test.test", available=False) + assert str(my_entity) == "" async def test_warn_using_async_update_ha_state( @@ -1761,3 +1762,158 @@ def __init__(self, extra, *args, **kwargs) -> None: assert obj == snapshot assert obj == CustomInitEntityDescription(key="blah", extra="foo", name="name") assert repr(obj) == snapshot + + +async def test_update_capabilities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity capabilities are updated automatically.""" + platform = MockEntityPlatform(hass) + + ent = MockEntity(unique_id="qwer") + await platform.async_add_entities([ent]) + + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities is None + assert entry.device_class is None + assert entry.supported_features == 0 + + ent._values["capability_attributes"] = {"bla": "blu"} + ent._values["device_class"] = "some_class" + ent._values["supported_features"] = 127 + ent.async_write_ha_state() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities == {"bla": "blu"} + assert entry.original_device_class == "some_class" + assert entry.supported_features == 127 + + ent._values["capability_attributes"] = None + ent._values["device_class"] = None + ent._values["supported_features"] = None + ent.async_write_ha_state() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities is None + assert entry.original_device_class is None + assert entry.supported_features == 0 + + # Device class can be overridden by user, make sure that does not break the + # automatic updating. + entity_registry.async_update_entity(ent.entity_id, device_class="set_by_user") + await hass.async_block_till_done() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities is None + assert entry.original_device_class is None + assert entry.supported_features == 0 + + # This will not trigger a state change because the device class is shadowed + # by the entity registry + ent._values["device_class"] = "some_class" + ent.async_write_ha_state() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities is None + assert entry.original_device_class == "some_class" + assert entry.supported_features == 0 + + +async def test_update_capabilities_no_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity capabilities are updated automatically.""" + platform = MockEntityPlatform(hass) + + ent = MockEntity() + await platform.async_add_entities([ent]) + + assert entity_registry.async_get(ent.entity_id) is None + + ent._values["capability_attributes"] = {"bla": "blu"} + ent._values["supported_features"] = 127 + ent.async_write_ha_state() + assert entity_registry.async_get(ent.entity_id) is None + + +async def test_update_capabilities_too_often( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity capabilities are updated automatically.""" + capabilities_too_often_warning = "is updating its capabilities too often" + platform = MockEntityPlatform(hass) + + ent = MockEntity(unique_id="qwer") + await platform.async_add_entities([ent]) + + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities is None + assert entry.device_class is None + assert entry.supported_features == 0 + + for supported_features in range(1, entity.CAPABILITIES_UPDATE_LIMIT + 1): + ent._values["capability_attributes"] = {"bla": "blu"} + ent._values["device_class"] = "some_class" + ent._values["supported_features"] = supported_features + ent.async_write_ha_state() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities == {"bla": "blu"} + assert entry.original_device_class == "some_class" + assert entry.supported_features == supported_features + + assert capabilities_too_often_warning not in caplog.text + + ent._values["capability_attributes"] = {"bla": "blu"} + ent._values["device_class"] = "some_class" + ent._values["supported_features"] = supported_features + 1 + ent.async_write_ha_state() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities == {"bla": "blu"} + assert entry.original_device_class == "some_class" + assert entry.supported_features == supported_features + 1 + + assert capabilities_too_often_warning in caplog.text + + +async def test_update_capabilities_too_often_cooldown( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test entity capabilities are updated automatically.""" + capabilities_too_often_warning = "is updating its capabilities too often" + platform = MockEntityPlatform(hass) + + ent = MockEntity(unique_id="qwer") + await platform.async_add_entities([ent]) + + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities is None + assert entry.device_class is None + assert entry.supported_features == 0 + + for supported_features in range(1, entity.CAPABILITIES_UPDATE_LIMIT + 1): + ent._values["capability_attributes"] = {"bla": "blu"} + ent._values["device_class"] = "some_class" + ent._values["supported_features"] = supported_features + ent.async_write_ha_state() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities == {"bla": "blu"} + assert entry.original_device_class == "some_class" + assert entry.supported_features == supported_features + + assert capabilities_too_often_warning not in caplog.text + + freezer.tick(timedelta(minutes=60) + timedelta(seconds=1)) + + ent._values["capability_attributes"] = {"bla": "blu"} + ent._values["device_class"] = "some_class" + ent._values["supported_features"] = supported_features + 1 + ent.async_write_ha_state() + entry = entity_registry.async_get(ent.entity_id) + assert entry.capabilities == {"bla": "blu"} + assert entry.original_device_class == "some_class" + assert entry.supported_features == supported_features + 1 + + assert capabilities_too_often_warning not in caplog.text From 08ca3678daafaa60bc127c21d9a33df53ddc286d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:07:29 +0100 Subject: [PATCH 112/118] Ensure platform setup for all AVM FRITZ!SmartHome devices (#105515) --- .../components/fritzbox/binary_sensor.py | 10 ++++++---- homeassistant/components/fritzbox/button.py | 12 ++++++------ homeassistant/components/fritzbox/climate.py | 10 ++++++---- homeassistant/components/fritzbox/cover.py | 10 ++++++---- homeassistant/components/fritzbox/light.py | 17 ++++++++--------- homeassistant/components/fritzbox/sensor.py | 10 ++++++---- homeassistant/components/fritzbox/switch.py | 10 ++++++---- 7 files changed, 44 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 2460635351e2d0..e766a53518aadb 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -70,20 +70,22 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxBinarySensor(coordinator, ain, description) - for ain in coordinator.new_devices + for ain in devices for description in BINARY_SENSOR_TYPES if description.suitable(coordinator.data.devices[ain]) ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity): diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py index 732c41bfb7de13..e56ebc1e3b05d4 100644 --- a/homeassistant/components/fritzbox/button.py +++ b/homeassistant/components/fritzbox/button.py @@ -19,17 +19,17 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(templates: set[str] | None = None) -> None: """Add templates.""" - if not coordinator.new_templates: + if templates is None: + templates = coordinator.new_templates + if not templates: return - async_add_entities( - FritzBoxTemplate(coordinator, ain) for ain in coordinator.new_templates - ) + async_add_entities(FritzBoxTemplate(coordinator, ain) for ain in templates) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.templates.keys())) class FritzBoxTemplate(FritzBoxEntity, ButtonEntity): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 70359d9b2af761..6ce885a3fdbb32 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -52,19 +52,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxThermostat(coordinator, ain) - for ain in coordinator.new_devices + for ain in devices if coordinator.data.devices[ain].has_thermostat ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py index 7d27356fdf9fc1..d3d4c9080ea0dd 100644 --- a/homeassistant/components/fritzbox/cover.py +++ b/homeassistant/components/fritzbox/cover.py @@ -24,19 +24,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxCover(coordinator, ain) - for ain in coordinator.new_devices + for ain in devices if coordinator.data.devices[ain].has_blind ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxCover(FritzBoxDeviceEntity, CoverEntity): diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 8dc51e5973826d..88d32fe33a56f4 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -30,22 +30,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( - FritzboxLight( - coordinator, - ain, - ) - for ain in coordinator.new_devices - if (coordinator.data.devices[ain]).has_lightbulb + FritzboxLight(coordinator, ain) + for ain in devices + if coordinator.data.devices[ain].has_lightbulb ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxLight(FritzBoxDeviceEntity, LightEntity): diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 1e5d7754934b19..fda8b239859f7d 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -215,20 +215,22 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzBoxSensor(coordinator, ain, description) - for ain in coordinator.new_devices + for ain in devices for description in SENSOR_TYPES if description.suitable(coordinator.data.devices[ain]) ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity): diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 617a5242c5bfca..4a2960a18ea298 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -19,19 +19,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxSwitch(coordinator, ain) - for ain in coordinator.new_devices + for ain in devices if coordinator.data.devices[ain].has_switch ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): From d322cb5fdffdaffe7585e93f93f433d2785fca4d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 13 Dec 2023 19:37:51 +0100 Subject: [PATCH 113/118] Migrate homekit_controller tests to use freezegun (#105646) --- .../components/homekit_controller/conftest.py | 11 ++- .../specific_devices/test_koogeek_ls1.py | 2 +- .../test_alarm_control_panel.py | 6 +- .../homekit_controller/test_binary_sensor.py | 14 ++-- .../homekit_controller/test_button.py | 2 +- .../homekit_controller/test_camera.py | 6 +- .../homekit_controller/test_climate.py | 78 +++++++------------ .../homekit_controller/test_cover.py | 30 ++++--- .../homekit_controller/test_device_trigger.py | 5 -- .../homekit_controller/test_diagnostics.py | 3 +- .../homekit_controller/test_event.py | 10 +-- .../components/homekit_controller/test_fan.py | 44 +++++------ .../homekit_controller/test_humidifier.py | 26 +++---- .../homekit_controller/test_init.py | 2 +- .../homekit_controller/test_light.py | 28 +++---- .../homekit_controller/test_lock.py | 6 +- .../homekit_controller/test_media_player.py | 20 ++--- .../homekit_controller/test_number.py | 6 +- .../homekit_controller/test_select.py | 10 +-- .../homekit_controller/test_sensor.py | 24 +++--- .../homekit_controller/test_storage.py | 2 +- .../homekit_controller/test_switch.py | 14 ++-- 22 files changed, 152 insertions(+), 197 deletions(-) diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 043213ec159bb4..904b752205efd3 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,9 +1,9 @@ """HomeKit controller session fixtures.""" import datetime -from unittest import mock import unittest.mock from aiohomekit.testing import FakeController +from freezegun import freeze_time import pytest import homeassistant.util.dt as dt_util @@ -13,14 +13,13 @@ pytest.register_assert_rewrite("tests.components.homekit_controller.common") -@pytest.fixture -def utcnow(request): +@pytest.fixture(autouse=True) +def freeze_time_in_future(request): """Freeze time at a known point.""" now = dt_util.utcnow() start_dt = datetime.datetime(now.year + 1, 1, 1, 0, 0, 0, tzinfo=now.tzinfo) - with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt - yield dt_utcnow + with freeze_time(start_dt) as frozen_time: + yield frozen_time @pytest.fixture diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 2c2c0b5e1c57af..baee3082106055 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -21,7 +21,7 @@ @pytest.mark.parametrize("failure_cls", [AccessoryDisconnectedError, EncryptionError]) -async def test_recover_from_failure(hass: HomeAssistant, utcnow, failure_cls) -> None: +async def test_recover_from_failure(hass: HomeAssistant, failure_cls) -> None: """Test that entity actually recovers from a network connection drop. See https://github.com/home-assistant/core/issues/18949 diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index c38c3d47bfec0d..19991d7cc1399c 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -26,7 +26,7 @@ def create_security_system_service(accessory): targ_state.value = 50 -async def test_switch_change_alarm_state(hass: HomeAssistant, utcnow) -> None: +async def test_switch_change_alarm_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit alarm on and off again.""" helper = await setup_test_component(hass, create_security_system_service) @@ -83,7 +83,7 @@ async def test_switch_change_alarm_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_switch_read_alarm_state(hass: HomeAssistant, utcnow) -> None: +async def test_switch_read_alarm_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit alarm accessory.""" helper = await setup_test_component(hass, create_security_system_service) @@ -125,7 +125,7 @@ async def test_switch_read_alarm_state(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a alarm_control_panel unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index 382d6182733dc2..92c303cab45a96 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -17,7 +17,7 @@ def create_motion_sensor_service(accessory): cur_state.value = 0 -async def test_motion_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_motion_sensor_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit motion sensor accessory.""" helper = await setup_test_component(hass, create_motion_sensor_service) @@ -44,7 +44,7 @@ def create_contact_sensor_service(accessory): cur_state.value = 0 -async def test_contact_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_contact_sensor_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit contact accessory.""" helper = await setup_test_component(hass, create_contact_sensor_service) @@ -71,7 +71,7 @@ def create_smoke_sensor_service(accessory): cur_state.value = 0 -async def test_smoke_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_smoke_sensor_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit contact accessory.""" helper = await setup_test_component(hass, create_smoke_sensor_service) @@ -98,7 +98,7 @@ def create_carbon_monoxide_sensor_service(accessory): cur_state.value = 0 -async def test_carbon_monoxide_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_carbon_monoxide_sensor_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit contact accessory.""" helper = await setup_test_component(hass, create_carbon_monoxide_sensor_service) @@ -127,7 +127,7 @@ def create_occupancy_sensor_service(accessory): cur_state.value = 0 -async def test_occupancy_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_occupancy_sensor_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit occupancy sensor accessory.""" helper = await setup_test_component(hass, create_occupancy_sensor_service) @@ -154,7 +154,7 @@ def create_leak_sensor_service(accessory): cur_state.value = 0 -async def test_leak_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_leak_sensor_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit leak sensor accessory.""" helper = await setup_test_component(hass, create_leak_sensor_service) @@ -174,7 +174,7 @@ async def test_leak_sensor_read_state(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a binary_sensor unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_button.py b/tests/components/homekit_controller/test_button.py index 1f08b578a93a92..57592fb7a2789e 100644 --- a/tests/components/homekit_controller/test_button.py +++ b/tests/components/homekit_controller/test_button.py @@ -95,7 +95,7 @@ async def test_ecobee_clear_hold_press_button(hass: HomeAssistant) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a button unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_camera.py b/tests/components/homekit_controller/test_camera.py index bbb8e5a8eaa675..f74f2e62772b37 100644 --- a/tests/components/homekit_controller/test_camera.py +++ b/tests/components/homekit_controller/test_camera.py @@ -17,7 +17,7 @@ def create_camera(accessory): async def test_migrate_unique_ids( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test migrating entity unique ids.""" aid = get_next_aid() @@ -33,7 +33,7 @@ async def test_migrate_unique_ids( ) -async def test_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_read_state(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit camera.""" helper = await setup_test_component(hass, create_camera) @@ -41,7 +41,7 @@ async def test_read_state(hass: HomeAssistant, utcnow) -> None: assert state.state == "idle" -async def test_get_image(hass: HomeAssistant, utcnow) -> None: +async def test_get_image(hass: HomeAssistant) -> None: """Test getting a JPEG from a camera.""" helper = await setup_test_component(hass, create_camera) image = await camera.async_get_image(hass, helper.entity_id) diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index c80016770fdac9..c3882553ea0294 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -72,9 +72,7 @@ def create_thermostat_service_min_max(accessory): char.maxValue = 1 -async def test_climate_respect_supported_op_modes_1( - hass: HomeAssistant, utcnow -) -> None: +async def test_climate_respect_supported_op_modes_1(hass: HomeAssistant) -> None: """Test that climate respects minValue/maxValue hints.""" helper = await setup_test_component(hass, create_thermostat_service_min_max) state = await helper.poll_and_get_state() @@ -89,16 +87,14 @@ def create_thermostat_service_valid_vals(accessory): char.valid_values = [0, 1, 2] -async def test_climate_respect_supported_op_modes_2( - hass: HomeAssistant, utcnow -) -> None: +async def test_climate_respect_supported_op_modes_2(hass: HomeAssistant) -> None: """Test that climate respects validValue hints.""" helper = await setup_test_component(hass, create_thermostat_service_valid_vals) state = await helper.poll_and_get_state() assert state.attributes["hvac_modes"] == ["off", "heat", "cool"] -async def test_climate_change_thermostat_state(hass: HomeAssistant, utcnow) -> None: +async def test_climate_change_thermostat_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -181,9 +177,7 @@ async def test_climate_change_thermostat_state(hass: HomeAssistant, utcnow) -> N ) -async def test_climate_check_min_max_values_per_mode( - hass: HomeAssistant, utcnow -) -> None: +async def test_climate_check_min_max_values_per_mode(hass: HomeAssistant) -> None: """Test that we we get the appropriate min/max values for each mode.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -218,9 +212,7 @@ async def test_climate_check_min_max_values_per_mode( assert climate_state.attributes["max_temp"] == 40 -async def test_climate_change_thermostat_temperature( - hass: HomeAssistant, utcnow -) -> None: +async def test_climate_change_thermostat_temperature(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -251,9 +243,7 @@ async def test_climate_change_thermostat_temperature( ) -async def test_climate_change_thermostat_temperature_range( - hass: HomeAssistant, utcnow -) -> None: +async def test_climate_change_thermostat_temperature_range(hass: HomeAssistant) -> None: """Test that we can set separate heat and cool setpoints in heat_cool mode.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -287,7 +277,7 @@ async def test_climate_change_thermostat_temperature_range( async def test_climate_change_thermostat_temperature_range_iphone( - hass: HomeAssistant, utcnow + hass: HomeAssistant ) -> None: """Test that we can set all three set points at once (iPhone heat_cool mode support).""" helper = await setup_test_component(hass, create_thermostat_service) @@ -322,7 +312,7 @@ async def test_climate_change_thermostat_temperature_range_iphone( async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode( - hass: HomeAssistant, utcnow + hass: HomeAssistant ) -> None: """Test that we cannot set range values when not in heat_cool mode.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -381,7 +371,7 @@ def create_thermostat_single_set_point_auto(accessory): async def test_climate_check_min_max_values_per_mode_sspa_device( - hass: HomeAssistant, utcnow + hass: HomeAssistant ) -> None: """Test appropriate min/max values for each mode on sspa devices.""" helper = await setup_test_component(hass, create_thermostat_single_set_point_auto) @@ -417,9 +407,7 @@ async def test_climate_check_min_max_values_per_mode_sspa_device( assert climate_state.attributes["max_temp"] == 35 -async def test_climate_set_thermostat_temp_on_sspa_device( - hass: HomeAssistant, utcnow -) -> None: +async def test_climate_set_thermostat_temp_on_sspa_device(hass: HomeAssistant) -> None: """Test setting temperature in different modes on device with single set point in auto.""" helper = await setup_test_component(hass, create_thermostat_single_set_point_auto) @@ -473,7 +461,7 @@ async def test_climate_set_thermostat_temp_on_sspa_device( ) -async def test_climate_set_mode_via_temp(hass: HomeAssistant, utcnow) -> None: +async def test_climate_set_mode_via_temp(hass: HomeAssistant) -> None: """Test setting temperature and mode at same tims.""" helper = await setup_test_component(hass, create_thermostat_single_set_point_auto) @@ -514,7 +502,7 @@ async def test_climate_set_mode_via_temp(hass: HomeAssistant, utcnow) -> None: ) -async def test_climate_change_thermostat_humidity(hass: HomeAssistant, utcnow) -> None: +async def test_climate_change_thermostat_humidity(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -545,7 +533,7 @@ async def test_climate_change_thermostat_humidity(hass: HomeAssistant, utcnow) - ) -async def test_climate_read_thermostat_state(hass: HomeAssistant, utcnow) -> None: +async def test_climate_read_thermostat_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit thermostat accessory.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -602,7 +590,7 @@ async def test_climate_read_thermostat_state(hass: HomeAssistant, utcnow) -> Non assert state.state == HVACMode.HEAT_COOL -async def test_hvac_mode_vs_hvac_action(hass: HomeAssistant, utcnow) -> None: +async def test_hvac_mode_vs_hvac_action(hass: HomeAssistant) -> None: """Check that we haven't conflated hvac_mode and hvac_action.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -639,9 +627,7 @@ async def test_hvac_mode_vs_hvac_action(hass: HomeAssistant, utcnow) -> None: assert state.attributes["hvac_action"] == "heating" -async def test_hvac_mode_vs_hvac_action_current_mode_wrong( - hass: HomeAssistant, utcnow -) -> None: +async def test_hvac_mode_vs_hvac_action_current_mode_wrong(hass: HomeAssistant) -> None: """Check that we cope with buggy HEATING_COOLING_CURRENT.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -705,9 +691,7 @@ def create_heater_cooler_service_min_max(accessory): char.maxValue = 2 -async def test_heater_cooler_respect_supported_op_modes_1( - hass: HomeAssistant, utcnow -) -> None: +async def test_heater_cooler_respect_supported_op_modes_1(hass: HomeAssistant) -> None: """Test that climate respects minValue/maxValue hints.""" helper = await setup_test_component(hass, create_heater_cooler_service_min_max) state = await helper.poll_and_get_state() @@ -722,18 +706,14 @@ def create_theater_cooler_service_valid_vals(accessory): char.valid_values = [1, 2] -async def test_heater_cooler_respect_supported_op_modes_2( - hass: HomeAssistant, utcnow -) -> None: +async def test_heater_cooler_respect_supported_op_modes_2(hass: HomeAssistant) -> None: """Test that climate respects validValue hints.""" helper = await setup_test_component(hass, create_theater_cooler_service_valid_vals) state = await helper.poll_and_get_state() assert state.attributes["hvac_modes"] == ["heat", "cool", "off"] -async def test_heater_cooler_change_thermostat_state( - hass: HomeAssistant, utcnow -) -> None: +async def test_heater_cooler_change_thermostat_state(hass: HomeAssistant) -> None: """Test that we can change the operational mode.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -790,7 +770,7 @@ async def test_heater_cooler_change_thermostat_state( ) -async def test_can_turn_on_after_off(hass: HomeAssistant, utcnow) -> None: +async def test_can_turn_on_after_off(hass: HomeAssistant) -> None: """Test that we always force device from inactive to active when setting mode. This is a regression test for #81863. @@ -825,9 +805,7 @@ async def test_can_turn_on_after_off(hass: HomeAssistant, utcnow) -> None: ) -async def test_heater_cooler_change_thermostat_temperature( - hass: HomeAssistant, utcnow -) -> None: +async def test_heater_cooler_change_thermostat_temperature(hass: HomeAssistant) -> None: """Test that we can change the target temperature.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -870,7 +848,7 @@ async def test_heater_cooler_change_thermostat_temperature( ) -async def test_heater_cooler_change_fan_speed(hass: HomeAssistant, utcnow) -> None: +async def test_heater_cooler_change_fan_speed(hass: HomeAssistant) -> None: """Test that we can change the target fan speed.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -918,7 +896,7 @@ async def test_heater_cooler_change_fan_speed(hass: HomeAssistant, utcnow) -> No ) -async def test_heater_cooler_read_fan_speed(hass: HomeAssistant, utcnow) -> None: +async def test_heater_cooler_read_fan_speed(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit thermostat accessory.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -967,7 +945,7 @@ async def test_heater_cooler_read_fan_speed(hass: HomeAssistant, utcnow) -> None assert state.attributes["fan_mode"] == "high" -async def test_heater_cooler_read_thermostat_state(hass: HomeAssistant, utcnow) -> None: +async def test_heater_cooler_read_thermostat_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit thermostat accessory.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -1021,9 +999,7 @@ async def test_heater_cooler_read_thermostat_state(hass: HomeAssistant, utcnow) assert state.state == HVACMode.HEAT_COOL -async def test_heater_cooler_hvac_mode_vs_hvac_action( - hass: HomeAssistant, utcnow -) -> None: +async def test_heater_cooler_hvac_mode_vs_hvac_action(hass: HomeAssistant) -> None: """Check that we haven't conflated hvac_mode and hvac_action.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -1062,7 +1038,7 @@ async def test_heater_cooler_hvac_mode_vs_hvac_action( assert state.attributes["hvac_action"] == "heating" -async def test_heater_cooler_change_swing_mode(hass: HomeAssistant, utcnow) -> None: +async def test_heater_cooler_change_swing_mode(hass: HomeAssistant) -> None: """Test that we can change the swing mode.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -1093,7 +1069,7 @@ async def test_heater_cooler_change_swing_mode(hass: HomeAssistant, utcnow) -> N ) -async def test_heater_cooler_turn_off(hass: HomeAssistant, utcnow) -> None: +async def test_heater_cooler_turn_off(hass: HomeAssistant) -> None: """Test that both hvac_action and hvac_mode return "off" when turned off.""" helper = await setup_test_component(hass, create_heater_cooler_service) @@ -1113,7 +1089,7 @@ async def test_heater_cooler_turn_off(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a switch unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 49462a035e98c7..7d004a8a428e91 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -93,7 +93,7 @@ def create_window_covering_service_with_v_tilt_2(accessory): tilt_target.maxValue = 0 -async def test_change_window_cover_state(hass: HomeAssistant, utcnow) -> None: +async def test_change_window_cover_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit alarm on and off again.""" helper = await setup_test_component(hass, create_window_covering_service) @@ -118,7 +118,7 @@ async def test_change_window_cover_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_read_window_cover_state(hass: HomeAssistant, utcnow) -> None: +async def test_read_window_cover_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit alarm accessory.""" helper = await setup_test_component(hass, create_window_covering_service) @@ -151,7 +151,7 @@ async def test_read_window_cover_state(hass: HomeAssistant, utcnow) -> None: assert state.attributes["obstruction-detected"] is True -async def test_read_window_cover_tilt_horizontal(hass: HomeAssistant, utcnow) -> None: +async def test_read_window_cover_tilt_horizontal(hass: HomeAssistant) -> None: """Test that horizontal tilt is handled correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_h_tilt @@ -166,7 +166,7 @@ async def test_read_window_cover_tilt_horizontal(hass: HomeAssistant, utcnow) -> assert state.attributes["current_tilt_position"] == 83 -async def test_read_window_cover_tilt_horizontal_2(hass: HomeAssistant, utcnow) -> None: +async def test_read_window_cover_tilt_horizontal_2(hass: HomeAssistant) -> None: """Test that horizontal tilt is handled correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_h_tilt_2 @@ -181,7 +181,7 @@ async def test_read_window_cover_tilt_horizontal_2(hass: HomeAssistant, utcnow) assert state.attributes["current_tilt_position"] == 83 -async def test_read_window_cover_tilt_vertical(hass: HomeAssistant, utcnow) -> None: +async def test_read_window_cover_tilt_vertical(hass: HomeAssistant) -> None: """Test that vertical tilt is handled correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_v_tilt @@ -196,7 +196,7 @@ async def test_read_window_cover_tilt_vertical(hass: HomeAssistant, utcnow) -> N assert state.attributes["current_tilt_position"] == 83 -async def test_read_window_cover_tilt_vertical_2(hass: HomeAssistant, utcnow) -> None: +async def test_read_window_cover_tilt_vertical_2(hass: HomeAssistant) -> None: """Test that vertical tilt is handled correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_v_tilt_2 @@ -211,7 +211,7 @@ async def test_read_window_cover_tilt_vertical_2(hass: HomeAssistant, utcnow) -> assert state.attributes["current_tilt_position"] == 83 -async def test_write_window_cover_tilt_horizontal(hass: HomeAssistant, utcnow) -> None: +async def test_write_window_cover_tilt_horizontal(hass: HomeAssistant) -> None: """Test that horizontal tilt is written correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_h_tilt @@ -232,9 +232,7 @@ async def test_write_window_cover_tilt_horizontal(hass: HomeAssistant, utcnow) - ) -async def test_write_window_cover_tilt_horizontal_2( - hass: HomeAssistant, utcnow -) -> None: +async def test_write_window_cover_tilt_horizontal_2(hass: HomeAssistant) -> None: """Test that horizontal tilt is written correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_h_tilt_2 @@ -255,7 +253,7 @@ async def test_write_window_cover_tilt_horizontal_2( ) -async def test_write_window_cover_tilt_vertical(hass: HomeAssistant, utcnow) -> None: +async def test_write_window_cover_tilt_vertical(hass: HomeAssistant) -> None: """Test that vertical tilt is written correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_v_tilt @@ -276,7 +274,7 @@ async def test_write_window_cover_tilt_vertical(hass: HomeAssistant, utcnow) -> ) -async def test_write_window_cover_tilt_vertical_2(hass: HomeAssistant, utcnow) -> None: +async def test_write_window_cover_tilt_vertical_2(hass: HomeAssistant) -> None: """Test that vertical tilt is written correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_v_tilt_2 @@ -297,7 +295,7 @@ async def test_write_window_cover_tilt_vertical_2(hass: HomeAssistant, utcnow) - ) -async def test_window_cover_stop(hass: HomeAssistant, utcnow) -> None: +async def test_window_cover_stop(hass: HomeAssistant) -> None: """Test that vertical tilt is written correctly.""" helper = await setup_test_component( hass, create_window_covering_service_with_v_tilt @@ -333,7 +331,7 @@ def create_garage_door_opener_service(accessory): return service -async def test_change_door_state(hass: HomeAssistant, utcnow) -> None: +async def test_change_door_state(hass: HomeAssistant) -> None: """Test that we can turn open and close a HomeKit garage door.""" helper = await setup_test_component(hass, create_garage_door_opener_service) @@ -358,7 +356,7 @@ async def test_change_door_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_read_door_state(hass: HomeAssistant, utcnow) -> None: +async def test_read_door_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit garage door.""" helper = await setup_test_component(hass, create_garage_door_opener_service) @@ -399,7 +397,7 @@ async def test_read_door_state(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a cover unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index ed3894c331b64f..2f66a1eea26661 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -87,7 +87,6 @@ async def test_enumerate_remote( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - utcnow, ) -> None: """Test that remote is correctly enumerated.""" await setup_test_component(hass, create_remote) @@ -139,7 +138,6 @@ async def test_enumerate_button( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - utcnow, ) -> None: """Test that a button is correctly enumerated.""" await setup_test_component(hass, create_button) @@ -190,7 +188,6 @@ async def test_enumerate_doorbell( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - utcnow, ) -> None: """Test that a button is correctly enumerated.""" await setup_test_component(hass, create_doorbell) @@ -241,7 +238,6 @@ async def test_handle_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - utcnow, calls, ) -> None: """Test that events are handled.""" @@ -362,7 +358,6 @@ async def test_handle_events_late_setup( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - utcnow, calls, ) -> None: """Test that events are handled when setup happens after startup.""" diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index 0f1073b877d56d..a9780c7f80cf61 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -15,7 +15,7 @@ async def test_config_entry( - hass: HomeAssistant, hass_client: ClientSessionGenerator, utcnow + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test generating diagnostics for a config entry.""" accessories = await setup_accessories_from_file(hass, "koogeek_ls1.json") @@ -293,7 +293,6 @@ async def test_device( hass: HomeAssistant, hass_client: ClientSessionGenerator, device_registry: dr.DeviceRegistry, - utcnow, ) -> None: """Test generating diagnostics for a device entry.""" accessories = await setup_accessories_from_file(hass, "koogeek_ls1.json") diff --git a/tests/components/homekit_controller/test_event.py b/tests/components/homekit_controller/test_event.py index 7fb0d1fd55f4d4..a836fb1c669841 100644 --- a/tests/components/homekit_controller/test_event.py +++ b/tests/components/homekit_controller/test_event.py @@ -64,9 +64,7 @@ def create_doorbell(accessory): battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) -async def test_remote( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow -) -> None: +async def test_remote(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: """Test that remote is supported.""" helper = await setup_test_component(hass, create_remote) @@ -109,9 +107,7 @@ async def test_remote( assert state.attributes["event_type"] == "long_press" -async def test_button( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow -) -> None: +async def test_button(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: """Test that a button is correctly enumerated.""" helper = await setup_test_component(hass, create_button) entity_id = "event.testdevice_button_1" @@ -148,7 +144,7 @@ async def test_button( async def test_doorbell( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test that doorbell service is handled.""" helper = await setup_test_component(hass, create_doorbell) diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index 2fb64fc345d3a0..7afadadcd98787 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -89,7 +89,7 @@ def create_fanv2_service_without_rotation_speed(accessory): swing_mode.value = 0 -async def test_fan_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_fan_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit fan accessory.""" helper = await setup_test_component(hass, create_fan_service) @@ -104,7 +104,7 @@ async def test_fan_read_state(hass: HomeAssistant, utcnow) -> None: assert state.state == "on" -async def test_turn_on(hass: HomeAssistant, utcnow) -> None: +async def test_turn_on(hass: HomeAssistant) -> None: """Test that we can turn a fan on.""" helper = await setup_test_component(hass, create_fan_service) @@ -151,7 +151,7 @@ async def test_turn_on(hass: HomeAssistant, utcnow) -> None: ) -async def test_turn_on_off_without_rotation_speed(hass: HomeAssistant, utcnow) -> None: +async def test_turn_on_off_without_rotation_speed(hass: HomeAssistant) -> None: """Test that we can turn a fan on.""" helper = await setup_test_component( hass, create_fanv2_service_without_rotation_speed @@ -184,7 +184,7 @@ async def test_turn_on_off_without_rotation_speed(hass: HomeAssistant, utcnow) - ) -async def test_turn_off(hass: HomeAssistant, utcnow) -> None: +async def test_turn_off(hass: HomeAssistant) -> None: """Test that we can turn a fan off.""" helper = await setup_test_component(hass, create_fan_service) @@ -204,7 +204,7 @@ async def test_turn_off(hass: HomeAssistant, utcnow) -> None: ) -async def test_set_speed(hass: HomeAssistant, utcnow) -> None: +async def test_set_speed(hass: HomeAssistant) -> None: """Test that we set fan speed.""" helper = await setup_test_component(hass, create_fan_service) @@ -263,7 +263,7 @@ async def test_set_speed(hass: HomeAssistant, utcnow) -> None: ) -async def test_set_percentage(hass: HomeAssistant, utcnow) -> None: +async def test_set_percentage(hass: HomeAssistant) -> None: """Test that we set fan speed by percentage.""" helper = await setup_test_component(hass, create_fan_service) @@ -296,7 +296,7 @@ async def test_set_percentage(hass: HomeAssistant, utcnow) -> None: ) -async def test_speed_read(hass: HomeAssistant, utcnow) -> None: +async def test_speed_read(hass: HomeAssistant) -> None: """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fan_service) @@ -336,7 +336,7 @@ async def test_speed_read(hass: HomeAssistant, utcnow) -> None: assert state.attributes["percentage"] == 0 -async def test_set_direction(hass: HomeAssistant, utcnow) -> None: +async def test_set_direction(hass: HomeAssistant) -> None: """Test that we can set fan spin direction.""" helper = await setup_test_component(hass, create_fan_service) @@ -367,7 +367,7 @@ async def test_set_direction(hass: HomeAssistant, utcnow) -> None: ) -async def test_direction_read(hass: HomeAssistant, utcnow) -> None: +async def test_direction_read(hass: HomeAssistant) -> None: """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fan_service) @@ -382,7 +382,7 @@ async def test_direction_read(hass: HomeAssistant, utcnow) -> None: assert state.attributes["direction"] == "reverse" -async def test_fanv2_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_fanv2_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit fan accessory.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -397,7 +397,7 @@ async def test_fanv2_read_state(hass: HomeAssistant, utcnow) -> None: assert state.state == "on" -async def test_v2_turn_on(hass: HomeAssistant, utcnow) -> None: +async def test_v2_turn_on(hass: HomeAssistant) -> None: """Test that we can turn a fan on.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -472,7 +472,7 @@ async def test_v2_turn_on(hass: HomeAssistant, utcnow) -> None: ) -async def test_v2_turn_off(hass: HomeAssistant, utcnow) -> None: +async def test_v2_turn_off(hass: HomeAssistant) -> None: """Test that we can turn a fan off.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -492,7 +492,7 @@ async def test_v2_turn_off(hass: HomeAssistant, utcnow) -> None: ) -async def test_v2_set_speed(hass: HomeAssistant, utcnow) -> None: +async def test_v2_set_speed(hass: HomeAssistant) -> None: """Test that we set fan speed.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -551,7 +551,7 @@ async def test_v2_set_speed(hass: HomeAssistant, utcnow) -> None: ) -async def test_v2_set_percentage(hass: HomeAssistant, utcnow) -> None: +async def test_v2_set_percentage(hass: HomeAssistant) -> None: """Test that we set fan speed by percentage.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -584,7 +584,7 @@ async def test_v2_set_percentage(hass: HomeAssistant, utcnow) -> None: ) -async def test_v2_set_percentage_with_min_step(hass: HomeAssistant, utcnow) -> None: +async def test_v2_set_percentage_with_min_step(hass: HomeAssistant) -> None: """Test that we set fan speed by percentage.""" helper = await setup_test_component(hass, create_fanv2_service_with_min_step) @@ -617,7 +617,7 @@ async def test_v2_set_percentage_with_min_step(hass: HomeAssistant, utcnow) -> N ) -async def test_v2_speed_read(hass: HomeAssistant, utcnow) -> None: +async def test_v2_speed_read(hass: HomeAssistant) -> None: """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -656,7 +656,7 @@ async def test_v2_speed_read(hass: HomeAssistant, utcnow) -> None: assert state.attributes["percentage"] == 0 -async def test_v2_set_direction(hass: HomeAssistant, utcnow) -> None: +async def test_v2_set_direction(hass: HomeAssistant) -> None: """Test that we can set fan spin direction.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -687,7 +687,7 @@ async def test_v2_set_direction(hass: HomeAssistant, utcnow) -> None: ) -async def test_v2_direction_read(hass: HomeAssistant, utcnow) -> None: +async def test_v2_direction_read(hass: HomeAssistant) -> None: """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -702,7 +702,7 @@ async def test_v2_direction_read(hass: HomeAssistant, utcnow) -> None: assert state.attributes["direction"] == "reverse" -async def test_v2_oscillate(hass: HomeAssistant, utcnow) -> None: +async def test_v2_oscillate(hass: HomeAssistant) -> None: """Test that we can control a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -733,7 +733,7 @@ async def test_v2_oscillate(hass: HomeAssistant, utcnow) -> None: ) -async def test_v2_oscillate_read(hass: HomeAssistant, utcnow) -> None: +async def test_v2_oscillate_read(hass: HomeAssistant) -> None: """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -749,7 +749,7 @@ async def test_v2_oscillate_read(hass: HomeAssistant, utcnow) -> None: async def test_v2_set_percentage_non_standard_rotation_range( - hass: HomeAssistant, utcnow + hass: HomeAssistant ) -> None: """Test that we set fan speed with a non-standard rotation range.""" helper = await setup_test_component( @@ -812,7 +812,7 @@ async def test_v2_set_percentage_non_standard_rotation_range( async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a fan unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index 718c69573569c7..1a1db53d8dd770 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -63,7 +63,7 @@ def create_dehumidifier_service(accessory): return service -async def test_humidifier_active_state(hass: HomeAssistant, utcnow) -> None: +async def test_humidifier_active_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit humidifier on and off again.""" helper = await setup_test_component(hass, create_humidifier_service) @@ -86,7 +86,7 @@ async def test_humidifier_active_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_dehumidifier_active_state(hass: HomeAssistant, utcnow) -> None: +async def test_dehumidifier_active_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit dehumidifier on and off again.""" helper = await setup_test_component(hass, create_dehumidifier_service) @@ -109,7 +109,7 @@ async def test_dehumidifier_active_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_humidifier_read_humidity(hass: HomeAssistant, utcnow) -> None: +async def test_humidifier_read_humidity(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) @@ -148,7 +148,7 @@ async def test_humidifier_read_humidity(hass: HomeAssistant, utcnow) -> None: assert state.state == "off" -async def test_dehumidifier_read_humidity(hass: HomeAssistant, utcnow) -> None: +async def test_dehumidifier_read_humidity(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit dehumidifier accessory.""" helper = await setup_test_component(hass, create_dehumidifier_service) @@ -185,7 +185,7 @@ async def test_dehumidifier_read_humidity(hass: HomeAssistant, utcnow) -> None: assert state.attributes["humidity"] == 40 -async def test_humidifier_set_humidity(hass: HomeAssistant, utcnow) -> None: +async def test_humidifier_set_humidity(hass: HomeAssistant) -> None: """Test that we can set the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) @@ -201,7 +201,7 @@ async def test_humidifier_set_humidity(hass: HomeAssistant, utcnow) -> None: ) -async def test_dehumidifier_set_humidity(hass: HomeAssistant, utcnow) -> None: +async def test_dehumidifier_set_humidity(hass: HomeAssistant) -> None: """Test that we can set the state of a HomeKit dehumidifier accessory.""" helper = await setup_test_component(hass, create_dehumidifier_service) @@ -217,7 +217,7 @@ async def test_dehumidifier_set_humidity(hass: HomeAssistant, utcnow) -> None: ) -async def test_humidifier_set_mode(hass: HomeAssistant, utcnow) -> None: +async def test_humidifier_set_mode(hass: HomeAssistant) -> None: """Test that we can set the mode of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) @@ -250,7 +250,7 @@ async def test_humidifier_set_mode(hass: HomeAssistant, utcnow) -> None: ) -async def test_dehumidifier_set_mode(hass: HomeAssistant, utcnow) -> None: +async def test_dehumidifier_set_mode(hass: HomeAssistant) -> None: """Test that we can set the mode of a HomeKit dehumidifier accessory.""" helper = await setup_test_component(hass, create_dehumidifier_service) @@ -283,7 +283,7 @@ async def test_dehumidifier_set_mode(hass: HomeAssistant, utcnow) -> None: ) -async def test_humidifier_read_only_mode(hass: HomeAssistant, utcnow) -> None: +async def test_humidifier_read_only_mode(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) @@ -323,7 +323,7 @@ async def test_humidifier_read_only_mode(hass: HomeAssistant, utcnow) -> None: assert state.attributes["mode"] == "normal" -async def test_dehumidifier_read_only_mode(hass: HomeAssistant, utcnow) -> None: +async def test_dehumidifier_read_only_mode(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit dehumidifier accessory.""" helper = await setup_test_component(hass, create_dehumidifier_service) @@ -363,7 +363,7 @@ async def test_dehumidifier_read_only_mode(hass: HomeAssistant, utcnow) -> None: assert state.attributes["mode"] == "normal" -async def test_humidifier_target_humidity_modes(hass: HomeAssistant, utcnow) -> None: +async def test_humidifier_target_humidity_modes(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) @@ -408,7 +408,7 @@ async def test_humidifier_target_humidity_modes(hass: HomeAssistant, utcnow) -> assert state.attributes["humidity"] == 37 -async def test_dehumidifier_target_humidity_modes(hass: HomeAssistant, utcnow) -> None: +async def test_dehumidifier_target_humidity_modes(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit dehumidifier accessory.""" helper = await setup_test_component(hass, create_dehumidifier_service) @@ -456,7 +456,7 @@ async def test_dehumidifier_target_humidity_modes(hass: HomeAssistant, utcnow) - async def test_migrate_entity_ids( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test that we can migrate humidifier entity ids.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 7f7bec3bb2fe60..57d206a6025be2 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -46,7 +46,7 @@ def create_motion_sensor_service(accessory): cur_state.value = 0 -async def test_unload_on_stop(hass: HomeAssistant, utcnow) -> None: +async def test_unload_on_stop(hass: HomeAssistant) -> None: """Test async_unload is called on stop.""" await setup_test_component(hass, create_motion_sensor_service) with patch( diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 5d33d744de7aa6..72bf579b36ef99 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -54,7 +54,7 @@ def create_lightbulb_service_with_color_temp(accessory): return service -async def test_switch_change_light_state(hass: HomeAssistant, utcnow) -> None: +async def test_switch_change_light_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit light on and off again.""" helper = await setup_test_component(hass, create_lightbulb_service_with_hs) @@ -85,9 +85,7 @@ async def test_switch_change_light_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_switch_change_light_state_color_temp( - hass: HomeAssistant, utcnow -) -> None: +async def test_switch_change_light_state_color_temp(hass: HomeAssistant) -> None: """Test that we can turn change color_temp.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) @@ -107,7 +105,7 @@ async def test_switch_change_light_state_color_temp( ) -async def test_switch_read_light_state_dimmer(hass: HomeAssistant, utcnow) -> None: +async def test_switch_read_light_state_dimmer(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit light accessory.""" helper = await setup_test_component(hass, create_lightbulb_service) @@ -142,7 +140,7 @@ async def test_switch_read_light_state_dimmer(hass: HomeAssistant, utcnow) -> No assert state.state == "off" -async def test_switch_push_light_state_dimmer(hass: HomeAssistant, utcnow) -> None: +async def test_switch_push_light_state_dimmer(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit light accessory.""" helper = await setup_test_component(hass, create_lightbulb_service) @@ -170,7 +168,7 @@ async def test_switch_push_light_state_dimmer(hass: HomeAssistant, utcnow) -> No assert state.state == "off" -async def test_switch_read_light_state_hs(hass: HomeAssistant, utcnow) -> None: +async def test_switch_read_light_state_hs(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit light accessory.""" helper = await setup_test_component(hass, create_lightbulb_service_with_hs) @@ -208,7 +206,7 @@ async def test_switch_read_light_state_hs(hass: HomeAssistant, utcnow) -> None: assert state.state == "off" -async def test_switch_push_light_state_hs(hass: HomeAssistant, utcnow) -> None: +async def test_switch_push_light_state_hs(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit light accessory.""" helper = await setup_test_component(hass, create_lightbulb_service_with_hs) @@ -239,7 +237,7 @@ async def test_switch_push_light_state_hs(hass: HomeAssistant, utcnow) -> None: assert state.state == "off" -async def test_switch_read_light_state_color_temp(hass: HomeAssistant, utcnow) -> None: +async def test_switch_read_light_state_color_temp(hass: HomeAssistant) -> None: """Test that we can read the color_temp of a light accessory.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) @@ -267,7 +265,7 @@ async def test_switch_read_light_state_color_temp(hass: HomeAssistant, utcnow) - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_switch_push_light_state_color_temp(hass: HomeAssistant, utcnow) -> None: +async def test_switch_push_light_state_color_temp(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit light accessory.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) @@ -288,9 +286,7 @@ async def test_switch_push_light_state_color_temp(hass: HomeAssistant, utcnow) - assert state.attributes["color_temp"] == 400 -async def test_light_becomes_unavailable_but_recovers( - hass: HomeAssistant, utcnow -) -> None: +async def test_light_becomes_unavailable_but_recovers(hass: HomeAssistant) -> None: """Test transition to and from unavailable state.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) @@ -318,7 +314,7 @@ async def test_light_becomes_unavailable_but_recovers( assert state.attributes["color_temp"] == 400 -async def test_light_unloaded_removed(hass: HomeAssistant, utcnow) -> None: +async def test_light_unloaded_removed(hass: HomeAssistant) -> None: """Test entity and HKDevice are correctly unloaded and removed.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) @@ -344,7 +340,7 @@ async def test_light_unloaded_removed(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a light unique id.""" aid = get_next_aid() @@ -362,7 +358,7 @@ async def test_migrate_unique_id( async def test_only_migrate_once( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we handle migration happening after an upgrade and than a downgrade and then an upgrade.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py index e265bf586a2c9c..9aacda81683942 100644 --- a/tests/components/homekit_controller/test_lock.py +++ b/tests/components/homekit_controller/test_lock.py @@ -28,7 +28,7 @@ def create_lock_service(accessory): return service -async def test_switch_change_lock_state(hass: HomeAssistant, utcnow) -> None: +async def test_switch_change_lock_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit lock on and off again.""" helper = await setup_test_component(hass, create_lock_service) @@ -53,7 +53,7 @@ async def test_switch_change_lock_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_switch_read_lock_state(hass: HomeAssistant, utcnow) -> None: +async def test_switch_read_lock_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit lock accessory.""" helper = await setup_test_component(hass, create_lock_service) @@ -118,7 +118,7 @@ async def test_switch_read_lock_state(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a lock unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_media_player.py b/tests/components/homekit_controller/test_media_player.py index e9ea1d552ced75..1573fccea02203 100644 --- a/tests/components/homekit_controller/test_media_player.py +++ b/tests/components/homekit_controller/test_media_player.py @@ -61,7 +61,7 @@ def create_tv_service_with_target_media_state(accessory): return service -async def test_tv_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_tv_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit fan accessory.""" helper = await setup_test_component(hass, create_tv_service) @@ -90,7 +90,7 @@ async def test_tv_read_state(hass: HomeAssistant, utcnow) -> None: assert state.state == "idle" -async def test_tv_read_sources(hass: HomeAssistant, utcnow) -> None: +async def test_tv_read_sources(hass: HomeAssistant) -> None: """Test that we can read the input source of a HomeKit TV.""" helper = await setup_test_component(hass, create_tv_service) @@ -99,7 +99,7 @@ async def test_tv_read_sources(hass: HomeAssistant, utcnow) -> None: assert state.attributes["source_list"] == ["HDMI 1", "HDMI 2"] -async def test_play_remote_key(hass: HomeAssistant, utcnow) -> None: +async def test_play_remote_key(hass: HomeAssistant) -> None: """Test that we can play media on a media player.""" helper = await setup_test_component(hass, create_tv_service) @@ -146,7 +146,7 @@ async def test_play_remote_key(hass: HomeAssistant, utcnow) -> None: ) -async def test_pause_remote_key(hass: HomeAssistant, utcnow) -> None: +async def test_pause_remote_key(hass: HomeAssistant) -> None: """Test that we can pause a media player.""" helper = await setup_test_component(hass, create_tv_service) @@ -193,7 +193,7 @@ async def test_pause_remote_key(hass: HomeAssistant, utcnow) -> None: ) -async def test_play(hass: HomeAssistant, utcnow) -> None: +async def test_play(hass: HomeAssistant) -> None: """Test that we can play media on a media player.""" helper = await setup_test_component(hass, create_tv_service_with_target_media_state) @@ -242,7 +242,7 @@ async def test_play(hass: HomeAssistant, utcnow) -> None: ) -async def test_pause(hass: HomeAssistant, utcnow) -> None: +async def test_pause(hass: HomeAssistant) -> None: """Test that we can turn pause a media player.""" helper = await setup_test_component(hass, create_tv_service_with_target_media_state) @@ -290,7 +290,7 @@ async def test_pause(hass: HomeAssistant, utcnow) -> None: ) -async def test_stop(hass: HomeAssistant, utcnow) -> None: +async def test_stop(hass: HomeAssistant) -> None: """Test that we can stop a media player.""" helper = await setup_test_component(hass, create_tv_service_with_target_media_state) @@ -331,7 +331,7 @@ async def test_stop(hass: HomeAssistant, utcnow) -> None: ) -async def test_tv_set_source(hass: HomeAssistant, utcnow) -> None: +async def test_tv_set_source(hass: HomeAssistant) -> None: """Test that we can set the input source of a HomeKit TV.""" helper = await setup_test_component(hass, create_tv_service) @@ -352,7 +352,7 @@ async def test_tv_set_source(hass: HomeAssistant, utcnow) -> None: assert state.attributes["source"] == "HDMI 2" -async def test_tv_set_source_fail(hass: HomeAssistant, utcnow) -> None: +async def test_tv_set_source_fail(hass: HomeAssistant) -> None: """Test that we can set the input source of a HomeKit TV.""" helper = await setup_test_component(hass, create_tv_service) @@ -369,7 +369,7 @@ async def test_tv_set_source_fail(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a media_player unique id.""" aid = get_next_aid() diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index dedff37fa4bba9..d35df281eaba5a 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -30,7 +30,7 @@ def create_switch_with_spray_level(accessory): async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a number unique id.""" aid = get_next_aid() @@ -48,7 +48,7 @@ async def test_migrate_unique_id( ) -async def test_read_number(hass: HomeAssistant, utcnow) -> None: +async def test_read_number(hass: HomeAssistant) -> None: """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) @@ -74,7 +74,7 @@ async def test_read_number(hass: HomeAssistant, utcnow) -> None: assert state.state == "5" -async def test_write_number(hass: HomeAssistant, utcnow) -> None: +async def test_write_number(hass: HomeAssistant) -> None: """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) diff --git a/tests/components/homekit_controller/test_select.py b/tests/components/homekit_controller/test_select.py index 70228ef3dbb8db..baae2cf821967b 100644 --- a/tests/components/homekit_controller/test_select.py +++ b/tests/components/homekit_controller/test_select.py @@ -34,7 +34,7 @@ def create_service_with_temperature_units(accessory: Accessory): async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test we can migrate a select unique id.""" aid = get_next_aid() @@ -53,7 +53,7 @@ async def test_migrate_unique_id( ) -async def test_read_current_mode(hass: HomeAssistant, utcnow) -> None: +async def test_read_current_mode(hass: HomeAssistant) -> None: """Test that Ecobee mode can be correctly read and show as human readable text.""" helper = await setup_test_component(hass, create_service_with_ecobee_mode) @@ -91,7 +91,7 @@ async def test_read_current_mode(hass: HomeAssistant, utcnow) -> None: assert state.state == "away" -async def test_write_current_mode(hass: HomeAssistant, utcnow) -> None: +async def test_write_current_mode(hass: HomeAssistant) -> None: """Test can set a specific mode.""" helper = await setup_test_component(hass, create_service_with_ecobee_mode) helper.accessory.services.first(service_type=ServicesTypes.THERMOSTAT) @@ -139,7 +139,7 @@ async def test_write_current_mode(hass: HomeAssistant, utcnow) -> None: ) -async def test_read_select(hass: HomeAssistant, utcnow) -> None: +async def test_read_select(hass: HomeAssistant) -> None: """Test the generic select can read the current value.""" helper = await setup_test_component(hass, create_service_with_temperature_units) @@ -169,7 +169,7 @@ async def test_read_select(hass: HomeAssistant, utcnow) -> None: assert state.state == "fahrenheit" -async def test_write_select(hass: HomeAssistant, utcnow) -> None: +async def test_write_select(hass: HomeAssistant) -> None: """Test can set a value.""" helper = await setup_test_component(hass, create_service_with_temperature_units) helper.accessory.services.first(service_type=ServicesTypes.THERMOSTAT) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index e15227d9d8735f..3134605125e905 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -69,7 +69,7 @@ def create_battery_level_sensor(accessory): return service -async def test_temperature_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_temperature_sensor_read_state(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit temperature sensor accessory.""" helper = await setup_test_component( hass, create_temperature_sensor_service, suffix="temperature" @@ -95,7 +95,7 @@ async def test_temperature_sensor_read_state(hass: HomeAssistant, utcnow) -> Non assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT -async def test_temperature_sensor_not_added_twice(hass: HomeAssistant, utcnow) -> None: +async def test_temperature_sensor_not_added_twice(hass: HomeAssistant) -> None: """A standalone temperature sensor should not get a characteristic AND a service entity.""" helper = await setup_test_component( hass, create_temperature_sensor_service, suffix="temperature" @@ -109,7 +109,7 @@ async def test_temperature_sensor_not_added_twice(hass: HomeAssistant, utcnow) - assert created_sensors == {helper.entity_id} -async def test_humidity_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_humidity_sensor_read_state(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit humidity sensor accessory.""" helper = await setup_test_component( hass, create_humidity_sensor_service, suffix="humidity" @@ -134,7 +134,7 @@ async def test_humidity_sensor_read_state(hass: HomeAssistant, utcnow) -> None: assert state.attributes["device_class"] == SensorDeviceClass.HUMIDITY -async def test_light_level_sensor_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_light_level_sensor_read_state(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit temperature sensor accessory.""" helper = await setup_test_component( hass, create_light_level_sensor_service, suffix="light_level" @@ -159,9 +159,7 @@ async def test_light_level_sensor_read_state(hass: HomeAssistant, utcnow) -> Non assert state.attributes["device_class"] == SensorDeviceClass.ILLUMINANCE -async def test_carbon_dioxide_level_sensor_read_state( - hass: HomeAssistant, utcnow -) -> None: +async def test_carbon_dioxide_level_sensor_read_state(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit carbon dioxide sensor accessory.""" helper = await setup_test_component( hass, create_carbon_dioxide_level_sensor_service, suffix="carbon_dioxide" @@ -184,7 +182,7 @@ async def test_carbon_dioxide_level_sensor_read_state( assert state.state == "20" -async def test_battery_level_sensor(hass: HomeAssistant, utcnow) -> None: +async def test_battery_level_sensor(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit battery level sensor.""" helper = await setup_test_component( hass, create_battery_level_sensor, suffix="battery" @@ -211,7 +209,7 @@ async def test_battery_level_sensor(hass: HomeAssistant, utcnow) -> None: assert state.attributes["device_class"] == SensorDeviceClass.BATTERY -async def test_battery_charging(hass: HomeAssistant, utcnow) -> None: +async def test_battery_charging(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit battery's charging state.""" helper = await setup_test_component( hass, create_battery_level_sensor, suffix="battery" @@ -235,7 +233,7 @@ async def test_battery_charging(hass: HomeAssistant, utcnow) -> None: assert state.attributes["icon"] == "mdi:battery-charging-20" -async def test_battery_low(hass: HomeAssistant, utcnow) -> None: +async def test_battery_low(hass: HomeAssistant) -> None: """Test reading the state of a HomeKit battery's low state.""" helper = await setup_test_component( hass, create_battery_level_sensor, suffix="battery" @@ -277,7 +275,7 @@ def create_switch_with_sensor(accessory): return service -async def test_switch_with_sensor(hass: HomeAssistant, utcnow) -> None: +async def test_switch_with_sensor(hass: HomeAssistant) -> None: """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_sensor) @@ -307,7 +305,7 @@ async def test_switch_with_sensor(hass: HomeAssistant, utcnow) -> None: assert state.state == "50" -async def test_sensor_unavailable(hass: HomeAssistant, utcnow) -> None: +async def test_sensor_unavailable(hass: HomeAssistant) -> None: """Test a sensor becoming unavailable.""" helper = await setup_test_component(hass, create_switch_with_sensor) @@ -384,7 +382,6 @@ def test_thread_status_to_str() -> None: async def test_rssi_sensor( hass: HomeAssistant, - utcnow, entity_registry_enabled_by_default: None, enable_bluetooth: None, ) -> None: @@ -410,7 +407,6 @@ def transport(self): async def test_migrate_rssi_sensor_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, - utcnow, entity_registry_enabled_by_default: None, enable_bluetooth: None, ) -> None: diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 583640854a697b..afab63983e2020 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -71,7 +71,7 @@ def create_lightbulb_service(accessory): async def test_storage_is_updated_on_add( - hass: HomeAssistant, hass_storage: dict[str, Any], utcnow + hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test entity map storage is cleaned up on adding an accessory.""" await setup_test_component(hass, create_lightbulb_service) diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py index 8867ffc9bd1e76..5b6a77b75c909d 100644 --- a/tests/components/homekit_controller/test_switch.py +++ b/tests/components/homekit_controller/test_switch.py @@ -49,7 +49,7 @@ def create_char_switch_service(accessory): on_char.value = False -async def test_switch_change_outlet_state(hass: HomeAssistant, utcnow) -> None: +async def test_switch_change_outlet_state(hass: HomeAssistant) -> None: """Test that we can turn a HomeKit outlet on and off again.""" helper = await setup_test_component(hass, create_switch_service) @@ -74,7 +74,7 @@ async def test_switch_change_outlet_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_switch_read_outlet_state(hass: HomeAssistant, utcnow) -> None: +async def test_switch_read_outlet_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit outlet accessory.""" helper = await setup_test_component(hass, create_switch_service) @@ -107,7 +107,7 @@ async def test_switch_read_outlet_state(hass: HomeAssistant, utcnow) -> None: assert switch_1.attributes["outlet_in_use"] is True -async def test_valve_change_active_state(hass: HomeAssistant, utcnow) -> None: +async def test_valve_change_active_state(hass: HomeAssistant) -> None: """Test that we can turn a valve on and off again.""" helper = await setup_test_component(hass, create_valve_service) @@ -132,7 +132,7 @@ async def test_valve_change_active_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_valve_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_valve_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a valve accessory.""" helper = await setup_test_component(hass, create_valve_service) @@ -165,7 +165,7 @@ async def test_valve_read_state(hass: HomeAssistant, utcnow) -> None: assert switch_1.attributes["in_use"] is False -async def test_char_switch_change_state(hass: HomeAssistant, utcnow) -> None: +async def test_char_switch_change_state(hass: HomeAssistant) -> None: """Test that we can turn a characteristic on and off again.""" helper = await setup_test_component( hass, create_char_switch_service, suffix="pairing_mode" @@ -198,7 +198,7 @@ async def test_char_switch_change_state(hass: HomeAssistant, utcnow) -> None: ) -async def test_char_switch_read_state(hass: HomeAssistant, utcnow) -> None: +async def test_char_switch_read_state(hass: HomeAssistant) -> None: """Test that we can read the state of a HomeKit characteristic switch.""" helper = await setup_test_component( hass, create_char_switch_service, suffix="pairing_mode" @@ -220,7 +220,7 @@ async def test_char_switch_read_state(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, utcnow + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a we can migrate a switch unique id.""" aid = get_next_aid() From 5f697494209f0baedd6198bc8645a4a41bc77604 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 13 Dec 2023 19:39:19 +0100 Subject: [PATCH 114/118] Use Textselector in Trafikverket Camera (#105677) * Use Textselector in Trafikverket Camera * Update homeassistant/components/trafikverket_camera/strings.json Co-authored-by: Jan-Philipp Benecke --------- Co-authored-by: Jan-Philipp Benecke --- .../components/trafikverket_camera/config_flow.py | 8 ++++---- homeassistant/components/trafikverket_camera/strings.json | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py index 104a6a470dc10a..a5257455e7ac6a 100644 --- a/homeassistant/components/trafikverket_camera/config_flow.py +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_LOCATION from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import TextSelector from .const import DOMAIN @@ -90,7 +90,7 @@ async def async_step_reauth_confirm( step_id="reauth_confirm", data_schema=vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_API_KEY): TextSelector(), } ), errors=errors, @@ -123,8 +123,8 @@ async def async_step_user( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_LOCATION): cv.string, + vol.Required(CONF_API_KEY): TextSelector(), + vol.Required(CONF_LOCATION): TextSelector(), } ), errors=errors, diff --git a/homeassistant/components/trafikverket_camera/strings.json b/homeassistant/components/trafikverket_camera/strings.json index 651225934cd587..35dbbb1f540a07 100644 --- a/homeassistant/components/trafikverket_camera/strings.json +++ b/homeassistant/components/trafikverket_camera/strings.json @@ -15,6 +15,9 @@ "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "location": "[%key:common::config_flow::data::location%]" + }, + "data_description": { + "location": "Equal or part of name, description or camera id" } } } From 6dc8c2c37014de201578b5cbe880f7a1bbcecfc4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 19:40:51 +0100 Subject: [PATCH 115/118] Set volume_step in sonos media_player (#105671) --- homeassistant/components/sonos/media_player.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 27059bba1807f5..031e46061483d6 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -67,7 +67,6 @@ LONG_SERVICE_TIMEOUT = 30.0 UNJOIN_SERVICE_TIMEOUT = 0.1 -VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { RepeatMode.OFF: False, @@ -212,6 +211,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) _attr_media_content_type = MediaType.MUSIC _attr_device_class = MediaPlayerDeviceClass.SPEAKER + _attr_volume_step = 2 / 100 def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the media player entity.""" @@ -373,16 +373,6 @@ def source(self) -> str | None: """Name of the current input source.""" return self.media.source_name or None - @soco_error() - def volume_up(self) -> None: - """Volume up media player.""" - self.soco.volume += VOLUME_INCREMENT - - @soco_error() - def volume_down(self) -> None: - """Volume down media player.""" - self.soco.volume -= VOLUME_INCREMENT - @soco_error() def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" From 65514fbd733bdfb7f1d0b0ee43581cddea680e1e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 13 Dec 2023 19:40:57 +0100 Subject: [PATCH 116/118] Add error translations for Sensibo (#105600) --- homeassistant/components/sensibo/climate.py | 34 +++++++++++++++---- homeassistant/components/sensibo/entity.py | 16 +++++++-- homeassistant/components/sensibo/select.py | 9 ++++- homeassistant/components/sensibo/strings.json | 32 +++++++++++++++++ homeassistant/components/sensibo/switch.py | 4 ++- tests/components/sensibo/test_climate.py | 4 +-- 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 40aa54e5d56ffc..89e1fafa213c7a 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -22,7 +22,7 @@ UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_conversion import TemperatureConverter @@ -314,11 +314,17 @@ async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if "targetTemperature" not in self.device_data.active_features: raise HomeAssistantError( - "Current mode doesn't support setting Target Temperature" + "Current mode doesn't support setting Target Temperature", + translation_domain=DOMAIN, + translation_key="no_target_temperature_in_features", ) if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: - raise ValueError("No target temperature provided") + raise ServiceValidationError( + "No target temperature provided", + translation_domain=DOMAIN, + translation_key="no_target_temperature", + ) if temperature == self.target_temperature: return @@ -334,10 +340,17 @@ async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if "fanLevel" not in self.device_data.active_features: - raise HomeAssistantError("Current mode doesn't support setting Fanlevel") + raise HomeAssistantError( + "Current mode doesn't support setting Fanlevel", + translation_domain=DOMAIN, + translation_key="no_fan_level_in_features", + ) if fan_mode not in AVAILABLE_FAN_MODES: raise HomeAssistantError( - f"Climate fan mode {fan_mode} is not supported by the integration, please open an issue" + f"Climate fan mode {fan_mode} is not supported by the integration, please open an issue", + translation_domain=DOMAIN, + translation_key="fan_mode_not_supported", + translation_placeholders={"fan_mode": fan_mode}, ) transformation = self.device_data.fan_modes_translated @@ -379,10 +392,17 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" if "swing" not in self.device_data.active_features: - raise HomeAssistantError("Current mode doesn't support setting Swing") + raise HomeAssistantError( + "Current mode doesn't support setting Swing", + translation_domain=DOMAIN, + translation_key="no_swing_in_features", + ) if swing_mode not in AVAILABLE_SWING_MODES: raise HomeAssistantError( - f"Climate swing mode {swing_mode} is not supported by the integration, please open an issue" + f"Climate swing mode {swing_mode} is not supported by the integration, please open an issue", + translation_domain=DOMAIN, + translation_key="swing_not_supported", + translation_placeholders={"swing_mode": swing_mode}, ) transformation = self.device_data.swing_modes_translated diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 0a60fc4a85d98d..f9056fa6624801 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -26,15 +26,27 @@ def async_handle_api_call( async def wrap_api_call(entity: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: """Wrap services for api calls.""" res: bool = False + if TYPE_CHECKING: + assert isinstance(entity.name, str) try: async with asyncio.timeout(TIMEOUT): res = await function(entity, *args, **kwargs) except SENSIBO_ERRORS as err: - raise HomeAssistantError from err + raise HomeAssistantError( + str(err), + translation_domain=DOMAIN, + translation_key="service_raised", + translation_placeholders={"error": str(err), "name": entity.name}, + ) from err LOGGER.debug("Result %s for entity %s with arguments %s", res, entity, kwargs) if res is not True: - raise HomeAssistantError(f"Could not execute service for {entity.name}") + raise HomeAssistantError( + f"Could not execute service for {entity.name}", + translation_domain=DOMAIN, + translation_key="service_result_not_true", + translation_placeholders={"name": entity.name}, + ) if ( isinstance(key := kwargs.get("key"), str) and (value := kwargs.get("value")) is not None diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index cda8a972ede14c..3e13c6cec705a3 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -106,9 +106,16 @@ def options(self) -> list[str]: async def async_select_option(self, option: str) -> None: """Set state to the selected option.""" if self.entity_description.key not in self.device_data.active_features: + hvac_mode = self.device_data.hvac_mode if self.device_data.hvac_mode else "" raise HomeAssistantError( f"Current mode {self.device_data.hvac_mode} doesn't support setting" - f" {self.entity_description.name}" + f" {self.entity_description.name}", + translation_domain=DOMAIN, + translation_key="select_option_not_available", + translation_placeholders={ + "hvac_mode": hvac_mode, + "key": self.entity_description.key, + }, ) await self.async_send_api_call( diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 6081c668d898f8..a5f71e53c17f4c 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -478,5 +478,37 @@ } } } + }, + "exceptions": { + "no_target_temperature_in_features": { + "message": "Current mode doesn't support setting target temperature" + }, + "no_target_temperature": { + "message": "No target temperature provided" + }, + "no_fan_level_in_features": { + "message": "Current mode doesn't support setting fan level" + }, + "fan_mode_not_supported": { + "message": "Climate fan mode {fan_mode} is not supported by the integration, please open an issue" + }, + "no_swing_in_features": { + "message": "Current mode doesn't support setting swing" + }, + "swing_not_supported": { + "message": "Climate swing mode {swing_mode} is not supported by the integration, please open an issue" + }, + "service_result_not_true": { + "message": "Could not execute service for {name}" + }, + "service_raised": { + "message": "Could not execute service for {name} with error {error}" + }, + "select_option_not_available": { + "message": "Current mode {hvac_mode} doesn't support setting {key}" + }, + "climate_react_not_available": { + "message": "Use Sensibo Enable Climate React Service once to enable switch or the Sensibo app" + } } } diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index 204ed622f13e11..a27307fcceb5c4 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -184,7 +184,9 @@ async def async_turn_on_off_smart(self, key: str, value: bool) -> bool: if self.device_data.smart_type is None: raise HomeAssistantError( "Use Sensibo Enable Climate React Service once to enable switch or the" - " Sensibo app" + " Sensibo app", + translation_domain=DOMAIN, + translation_key="climate_react_not_available", ) data: dict[str, Any] = {"enabled": value} result = await self._client.async_enable_climate_react(self._device_id, data) diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 9cf0a8972a9d0f..71680733098471 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -55,7 +55,7 @@ STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -438,7 +438,7 @@ async def test_climate_temperature_is_none( with patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises(ValueError): + ), pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, From 08b6d2af5e28faf6ebdb7f0e524b450e9f18ede1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 13 Dec 2023 19:41:31 +0100 Subject: [PATCH 117/118] Add error translations to Yale Smart Living (#105678) --- .../yale_smart_alarm/alarm_control_panel.py | 12 ++++++++++-- homeassistant/components/yale_smart_alarm/lock.py | 14 ++++++++++++-- .../components/yale_smart_alarm/strings.json | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 7ced3487269366..31851ad3ceb3d0 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -83,7 +83,13 @@ async def async_set_alarm(self, command: str, code: str | None = None) -> None: except YALE_ALL_ERRORS as error: raise HomeAssistantError( f"Could not set alarm for {self.coordinator.entry.data[CONF_NAME]}:" - f" {error}" + f" {error}", + translation_domain=DOMAIN, + translation_key="set_alarm", + translation_placeholders={ + "name": self.coordinator.entry.data[CONF_NAME], + "error": str(error), + }, ) from error if alarm_state: @@ -91,7 +97,9 @@ async def async_set_alarm(self, command: str, code: str | None = None) -> None: self.async_write_ha_state() return raise HomeAssistantError( - "Could not change alarm check system ready for arming." + "Could not change alarm, check system ready for arming", + translation_domain=DOMAIN, + translation_key="could_not_change_alarm", ) @property diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 50d7b28c52b4bc..c5a9bb79ba8491 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -79,14 +79,24 @@ async def async_set_lock(self, command: str, code: str | None) -> None: ) except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not set lock for {self.lock_name}: {error}" + f"Could not set lock for {self.lock_name}: {error}", + translation_domain=DOMAIN, + translation_key="set_lock", + translation_placeholders={ + "name": self.lock_name, + "error": str(error), + }, ) from error if lock_state: self.coordinator.data["lock_map"][self._attr_unique_id] = command self.async_write_ha_state() return - raise HomeAssistantError("Could not set lock, check system ready for lock.") + raise HomeAssistantError( + "Could not set lock, check system ready for lock", + translation_domain=DOMAIN, + translation_key="could_not_change_lock", + ) @property def is_locked(self) -> bool | None: diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index a51d151d7d9248..a698da20d8d147 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -56,5 +56,19 @@ "name": "Panic button" } } + }, + "exceptions": { + "set_alarm": { + "message": "Could not set alarm for {name}: {error}" + }, + "could_not_change_alarm": { + "message": "Could not change alarm, check system ready for arming" + }, + "set_lock": { + "message": "Could not set lock for {name}: {error}" + }, + "could_not_change_lock": { + "message": "Could not set lock, check system ready for lock" + } } } From 72cb21d875f3cc342dd17523bd099f2287ce63f5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Dec 2023 19:42:11 +0100 Subject: [PATCH 118/118] Set volume_step in enigma2 media_player (#105669) --- homeassistant/components/enigma2/media_player.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index a479590f4648db..345ba1f8acb8db 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -115,6 +115,7 @@ class Enigma2Device(MediaPlayerEntity): | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.SELECT_SOURCE ) + _attr_volume_step = 5 / 100 def __init__(self, name, device): """Initialize the Enigma2 device.""" @@ -185,14 +186,6 @@ def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.e2_box.set_volume(int(volume * 100)) - def volume_up(self) -> None: - """Volume up the media player.""" - self.e2_box.set_volume(int(self.e2_box.volume * 100) + 5) - - def volume_down(self) -> None: - """Volume down media player.""" - self.e2_box.set_volume(int(self.e2_box.volume * 100) - 5) - @property def volume_level(self): """Volume level of the media player (0..1)."""