diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 83ee0a2e422..44a81718e10 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,7 @@ "customizations": { "vscode": { "extensions": [ - "ms-python.black-formatter", + "charliermarsh.ruff", "ms-python.pylint", "ms-python.vscode-pylance", "visualstudioexptteam.vscodeintellicode", @@ -39,7 +39,10 @@ "!include_dir_list scalar", "!include_dir_merge_list scalar", "!include_dir_merge_named scalar" - ] + ], + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + } } } } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4bc1442d9e9..d69b1ac0c7d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -60,7 +60,7 @@ - [ ] There is no commented out code in this PR. - [ ] I have followed the [development checklist][dev-checklist] - [ ] I have followed the [perfect PR recommendations][perfect-pr] -- [ ] The code has been formatted using Black (`black --fast homeassistant tests`) +- [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`) - [ ] Tests have been added to verify that the new code works. If user exposed functionality or configuration variables are added/changed: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4b99c3ddc04..71030e50074 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,6 @@ env: CACHE_VERSION: 5 PIP_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 6 - BLACK_CACHE_VERSION: 1 HA_SHORT_VERSION: "2023.12" DEFAULT_PYTHON: "3.11" ALL_PYTHON_VERSIONS: "['3.11', '3.12']" @@ -58,7 +57,6 @@ env: POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']" PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache - BLACK_CACHE: /tmp/black-cache SQLALCHEMY_WARN_20: 1 PYTHONASYNCIODEBUG: 1 HASS_CI: 1 @@ -261,8 +259,8 @@ jobs: . venv/bin/activate pre-commit install-hooks - lint-black: - name: Check black + lint-ruff-format: + name: Check ruff-format runs-on: ubuntu-22.04 needs: - info @@ -276,13 +274,6 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - - name: Generate partial black restore key - id: generate-black-key - run: | - black_version=$(cat requirements_test_pre_commit.txt | grep black | cut -d '=' -f 3) - echo "version=$black_version" >> $GITHUB_OUTPUT - echo "key=black-${{ env.BLACK_CACHE_VERSION }}-$black_version-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv uses: actions/cache/restore@v3.3.2 @@ -301,33 +292,12 @@ jobs: key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - - name: Restore black cache - uses: actions/cache@v3.3.2 - with: - path: ${{ env.BLACK_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-black-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-black-${{ - env.BLACK_CACHE_VERSION }}-${{ steps.generate-black-key.outputs.version }}-${{ - env.HA_SHORT_VERSION }}- - - name: Run black (fully) - if: needs.info.outputs.test_full_suite == 'true' - env: - BLACK_CACHE_DIR: ${{ env.BLACK_CACHE }} + - name: Run ruff-format run: | . venv/bin/activate - pre-commit run --hook-stage manual black --all-files --show-diff-on-failure - - name: Run black (partially) - if: needs.info.outputs.test_full_suite == 'false' - shell: bash + pre-commit run --hook-stage manual ruff-format --all-files --show-diff-on-failure env: - BLACK_CACHE_DIR: ${{ env.BLACK_CACHE }} - run: | - . venv/bin/activate - shopt -s globstar - pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} --show-diff-on-failure + RUFF_OUTPUT_FORMAT: github lint-ruff: name: Check ruff @@ -362,22 +332,12 @@ jobs: key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - - name: Register ruff problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/ruff.json" - - name: Run ruff (fully) - if: needs.info.outputs.test_full_suite == 'true' + - name: Run ruff run: | . venv/bin/activate pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure - - name: Run ruff (partially) - if: needs.info.outputs.test_full_suite == 'false' - shell: bash - run: | - . venv/bin/activate - shopt -s globstar - pre-commit run --hook-stage manual ruff --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} --show-diff-on-failure - + env: + RUFF_OUTPUT_FORMAT: github lint-other: name: Check other linters runs-on: ubuntu-22.04 @@ -787,7 +747,7 @@ jobs: cov_params+=(--cov-report=xml) fi - python3 -X dev -m pytest \ + python3 -b -X dev -m pytest \ -qq \ --timeout=9 \ --durations=10 \ @@ -824,7 +784,7 @@ jobs: cov_params+=(--cov-report=term-missing) fi - python3 -X dev -m pytest \ + python3 -b -X dev -m pytest \ -qq \ --timeout=9 \ -n auto \ @@ -945,7 +905,7 @@ jobs: cov_params+=(--cov-report=term-missing) fi - python3 -X dev -m pytest \ + python3 -b -X dev -m pytest \ -qq \ --timeout=20 \ -n 1 \ @@ -1069,7 +1029,7 @@ jobs: cov_params+=(--cov-report=term-missing) fi - python3 -X dev -m pytest \ + python3 -b -X dev -m pytest \ -qq \ --timeout=9 \ -n 1 \ diff --git a/.github/workflows/matchers/ruff.json b/.github/workflows/matchers/ruff.json deleted file mode 100644 index d189a3656a5..00000000000 --- a/.github/workflows/matchers/ruff.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "problemMatcher": [ - { - "owner": "ruff-error", - "severity": "error", - "pattern": [ - { - "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", - "file": 1, - "line": 2, - "column": 3, - "message": 4 - } - ] - }, - { - "owner": "ruff-warning", - "severity": "warning", - "pattern": [ - { - "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", - "file": 1, - "line": 2, - "column": 3, - "message": 4 - } - ] - } - ] -} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d43bcf1b02..ae135f30407 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,11 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.6 hooks: - id: ruff args: - --fix - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 - hooks: - - id: black - args: - - --quiet + - id: ruff-format files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell rev: v2.2.2 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 951134133e5..8a5d7d486b7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["esbenp.prettier-vscode", "ms-python.python"] + "recommendations": [ + "charliermarsh.ruff", + "esbenp.prettier-vscode", + "ms-python.python" + ] } diff --git a/Dockerfile.dev b/Dockerfile.dev index 857ccfa3997..a1143adde89 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -5,8 +5,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Uninstall pre-installed formatting and linting tools # They would conflict with our pinned versions RUN \ - pipx uninstall black \ - && pipx uninstall pydocstyle \ + pipx uninstall pydocstyle \ && pipx uninstall pycodestyle \ && pipx uninstall mypy \ && pipx uninstall pylint diff --git a/homeassistant/auth/permissions/types.py b/homeassistant/auth/permissions/types.py index 0aa8807211a..cf3632d06d5 100644 --- a/homeassistant/auth/permissions/types.py +++ b/homeassistant/auth/permissions/types.py @@ -5,9 +5,7 @@ from collections.abc import Mapping ValueType = ( # Example: entities.all = { read: true, control: true } - Mapping[str, bool] - | bool - | None + Mapping[str, bool] | bool | None ) # Example: entities.domains = { light: … } diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 0cadbf07589..98c246d74e4 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -10,10 +10,11 @@ from typing import Any, cast import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import async_get_hass, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from ..models import Credentials, UserMeta from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -21,10 +22,28 @@ from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow AUTH_PROVIDER_TYPE = "legacy_api_password" CONF_API_PASSWORD = "api_password" -CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( +_CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( {vol.Required(CONF_API_PASSWORD): cv.string}, extra=vol.PREVENT_EXTRA ) + +def _create_repair_and_validate(config: dict[str, Any]) -> dict[str, Any]: + async_create_issue( + async_get_hass(), + "auth", + "deprecated_legacy_api_password", + breaks_in_ha_version="2024.6.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_legacy_api_password", + ) + + return _CONFIG_SCHEMA(config) # type: ignore[no-any-return] + + +CONFIG_SCHEMA = _create_repair_and_validate + + LEGACY_USER_NAME = "Legacy API password user" diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index fa7d2115769..1eb32a9dc3f 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1315,9 +1315,9 @@ class PipelineInput: if stt_audio_buffer: # Send audio in the buffer first to speech-to-text, then move on to stt_stream. # This is basically an async itertools.chain. - async def buffer_then_audio_stream() -> AsyncGenerator[ - ProcessedAudioChunk, None - ]: + async def buffer_then_audio_stream() -> ( + AsyncGenerator[ProcessedAudioChunk, None] + ): # Buffered audio for chunk in stt_audio_buffer: yield chunk diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 6bfe969dc3e..89cced519df 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -417,8 +417,7 @@ async def websocket_device_capture( # single sample (16 bits) per queue item. max_queue_items = ( # +1 for None to signal end - int(math.ceil(timeout_seconds * CAPTURE_RATE)) - + 1 + int(math.ceil(timeout_seconds * CAPTURE_RATE)) + 1 ) audio_queue = DeviceAudioQueue(queue=asyncio.Queue(maxsize=max_queue_items)) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index d386bb7a488..0dd3ee64cdf 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -31,5 +31,11 @@ "invalid_code": "Invalid code, please try again." } } + }, + "issues": { + "deprecated_legacy_api_password": { + "title": "The legacy API password is deprecated", + "description": "The legacy API password authentication provider is deprecated and will be removed. Please remove it from your YAML configuration and use the default Home Assistant authentication provider instead." + } } } diff --git a/homeassistant/components/bmw_connected_drive/select.py b/homeassistant/components/bmw_connected_drive/select.py index 3467322a4af..1d8b736f4dd 100644 --- a/homeassistant/components/bmw_connected_drive/select.py +++ b/homeassistant/components/bmw_connected_drive/select.py @@ -44,7 +44,8 @@ SELECT_TYPES: dict[str, BMWSelectEntityDescription] = { translation_key="ac_limit", is_available=lambda v: v.is_remote_set_ac_limit_enabled, dynamic_options=lambda v: [ - str(lim) for lim in v.charging_profile.ac_available_limits # type: ignore[union-attr] + str(lim) + for lim in v.charging_profile.ac_available_limits # type: ignore[union-attr] ], current_option=lambda v: str(v.charging_profile.ac_current_limit), # type: ignore[union-attr] remote_service=lambda v, o: v.remote_services.trigger_charging_settings_update( diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index e3b1b39f687..634a5e20b33 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -140,7 +140,7 @@ def _ws_handle_cloud_errors( handler: Callable[ [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], Coroutine[None, None, None], - ] + ], ) -> Callable[ [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], Coroutine[None, None, None], @@ -362,8 +362,11 @@ def _require_cloud_login( handler: Callable[ [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], None, - ] -) -> Callable[[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], None,]: + ], +) -> Callable[ + [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], + None, +]: """Websocket decorator that requires cloud to be logged in.""" @wraps(handler) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 4c0f35266f9..8a5ced2c678 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -129,9 +129,8 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): if self.gateway.ignore_state_updates: return - if ( - self._update_keys is not None - and not self._device.changed_keys.intersection(self._update_keys) + if self._update_keys is not None and not self._device.changed_keys.intersection( + self._update_keys ): return diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 0fee65d57b6..842d1bee40f 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -63,7 +63,8 @@ async def async_setup_entry( # noqa: C901 ) await device.async_connect(session_instance=async_client) device.password = entry.data.get( - CONF_PASSWORD, "" # This key was added in HA Core 2022.6 + CONF_PASSWORD, + "", # This key was added in HA Core 2022.6 ) except DeviceNotFound as err: raise ConfigEntryNotReady( diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 3a57ba2c8ce..cd2f1ae2f50 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -453,10 +453,9 @@ class DlnaDmrEntity(MediaPlayerEntity): for state_variable in state_variables: # Force a state refresh when player begins or pauses playback # to update the position info. - if ( - state_variable.name == "TransportState" - and state_variable.value - in (TransportState.PLAYING, TransportState.PAUSED_PLAYBACK) + if state_variable.name == "TransportState" and state_variable.value in ( + TransportState.PLAYING, + TransportState.PAUSED_PLAYBACK, ): force_refresh = True diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 696698cc176..3dbd446001f 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -441,9 +441,7 @@ async def async_setup_entry( description, entry, telegram, - *device_class_and_uom( - telegram, description - ), # type: ignore[arg-type] + *device_class_and_uom(telegram, description), # type: ignore[arg-type] ) for description in all_sensors if ( diff --git a/homeassistant/components/elmax/cover.py b/homeassistant/components/elmax/cover.py index 8a6acb154aa..e05b17b9171 100644 --- a/homeassistant/components/elmax/cover.py +++ b/homeassistant/components/elmax/cover.py @@ -18,13 +18,11 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -_COMMAND_BY_MOTION_STATUS = ( - { # Maps the stop command to use for every cover motion status - CoverStatus.DOWN: CoverCommand.DOWN, - CoverStatus.UP: CoverCommand.UP, - CoverStatus.IDLE: None, - } -) +_COMMAND_BY_MOTION_STATUS = { # Maps the stop command to use for every cover motion status + CoverStatus.DOWN: CoverCommand.DOWN, + CoverStatus.UP: CoverCommand.UP, + CoverStatus.IDLE: None, +} async def async_setup_entry( diff --git a/homeassistant/components/esphome/enum_mapper.py b/homeassistant/components/esphome/enum_mapper.py index 566f0bc503b..fd09f9a05b6 100644 --- a/homeassistant/components/esphome/enum_mapper.py +++ b/homeassistant/components/esphome/enum_mapper.py @@ -14,9 +14,7 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): def __init__(self, mapping: dict[_EnumT, _ValT]) -> None: """Construct a EsphomeEnumMapper.""" # Add none mapping - augmented_mapping: dict[ - _EnumT | None, _ValT | None - ] = mapping # type: ignore[assignment] + augmented_mapping: dict[_EnumT | None, _ValT | None] = mapping # type: ignore[assignment] augmented_mapping[None] = None self._mapping = augmented_mapping diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index a6ca52d6c1a..9942498e12d 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -117,7 +117,8 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): """Return the current speed percentage.""" if not self._supports_speed_levels: return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, self._state.speed # type: ignore[misc] + ORDERED_NAMED_FAN_SPEEDS, + self._state.speed, # type: ignore[misc] ) return ranged_value_to_percentage( diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index c26310bf61c..f4ceaf2c48c 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -124,10 +124,13 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: def convert_key(key: str) -> str: """Convert a string to snake_case.""" string = re.sub(r"[\-\.\s]", "_", str(key)) - return (string[0]).lower() + re.sub( - r"[A-Z]", - lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] - string[1:], + return ( + (string[0]).lower() + + re.sub( + r"[A-Z]", + lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] + string[1:], + ) ) return { diff --git a/homeassistant/components/fronius/const.py b/homeassistant/components/fronius/const.py index 4060731b21c..18f35de8336 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -1,7 +1,9 @@ """Constants for the Fronius integration.""" +from enum import StrEnum from typing import Final, NamedTuple, TypedDict from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.typing import StateType DOMAIN: Final = "fronius" @@ -25,3 +27,97 @@ class FroniusDeviceInfo(NamedTuple): device_info: DeviceInfo solar_net_id: SolarNetId unique_id: str + + +class InverterStatusCodeOption(StrEnum): + """Status codes for Fronius inverters.""" + + # these are keys for state translations - so snake_case is used + STARTUP = "startup" + RUNNING = "running" + STANDBY = "standby" + BOOTLOADING = "bootloading" + ERROR = "error" + IDLE = "idle" + READY = "ready" + SLEEPING = "sleeping" + UNKNOWN = "unknown" + INVALID = "invalid" + + +_INVERTER_STATUS_CODES: Final[dict[int, InverterStatusCodeOption]] = { + 0: InverterStatusCodeOption.STARTUP, + 1: InverterStatusCodeOption.STARTUP, + 2: InverterStatusCodeOption.STARTUP, + 3: InverterStatusCodeOption.STARTUP, + 4: InverterStatusCodeOption.STARTUP, + 5: InverterStatusCodeOption.STARTUP, + 6: InverterStatusCodeOption.STARTUP, + 7: InverterStatusCodeOption.RUNNING, + 8: InverterStatusCodeOption.STANDBY, + 9: InverterStatusCodeOption.BOOTLOADING, + 10: InverterStatusCodeOption.ERROR, + 11: InverterStatusCodeOption.IDLE, + 12: InverterStatusCodeOption.READY, + 13: InverterStatusCodeOption.SLEEPING, + 255: InverterStatusCodeOption.UNKNOWN, +} + + +def get_inverter_status_message(code: StateType) -> InverterStatusCodeOption: + """Return a status message for a given status code.""" + return _INVERTER_STATUS_CODES.get(code, InverterStatusCodeOption.INVALID) # type: ignore[arg-type] + + +class MeterLocationCodeOption(StrEnum): + """Meter location codes for Fronius meters.""" + + # these are keys for state translations - so snake_case is used + FEED_IN = "feed_in" + CONSUMPTION_PATH = "consumption_path" + GENERATOR = "external_generator" + EXT_BATTERY = "external_battery" + SUBLOAD = "subload" + + +def get_meter_location_description(code: StateType) -> MeterLocationCodeOption | None: + """Return a location_description for a given location code.""" + match int(code): # type: ignore[arg-type] + case 0: + return MeterLocationCodeOption.FEED_IN + case 1: + return MeterLocationCodeOption.CONSUMPTION_PATH + case 3: + return MeterLocationCodeOption.GENERATOR + case 4: + return MeterLocationCodeOption.EXT_BATTERY + case _ as _code if 256 <= _code <= 511: + return MeterLocationCodeOption.SUBLOAD + return None + + +class OhmPilotStateCodeOption(StrEnum): + """OhmPilot state codes for Fronius inverters.""" + + # these are keys for state translations - so snake_case is used + UP_AND_RUNNING = "up_and_running" + KEEP_MINIMUM_TEMPERATURE = "keep_minimum_temperature" + LEGIONELLA_PROTECTION = "legionella_protection" + CRITICAL_FAULT = "critical_fault" + FAULT = "fault" + BOOST_MODE = "boost_mode" + + +_OHMPILOT_STATE_CODES: Final[dict[int, OhmPilotStateCodeOption]] = { + 0: OhmPilotStateCodeOption.UP_AND_RUNNING, + 1: OhmPilotStateCodeOption.KEEP_MINIMUM_TEMPERATURE, + 2: OhmPilotStateCodeOption.LEGIONELLA_PROTECTION, + 3: OhmPilotStateCodeOption.CRITICAL_FAULT, + 4: OhmPilotStateCodeOption.FAULT, + 5: OhmPilotStateCodeOption.BOOST_MODE, +} + + +def get_ohmpilot_state_message(code: StateType) -> OhmPilotStateCodeOption | None: + """Return a status message for a given status code.""" + return _OHMPILOT_STATE_CODES.get(code) # type: ignore[arg-type] diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index 94fd5f256aa..fcf9ce0a389 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -49,8 +49,10 @@ class FroniusCoordinatorBase( """Set up the FroniusCoordinatorBase class.""" self._failed_update_count = 0 self.solar_net = solar_net - # unregistered_keys are used to create entities in platform module - self.unregistered_keys: dict[SolarNetId, set[str]] = {} + # unregistered_descriptors are used to create entities in platform module + self.unregistered_descriptors: dict[ + SolarNetId, list[FroniusSensorEntityDescription] + ] = {} super().__init__(*args, update_interval=self.default_interval, **kwargs) @abstractmethod @@ -73,11 +75,11 @@ class FroniusCoordinatorBase( self.update_interval = self.default_interval for solar_net_id in data: - if solar_net_id not in self.unregistered_keys: + if solar_net_id not in self.unregistered_descriptors: # id seen for the first time - self.unregistered_keys[solar_net_id] = { - desc.key for desc in self.valid_descriptions - } + self.unregistered_descriptors[ + solar_net_id + ] = self.valid_descriptions.copy() return data @callback @@ -92,22 +94,34 @@ class FroniusCoordinatorBase( """ @callback - def _add_entities_for_unregistered_keys() -> None: + def _add_entities_for_unregistered_descriptors() -> None: """Add entities for keys seen for the first time.""" - new_entities: list = [] + new_entities: list[_FroniusEntityT] = [] for solar_net_id, device_data in self.data.items(): - for key in self.unregistered_keys[solar_net_id].intersection( - device_data - ): - if device_data[key]["value"] is None: + remaining_unregistered_descriptors = [] + for description in self.unregistered_descriptors[solar_net_id]: + key = description.response_key or description.key + if key not in device_data: + remaining_unregistered_descriptors.append(description) continue - new_entities.append(entity_constructor(self, key, solar_net_id)) - self.unregistered_keys[solar_net_id].remove(key) + if device_data[key]["value"] is None: + remaining_unregistered_descriptors.append(description) + continue + new_entities.append( + entity_constructor( + coordinator=self, + description=description, + solar_net_id=solar_net_id, + ) + ) + self.unregistered_descriptors[ + solar_net_id + ] = remaining_unregistered_descriptors async_add_entities(new_entities) - _add_entities_for_unregistered_keys() + _add_entities_for_unregistered_descriptors() self.solar_net.cleanup_callbacks.append( - self.async_add_listener(_add_entities_for_unregistered_keys) + self.async_add_listener(_add_entities_for_unregistered_descriptors) ) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index f11855ce7e2..f058a25a044 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,6 +1,7 @@ """Support for Fronius devices.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Final @@ -30,7 +31,16 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SOLAR_NET_DISCOVERY_NEW +from .const import ( + DOMAIN, + SOLAR_NET_DISCOVERY_NEW, + InverterStatusCodeOption, + MeterLocationCodeOption, + OhmPilotStateCodeOption, + get_inverter_status_message, + get_meter_location_description, + get_ohmpilot_state_message, +) if TYPE_CHECKING: from . import FroniusSolarNet @@ -102,6 +112,8 @@ class FroniusSensorEntityDescription(SensorEntityDescription): # Gen24 devices may report 0 for total energy while doing firmware updates. # Handling such values shall mitigate spikes in delta calculations. invalid_when_falsy: bool = False + response_key: str | None = None + value_fn: Callable[[StateType], StateType] | None = None INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ @@ -198,6 +210,15 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ FroniusSensorEntityDescription( key="status_code", entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + FroniusSensorEntityDescription( + key="status_message", + response_key="status_code", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=[opt.value for opt in InverterStatusCodeOption], + value_fn=get_inverter_status_message, ), FroniusSensorEntityDescription( key="led_state", @@ -306,6 +327,15 @@ METER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ FroniusSensorEntityDescription( key="meter_location", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=int, # type: ignore[arg-type] + ), + FroniusSensorEntityDescription( + key="meter_location_description", + response_key="meter_location", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=[opt.value for opt in MeterLocationCodeOption], + value_fn=get_meter_location_description, ), FroniusSensorEntityDescription( key="power_apparent_phase_1", @@ -495,7 +525,11 @@ OHMPILOT_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ ), FroniusSensorEntityDescription( key="state_message", + response_key="state_code", entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=[opt.value for opt in OhmPilotStateCodeOption], + value_fn=get_ohmpilot_state_message, ), ] @@ -630,24 +664,22 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn """Defines a Fronius coordinator entity.""" entity_description: FroniusSensorEntityDescription - entity_descriptions: list[FroniusSensorEntityDescription] _attr_has_entity_name = True def __init__( self, coordinator: FroniusCoordinatorBase, - key: str, + description: FroniusSensorEntityDescription, solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" super().__init__(coordinator) - self.entity_description = next( - desc for desc in self.entity_descriptions if desc.key == key - ) + self.entity_description = description + self.response_key = description.response_key or description.key self.solar_net_id = solar_net_id self._attr_native_value = self._get_entity_value() - self._attr_translation_key = self.entity_description.key + self._attr_translation_key = description.key def _device_data(self) -> dict[str, Any]: """Extract information for SolarNet device from coordinator data.""" @@ -655,13 +687,13 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn def _get_entity_value(self) -> Any: """Extract entity value from coordinator. Raises KeyError if not included in latest update.""" - new_value = self.coordinator.data[self.solar_net_id][ - self.entity_description.key - ]["value"] + new_value = self.coordinator.data[self.solar_net_id][self.response_key]["value"] if new_value is None: return self.entity_description.default_value if self.entity_description.invalid_when_falsy and not new_value: return None + if self.entity_description.value_fn is not None: + return self.entity_description.value_fn(new_value) if isinstance(new_value, float): return round(new_value, 4) return new_value @@ -681,54 +713,54 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn class InverterSensor(_FroniusSensorEntity): """Defines a Fronius inverter device sensor entity.""" - entity_descriptions = INVERTER_ENTITY_DESCRIPTIONS - def __init__( self, coordinator: FroniusInverterUpdateCoordinator, - key: str, + description: FroniusSensorEntityDescription, solar_net_id: str, ) -> None: """Set up an individual Fronius inverter sensor.""" - super().__init__(coordinator, key, solar_net_id) + super().__init__(coordinator, description, solar_net_id) # device_info created in __init__ from a `GetInverterInfo` request self._attr_device_info = coordinator.inverter_info.device_info - self._attr_unique_id = f"{coordinator.inverter_info.unique_id}-{key}" + self._attr_unique_id = ( + f"{coordinator.inverter_info.unique_id}-{description.key}" + ) class LoggerSensor(_FroniusSensorEntity): """Defines a Fronius logger device sensor entity.""" - entity_descriptions = LOGGER_ENTITY_DESCRIPTIONS - def __init__( self, coordinator: FroniusLoggerUpdateCoordinator, - key: str, + description: FroniusSensorEntityDescription, solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - super().__init__(coordinator, key, solar_net_id) + super().__init__(coordinator, description, solar_net_id) logger_data = self._device_data() # Logger device is already created in FroniusSolarNet._create_solar_net_device self._attr_device_info = coordinator.solar_net.system_device_info - self._attr_native_unit_of_measurement = logger_data[key].get("unit") - self._attr_unique_id = f'{logger_data["unique_identifier"]["value"]}-{key}' + self._attr_native_unit_of_measurement = logger_data[self.response_key].get( + "unit" + ) + self._attr_unique_id = ( + f'{logger_data["unique_identifier"]["value"]}-{description.key}' + ) class MeterSensor(_FroniusSensorEntity): """Defines a Fronius meter device sensor entity.""" - entity_descriptions = METER_ENTITY_DESCRIPTIONS - def __init__( self, coordinator: FroniusMeterUpdateCoordinator, - key: str, + description: FroniusSensorEntityDescription, solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - super().__init__(coordinator, key, solar_net_id) + super().__init__(coordinator, description, solar_net_id) meter_data = self._device_data() # S0 meters connected directly to inverters respond "n.a." as serial number # `model` contains the inverter id: "S0 Meter at inverter 1" @@ -745,22 +777,20 @@ class MeterSensor(_FroniusSensorEntity): name=meter_data["model"]["value"], via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), ) - self._attr_unique_id = f"{meter_uid}-{key}" + self._attr_unique_id = f"{meter_uid}-{description.key}" class OhmpilotSensor(_FroniusSensorEntity): """Defines a Fronius Ohmpilot sensor entity.""" - entity_descriptions = OHMPILOT_ENTITY_DESCRIPTIONS - def __init__( self, coordinator: FroniusOhmpilotUpdateCoordinator, - key: str, + description: FroniusSensorEntityDescription, solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - super().__init__(coordinator, key, solar_net_id) + super().__init__(coordinator, description, solar_net_id) device_data = self._device_data() self._attr_device_info = DeviceInfo( @@ -771,45 +801,41 @@ class OhmpilotSensor(_FroniusSensorEntity): sw_version=device_data["software"]["value"], via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), ) - self._attr_unique_id = f'{device_data["serial"]["value"]}-{key}' + self._attr_unique_id = f'{device_data["serial"]["value"]}-{description.key}' class PowerFlowSensor(_FroniusSensorEntity): """Defines a Fronius power flow sensor entity.""" - entity_descriptions = POWER_FLOW_ENTITY_DESCRIPTIONS - def __init__( self, coordinator: FroniusPowerFlowUpdateCoordinator, - key: str, + description: FroniusSensorEntityDescription, solar_net_id: str, ) -> None: """Set up an individual Fronius power flow sensor.""" - super().__init__(coordinator, key, solar_net_id) + super().__init__(coordinator, description, solar_net_id) # SolarNet device is already created in FroniusSolarNet._create_solar_net_device self._attr_device_info = coordinator.solar_net.system_device_info self._attr_unique_id = ( - f"{coordinator.solar_net.solar_net_device_id}-power_flow-{key}" + f"{coordinator.solar_net.solar_net_device_id}-power_flow-{description.key}" ) class StorageSensor(_FroniusSensorEntity): """Defines a Fronius storage device sensor entity.""" - entity_descriptions = STORAGE_ENTITY_DESCRIPTIONS - def __init__( self, coordinator: FroniusStorageUpdateCoordinator, - key: str, + description: FroniusSensorEntityDescription, solar_net_id: str, ) -> None: """Set up an individual Fronius storage sensor.""" - super().__init__(coordinator, key, solar_net_id) + super().__init__(coordinator, description, solar_net_id) storage_data = self._device_data() - self._attr_unique_id = f'{storage_data["serial"]["value"]}-{key}' + self._attr_unique_id = f'{storage_data["serial"]["value"]}-{description.key}' self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, storage_data["serial"]["value"])}, manufacturer=storage_data["manufacturer"]["value"], diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index 4a0f96ed8e6..de066704644 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -66,6 +66,21 @@ "status_code": { "name": "Status code" }, + "status_message": { + "name": "Status message", + "state": { + "startup": "Startup", + "running": "Running", + "standby": "Standby", + "bootloading": "Bootloading", + "error": "Error", + "idle": "Idle", + "ready": "Ready", + "sleeping": "Sleeping", + "unknown": "Unknown", + "invalid": "Invalid" + } + }, "led_state": { "name": "LED state" }, @@ -114,6 +129,16 @@ "meter_location": { "name": "Meter location" }, + "meter_location_description": { + "name": "Meter location description", + "state": { + "feed_in": "Grid interconnection point", + "consumption_path": "Consumption path", + "external_generator": "External generator", + "external_battery": "External battery", + "subload": "Subload" + } + }, "power_apparent_phase_1": { "name": "Apparent power phase 1" }, @@ -193,7 +218,15 @@ "name": "State code" }, "state_message": { - "name": "State message" + "name": "State message", + "state": { + "up_and_running": "Up and running", + "keep_minimum_temperature": "Keep minimum temperature", + "legionella_protection": "Legionella protection", + "critical_fault": "Critical fault", + "fault": "Fault", + "boost_mode": "Boost mode" + } }, "meter_mode": { "name": "Meter mode" diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 18ea52fc15f..2e33bc6741e 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["dacite", "gios"], "quality_scale": "platinum", - "requirements": ["gios==3.2.1"] + "requirements": ["gios==3.2.2"] } diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index 332280bac5a..0065d70dda9 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -79,12 +79,12 @@ _ICONS: dict[SensorKind, str] = { class GoodweSensorEntityDescription(SensorEntityDescription): """Class describing Goodwe sensor entities.""" - value: Callable[ - [GoodweUpdateCoordinator, str], Any - ] = lambda coordinator, sensor: coordinator.sensor_value(sensor) - available: Callable[ - [GoodweUpdateCoordinator], bool - ] = lambda coordinator: coordinator.last_update_success + value: Callable[[GoodweUpdateCoordinator, str], Any] = ( + lambda coordinator, sensor: coordinator.sensor_value(sensor) + ) + available: Callable[[GoodweUpdateCoordinator], bool] = ( + lambda coordinator: coordinator.last_update_success + ) _DESCRIPTIONS: dict[str, GoodweSensorEntityDescription] = { diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 2eeb1903c85..af892f15af4 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -59,7 +59,11 @@ LOCAL_SDK_MIN_VERSION = AwesomeVersion("2.1.5") @callback def _get_registry_entries( hass: HomeAssistant, entity_id: str -) -> tuple[er.RegistryEntry | None, dr.DeviceEntry | None, ar.AreaEntry | None,]: +) -> tuple[ + er.RegistryEntry | None, + dr.DeviceEntry | None, + ar.AreaEntry | None, +]: """Get registry entries.""" ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) diff --git a/homeassistant/components/google_tasks/todo.py b/homeassistant/components/google_tasks/todo.py index e5c90523a18..d3c4dfa6936 100644 --- a/homeassistant/components/google_tasks/todo.py +++ b/homeassistant/components/google_tasks/todo.py @@ -93,7 +93,8 @@ class GoogleTaskTodoListEntity( summary=item["title"], uid=item["id"], status=TODO_STATUS_MAP.get( - item.get("status"), TodoItemStatus.NEEDS_ACTION # type: ignore[arg-type] + item.get("status"), # type: ignore[arg-type] + TodoItemStatus.NEEDS_ACTION, ), ) for item in _order_tasks(self.coordinator.data) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 19621e28d03..54ea2f3e5bd 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -195,9 +195,7 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 loop = ( # Create own thread if more than 1 CPU - hass.loop - if multiprocessing.cpu_count() < 2 - else None + hass.loop if multiprocessing.cpu_count() < 2 else None ) host = base_config[DOMAIN].get(CONF_HOST) display_name = base_config[DOMAIN].get(CONF_DISPLAY_NAME, DEFAULT_DISPLAY_NAME) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 9b27653e4cf..d371998aaf8 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -124,12 +124,15 @@ class Fan(HomeAccessory): ), ) + setter_callback = ( + lambda value, preset_mode=preset_mode: self.set_preset_mode( + value, preset_mode + ) + ) self.preset_mode_chars[preset_mode] = preset_serv.configure_char( CHAR_ON, value=False, - setter_callback=lambda value, preset_mode=preset_mode: self.set_preset_mode( - value, preset_mode - ), + setter_callback=setter_callback, ) if CHAR_SWING_MODE in self.chars: diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index f2f8b51665a..c68ecd79d5f 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -8,7 +8,7 @@ "quality_scale": "internal", "requirements": [ "aiohttp_cors==0.7.0", - "aiohttp-fast-url-dispatcher==0.1.0", + "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.1.1" ] } diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index d0d1ce71161..62efabf1f5e 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -35,6 +35,7 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_URL, CONF_USERNAME, + CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, Platform, ) @@ -89,7 +90,7 @@ from .const import ( SERVICE_SUSPEND_INTEGRATION, UPDATE_SIGNAL, ) -from .utils import get_device_macs +from .utils import get_device_macs, non_verifying_requests_session _LOGGER = logging.getLogger(__name__) @@ -335,16 +336,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _connect() -> Connection: """Set up a connection.""" + kwargs: dict[str, Any] = { + "timeout": CONNECTION_TIMEOUT, + } + if url.startswith("https://") and not entry.data.get(CONF_VERIFY_SSL): + kwargs["requests_session"] = non_verifying_requests_session(url) if entry.options.get(CONF_UNAUTHENTICATED_MODE): _LOGGER.debug("Connecting in unauthenticated mode, reduced feature set") - connection = Connection(url, timeout=CONNECTION_TIMEOUT) + connection = Connection(url, **kwargs) else: _LOGGER.debug("Connecting in authenticated mode, full feature set") username = entry.data.get(CONF_USERNAME) or "" password = entry.data.get(CONF_PASSWORD) or "" - connection = Connection( - url, username=username, password=password, timeout=CONNECTION_TIMEOUT - ) + connection = Connection(url, username=username, password=password, **kwargs) return connection try: diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 6d7b0b9bb11..c97c8d6367b 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -16,7 +16,7 @@ from huawei_lte_api.exceptions import ( ResponseErrorException, ) from huawei_lte_api.Session import GetResponseType -from requests.exceptions import Timeout +from requests.exceptions import SSLError, Timeout from url_normalize import url_normalize import voluptuous as vol @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_URL, CONF_USERNAME, + CONF_VERIFY_SSL, ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -44,7 +45,7 @@ from .const import ( DEFAULT_UNAUTHENTICATED_MODE, DOMAIN, ) -from .utils import get_device_macs +from .utils import get_device_macs, non_verifying_requests_session _LOGGER = logging.getLogger(__name__) @@ -80,6 +81,13 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.context.get(CONF_URL, ""), ), ): str, + vol.Optional( + CONF_VERIFY_SSL, + default=user_input.get( + CONF_VERIFY_SSL, + False, + ), + ): bool, vol.Optional( CONF_USERNAME, default=user_input.get(CONF_USERNAME) or "" ): str, @@ -119,11 +127,20 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): password = user_input.get(CONF_PASSWORD) or "" def _get_connection() -> Connection: + if ( + user_input[CONF_URL].startswith("https://") + and not user_input[CONF_VERIFY_SSL] + ): + requests_session = non_verifying_requests_session(user_input[CONF_URL]) + else: + requests_session = None + return Connection( url=user_input[CONF_URL], username=username, password=password, timeout=CONNECTION_TIMEOUT, + requests_session=requests_session, ) conn = None @@ -140,6 +157,12 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except ResponseErrorException: _LOGGER.warning("Response error", exc_info=True) errors["base"] = "response_error" + except SSLError: + _LOGGER.warning("SSL error", exc_info=True) + if user_input[CONF_VERIFY_SSL]: + errors[CONF_URL] = "ssl_error_try_unverified" + else: + errors[CONF_URL] = "ssl_error_try_plain" except Timeout: _LOGGER.warning("Connection timeout", exc_info=True) errors[CONF_URL] = "connection_timeout" @@ -152,6 +175,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def _disconnect(conn: Connection) -> None: try: conn.close() + conn.requests_session.close() except Exception: # pylint: disable=broad-except _LOGGER.debug("Disconnect error", exc_info=True) diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 1e43aa818e9..9e46ca742b8 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -14,6 +14,8 @@ "invalid_url": "Invalid URL", "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", "response_error": "Unknown error from device", + "ssl_error_try_plain": "HTTPS error, please try a plain HTTP URL", + "ssl_error_try_unverified": "HTTPS error, please try disabling certificate verification or a plain HTTP URL", "unknown": "[%key:common::config_flow::error::unknown%]" }, "flow_title": "{name}", @@ -30,7 +32,8 @@ "data": { "password": "[%key:common::config_flow::data::password%]", "url": "[%key:common::config_flow::data::url%]", - "username": "[%key:common::config_flow::data::username%]" + "username": "[%key:common::config_flow::data::username%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "description": "Enter device access details.", "title": "Configure Huawei LTE" diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py index 172e8658928..df212a1c25d 100644 --- a/homeassistant/components/huawei_lte/utils.py +++ b/homeassistant/components/huawei_lte/utils.py @@ -2,8 +2,13 @@ from __future__ import annotations from contextlib import suppress +import re +from urllib.parse import urlparse +import warnings from huawei_lte_api.Session import GetResponseType +import requests +from urllib3.exceptions import InsecureRequestWarning from homeassistant.helpers.device_registry import format_mac @@ -25,3 +30,18 @@ def get_device_macs( macs.extend(x.get("WifiMac") for x in wlan_settings["Ssids"]["Ssid"]) return sorted({format_mac(str(x)) for x in macs if x}) + + +def non_verifying_requests_session(url: str) -> requests.Session: + """Get requests.Session that does not verify HTTPS, filter warnings about it.""" + parsed_url = urlparse(url) + assert parsed_url.hostname + requests_session = requests.Session() + requests_session.verify = False + warnings.filterwarnings( + "ignore", + message=rf"^.*\b{re.escape(parsed_url.hostname)}\b", + category=InsecureRequestWarning, + module=r"^urllib3\.connectionpool$", + ) + return requests_session diff --git a/homeassistant/components/hunterdouglas_powerview/select.py b/homeassistant/components/hunterdouglas_powerview/select.py index 37d1193e0e5..151b3a58011 100644 --- a/homeassistant/components/hunterdouglas_powerview/select.py +++ b/homeassistant/components/hunterdouglas_powerview/select.py @@ -116,5 +116,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.entity_description.select_fn(self._shade, option) - await self._shade.refresh() # force update data to ensure new info is in coordinator + # force update data to ensure new info is in coordinator + await self._shade.refresh() self.async_write_ha_state() diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 3914e0c52c1..fea2583a27a 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -66,8 +66,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator = hass.data[ DOMAIN - ].pop( - entry.entry_id - ) + ].pop(entry.entry_id) await coordinator.shutdown() return unload_ok diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index a6c00e62b62..21eb3f2e5a1 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -259,7 +259,8 @@ class KrakenSensor( return try: self._attr_native_value = self.entity_description.value_fn( - self.coordinator, self.tracked_asset_pair_wsname # type: ignore[arg-type] + self.coordinator, # type: ignore[arg-type] + self.tracked_asset_pair_wsname, ) self._received_data_at_least_once = True except KeyError: diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 8ef81e899b7..d7485e88fb0 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -316,7 +316,9 @@ class HeatMeterSensor( """Set up the sensor with the initial values.""" super().__init__(coordinator) self.key = description.key - self._attr_unique_id = f"{coordinator.config_entry.data['device_number']}_{description.key}" # type: ignore[union-attr] + self._attr_unique_id = ( + f"{coordinator.config_entry.data['device_number']}_{description.key}" # type: ignore[union-attr] + ) self._attr_name = f"Heat Meter {description.name}" self.entity_description = description self._attr_device_info = device diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 7656de8d385..37156e9ca08 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -118,7 +118,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: push_coordinator = LookinPushCoordinator(entry.title) if lookin_device.model >= 2: - meteo_coordinator = LookinDataUpdateCoordinator[MeteoSensor]( + coordinator_class = LookinDataUpdateCoordinator[MeteoSensor] + meteo_coordinator = coordinator_class( hass, push_coordinator, name=entry.title, diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index f9ef3593fe6..ddda50aa8b2 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -348,7 +348,10 @@ class MatrixBot: self._access_tokens[self._mx_id] = token await self.hass.async_add_executor_job( - save_json, self._session_filepath, self._access_tokens, True # private=True + save_json, + self._session_filepath, + self._access_tokens, + True, # private=True ) async def _login(self) -> None: diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index 3361c3fa146..e84fcec32d8 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -104,9 +104,11 @@ class MatterEventEntity(MatterEntity, EventEntity): """Call when Node attribute(s) changed.""" @callback - def _on_matter_node_event( - self, event: EventType, data: MatterNodeEvent - ) -> None: # noqa: F821 + def _on_matter_node_event( # noqa: F821 + self, + event: EventType, + data: MatterNodeEvent, + ) -> None: """Call on NodeEvent.""" if data.endpoint_id != self._endpoint.endpoint_id: return diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f3ff925a1a4..50365f90f1f 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1137,8 +1137,7 @@ class MediaPlayerImageView(HomeAssistantView): extra_urls = [ # Need to modify the default regex for media_content_id as it may # include arbitrary characters including '/','{', or '}' - url - + "/browse_media/{media_content_type}/{media_content_id:.+}", + url + "/browse_media/{media_content_type}/{media_content_id:.+}", ] def __init__(self, component: EntityComponent[MediaPlayerEntity]) -> None: diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 8eab83b5d41..9b3adb38e0c 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -1,11 +1,13 @@ """Support to interact with a Music Player Daemon.""" from __future__ import annotations -from contextlib import suppress +import asyncio +from contextlib import asynccontextmanager, suppress from datetime import timedelta import hashlib import logging import os +from socket import gaierror from typing import Any import mpd @@ -92,11 +94,11 @@ class MpdDevice(MediaPlayerEntity): self._name = name self.password = password - self._status = None + self._status = {} self._currentsong = None self._playlists = None self._currentplaylist = None - self._is_connected = False + self._is_available = None self._muted = False self._muted_volume = None self._media_position_updated_at = None @@ -104,67 +106,88 @@ class MpdDevice(MediaPlayerEntity): self._media_image_hash = None # Track if the song changed so image doesn't have to be loaded every update. self._media_image_file = None - self._commands = None # set up MPD client self._client = MPDClient() self._client.timeout = 30 - self._client.idletimeout = None + self._client.idletimeout = 10 + self._client_lock = asyncio.Lock() - async def _connect(self): - """Connect to MPD.""" - try: - await self._client.connect(self.server, self.port) - - if self.password is not None: - await self._client.password(self.password) - except mpd.ConnectionError: - return - - self._is_connected = True - - def _disconnect(self): - """Disconnect from MPD.""" - with suppress(mpd.ConnectionError): - self._client.disconnect() - self._is_connected = False - self._status = None - - async def _fetch_status(self): - """Fetch status from MPD.""" - self._status = await self._client.status() - self._currentsong = await self._client.currentsong() - await self._async_update_media_image_hash() - - if (position := self._status.get("elapsed")) is None: - position = self._status.get("time") - - if isinstance(position, str) and ":" in position: - position = position.split(":")[0] - - if position is not None and self._media_position != position: - self._media_position_updated_at = dt_util.utcnow() - self._media_position = int(float(position)) - - await self._update_playlists() - - @property - def available(self): - """Return true if MPD is available and connected.""" - return self._is_connected + # Instead of relying on python-mpd2 to maintain a (persistent) connection to + # MPD, the below explicitly sets up a *non*-persistent connection. This is + # done to workaround the issue as described in: + # + @asynccontextmanager + async def connection(self): + """Handle MPD connect and disconnect.""" + async with self._client_lock: + try: + # MPDClient.connect() doesn't always respect its timeout. To + # prevent a deadlock, enforce an additional (slightly longer) + # timeout on the coroutine itself. + try: + async with asyncio.timeout(self._client.timeout + 5): + await self._client.connect(self.server, self.port) + except asyncio.TimeoutError as error: + # TimeoutError has no message (which hinders logging further + # down the line), so provide one. + raise asyncio.TimeoutError( + "Connection attempt timed out" + ) from error + if self.password is not None: + await self._client.password(self.password) + self._is_available = True + yield + except ( + asyncio.TimeoutError, + gaierror, + mpd.ConnectionError, + OSError, + ) as error: + # Log a warning during startup or when previously connected; for + # subsequent errors a debug message is sufficient. + log_level = logging.DEBUG + if self._is_available is not False: + log_level = logging.WARNING + _LOGGER.log( + log_level, "Error connecting to '%s': %s", self.server, error + ) + self._is_available = False + self._status = {} + # Also yield on failure. Handling mpd.ConnectionErrors caused by + # attempting to control a disconnected client is the + # responsibility of the caller. + yield + finally: + with suppress(mpd.ConnectionError): + self._client.disconnect() async def async_update(self) -> None: - """Get the latest data and update the state.""" - try: - if not self._is_connected: - await self._connect() - self._commands = list(await self._client.commands()) + """Get the latest data from MPD and update the state.""" + async with self.connection(): + try: + self._status = await self._client.status() + self._currentsong = await self._client.currentsong() + await self._async_update_media_image_hash() - await self._fetch_status() - except (mpd.ConnectionError, OSError, ValueError) as error: - # Cleanly disconnect in case connection is not in valid state - _LOGGER.debug("Error updating status: %s", error) - self._disconnect() + if (position := self._status.get("elapsed")) is None: + position = self._status.get("time") + + if isinstance(position, str) and ":" in position: + position = position.split(":")[0] + + if position is not None and self._media_position != position: + self._media_position_updated_at = dt_util.utcnow() + self._media_position = int(float(position)) + + await self._update_playlists() + except (mpd.ConnectionError, ValueError) as error: + _LOGGER.debug("Error updating status: %s", error) + + @property + def available(self) -> bool: + """Return true if MPD is available and connected.""" + return self._is_available is True @property def name(self): @@ -174,13 +197,13 @@ class MpdDevice(MediaPlayerEntity): @property def state(self) -> MediaPlayerState: """Return the media state.""" - if self._status is None: + if not self._status: return MediaPlayerState.OFF - if self._status["state"] == "play": + if self._status.get("state") == "play": return MediaPlayerState.PLAYING - if self._status["state"] == "pause": + if self._status.get("state") == "pause": return MediaPlayerState.PAUSED - if self._status["state"] == "stop": + if self._status.get("state") == "stop": return MediaPlayerState.OFF return MediaPlayerState.OFF @@ -259,20 +282,26 @@ class MpdDevice(MediaPlayerEntity): async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing track.""" - if not (file := self._currentsong.get("file")): - return None, None - response = await self._async_get_file_image_response(file) - if response is None: - return None, None + async with self.connection(): + if self._currentsong is None or not (file := self._currentsong.get("file")): + return None, None - image = bytes(response["binary"]) - mime = response.get( - "type", "image/png" - ) # readpicture has type, albumart does not - return (image, mime) + with suppress(mpd.ConnectionError): + response = await self._async_get_file_image_response(file) + if response is None: + return None, None + + image = bytes(response["binary"]) + mime = response.get( + "type", "image/png" + ) # readpicture has type, albumart does not + return (image, mime) async def _async_update_media_image_hash(self): """Update the hash value for the media image.""" + if self._currentsong is None: + return + file = self._currentsong.get("file") if file == self._media_image_file: @@ -295,16 +324,21 @@ class MpdDevice(MediaPlayerEntity): self._media_image_file = file async def _async_get_file_image_response(self, file): - # not all MPD implementations and versions support the `albumart` and `fetchpicture` commands - can_albumart = "albumart" in self._commands - can_readpicture = "readpicture" in self._commands + # not all MPD implementations and versions support the `albumart` and + # `fetchpicture` commands. + commands = [] + with suppress(mpd.ConnectionError): + commands = list(await self._client.commands()) + can_albumart = "albumart" in commands + can_readpicture = "readpicture" in commands response = None # read artwork embedded into the media file if can_readpicture: try: - response = await self._client.readpicture(file) + with suppress(mpd.ConnectionError): + response = await self._client.readpicture(file) except mpd.CommandError as error: if error.errno is not mpd.FailureResponseCode.NO_EXIST: _LOGGER.warning( @@ -315,7 +349,8 @@ class MpdDevice(MediaPlayerEntity): # read artwork contained in the media directory (cover.{jpg,png,tiff,bmp}) if none is embedded if can_albumart and not response: try: - response = await self._client.albumart(file) + with suppress(mpd.ConnectionError): + response = await self._client.albumart(file) except mpd.CommandError as error: if error.errno is not mpd.FailureResponseCode.NO_EXIST: _LOGGER.warning( @@ -339,7 +374,7 @@ class MpdDevice(MediaPlayerEntity): @property def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - if self._status is None: + if not self._status: return MediaPlayerEntityFeature(0) supported = SUPPORT_MPD @@ -373,55 +408,64 @@ class MpdDevice(MediaPlayerEntity): """Update available MPD playlists.""" try: self._playlists = [] - for playlist_data in await self._client.listplaylists(): - self._playlists.append(playlist_data["playlist"]) + with suppress(mpd.ConnectionError): + for playlist_data in await self._client.listplaylists(): + self._playlists.append(playlist_data["playlist"]) except mpd.CommandError as error: self._playlists = None _LOGGER.warning("Playlists could not be updated: %s:", error) async def async_set_volume_level(self, volume: float) -> None: """Set volume of media player.""" - if "volume" in self._status: - await self._client.setvol(int(volume * 100)) + async with self.connection(): + if "volume" in self._status: + await self._client.setvol(int(volume * 100)) async def async_volume_up(self) -> None: """Service to send the MPD the command for volume up.""" - if "volume" in self._status: - current_volume = int(self._status["volume"]) + async with self.connection(): + if "volume" in self._status: + current_volume = int(self._status["volume"]) - if current_volume <= 100: - self._client.setvol(current_volume + 5) + if current_volume <= 100: + self._client.setvol(current_volume + 5) async def async_volume_down(self) -> None: """Service to send the MPD the command for volume down.""" - if "volume" in self._status: - current_volume = int(self._status["volume"]) + async with self.connection(): + if "volume" in self._status: + current_volume = int(self._status["volume"]) - if current_volume >= 0: - await self._client.setvol(current_volume - 5) + if current_volume >= 0: + await self._client.setvol(current_volume - 5) async def async_media_play(self) -> None: """Service to send the MPD the command for play/pause.""" - if self._status["state"] == "pause": - await self._client.pause(0) - else: - await self._client.play() + async with self.connection(): + if self._status.get("state") == "pause": + await self._client.pause(0) + else: + await self._client.play() async def async_media_pause(self) -> None: """Service to send the MPD the command for play/pause.""" - await self._client.pause(1) + async with self.connection(): + await self._client.pause(1) async def async_media_stop(self) -> None: """Service to send the MPD the command for stop.""" - await self._client.stop() + async with self.connection(): + await self._client.stop() async def async_media_next_track(self) -> None: """Service to send the MPD the command for next track.""" - await self._client.next() + async with self.connection(): + await self._client.next() async def async_media_previous_track(self) -> None: """Service to send the MPD the command for previous track.""" - await self._client.previous() + async with self.connection(): + await self._client.previous() async def async_mute_volume(self, mute: bool) -> None: """Mute. Emulated with set_volume_level.""" @@ -437,75 +481,82 @@ class MpdDevice(MediaPlayerEntity): self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the media player the command for playing a playlist.""" - if media_source.is_media_source_id(media_id): - media_type = MediaType.MUSIC - play_item = await media_source.async_resolve_media( - self.hass, media_id, self.entity_id - ) - media_id = async_process_play_media_url(self.hass, play_item.url) + async with self.connection(): + if media_source.is_media_source_id(media_id): + media_type = MediaType.MUSIC + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) + media_id = async_process_play_media_url(self.hass, play_item.url) - if media_type == MediaType.PLAYLIST: - _LOGGER.debug("Playing playlist: %s", media_id) - if media_id in self._playlists: - self._currentplaylist = media_id + if media_type == MediaType.PLAYLIST: + _LOGGER.debug("Playing playlist: %s", media_id) + if media_id in self._playlists: + self._currentplaylist = media_id + else: + self._currentplaylist = None + _LOGGER.warning("Unknown playlist name %s", media_id) + await self._client.clear() + await self._client.load(media_id) + await self._client.play() else: + await self._client.clear() self._currentplaylist = None - _LOGGER.warning("Unknown playlist name %s", media_id) - await self._client.clear() - await self._client.load(media_id) - await self._client.play() - else: - await self._client.clear() - self._currentplaylist = None - await self._client.add(media_id) - await self._client.play() + await self._client.add(media_id) + await self._client.play() @property def repeat(self) -> RepeatMode: """Return current repeat mode.""" - if self._status["repeat"] == "1": - if self._status["single"] == "1": + if self._status.get("repeat") == "1": + if self._status.get("single") == "1": return RepeatMode.ONE return RepeatMode.ALL return RepeatMode.OFF async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" - if repeat == RepeatMode.OFF: - await self._client.repeat(0) - await self._client.single(0) - else: - await self._client.repeat(1) - if repeat == RepeatMode.ONE: - await self._client.single(1) - else: + async with self.connection(): + if repeat == RepeatMode.OFF: + await self._client.repeat(0) await self._client.single(0) + else: + await self._client.repeat(1) + if repeat == RepeatMode.ONE: + await self._client.single(1) + else: + await self._client.single(0) @property def shuffle(self): """Boolean if shuffle is enabled.""" - return bool(int(self._status["random"])) + return bool(int(self._status.get("random"))) async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" - await self._client.random(int(shuffle)) + async with self.connection(): + await self._client.random(int(shuffle)) async def async_turn_off(self) -> None: """Service to send the MPD the command to stop playing.""" - await self._client.stop() + async with self.connection(): + await self._client.stop() async def async_turn_on(self) -> None: """Service to send the MPD the command to start playing.""" - await self._client.play() - await self._update_playlists(no_throttle=True) + async with self.connection(): + await self._client.play() + await self._update_playlists(no_throttle=True) async def async_clear_playlist(self) -> None: """Clear players playlist.""" - await self._client.clear() + async with self.connection(): + await self._client.clear() async def async_media_seek(self, position: float) -> None: """Send seek command.""" - await self._client.seekcur(position) + async with self.connection(): + await self._client.seekcur(position) async def async_browse_media( self, @@ -513,8 +564,11 @@ class MpdDevice(MediaPlayerEntity): media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - return await media_source.async_browse_media( - self.hass, - media_content_id, - content_filter=lambda item: item.media_content_type.startswith("audio/"), - ) + async with self.connection(): + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith( + "audio/" + ), + ) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 3fa3ebfd30c..c8696071fb4 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -470,9 +470,10 @@ class MqttTemperatureControlEntity(MqttEntity, ABC): except ValueError: _LOGGER.error("Could not parse %s from %s", template_name, payload) - def prepare_subscribe_topics( - self, topics: dict[str, dict[str, Any]] - ) -> None: # noqa: C901 + def prepare_subscribe_topics( # noqa: C901 + self, + topics: dict[str, dict[str, Any]], + ) -> None: """(Re)Subscribe to topics.""" @callback diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 6e364182cb0..f478ad712d7 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -63,9 +63,8 @@ async def async_wait_for_mqtt_client(hass: HomeAssistant) -> bool: state_reached_future: asyncio.Future[bool] if DATA_MQTT_AVAILABLE not in hass.data: - hass.data[ - DATA_MQTT_AVAILABLE - ] = state_reached_future = hass.loop.create_future() + state_reached_future = hass.loop.create_future() + hass.data[DATA_MQTT_AVAILABLE] = state_reached_future else: state_reached_future = hass.data[DATA_MQTT_AVAILABLE] if state_reached_future.done(): diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index 8344fb033b7..6800c403ee8 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -34,9 +34,9 @@ UNIT_OF_LOAD: Final[str] = "load" class NextcloudSensorEntityDescription(SensorEntityDescription): """Describes Nextcloud sensor entity.""" - value_fn: Callable[ - [str | int | float], str | int | float | datetime - ] = lambda value: value + value_fn: Callable[[str | int | float], str | int | float | datetime] = ( + lambda value: value + ) SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index ede7a20ccdb..3f17c0b795b 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -39,7 +39,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DEFAULT_TIMEOUT, DOMAIN, ERROR_STATES +from .const import CONF_ENCRYPT_TOKEN, DEFAULT_TIMEOUT, DOMAIN, ERROR_STATES from .helpers import NukiWebhookException, parse_id _NukiDeviceT = TypeVar("_NukiDeviceT", bound=NukiDevice) @@ -188,7 +188,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_HOST], entry.data[CONF_TOKEN], entry.data[CONF_PORT], - True, + entry.data.get(CONF_ENCRYPT_TOKEN, True), DEFAULT_TIMEOUT, ) diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 310197d55d8..4acfecf492b 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN +from .const import CONF_ENCRYPT_TOKEN, DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN from .helpers import CannotConnect, InvalidAuth, parse_id _LOGGER = logging.getLogger(__name__) @@ -26,7 +26,12 @@ USER_SCHEMA = vol.Schema( } ) -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_TOKEN): str, + vol.Optional(CONF_ENCRYPT_TOKEN, default=True): bool, + } +) async def validate_input(hass, data): @@ -41,7 +46,7 @@ async def validate_input(hass, data): data[CONF_HOST], data[CONF_TOKEN], data[CONF_PORT], - True, + data.get(CONF_ENCRYPT_TOKEN, True), DEFAULT_TIMEOUT, ) @@ -100,6 +105,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_HOST: self._data[CONF_HOST], CONF_PORT: self._data[CONF_PORT], CONF_TOKEN: user_input[CONF_TOKEN], + CONF_ENCRYPT_TOKEN: user_input[CONF_ENCRYPT_TOKEN], } try: @@ -131,8 +137,15 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_validate(self, user_input=None): """Handle init step of a flow.""" + data_schema = self.discovery_schema or USER_SCHEMA + errors = {} if user_input is not None: + data_schema = USER_SCHEMA.extend( + { + vol.Optional(CONF_ENCRYPT_TOKEN, default=True): bool, + } + ) try: info = await validate_input(self.hass, user_input) except CannotConnect: @@ -149,7 +162,8 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=bridge_id, data=user_input) - data_schema = self.discovery_schema or USER_SCHEMA return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id="user", + data_schema=self.add_suggested_values_to_schema(data_schema, user_input), + errors=errors, ) diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py index dee4a8b8ac5..21a2dcf9e5b 100644 --- a/homeassistant/components/nuki/const.py +++ b/homeassistant/components/nuki/const.py @@ -12,3 +12,6 @@ DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 20 ERROR_STATES = (0, 254, 255) + +# Encrypt token, instead of using a plaintext token +CONF_ENCRYPT_TOKEN = "encrypt_token" diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 19aeae989f4..eb380cabd04 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -5,14 +5,16 @@ "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", - "token": "[%key:common::config_flow::data::access_token%]" + "token": "[%key:common::config_flow::data::access_token%]", + "encrypt_token": "Use an encrypted token for authentication." } }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Nuki integration needs to re-authenticate with your bridge.", "data": { - "token": "[%key:common::config_flow::data::access_token%]" + "token": "[%key:common::config_flow::data::access_token%]", + "encrypt_token": "[%key:component::nuki::config::step::user::data::encrypt_token%]" } } }, diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index 8771ae7a701..5f8a7d978d1 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -32,8 +32,7 @@ class ONVIFBaseEntity(Entity): See: https://github.com/home-assistant/core/issues/35883 """ return ( - self.device.info.mac - or self.device.info.serial_number # type:ignore[return-value] + self.device.info.mac or self.device.info.serial_number # type:ignore[return-value] ) @property diff --git a/homeassistant/components/ourgroceries/coordinator.py b/homeassistant/components/ourgroceries/coordinator.py index a4b594c7e86..636ebcc300a 100644 --- a/homeassistant/components/ourgroceries/coordinator.py +++ b/homeassistant/components/ourgroceries/coordinator.py @@ -1,6 +1,7 @@ """The OurGroceries coordinator.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging @@ -25,6 +26,7 @@ class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Initialize global OurGroceries data updater.""" self.og = og self.lists = lists + self._ids = [sl["id"] for sl in lists] interval = timedelta(seconds=SCAN_INTERVAL) super().__init__( hass, @@ -35,7 +37,11 @@ class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): async def _async_update_data(self) -> dict[str, dict]: """Fetch data from OurGroceries.""" - return { - sl["id"]: (await self.og.get_list_items(list_id=sl["id"])) - for sl in self.lists - } + return dict( + zip( + self._ids, + await asyncio.gather( + *[self.og.get_list_items(list_id=id) for id in self._ids] + ), + ) + ) diff --git a/homeassistant/components/ourgroceries/strings.json b/homeassistant/components/ourgroceries/strings.json index 96dc8b371d1..78a46954183 100644 --- a/homeassistant/components/ourgroceries/strings.json +++ b/homeassistant/components/ourgroceries/strings.json @@ -12,9 +12,6 @@ "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%]" } } } diff --git a/homeassistant/components/ourgroceries/todo.py b/homeassistant/components/ourgroceries/todo.py index 98029b09ba8..8115066d0fb 100644 --- a/homeassistant/components/ourgroceries/todo.py +++ b/homeassistant/components/ourgroceries/todo.py @@ -1,6 +1,7 @@ """A todo platform for OurGroceries.""" import asyncio +from typing import Any from homeassistant.components.todo import ( TodoItem, @@ -28,6 +29,12 @@ async def async_setup_entry( ) +def _completion_status(item: dict[str, Any]) -> TodoItemStatus: + if item.get("crossedOffAt", False): + return TodoItemStatus.COMPLETED + return TodoItemStatus.NEEDS_ACTION + + class OurGroceriesTodoListEntity( CoordinatorEntity[OurGroceriesDataUpdateCoordinator], TodoListEntity ): @@ -58,12 +65,6 @@ class OurGroceriesTodoListEntity( if self.coordinator.data is None: self._attr_todo_items = None else: - - def _completion_status(item): - if item.get("crossedOffAt", False): - return TodoItemStatus.COMPLETED - return TodoItemStatus.NEEDS_ACTION - self._attr_todo_items = [ TodoItem( summary=item["name"], diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py index fcb83884694..7a9e50d7130 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py @@ -245,12 +245,13 @@ class HitachiAirToAirHeatPumpHLRRWIFI(OverkizEntity, ClimateEntity): MODE_CHANGE_STATE, OverkizCommandParam.AUTO, ).lower() # Overkiz can return states that have uppercase characters which are not accepted back as commands - if hvac_mode.replace( - " ", "" - ) in [ # Overkiz can return states like 'auto cooling' or 'autoHeating' that are not valid commands and need to be converted to 'auto' - OverkizCommandParam.AUTOCOOLING, - OverkizCommandParam.AUTOHEATING, - ]: + if ( + hvac_mode.replace(" ", "") + in [ # Overkiz can return states like 'auto cooling' or 'autoHeating' that are not valid commands and need to be converted to 'auto' + OverkizCommandParam.AUTOCOOLING, + OverkizCommandParam.AUTOHEATING, + ] + ): hvac_mode = OverkizCommandParam.AUTO swing_mode = self._control_backfill( diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index e7a69e0bf02..507ab82e8e2 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -17,10 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( @@ -44,6 +41,7 @@ from .const import ( SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, SENSOR_SELECTED_SLOT_START, ) +from .coordinator import PicnicUpdateCoordinator @dataclass @@ -237,7 +235,7 @@ async def async_setup_entry( ) -class PicnicSensor(SensorEntity, CoordinatorEntity): +class PicnicSensor(SensorEntity, CoordinatorEntity[PicnicUpdateCoordinator]): """The CoordinatorEntity subclass representing Picnic sensors.""" _attr_has_entity_name = True @@ -246,7 +244,7 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): def __init__( self, - coordinator: DataUpdateCoordinator[Any], + coordinator: PicnicUpdateCoordinator, config_entry: ConfigEntry, description: PicnicSensorEntityDescription, ) -> None: diff --git a/homeassistant/components/picnic/services.py b/homeassistant/components/picnic/services.py index fa00037462d..b44d4dd5a62 100644 --- a/homeassistant/components/picnic/services.py +++ b/homeassistant/components/picnic/services.py @@ -77,8 +77,11 @@ async def handle_add_product( ) -def product_search(api_client: PicnicAPI, product_name: str) -> None | str: +def product_search(api_client: PicnicAPI, product_name: str | None) -> None | str: """Query the api client for the product name.""" + if product_name is None: + return None + search_result = api_client.search(product_name) if not search_result or "items" not in search_result[0]: diff --git a/homeassistant/components/picnic/todo.py b/homeassistant/components/picnic/todo.py index 389909ca06e..47b9685c9ec 100644 --- a/homeassistant/components/picnic/todo.py +++ b/homeassistant/components/picnic/todo.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import cast from homeassistant.components.todo import ( TodoItem, @@ -14,12 +14,10 @@ from homeassistant.config_entries import ConfigEntry 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, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_COORDINATOR, DOMAIN +from .coordinator import PicnicUpdateCoordinator from .services import product_search _LOGGER = logging.getLogger(__name__) @@ -36,7 +34,7 @@ async def async_setup_entry( async_add_entities([PicnicCart(hass, picnic_coordinator, config_entry)]) -class PicnicCart(TodoListEntity, CoordinatorEntity): +class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]): """A Picnic Shopping Cart TodoListEntity.""" _attr_has_entity_name = True @@ -47,7 +45,7 @@ class PicnicCart(TodoListEntity, CoordinatorEntity): def __init__( self, hass: HomeAssistant, - coordinator: DataUpdateCoordinator[Any], + coordinator: PicnicUpdateCoordinator, config_entry: ConfigEntry, ) -> None: """Initialize PicnicCart.""" diff --git a/homeassistant/components/private_ble_device/sensor.py b/homeassistant/components/private_ble_device/sensor.py index b332d057ba9..d15ed1163b7 100644 --- a/homeassistant/components/private_ble_device/sensor.py +++ b/homeassistant/components/private_ble_device/sensor.py @@ -83,13 +83,17 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement=UnitOfTime.SECONDS, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda hass, service_info: bluetooth.async_get_learned_advertising_interval( - hass, service_info.address - ) - or bluetooth.async_get_fallback_availability_interval( - hass, service_info.address - ) - or bluetooth.FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + value_fn=( + lambda hass, service_info: ( + bluetooth.async_get_learned_advertising_interval( + hass, service_info.address + ) + or bluetooth.async_get_fallback_availability_interval( + hass, service_info.address + ) + or bluetooth.FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) + ), suggested_display_precision=1, ), ) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index e7a7c1200b9..c149c993acb 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -10,10 +10,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.entity_registry import async_entries_for_config_entry from .const import CONF_SERIAL_NUMBER from .coordinator import RainbirdData @@ -55,6 +54,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: format_mac(mac_address), str(entry.data[CONF_SERIAL_NUMBER]), ) + _async_fix_device_id( + hass, + dr.async_get(hass), + entry.entry_id, + format_mac(mac_address), + str(entry.data[CONF_SERIAL_NUMBER]), + ) try: model_info = await controller.get_model_and_version() @@ -124,7 +130,7 @@ def _async_fix_entity_unique_id( serial_number: str, ) -> None: """Migrate existing entity if current one can't be found and an old one exists.""" - entity_entries = async_entries_for_config_entry(entity_registry, config_entry_id) + entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) for entity_entry in entity_entries: unique_id = str(entity_entry.unique_id) if unique_id.startswith(mac_address): @@ -137,6 +143,70 @@ def _async_fix_entity_unique_id( ) +def _async_device_entry_to_keep( + old_entry: dr.DeviceEntry, new_entry: dr.DeviceEntry +) -> dr.DeviceEntry: + """Determine which device entry to keep when there are duplicates. + + As we transitioned to new unique ids, we did not update existing device entries + and as a result there are devices with both the old and new unique id format. We + have to pick which one to keep, and preferably this can repair things if the + user previously renamed devices. + """ + # Prefer the new device if the user already gave it a name or area. Otherwise, + # do the same for the old entry. If no entries have been modified then keep the new one. + if new_entry.disabled_by is None and ( + new_entry.area_id is not None or new_entry.name_by_user is not None + ): + return new_entry + if old_entry.disabled_by is None and ( + old_entry.area_id is not None or old_entry.name_by_user is not None + ): + return old_entry + return new_entry if new_entry.disabled_by is None else old_entry + + +def _async_fix_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + config_entry_id: str, + mac_address: str, + serial_number: str, +) -> None: + """Migrate existing device identifiers to the new format. + + This will rename any device ids that are prefixed with the serial number to be prefixed + with the mac address. This also cleans up from a bug that allowed devices to exist + in both the old and new format. + """ + device_entries = dr.async_entries_for_config_entry(device_registry, config_entry_id) + device_entry_map = {} + migrations = {} + for device_entry in device_entries: + unique_id = next(iter(device_entry.identifiers))[1] + device_entry_map[unique_id] = device_entry + if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id: + migrations[unique_id] = f"{mac_address}{suffix}" + + for unique_id, new_unique_id in migrations.items(): + old_entry = device_entry_map[unique_id] + if (new_entry := device_entry_map.get(new_unique_id)) is not None: + # Device entries exist for both the old and new format and one must be removed + entry_to_keep = _async_device_entry_to_keep(old_entry, new_entry) + if entry_to_keep == new_entry: + _LOGGER.debug("Removing device entry %s", unique_id) + device_registry.async_remove_device(old_entry.id) + continue + # Remove new entry and update old entry to new id below + _LOGGER.debug("Removing device entry %s", new_unique_id) + device_registry.async_remove_device(new_entry.id) + + _LOGGER.debug("Updating device id from %s to %s", unique_id, new_unique_id) + device_registry.async_update_device( + old_entry.id, new_identifiers={(DOMAIN, new_unique_id)} + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/homeassistant/components/recorder/auto_repairs/schema.py b/homeassistant/components/recorder/auto_repairs/schema.py index aa036f33999..aedf917dd22 100644 --- a/homeassistant/components/recorder/auto_repairs/schema.py +++ b/homeassistant/components/recorder/auto_repairs/schema.py @@ -101,9 +101,8 @@ def _validate_table_schema_has_correct_collation( collate = ( dialect_kwargs.get("mysql_collate") - or dialect_kwargs.get( - "mariadb_collate" - ) # pylint: disable-next=protected-access + or dialect_kwargs.get("mariadb_collate") + # pylint: disable-next=protected-access or connection.dialect._fetch_setting(connection, "collation_server") # type: ignore[attr-defined] ) if collate and collate != "utf8mb4_unicode_ci": diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 06c8cf68903..b864e104ae6 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -176,13 +176,17 @@ class NativeLargeBinary(LargeBinary): # For MariaDB and MySQL we can use an unsigned integer type since it will fit 2**32 # for sqlite and postgresql we use a bigint UINT_32_TYPE = BigInteger().with_variant( - mysql.INTEGER(unsigned=True), "mysql", "mariadb" # type: ignore[no-untyped-call] + mysql.INTEGER(unsigned=True), # type: ignore[no-untyped-call] + "mysql", + "mariadb", ) JSON_VARIANT_CAST = Text().with_variant( - postgresql.JSON(none_as_null=True), "postgresql" # type: ignore[no-untyped-call] + postgresql.JSON(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", ) JSONB_VARIANT_CAST = Text().with_variant( - postgresql.JSONB(none_as_null=True), "postgresql" # type: ignore[no-untyped-call] + postgresql.JSONB(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", ) DATETIME_TYPE = ( DateTime(timezone=True) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index bf76c7264d5..fda8716df27 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -244,7 +244,8 @@ class Filters: ), # Needs https://github.com/bdraco/home-assistant/commit/bba91945006a46f3a01870008eb048e4f9cbb1ef self._generate_filter_for_columns( - (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder # type: ignore[arg-type] + (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), # type: ignore[arg-type] + _encoder, ).self_group(), ) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 8808ed2fd2b..427e3acab2d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -532,7 +532,9 @@ def _update_states_table_with_foreign_key_options( states_key_constraints = Base.metadata.tables[TABLE_STATES].foreign_key_constraints old_states_table = Table( # noqa: F841 - TABLE_STATES, MetaData(), *(alter["old_fk"] for alter in alters) # type: ignore[arg-type] + TABLE_STATES, + MetaData(), + *(alter["old_fk"] for alter in alters), # type: ignore[arg-type] ) for alter in alters: diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 8425f29fbe8..46761beae00 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -89,9 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() - async def async_check_firmware_update() -> str | Literal[ - False - ] | NewSoftwareVersion: + async def async_check_firmware_update() -> ( + str | Literal[False] | NewSoftwareVersion + ): """Check for firmware updates.""" if not host.api.supported(None, "update"): return False diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 179dd04cfaa..54a60d34229 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -566,10 +566,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) list_of_ports = {} for port in ports: - list_of_ports[ - port.device - ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( - f" - {port.manufacturer}" if port.manufacturer else "" + list_of_ports[port.device] = ( + f"{port}, s/n: {port.serial_number or 'n/a'}" + + (f" - {port.manufacturer}" if port.manufacturer else "") ) list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index a20d9b4c90f..36514fc8f35 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.2"] + "requirements": ["ring-doorbell[listen]==0.8.3"] } diff --git a/homeassistant/components/sfr_box/diagnostics.py b/homeassistant/components/sfr_box/diagnostics.py index 1fb98053267..e0e84a7ec1a 100644 --- a/homeassistant/components/sfr_box/diagnostics.py +++ b/homeassistant/components/sfr_box/diagnostics.py @@ -27,16 +27,28 @@ async def async_get_config_entry_diagnostics( }, "data": { "dsl": async_redact_data( - dataclasses.asdict(await data.system.box.dsl_get_info()), TO_REDACT + dataclasses.asdict( + await data.system.box.dsl_get_info() # type:ignore [call-overload] + ), + TO_REDACT, ), "ftth": async_redact_data( - dataclasses.asdict(await data.system.box.ftth_get_info()), TO_REDACT + dataclasses.asdict( + await data.system.box.ftth_get_info() # type:ignore [call-overload] + ), + TO_REDACT, ), "system": async_redact_data( - dataclasses.asdict(await data.system.box.system_get_info()), TO_REDACT + dataclasses.asdict( + await data.system.box.system_get_info() # type:ignore [call-overload] + ), + TO_REDACT, ), "wan": async_redact_data( - dataclasses.asdict(await data.system.box.wan_get_info()), TO_REDACT + dataclasses.asdict( + await data.system.box.wan_get_info() # type:ignore [call-overload] + ), + TO_REDACT, ), }, } diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json index eb3c9cb1b68..bf4d91a50f1 100644 --- a/homeassistant/components/sfr_box/manifest.json +++ b/homeassistant/components/sfr_box/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sfr_box", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["sfrbox-api==0.0.6"] + "requirements": ["sfrbox-api==0.0.8"] } diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index 1c4540b1c74..f56a9765618 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -188,7 +188,7 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda x: x.temperature / 1000, + value_fn=lambda x: None if x.temperature is None else x.temperature / 1000, ), ) WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = ( diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 49caafcc774..27059bba180 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -280,9 +280,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def _async_fallback_poll(self) -> None: """Retrieve latest state by polling.""" - await self.hass.data[DATA_SONOS].favorites[ - self.speaker.household_id - ].async_poll() + await ( + self.hass.data[DATA_SONOS].favorites[self.speaker.household_id].async_poll() + ) await self.hass.async_add_executor_job(self._update) def _update(self) -> None: diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index a334171abb8..a3441eb76da 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -78,7 +78,9 @@ class RecorderOutput(StreamOutput): def write_segment(segment: Segment) -> None: """Write a segment to output.""" + # fmt: off nonlocal output, output_v, output_a, last_stream_id, running_duration, last_sequence + # fmt: on # Because the stream_worker is in a different thread from the record service, # the lookback segments may still have some overlap with the recorder segments if segment.sequence <= last_sequence: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index b76699631cb..a2f08202319 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -153,7 +153,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C if not self.available: return None try: - return await self._api.surveillance_station.get_camera_image(self.entity_description.key, self.snapshot_quality) # type: ignore[no-any-return] + return await self._api.surveillance_station.get_camera_image( # type: ignore[no-any-return] + self.entity_description.key, self.snapshot_quality + ) except ( SynologyDSMAPIErrorException, SynologyDSMRequestException, diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 4e9149ebd07..0a00d1e79b4 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -57,7 +57,8 @@ from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_con from .trigger_entity import TriggerEntity CHECK_FORECAST_KEYS = ( - set().union(Forecast.__annotations__.keys()) + set() + .union(Forecast.__annotations__.keys()) # Manually add the forecast resulting attributes that only exists # as native_* in the Forecast definition .union(("apparent_temperature", "wind_gust_speed", "dew_point")) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index b2b1cb31624..c23c1d5924e 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -119,9 +119,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle Memo Text service call.""" memo_text = call.data[CONF_MEMO_TEXT] memo_text.hass = hass - await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].get_module( - call.data[CONF_ADDRESS] - ).set_memo_text(memo_text.async_render()) + await ( + hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"] + .get_module(call.data[CONF_ADDRESS]) + .set_memo_text(memo_text.async_render()) + ) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index f3612c2d011..4277460c3ea 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -48,12 +48,12 @@ class VeSyncSensorEntityDescription( ): """Describe VeSync sensor entity.""" - exists_fn: Callable[ - [VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], bool - ] = lambda _: True - update_fn: Callable[ - [VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], None - ] = lambda _: None + exists_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], bool] = ( + lambda _: True + ) + update_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], None] = ( + lambda _: None + ) def update_energy(device): diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 00d21ec7c34..5154f68b21f 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -34,6 +34,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -294,22 +295,45 @@ class ViCareClimate(ViCareEntity, ClimateEntity): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode and deactivate any existing programs.""" - vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) - if vicare_program is None: - raise ValueError( - f"Cannot set invalid vicare program: {preset_mode}/{vicare_program}" + target_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) + if target_program is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="program_unknown", + translation_placeholders={ + "preset": preset_mode, + }, ) - _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) - if self._current_program != VICARE_PROGRAM_NORMAL: + _LOGGER.debug("Current preset %s", self._current_program) + if self._current_program and self._current_program != VICARE_PROGRAM_NORMAL: # We can't deactivate "normal" + _LOGGER.debug("deactivating %s", self._current_program) try: self._circuit.deactivateProgram(self._current_program) - except PyViCareCommandError: - _LOGGER.debug("Unable to deactivate program %s", self._current_program) - if vicare_program != VICARE_PROGRAM_NORMAL: - # And we can't explicitly activate normal, either - self._circuit.activateProgram(vicare_program) + except PyViCareCommandError as err: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="program_not_deactivated", + translation_placeholders={ + "program": self._current_program, + }, + ) from err + + _LOGGER.debug("Setting preset to %s / %s", preset_mode, target_program) + if target_program != VICARE_PROGRAM_NORMAL: + # And we can't explicitly activate "normal", either + _LOGGER.debug("activating %s", target_program) + try: + self._circuit.activateProgram(target_program) + except PyViCareCommandError as err: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="program_not_activated", + translation_placeholders={ + "program": target_program, + }, + ) from err @property def extra_state_attributes(self): diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index f3a51bde9e4..e9ee272edd8 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -288,6 +288,17 @@ } } }, + "exceptions": { + "program_unknown": { + "message": "Cannot translate preset {preset} into a valid ViCare program" + }, + "program_not_activated": { + "message": "Unable to activate ViCare program {program}" + }, + "program_not_deactivated": { + "message": "Unable to deactivate ViCare program {program}" + } + }, "services": { "set_vicare_mode": { "name": "Set ViCare mode", diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index 1bda3b1595d..8d9cb444fc9 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -28,9 +28,9 @@ NOT_AVAILABLE: list = ["", "N/A", "0.0.0.0"] class VodafoneStationBaseEntityDescription: """Vodafone Station entity base description.""" - value: Callable[ - [Any, Any], Any - ] = lambda coordinator, key: coordinator.data.sensors[key] + value: Callable[[Any, Any], Any] = ( + lambda coordinator, key: coordinator.data.sensors[key] + ) is_suitable: Callable[[dict], bool] = lambda val: True diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 14e1211639e..120f2d9559b 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -111,11 +111,13 @@ class HassVoipDatagramProtocol(VoipDatagramProtocol): valid_protocol_factory=lambda call_info, rtcp_state: make_protocol( hass, devices, call_info, rtcp_state ), - invalid_protocol_factory=lambda call_info, rtcp_state: PreRecordMessageProtocol( - hass, - "not_configured.pcm", - opus_payload_type=call_info.opus_payload_type, - rtcp_state=rtcp_state, + invalid_protocol_factory=( + lambda call_info, rtcp_state: PreRecordMessageProtocol( + hass, + "not_configured.pcm", + opus_payload_type=call_info.opus_payload_type, + rtcp_state=rtcp_state, + ) ), ) self.hass = hass diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index 94153a47fdc..b64f5aba6b7 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -95,9 +95,7 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): self.upnp_description = discovery_info.ssdp_location # ssdp_location and hostname have been checked in check_yamaha_ssdp so it is safe to ignore type assignment - self.host = urlparse( - discovery_info.ssdp_location - ).hostname # type: ignore[assignment] + self.host = urlparse(discovery_info.ssdp_location).hostname # type: ignore[assignment] await self.async_set_unique_id(self.serial_number) self._abort_if_unique_id_configured( diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index ef5cdd1b1d2..acd6780d39f 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -276,9 +276,7 @@ async def async_setup_entry( if state_key == "0": continue - notification_description: NotificationZWaveJSEntityDescription | None = ( - None - ) + notification_description: NotificationZWaveJSEntityDescription | None = None for description in NOTIFICATION_SENSOR_MAPPINGS: if ( diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 37cfdc68569..cf743a3e85a 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -344,7 +344,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): is not None and (extra_data := await self.async_get_last_extra_data()) and ( - latest_version_firmware := ZWaveNodeFirmwareUpdateExtraStoredData.from_dict( + latest_version_firmware + := ZWaveNodeFirmwareUpdateExtraStoredData.from_dict( extra_data.as_dict() ).latest_version_firmware ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 648e0e5bd09..1de7a6c6a43 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -251,7 +251,9 @@ def async_track_state_change( return async_track_state_change_event(hass, entity_ids, state_change_listener) return hass.bus.async_listen( - EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter # type: ignore[arg-type] + EVENT_STATE_CHANGED, + state_change_dispatcher, # type: ignore[arg-type] + event_filter=state_change_filter, # type: ignore[arg-type] ) @@ -761,7 +763,8 @@ class _TrackStateChangeFiltered: @callback def _setup_all_listener(self) -> None: self._listeners[_ALL_LISTENER] = self.hass.bus.async_listen( - EVENT_STATE_CHANGED, self._action # type: ignore[arg-type] + EVENT_STATE_CHANGED, + self._action, # type: ignore[arg-type] ) @@ -1335,7 +1338,8 @@ def async_track_same_state( if entity_ids == MATCH_ALL: async_remove_state_for_cancel = hass.bus.async_listen( - EVENT_STATE_CHANGED, state_for_cancel_listener # type: ignore[arg-type] + EVENT_STATE_CHANGED, + state_for_cancel_listener, # type: ignore[arg-type] ) else: async_remove_state_for_cancel = async_track_state_change_event( diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 4dd71a584ec..625bab8b218 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -190,7 +190,8 @@ class RestoreStateData: state, self.entities[state.entity_id].extra_restore_state_data, now ) for state in all_states - if state.entity_id in self.entities and + if state.entity_id in self.entities + and # Ignore all states that are entity registry placeholders not state.attributes.get(ATTR_RESTORED) ] diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index b74c22c9ead..606b90e6005 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -99,8 +99,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): # Pick a random microsecond in range 0.05..0.50 to stagger the refreshes # and avoid a thundering herd. self._microsecond = ( - randint(event.RANDOM_MICROSECOND_MIN, event.RANDOM_MICROSECOND_MAX) - / 10**6 + randint(event.RANDOM_MICROSECOND_MIN, event.RANDOM_MICROSECOND_MAX) / 10**6 ) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ce868ab85f3..6fb538a5aef 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -403,9 +403,7 @@ async def async_get_zeroconf( hass: HomeAssistant, ) -> dict[str, list[dict[str, str | dict[str, str]]]]: """Return cached list of zeroconf types.""" - zeroconf: dict[ - str, list[dict[str, str | dict[str, str]]] - ] = ZEROCONF.copy() # type: ignore[assignment] + zeroconf: dict[str, list[dict[str, str | dict[str, str]]]] = ZEROCONF.copy() # type: ignore[assignment] integrations = await async_get_custom_components(hass) for integration in integrations.values(): @@ -1013,9 +1011,7 @@ def _load_file( Async friendly. """ with suppress(KeyError): - return hass.data[DATA_COMPONENTS][ # type: ignore[no-any-return] - comp_or_platform - ] + return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore[no-any-return] cache = hass.data[DATA_COMPONENTS] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d338d8a8d9a..bae4d616903 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ # Automatically generated by gen_requirements_all.py, do not edit aiodiscover==1.5.1 -aiohttp-fast-url-dispatcher==0.1.0 +aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.1.1 aiohttp==3.8.5;python_version<'3.12' aiohttp==3.9.0;python_version>='3.12' diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 7f81c281340..ac18d43727c 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -57,7 +57,8 @@ def json_loads_object(__obj: bytes | bytearray | memoryview | str) -> JsonObject def load_json( - filename: str | PathLike, default: JsonValueType = _SENTINEL # type: ignore[assignment] + filename: str | PathLike, + default: JsonValueType = _SENTINEL, # type: ignore[assignment] ) -> JsonValueType: """Load JSON data from a file. @@ -79,7 +80,8 @@ def load_json( def load_json_array( - filename: str | PathLike, default: JsonArrayType = _SENTINEL # type: ignore[assignment] + filename: str | PathLike, + default: JsonArrayType = _SENTINEL, # type: ignore[assignment] ) -> JsonArrayType: """Load JSON data from a file and return as list. @@ -98,7 +100,8 @@ def load_json_array( def load_json_object( - filename: str | PathLike, default: JsonObjectType = _SENTINEL # type: ignore[assignment] + filename: str | PathLike, + default: JsonObjectType = _SENTINEL, # type: ignore[assignment] ) -> JsonObjectType: """Load JSON data from a file and return as dict. diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 44fcaa07067..b2ef7330660 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -129,6 +129,7 @@ def vincenty( uSq = cosSqAlpha * (AXIS_A**2 - AXIS_B**2) / (AXIS_B**2) A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) + # fmt: off deltaSigma = ( B * sinSigma @@ -141,11 +142,12 @@ def vincenty( - B / 6 * cos2SigmaM - * (-3 + 4 * sinSigma**2) - * (-3 + 4 * cos2SigmaM**2) + * (-3 + 4 * sinSigma ** 2) + * (-3 + 4 * cos2SigmaM ** 2) ) ) ) + # fmt: on s = AXIS_B * A * (sigma - deltaSigma) s /= 1000 # Conversion of meters to kilometers diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index e1cfc81019c..fbffae448b2 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -340,7 +340,12 @@ def _handle_mapping_tag( raise yaml.MarkedYAMLError( context=f'invalid key: "{key}"', context_mark=yaml.Mark( - fname, 0, line, -1, None, None # type: ignore[arg-type] + fname, + 0, + line, + -1, + None, + None, # type: ignore[arg-type] ), ) from exc diff --git a/pyproject.toml b/pyproject.toml index 13bcc9987ff..5661b7ca130 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "aiohttp==3.9.0;python_version>='3.12'", "aiohttp==3.8.5;python_version<'3.12'", "aiohttp_cors==0.7.0", - "aiohttp-fast-url-dispatcher==0.1.0", + "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.1.1", "astral==2.2", "attrs==23.1.0", @@ -79,9 +79,6 @@ include-package-data = true [tool.setuptools.packages.find] include = ["homeassistant*"] -[tool.black] -extend-exclude = "/generated/" - [tool.pylint.MAIN] py-version = "3.11" ignore = [ @@ -128,7 +125,7 @@ class-const-naming-style = "any" [tool.pylint."MESSAGES CONTROL"] # Reasons disabled: -# format - handled by black +# format - handled by ruff # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load diff --git a/requirements.txt b/requirements.txt index 6c10af6f2ad..1d1837d9bce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ aiohttp==3.9.0;python_version>='3.12' aiohttp==3.8.5;python_version<'3.12' aiohttp_cors==0.7.0 -aiohttp-fast-url-dispatcher==0.1.0 +aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.1.1 astral==2.2 attrs==23.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 45cd6999f2d..0dc5ca8b2bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioharmony==0.2.10 aiohomekit==3.0.9 # homeassistant.components.http -aiohttp-fast-url-dispatcher==0.1.0 +aiohttp-fast-url-dispatcher==0.3.0 # homeassistant.components.http aiohttp-zlib-ng==0.1.1 @@ -890,7 +890,7 @@ georss-qld-bushfire-alert-client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==3.2.1 +gios==3.2.2 # homeassistant.components.gitter gitterpy==0.1.7 @@ -2354,7 +2354,7 @@ rfk101py==0.0.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.2 +ring-doorbell[listen]==0.8.3 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -2436,7 +2436,7 @@ sensorpush-ble==1.5.5 sentry-sdk==1.37.1 # homeassistant.components.sfr_box -sfrbox-api==0.0.6 +sfrbox-api==0.0.8 # homeassistant.components.sharkiq sharkiq==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a70c14781a8..5a7f61d47ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioharmony==0.2.10 aiohomekit==3.0.9 # homeassistant.components.http -aiohttp-fast-url-dispatcher==0.1.0 +aiohttp-fast-url-dispatcher==0.3.0 # homeassistant.components.http aiohttp-zlib-ng==0.1.1 @@ -710,7 +710,7 @@ georss-qld-bushfire-alert-client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==3.2.1 +gios==3.2.2 # homeassistant.components.glances glances-api==0.4.3 @@ -1760,7 +1760,7 @@ reolink-aio==0.8.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.2 +ring-doorbell[listen]==0.8.3 # homeassistant.components.roku rokuecp==0.18.1 @@ -1815,7 +1815,7 @@ sensorpush-ble==1.5.5 sentry-sdk==1.37.1 # homeassistant.components.sfr_box -sfrbox-api==0.0.6 +sfrbox-api==0.0.8 # homeassistant.components.sharkiq sharkiq==1.0.2 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 03c46de6b37..c797db4b7a3 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,6 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit -black==23.11.0 codespell==2.2.2 -ruff==0.1.1 +ruff==0.1.6 yamllint==1.32.0 diff --git a/script/check_format b/script/check_format index bed35ec63e4..09dbb0abe86 100755 --- a/script/check_format +++ b/script/check_format @@ -1,10 +1,10 @@ #!/bin/sh -# Format code with black. +# Format code with ruff-format. cd "$(dirname "$0")/.." -black \ +ruff \ + format \ --check \ - --fast \ --quiet \ homeassistant tests script *.py diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 404d257c414..f62d6e936a7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -192,6 +192,7 @@ IGNORE_PRE_COMMIT_HOOK_ID = ( "no-commit-to-branch", "prettier", "python-typing-update", + "ruff-format", # it's just ruff ) PACKAGE_REGEX = re.compile(r"^(?:--.+\s)?([-_\.\w\d]+).*==.+$") @@ -394,7 +395,8 @@ def requirements_test_all_output(reqs: dict[str, list[str]]) -> str: for requirement, modules in reqs.items() if any( # Always install requirements that are not part of integrations - not mdl.startswith("homeassistant.components.") or + not mdl.startswith("homeassistant.components.") + or # Install tests for integrations that have tests has_tests(mdl) for mdl in modules diff --git a/script/hassfest/serializer.py b/script/hassfest/serializer.py index 499ee9d51d9..b56306a8d7e 100644 --- a/script/hassfest/serializer.py +++ b/script/hassfest/serializer.py @@ -2,11 +2,10 @@ from __future__ import annotations from collections.abc import Collection, Iterable, Mapping +import shutil +import subprocess from typing import Any -import black -from black.mode import Mode - DEFAULT_GENERATOR = "script.hassfest" @@ -72,7 +71,14 @@ To update, run python3 -m {generator} {content} """ - return black.format_str(content.strip(), mode=Mode()) + ruff = shutil.which("ruff") + if not ruff: + raise RuntimeError("ruff not found") + return subprocess.check_output( + [ruff, "format", "-"], + input=content.strip(), + encoding="utf-8", + ) def format_python_namespace( diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 950eeb827ba..fa2956dd47d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -215,6 +215,29 @@ def gen_data_entry_schema( return vol.All(*validators) +def gen_issues_schema(config: Config, integration: Integration) -> dict[str, Any]: + """Generate the issues schema.""" + return { + str: vol.All( + cv.has_at_least_one_key("description", "fix_flow"), + vol.Schema( + { + vol.Required("title"): translation_value_validator, + vol.Exclusive( + "description", "fixable" + ): translation_value_validator, + vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( + config=config, + integration=integration, + flow_title=UNDEFINED, + require_step_title=False, + ), + }, + ), + ) + } + + def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate a strings schema.""" return vol.Schema( @@ -266,25 +289,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Optional("application_credentials"): { vol.Optional("description"): translation_value_validator, }, - vol.Optional("issues"): { - str: vol.All( - cv.has_at_least_one_key("description", "fix_flow"), - vol.Schema( - { - vol.Required("title"): translation_value_validator, - vol.Exclusive( - "description", "fixable" - ): translation_value_validator, - vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( - config=config, - integration=integration, - flow_title=UNDEFINED, - require_step_title=False, - ), - }, - ), - ) - }, + vol.Optional("issues"): gen_issues_schema(config, integration), vol.Optional("entity_component"): cv.schema_with_slug_keys( { vol.Optional("name"): str, @@ -362,7 +367,8 @@ def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: flow_title=REQUIRED, require_step_title=True, ) - } + }, + vol.Optional("issues"): gen_issues_schema(config, integration), } ) diff --git a/script/lint_and_test.py b/script/lint_and_test.py index ee28d4765d6..48809ae4dcd 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -224,6 +224,7 @@ async def main(): code, _ = await async_exec( "python3", + "-b", "-m", "pytest", "-vv", diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 8dafd8fa802..ddbd1189e11 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -103,10 +103,11 @@ def main(): if args.develop: print("Running tests") - print(f"$ python3 -m pytest -vvv tests/components/{info.domain}") + print(f"$ python3 -b -m pytest -vvv tests/components/{info.domain}") subprocess.run( [ "python3", + "-b", "-m", "pytest", "-vvv", diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py index 7c2335f7ccc..3d89c577ebf 100644 --- a/tests/auth/providers/test_legacy_api_password.py +++ b/tests/auth/providers/test_legacy_api_password.py @@ -5,6 +5,12 @@ from homeassistant import auth, data_entry_flow from homeassistant.auth import auth_store from homeassistant.auth.providers import legacy_api_password from homeassistant.core import HomeAssistant +import homeassistant.helpers.issue_registry as ir +from homeassistant.setup import async_setup_component + +from tests.common import ensure_auth_manager_loaded + +CONFIG = {"type": "legacy_api_password", "api_password": "test-password"} @pytest.fixture @@ -16,9 +22,7 @@ def store(hass): @pytest.fixture def provider(hass, store): """Mock provider.""" - return legacy_api_password.LegacyApiPasswordAuthProvider( - hass, store, {"type": "legacy_api_password", "api_password": "test-password"} - ) + return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, CONFIG) @pytest.fixture @@ -68,3 +72,15 @@ async def test_login_flow_works(hass: HomeAssistant, manager) -> None: flow_id=result["flow_id"], user_input={"password": "test-password"} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + +async def test_create_repair_issue(hass: HomeAssistant): + """Test legacy api password auth provider creates a reapir issue.""" + hass.auth = await auth.auth_manager_from_config(hass, [CONFIG], []) + ensure_auth_manager_loaded(hass.auth) + await async_setup_component(hass, "auth", {}) + issue_registry: ir.IssueRegistry = ir.async_get(hass) + + assert issue_registry.async_get_issue( + domain="auth", issue_id="deprecated_legacy_api_password" + ) diff --git a/tests/common.py b/tests/common.py index 30ea779295c..a4979c85853 100644 --- a/tests/common.py +++ b/tests/common.py @@ -267,7 +267,7 @@ async def async_test_home_assistant(event_loop, load_registries=True): "homeassistant.helpers.restore_state.RestoreStateData.async_setup_dump", return_value=None, ), patch( - "homeassistant.helpers.restore_state.start.async_at_start" + "homeassistant.helpers.restore_state.start.async_at_start", ): await asyncio.gather( ar.async_load(hass), diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index 4376db23366..9ebe13c83e6 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -78,9 +78,7 @@ async def setup_airvisual_pro_fixture(hass, config, pro): "homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro ), patch( "homeassistant.components.airvisual_pro.NodeSamba", return_value=pro - ), patch( - "homeassistant.components.airvisual.PLATFORMS", [] - ): + ), patch("homeassistant.components.airvisual.PLATFORMS", []): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 4e51880c754..d22738a7e6b 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -180,9 +180,11 @@ async def test_send_base_with_supervisor( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), ), patch( - "uuid.UUID.hex", new_callable=PropertyMock + "uuid.UUID.hex", + new_callable=PropertyMock, ) as hex, patch( - "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + "homeassistant.components.analytics.analytics.HA_VERSION", + MOCK_VERSION, ): hex.return_value = MOCK_UUID await analytics.load() @@ -289,7 +291,8 @@ async def test_send_usage_with_supervisor( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), ), patch( - "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + "homeassistant.components.analytics.analytics.HA_VERSION", + MOCK_VERSION, ): await analytics.send_analytics() assert ( @@ -492,7 +495,8 @@ async def test_send_statistics_with_supervisor( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), ), patch( - "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + "homeassistant.components.analytics.analytics.HA_VERSION", + MOCK_VERSION, ): await analytics.send_analytics() assert "'addon_count': 1" in caplog.text diff --git a/tests/components/anova/__init__.py b/tests/components/anova/__init__.py index 5bcb84cb974..aa58ee5bbb5 100644 --- a/tests/components/anova/__init__.py +++ b/tests/components/anova/__init__.py @@ -51,7 +51,7 @@ async def async_init_integration( ) as update_patch, patch( "homeassistant.components.anova.AnovaApi.authenticate" ), patch( - "homeassistant.components.anova.AnovaApi.get_devices" + "homeassistant.components.anova.AnovaApi.get_devices", ) as device_patch: update_patch.return_value = ONLINE_UPDATE device_patch.return_value = [ diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 9d3b9889cd3..e23f86e545b 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -92,7 +92,8 @@ async def test_load_backups(hass: HomeAssistant) -> None: "date": TEST_BACKUP.date, }, ), patch( - "pathlib.Path.stat", return_value=MagicMock(st_size=TEST_BACKUP.size) + "pathlib.Path.stat", + return_value=MagicMock(st_size=TEST_BACKUP.size), ): await manager.load_backups() backups = await manager.get_backups() diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index ab04499c827..ada38451754 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -120,7 +120,8 @@ async def test_form_2fa_connect_error(hass: HomeAssistant) -> None: "homeassistant.components.blink.config_flow.Blink.setup_urls", side_effect=BlinkSetupError, ), patch( - "homeassistant.components.blink.async_setup_entry", return_value=True + "homeassistant.components.blink.async_setup_entry", + return_value=True, ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"pin": "1234"} @@ -161,7 +162,8 @@ async def test_form_2fa_invalid_key(hass: HomeAssistant) -> None: "homeassistant.components.blink.config_flow.Blink.setup_urls", return_value=True, ), patch( - "homeassistant.components.blink.async_setup_entry", return_value=True + "homeassistant.components.blink.async_setup_entry", + return_value=True, ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"pin": "1234"} @@ -200,7 +202,8 @@ async def test_form_2fa_unknown_error(hass: HomeAssistant) -> None: "homeassistant.components.blink.config_flow.Blink.setup_urls", side_effect=KeyError, ), patch( - "homeassistant.components.blink.async_setup_entry", return_value=True + "homeassistant.components.blink.async_setup_entry", + return_value=True, ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"pin": "1234"} diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 59c5cc822df..5f166a3fca2 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -47,12 +47,14 @@ def mock_operating_system_90(): def macos_adapter(): """Fixture that mocks the macos adapter.""" with patch("bleak.get_platform_scanner_backend_type"), patch( - "homeassistant.components.bluetooth.platform.system", return_value="Darwin" + "homeassistant.components.bluetooth.platform.system", + return_value="Darwin", ), patch( "homeassistant.components.bluetooth.scanner.platform.system", return_value="Darwin", ), patch( - "bluetooth_adapters.systems.platform.system", return_value="Darwin" + "bluetooth_adapters.systems.platform.system", + return_value="Darwin", ): yield @@ -71,14 +73,16 @@ def windows_adapter(): def no_adapter_fixture(): """Fixture that mocks no adapters on Linux.""" with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" + "homeassistant.components.bluetooth.platform.system", + return_value="Linux", ), patch( "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", ), patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" + "bluetooth_adapters.systems.platform.system", + return_value="Linux", ), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", ), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", {}, @@ -90,14 +94,16 @@ def no_adapter_fixture(): def one_adapter_fixture(): """Fixture that mocks one adapter on Linux.""" with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" + "homeassistant.components.bluetooth.platform.system", + return_value="Linux", ), patch( "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", ), patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" + "bluetooth_adapters.systems.platform.system", + return_value="Linux", ), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", ), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", { @@ -124,9 +130,7 @@ def two_adapters_fixture(): ), patch( "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", - ), patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch( + ), patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" ), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", @@ -166,9 +170,7 @@ def one_adapter_old_bluez(): ), patch( "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", - ), patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch( + ), patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" ), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 6fbcb928b5a..ff1f986583e 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -67,13 +67,9 @@ async def setup_bond_entity( enabled=patch_token ), patch_bond_version(enabled=patch_version), patch_bond_device_ids( enabled=patch_device_ids - ), patch_setup_entry( - "cover", enabled=patch_platforms - ), patch_setup_entry( + ), patch_setup_entry("cover", enabled=patch_platforms), patch_setup_entry( "fan", enabled=patch_platforms - ), patch_setup_entry( - "light", enabled=patch_platforms - ), patch_setup_entry( + ), patch_setup_entry("light", enabled=patch_platforms), patch_setup_entry( "switch", enabled=patch_platforms ): return await hass.config_entries.async_setup(config_entry.entry_id) @@ -102,15 +98,11 @@ async def setup_platform( "homeassistant.components.bond.PLATFORMS", [platform] ), patch_bond_version(return_value=bond_version), patch_bond_bridge( return_value=bridge - ), patch_bond_token( - return_value=token - ), patch_bond_device_ids( + ), patch_bond_token(return_value=token), patch_bond_device_ids( return_value=[bond_device_id] ), patch_start_bpup(), patch_bond_device( return_value=discovered_device - ), patch_bond_device_properties( - return_value=props - ), patch_bond_device_state( + ), patch_bond_device_properties(return_value=props), patch_bond_device_state( return_value=state ): assert await async_setup_component(hass, BOND_DOMAIN, {}) diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 92c11028173..6b462a02c26 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -184,9 +184,7 @@ async def test_old_identifiers_are_removed( "name": "test1", "type": DeviceType.GENERIC_DEVICE, } - ), patch_bond_device_properties( - return_value={} - ), patch_bond_device_state( + ), patch_bond_device_properties(return_value={}), patch_bond_device_state( return_value={} ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True @@ -228,9 +226,7 @@ async def test_smart_by_bond_device_suggested_area( "type": DeviceType.GENERIC_DEVICE, "location": "Den", } - ), patch_bond_device_properties( - return_value={} - ), patch_bond_device_state( + ), patch_bond_device_properties(return_value={}), patch_bond_device_state( return_value={} ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True @@ -275,9 +271,7 @@ async def test_bridge_device_suggested_area( "type": DeviceType.GENERIC_DEVICE, "location": "Bathroom", } - ), patch_bond_device_properties( - return_value={} - ), patch_bond_device_state( + ), patch_bond_device_properties(return_value={}), patch_bond_device_state( return_value={} ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 44d87745b3f..b7939e4cb50 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -38,25 +38,15 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup -@pytest.fixture -def mock_bsblan_config_flow() -> Generator[None, MagicMock, None]: - """Return a mocked BSBLAN client.""" - with patch( - "homeassistant.components.bsblan.config_flow.BSBLAN", autospec=True - ) as bsblan_mock: - bsblan = bsblan_mock.return_value - bsblan.device.return_value = Device.parse_raw( - load_fixture("device.json", DOMAIN) - ) - bsblan.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN)) - yield bsblan - - @pytest.fixture def mock_bsblan(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: """Return a mocked BSBLAN client.""" - with patch("homeassistant.components.bsblan.BSBLAN", autospec=True) as bsblan_mock: + with patch( + "homeassistant.components.bsblan.BSBLAN", autospec=True + ) as bsblan_mock, patch( + "homeassistant.components.bsblan.config_flow.BSBLAN", new=bsblan_mock + ): bsblan = bsblan_mock.return_value bsblan.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN)) bsblan.device.return_value = Device.parse_raw( diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index dce881f2f7d..d82c32463d8 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry async def test_full_user_flow_implementation( hass: HomeAssistant, - mock_bsblan_config_flow: MagicMock, + mock_bsblan: MagicMock, mock_setup_entry: AsyncMock, ) -> None: """Test the full manual user flow from start to finish.""" @@ -52,7 +52,7 @@ async def test_full_user_flow_implementation( assert result2["result"].unique_id == format_mac("00:80:41:19:69:90") assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_bsblan_config_flow.device.mock_calls) == 1 + assert len(mock_bsblan.device.mock_calls) == 1 async def test_show_user_form(hass: HomeAssistant) -> None: @@ -68,10 +68,10 @@ async def test_show_user_form(hass: HomeAssistant) -> None: async def test_connection_error( hass: HomeAssistant, - mock_bsblan_config_flow: MagicMock, + mock_bsblan: MagicMock, ) -> None: """Test we show user form on BSBLan connection error.""" - mock_bsblan_config_flow.device.side_effect = BSBLANConnectionError + mock_bsblan.device.side_effect = BSBLANConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -92,7 +92,7 @@ async def test_connection_error( async def test_user_device_exists_abort( hass: HomeAssistant, - mock_bsblan_config_flow: MagicMock, + mock_bsblan: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test we abort flow if BSBLAN device already configured.""" diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 2d688489d39..9b5c2d56d4c 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -19,7 +19,7 @@ async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: ) as mock_setup, patch( "pychromecast.discovery.discover_chromecasts", return_value=(True, None) ), patch( - "pychromecast.discovery.stop_discovery" + "pychromecast.discovery.stop_discovery", ): result = await hass.config_entries.flow.async_init( cast.DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py index f2d59f46114..dd15eca05cd 100644 --- a/tests/components/comelit/test_config_flow.py +++ b/tests/components/comelit/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.comelit.async_setup_entry" ) as mock_setup_entry, patch( - "requests.get" + "requests.get", ) as mock_request_get: mock_request_get.return_value.status_code = 200 @@ -70,7 +70,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> ), patch( "aiocomelit.api.ComeliteSerialBridgeApi.logout", ), patch( - "homeassistant.components.comelit.async_setup_entry" + "homeassistant.components.comelit.async_setup_entry", ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA @@ -135,9 +135,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> "aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect ), patch( "aiocomelit.api.ComeliteSerialBridgeApi.logout", - ), patch( - "homeassistant.components.comelit.async_setup_entry" - ): + ), patch("homeassistant.components.comelit.async_setup_entry"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index ad4c7e90851..1a099c05b16 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -23,7 +23,9 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture async def setup_automation( - hass, automation_config, stub_blueprint_populate # noqa: F811 + hass, + automation_config, + stub_blueprint_populate, # noqa: F811 ): """Set up automation integration.""" assert await async_setup_component( diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 93a6305655b..a0fb908d920 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -65,7 +65,8 @@ def denonavr_connect_fixture(): "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", TEST_RECEIVER_TYPE, ), patch( - "homeassistant.components.denonavr.async_setup_entry", return_value=True + "homeassistant.components.denonavr.async_setup_entry", + return_value=True, ): yield diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 47933c30537..5013568ad39 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -151,8 +151,11 @@ async def _async_get_handle_dhcp_packet(hass, integration_matchers): with patch( "homeassistant.components.dhcp._verify_l2socket_setup", ), patch( - "scapy.arch.common.compile_filter" - ), patch("scapy.sendrecv.AsyncSniffer", _mock_sniffer): + "scapy.arch.common.compile_filter", + ), patch( + "scapy.sendrecv.AsyncSniffer", + _mock_sniffer, + ): await dhcp_watcher.async_start() return async_handle_dhcp_packet diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 7d79a10e912..a0f34e3cd21 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -198,9 +198,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_t return_value=MOCK_ECOBEE_CONF, ), patch( "homeassistant.components.ecobee.config_flow.Ecobee" - ) as mock_ecobee, patch.object( - flow, "async_step_user" - ) as mock_async_step_user: + ) as mock_ecobee, patch.object(flow, "async_step_user") as mock_async_step_user: mock_ecobee = mock_ecobee.return_value mock_ecobee.refresh_tokens.return_value = False diff --git a/tests/components/electrasmart/test_config_flow.py b/tests/components/electrasmart/test_config_flow.py index f53bea3e96c..929259a0ccf 100644 --- a/tests/components/electrasmart/test_config_flow.py +++ b/tests/components/electrasmart/test_config_flow.py @@ -55,7 +55,8 @@ async def test_one_time_password(hass: HomeAssistant): "electrasmart.api.ElectraAPI.validate_one_time_password", return_value=mock_otp_response, ), patch( - "electrasmart.api.ElectraAPI.fetch_devices", return_value=[] + "electrasmart.api.ElectraAPI.fetch_devices", + return_value=[], ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 216fc019778..5e33a8aa4c3 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -229,9 +229,7 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non 0, ), patch( "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0 - ), _patch_discovery(), _patch_elk( - elk=mocked_elk - ): + ), _patch_discovery(), _patch_elk(elk=mocked_elk): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 41cbb239129..c1fb03545cb 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -89,7 +89,8 @@ async def setup_enphase_envoy_fixture(hass, config, mock_envoy): "homeassistant.components.enphase_envoy.Envoy", return_value=mock_envoy, ), patch( - "homeassistant.components.enphase_envoy.PLATFORMS", [] + "homeassistant.components.enphase_envoy.PLATFORMS", + [], ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/epson/test_media_player.py b/tests/components/epson/test_media_player.py index d44036c680c..8d6af04c174 100644 --- a/tests/components/epson/test_media_player.py +++ b/tests/components/epson/test_media_player.py @@ -38,7 +38,7 @@ async def test_set_unique_id( ), patch( "homeassistant.components.epson.Projector.get_serial_number", return_value="123" ), patch( - "homeassistant.components.epson.Projector.get_property" + "homeassistant.components.epson.Projector.get_property", ): freezer.tick(timedelta(seconds=30)) async_fire_time_changed(hass) diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index d7b04f8448c..9ab00421cbc 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -100,7 +100,8 @@ async def test_update_entity( ) as mock_compile, patch( "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True ) as mock_upload, pytest.raises( - HomeAssistantError, match="compiling" + HomeAssistantError, + match="compiling", ): await hass.services.async_call( "update", @@ -120,7 +121,8 @@ async def test_update_entity( ) as mock_compile, patch( "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=False ) as mock_upload, pytest.raises( - HomeAssistantError, match="OTA" + HomeAssistantError, + match="OTA", ): await hass.services.async_call( "update", diff --git a/tests/components/evil_genius_labs/conftest.py b/tests/components/evil_genius_labs/conftest.py index 66dd8979d67..a4f10fe97c4 100644 --- a/tests/components/evil_genius_labs/conftest.py +++ b/tests/components/evil_genius_labs/conftest.py @@ -51,7 +51,8 @@ async def setup_evil_genius_labs( "pyevilgenius.EvilGeniusDevice.get_product", return_value=product_fixture, ), patch( - "homeassistant.components.evil_genius_labs.PLATFORMS", platforms + "homeassistant.components.evil_genius_labs.PLATFORMS", + platforms, ): assert await async_setup_component(hass, "evil_genius_labs", {}) await hass.async_block_till_done() diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index bb34af7c400..ded7cda0dea 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -48,9 +48,9 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N ), patch( "homeassistant.components.fritz.async_setup_entry" ) as mock_setup_entry, patch( - "requests.get" + "requests.get", ) as mock_request_get, patch( - "requests.post" + "requests.post", ) as mock_request_post, patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], @@ -98,9 +98,9 @@ async def test_user_already_configured( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", return_value=MOCK_FIRMWARE_INFO, ), patch( - "requests.get" + "requests.get", ) as mock_request_get, patch( - "requests.post" + "requests.post", ) as mock_request_post, patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], @@ -211,11 +211,11 @@ async def test_reauth_successful( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", return_value=MOCK_FIRMWARE_INFO, ), patch( - "homeassistant.components.fritz.async_setup_entry" + "homeassistant.components.fritz.async_setup_entry", ) as mock_setup_entry, patch( - "requests.get" + "requests.get", ) as mock_request_get, patch( - "requests.post" + "requests.post", ) as mock_request_post: mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST @@ -399,9 +399,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N return_value=MOCK_FIRMWARE_INFO, ), patch( "homeassistant.components.fritz.async_setup_entry" - ) as mock_setup_entry, patch( - "requests.get" - ) as mock_request_get, patch( + ) as mock_setup_entry, patch("requests.get") as mock_request_get, patch( "requests.post" ) as mock_request_post: mock_request_get.return_value.status_code = 200 diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index f94b0f3a55c..684e9a3ae5f 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,6 +1,6 @@ """Tests for the Fronius sensor platform.""" - from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.fronius.coordinator import ( @@ -33,33 +33,34 @@ async def test_symo_inverter( mock_responses(aioclient_mock, night=True) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 21 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 assert_state("sensor.symo_20_dc_current", 0) assert_state("sensor.symo_20_energy_day", 10828) assert_state("sensor.symo_20_total_energy", 44186900) assert_state("sensor.symo_20_energy_year", 25507686) assert_state("sensor.symo_20_dc_voltage", 16) + assert_state("sensor.symo_20_status_message", "startup") # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) freezer.tick(FroniusInverterUpdateCoordinator.default_interval) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60 # 4 additional AC entities assert_state("sensor.symo_20_dc_current", 2.19) assert_state("sensor.symo_20_energy_day", 1113) @@ -70,6 +71,7 @@ async def test_symo_inverter( assert_state("sensor.symo_20_frequency", 49.94) assert_state("sensor.symo_20_ac_power", 1190) assert_state("sensor.symo_20_ac_voltage", 227.90) + assert_state("sensor.symo_20_status_message", "running") # Third test at nighttime - additional AC entities default to 0 mock_responses(aioclient_mock, night=True) @@ -94,7 +96,7 @@ async def test_symo_logger( mock_responses(aioclient_mock) await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 # states are rounded to 4 decimals assert_state("sensor.solarnet_grid_export_tariff", 0.078) assert_state("sensor.solarnet_co2_factor", 0.53) @@ -116,14 +118,14 @@ async def test_symo_meter( mock_responses(aioclient_mock) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60 # states are rounded to 4 decimals assert_state("sensor.smart_meter_63a_current_phase_1", 7.755) assert_state("sensor.smart_meter_63a_current_phase_2", 6.68) @@ -157,6 +159,50 @@ async def test_symo_meter( assert_state("sensor.smart_meter_63a_voltage_phase_1_2", 395.9) assert_state("sensor.smart_meter_63a_voltage_phase_2_3", 398) assert_state("sensor.smart_meter_63a_voltage_phase_3_1", 398) + assert_state("sensor.smart_meter_63a_meter_location", 0) + assert_state("sensor.smart_meter_63a_meter_location_description", "feed_in") + + +@pytest.mark.parametrize( + ("location_code", "expected_code", "expected_description"), + [ + (-1, -1, "unknown"), + (3, 3, "external_generator"), + (4, 4, "external_battery"), + (7, 7, "unknown"), + (256, 256, "subload"), + (511, 511, "subload"), + (512, 512, "unknown"), + ], +) +async def test_symo_meter_forged( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + location_code: int | None, + expected_code: int | str, + expected_description: str, +) -> None: + """Tests for meter location codes we have no fixture for.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses( + aioclient_mock, + fixture_set="symo", + override_data={ + "symo/GetMeterRealtimeData.json": [ + (["Body", "Data", "0", "Meter_Location_Current"], location_code), + ], + }, + ) + await setup_fronius_integration(hass) + assert_state("sensor.smart_meter_63a_meter_location", expected_code) + assert_state( + "sensor.smart_meter_63a_meter_location_description", expected_description + ) async def test_symo_power_flow( @@ -175,14 +221,14 @@ async def test_symo_power_flow( mock_responses(aioclient_mock, night=True) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 21 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 # states are rounded to 4 decimals assert_state("sensor.solarnet_energy_day", 10828) assert_state("sensor.solarnet_total_energy", 44186900) @@ -197,7 +243,7 @@ async def test_symo_power_flow( async_fire_time_changed(hass) await hass.async_block_till_done() # 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56 assert_state("sensor.solarnet_energy_day", 1101.7001) assert_state("sensor.solarnet_total_energy", 44188000) assert_state("sensor.solarnet_energy_year", 25508788) @@ -212,7 +258,7 @@ async def test_symo_power_flow( freezer.tick(FroniusPowerFlowUpdateCoordinator.default_interval) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56 assert_state("sensor.solarnet_energy_day", 10828) assert_state("sensor.solarnet_total_energy", 44186900) assert_state("sensor.solarnet_energy_year", 25507686) @@ -238,18 +284,19 @@ async def test_gen24( mock_responses(aioclient_mock, fixture_set="gen24") config_entry = await setup_fronius_integration(hass, is_logger=False) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 22 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 23 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 # inverter 1 assert_state("sensor.inverter_name_ac_current", 0.1589) assert_state("sensor.inverter_name_dc_current_2", 0.0754) assert_state("sensor.inverter_name_status_code", 7) + assert_state("sensor.inverter_name_status_message", "running") assert_state("sensor.inverter_name_dc_current", 0.0783) assert_state("sensor.inverter_name_dc_voltage_2", 403.4312) assert_state("sensor.inverter_name_ac_power", 37.3204) @@ -264,7 +311,8 @@ async def test_gen24( assert_state("sensor.smart_meter_ts_65a_3_real_energy_consumed", 2013105.0) assert_state("sensor.smart_meter_ts_65a_3_real_power", 653.1) assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) - assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0) + assert_state("sensor.smart_meter_ts_65a_3_meter_location_description", "feed_in") assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.828) assert_state("sensor.smart_meter_ts_65a_3_reactive_energy_consumed", 88221.0) assert_state("sensor.smart_meter_ts_65a_3_real_energy_minus", 3863340.0) @@ -336,14 +384,14 @@ async def test_gen24_storage( hass, is_logger=False, unique_id="12345678" ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 34 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 35 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 66 # inverter 1 assert_state("sensor.gen24_storage_dc_current", 0.3952) assert_state("sensor.gen24_storage_dc_voltage_2", 318.8103) @@ -352,6 +400,7 @@ async def test_gen24_storage( assert_state("sensor.gen24_storage_ac_power", 250.9093) assert_state("sensor.gen24_storage_error_code", 0) assert_state("sensor.gen24_storage_status_code", 7) + assert_state("sensor.gen24_storage_status_message", "running") assert_state("sensor.gen24_storage_total_energy", 7512794.0117) assert_state("sensor.gen24_storage_inverter_state", "Running") assert_state("sensor.gen24_storage_dc_voltage", 419.1009) @@ -363,7 +412,8 @@ async def test_gen24_storage( assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.698) assert_state("sensor.smart_meter_ts_65a_3_real_energy_consumed", 1247204.0) assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) - assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0) + assert_state("sensor.smart_meter_ts_65a_3_meter_location_description", "feed_in") assert_state("sensor.smart_meter_ts_65a_3_reactive_power", -501.5) assert_state("sensor.smart_meter_ts_65a_3_reactive_energy_produced", 3266105.0) assert_state("sensor.smart_meter_ts_65a_3_real_power_phase_3", 19.6) @@ -396,7 +446,7 @@ async def test_gen24_storage( assert_state("sensor.ohmpilot_power", 0.0) assert_state("sensor.ohmpilot_temperature", 38.9) assert_state("sensor.ohmpilot_state_code", 0.0) - assert_state("sensor.ohmpilot_state_message", "Up and running") + assert_state("sensor.ohmpilot_state_message", "up_and_running") # power_flow assert_state("sensor.solarnet_power_grid", 2274.9) assert_state("sensor.solarnet_power_battery", 0.1591) @@ -463,14 +513,14 @@ async def test_primo_s0( mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2]) config_entry = await setup_fronius_integration(hass, is_logger=True) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 29 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 40 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 43 # logger assert_state("sensor.solarnet_grid_export_tariff", 1) assert_state("sensor.solarnet_co2_factor", 0.53) @@ -483,6 +533,7 @@ async def test_primo_s0( assert_state("sensor.primo_5_0_1_error_code", 0) assert_state("sensor.primo_5_0_1_dc_current", 4.23) assert_state("sensor.primo_5_0_1_status_code", 7) + assert_state("sensor.primo_5_0_1_status_message", "running") assert_state("sensor.primo_5_0_1_energy_year", 7532755.5) assert_state("sensor.primo_5_0_1_ac_current", 3.85) assert_state("sensor.primo_5_0_1_ac_voltage", 223.9) @@ -497,6 +548,7 @@ async def test_primo_s0( assert_state("sensor.primo_3_0_1_error_code", 0) assert_state("sensor.primo_3_0_1_dc_current", 0.97) assert_state("sensor.primo_3_0_1_status_code", 7) + assert_state("sensor.primo_3_0_1_status_message", "running") assert_state("sensor.primo_3_0_1_energy_year", 3596193.25) assert_state("sensor.primo_3_0_1_ac_current", 1.32) assert_state("sensor.primo_3_0_1_ac_voltage", 223.6) @@ -505,6 +557,9 @@ async def test_primo_s0( assert_state("sensor.primo_3_0_1_led_state", 0) # meter assert_state("sensor.s0_meter_at_inverter_1_meter_location", 1) + assert_state( + "sensor.s0_meter_at_inverter_1_meter_location_description", "consumption_path" + ) assert_state("sensor.s0_meter_at_inverter_1_real_power", -2216.7487) # power_flow assert_state("sensor.solarnet_power_load", -2218.9349) diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 946cceac786..4e69420f66e 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -43,7 +43,8 @@ async def init_integration( "homeassistant.components.gios.Gios._get_all_sensors", return_value=sensors, ), patch( - "homeassistant.components.gios.Gios._get_indexes", return_value=indexes + "homeassistant.components.gios.Gios._get_indexes", + return_value=indexes, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 3d52c122791..efe46be9b8d 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -55,7 +55,8 @@ async def test_invalid_sensor_data(hass: HomeAssistant) -> None: "homeassistant.components.gios.Gios._get_station", return_value=json.loads(load_fixture("gios/station.json")), ), patch( - "homeassistant.components.gios.Gios._get_sensor", return_value={} + "homeassistant.components.gios.Gios._get_sensor", + return_value={}, ): flow = config_flow.GiosFlowHandler() flow.hass = hass @@ -83,7 +84,8 @@ async def test_cannot_connect(hass: HomeAssistant) -> None: async def test_create_entry(hass: HomeAssistant) -> None: """Test that the user step works.""" with patch( - "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + "homeassistant.components.gios.Gios._get_stations", + return_value=STATIONS, ), patch( "homeassistant.components.gios.Gios._get_station", return_value=json.loads(load_fixture("gios/station.json")), diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 0d4484c6d0d..d20aecad3df 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -82,9 +82,7 @@ async def test_migrate_device_and_config_entry( ), patch( "homeassistant.components.gios.Gios._get_all_sensors", return_value=sensors, - ), patch( - "homeassistant.components.gios.Gios._get_indexes", return_value=indexes - ): + ), patch("homeassistant.components.gios.Gios._get_indexes", return_value=indexes): config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 62d2722c445..aa7f8472cab 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -92,7 +92,7 @@ async def test_update_access_token(hass: HomeAssistant) -> None: ) as mock_get_token, patch( "homeassistant.components.google_assistant.http._get_homegraph_jwt" ) as mock_get_jwt, patch( - "homeassistant.core.dt_util.utcnow" + "homeassistant.core.dt_util.utcnow", ) as mock_utcnow: mock_utcnow.return_value = base_time mock_get_jwt.return_value = jwt diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index f35d19e3805..cf3f90097ce 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -66,7 +66,12 @@ async def test_broadcast_no_targets( "Anuncia en el salón Es hora de hacer los deberes", ), ("ko-KR", "숙제할 시간이야", "거실", "숙제할 시간이야 라고 거실에 방송해 줘"), - ("ja-JP", "宿題の時間だよ", "リビング", "宿題の時間だよとリビングにブロードキャストして"), + ( + "ja-JP", + "宿題の時間だよ", + "リビング", + "宿題の時間だよとリビングにブロードキャストして", + ), ], ids=["english", "spanish", "korean", "japanese"], ) diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index acf59aeea86..f2cde0a553d 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -131,9 +131,10 @@ async def setup_guardian_fixture( "aioguardian.commands.wifi.WiFiCommands.status", return_value=data_wifi_status, ), patch( - "aioguardian.client.Client.disconnect" + "aioguardian.client.Client.disconnect", ), patch( - "homeassistant.components.guardian.PLATFORMS", [] + "homeassistant.components.guardian.PLATFORMS", + [], ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 22051808ccc..0cce33f6dfd 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -54,9 +54,9 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): "homeassistant.components.hassio.HassIO.get_ingress_panels", return_value={"panels": []}, ), patch( - "homeassistant.components.hassio.issues.SupervisorIssues.setup" + "homeassistant.components.hassio.issues.SupervisorIssues.setup", ), patch( - "homeassistant.components.hassio.HassIO.refresh_updates" + "homeassistant.components.hassio.HassIO.refresh_updates", ): hass.state = CoreState.starting hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index fe151c902cb..8c6d4328065 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -31,7 +31,7 @@ def run_driver(hass, event_loop, iid_storage): ), patch("pyhap.accessory_driver.HAPServer"), patch( "pyhap.accessory_driver.AccessoryDriver.publish" ), patch( - "pyhap.accessory_driver.AccessoryDriver.persist" + "pyhap.accessory_driver.AccessoryDriver.persist", ): yield HomeDriver( hass, @@ -53,9 +53,9 @@ def hk_driver(hass, event_loop, iid_storage): ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( "pyhap.accessory_driver.HAPServer.async_start" ), patch( - "pyhap.accessory_driver.AccessoryDriver.publish" + "pyhap.accessory_driver.AccessoryDriver.publish", ), patch( - "pyhap.accessory_driver.AccessoryDriver.persist" + "pyhap.accessory_driver.AccessoryDriver.persist", ): yield HomeDriver( hass, @@ -77,13 +77,13 @@ def mock_hap(hass, event_loop, iid_storage, mock_zeroconf): ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( "pyhap.accessory_driver.HAPServer.async_start" ), patch( - "pyhap.accessory_driver.AccessoryDriver.publish" + "pyhap.accessory_driver.AccessoryDriver.publish", ), patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + "pyhap.accessory_driver.AccessoryDriver.async_start", ), patch( - "pyhap.accessory_driver.AccessoryDriver.async_stop" + "pyhap.accessory_driver.AccessoryDriver.async_stop", ), patch( - "pyhap.accessory_driver.AccessoryDriver.persist" + "pyhap.accessory_driver.AccessoryDriver.persist", ): yield HomeDriver( hass, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 158efa477d4..1d42325d54c 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1202,9 +1202,7 @@ async def test_homekit_reset_accessories_not_supported( "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" ) as hk_driver_async_update_advertisement, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch.object( - homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 - ): + ), patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0): await async_init_entry(hass, entry) acc_mock = MagicMock() @@ -1247,9 +1245,7 @@ async def test_homekit_reset_accessories_state_missing( "pyhap.accessory_driver.AccessoryDriver.config_changed" ) as hk_driver_config_changed, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch.object( - homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 - ): + ), patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0): await async_init_entry(hass, entry) acc_mock = MagicMock() @@ -1291,9 +1287,7 @@ async def test_homekit_reset_accessories_not_bridged( "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" ) as hk_driver_async_update_advertisement, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch.object( - homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 - ): + ), patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0): await async_init_entry(hass, entry) assert hk_driver_async_update_advertisement.call_count == 0 @@ -1338,7 +1332,7 @@ async def test_homekit_reset_single_accessory( ) as hk_driver_async_update_advertisement, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ), patch( - f"{PATH_HOMEKIT}.accessories.HomeAccessory.run" + f"{PATH_HOMEKIT}.accessories.HomeAccessory.run", ) as mock_run: await async_init_entry(hass, entry) homekit.status = STATUS_RUNNING @@ -2071,9 +2065,9 @@ async def test_reload(hass: HomeAssistant, mock_async_zeroconf: None) -> None: ) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.async_show_setup_message" ), patch( - f"{PATH_HOMEKIT}.get_accessory" + f"{PATH_HOMEKIT}.get_accessory", ), patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + "pyhap.accessory_driver.AccessoryDriver.async_start", ), patch( "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" ): diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 909e94a0d84..b1f063615f3 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -102,7 +102,7 @@ async def test_hmip_add_device( ), patch.object(reloaded_hap, "async_connect"), patch.object( reloaded_hap, "get_hap", return_value=mock_hap.home ), patch( - "homeassistant.components.homematicip_cloud.hap.asyncio.sleep" + "homeassistant.components.homematicip_cloud.hap.asyncio.sleep", ): mock_hap.home.fire_create_event(event_type=EventType.DEVICE_ADDED) await hass.async_block_till_done() diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 4569a6fff6b..0d950968191 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -53,7 +53,8 @@ async def test_auth_auth_check_and_register(hass: HomeAssistant) -> None: ), patch.object( hmip_auth.auth, "requestAuthToken", return_value="ABC" ), patch.object( - hmip_auth.auth, "confirmAuthToken" + hmip_auth.auth, + "confirmAuthToken", ): assert await hmip_auth.async_checkbutton() assert await hmip_auth.async_register() == "ABC" diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 13307e43648..e358920b07b 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the Huawei LTE config flow.""" +from typing import Any from unittest.mock import patch +from urllib.parse import urlparse, urlunparse from huawei_lte_api.enums.client import ResponseCodeEnum from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum @@ -18,6 +20,7 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_URL, CONF_USERNAME, + CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant @@ -25,8 +28,9 @@ from tests.common import MockConfigEntry FIXTURE_UNIQUE_ID = "SERIALNUMBER" -FIXTURE_USER_INPUT = { +FIXTURE_USER_INPUT: dict[str, Any] = { CONF_URL: "http://192.168.1.1/", + CONF_VERIFY_SSL: False, CONF_USERNAME: "admin", CONF_PASSWORD: "secret", } @@ -95,34 +99,59 @@ async def test_already_configured( assert result["reason"] == "already_configured" -async def test_connection_error( - hass: HomeAssistant, requests_mock: requests_mock.Mocker -) -> None: - """Test we show user form on connection error.""" - requests_mock.request(ANY, ANY, exc=ConnectionError()) +@pytest.mark.parametrize( + ("exception", "errors", "data_patch"), + ( + (ConnectionError(), {CONF_URL: "unknown"}, {}), + (requests.exceptions.SSLError(), {CONF_URL: "ssl_error_try_plain"}, {}), + ( + requests.exceptions.SSLError(), + {CONF_URL: "ssl_error_try_unverified"}, + {CONF_VERIFY_SSL: True}, + ), + ), +) +async def test_connection_errors( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + exception: Exception, + errors: dict[str, str], + data_patch: dict[str, Any], +): + """Test we show user form on various errors.""" + requests_mock.request(ANY, ANY, exc=exception) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT | data_patch, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {CONF_URL: "unknown"} + assert result["errors"] == errors @pytest.fixture def login_requests_mock(requests_mock): """Set up a requests_mock with base mocks for login tests.""" - requests_mock.request( - ANY, FIXTURE_USER_INPUT[CONF_URL], text='' - ) - requests_mock.request( - ANY, - f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/state-login", - text=( - f"{LoginStateEnum.LOGGED_OUT}" - f"{PasswordTypeEnum.SHA256}" - ), + https_url = urlunparse( + urlparse(FIXTURE_USER_INPUT[CONF_URL])._replace(scheme="https") ) + for url in FIXTURE_USER_INPUT[CONF_URL], https_url: + requests_mock.request(ANY, url, text='') + requests_mock.request( + ANY, + f"{url}api/user/state-login", + text=( + f"{LoginStateEnum.LOGGED_OUT}" + f"{PasswordTypeEnum.SHA256}" + ), + ) + requests_mock.request( + ANY, + f"{url}api/user/logout", + text="OK", + ) return requests_mock @@ -194,11 +223,19 @@ async def test_login_error( assert result["errors"] == errors -async def test_success(hass: HomeAssistant, login_requests_mock) -> None: +@pytest.mark.parametrize("scheme", ("http", "https")) +async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> None: """Test successful flow provides entry creation data.""" + user_input = { + **FIXTURE_USER_INPUT, + CONF_URL: urlunparse( + urlparse(FIXTURE_USER_INPUT[CONF_URL])._replace(scheme=scheme) + ), + } + login_requests_mock.request( ANY, - f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", + f"{user_input[CONF_URL]}api/user/login", text="OK", ) with patch("homeassistant.components.huawei_lte.async_setup"), patch( @@ -207,14 +244,14 @@ async def test_success(hass: HomeAssistant, login_requests_mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=user_input, ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] - assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] - assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result["data"][CONF_URL] == user_input[CONF_URL] + assert result["data"][CONF_USERNAME] == user_input[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == user_input[CONF_PASSWORD] @pytest.mark.parametrize( @@ -300,8 +337,9 @@ async def test_ssdp( ) for k, v in expected_result.items(): - assert result[k] == v + assert result[k] == v # type: ignore[literal-required] # expected is a subset if result.get("data_schema"): + assert result["data_schema"] is not None assert result["data_schema"]({})[CONF_URL] == url + "/" @@ -355,6 +393,7 @@ async def test_reauth( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" + assert result["data_schema"] is not None assert result["data_schema"]({}) == { CONF_USERNAME: mock_entry_data[CONF_USERNAME], CONF_PASSWORD: mock_entry_data[CONF_PASSWORD], @@ -376,7 +415,7 @@ async def test_reauth( await hass.async_block_till_done() for k, v in expected_result.items(): - assert result[k] == v + assert result[k] == v # type: ignore[literal-required] # expected is a subset for k, v in expected_entry_data.items(): assert entry.data[k] == v diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py index 7b61b42c9d2..646e9e4da86 100644 --- a/tests/components/iaqualink/test_init.py +++ b/tests/components/iaqualink/test_init.py @@ -114,7 +114,8 @@ async def test_setup_devices_exception( "homeassistant.components.iaqualink.AqualinkClient.get_systems", return_value=systems, ), patch.object( - system, "get_devices" + system, + "get_devices", ) as mock_get_devices: mock_get_devices.side_effect = AqualinkServiceException await hass.config_entries.async_setup(config_entry.entry_id) @@ -142,7 +143,8 @@ async def test_setup_all_good_no_recognized_devices( "homeassistant.components.iaqualink.AqualinkClient.get_systems", return_value=systems, ), patch.object( - system, "get_devices" + system, + "get_devices", ) as mock_get_devices: mock_get_devices.return_value = devices await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index 15f529babd8..f772eed2d26 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -76,7 +76,8 @@ async def test_import_frontend_dev_url(hass: HomeAssistant) -> None: ), patch.object(insteon, "close_insteon_connection"), patch.object( insteon, "devices", new=MockDevices() ), patch( - PATCH_CONNECTION, new=mock_successful_connection + PATCH_CONNECTION, + new=mock_successful_connection, ): assert await async_setup_component( hass, diff --git a/tests/components/insteon/test_lock.py b/tests/components/insteon/test_lock.py index f96e33af1c8..c100acae3ce 100644 --- a/tests/components/insteon/test_lock.py +++ b/tests/components/insteon/test_lock.py @@ -47,7 +47,9 @@ def patch_setup_and_devices(): ), patch.object(insteon, "devices", devices), patch.object( insteon_utils, "devices", devices ), patch.object( - insteon_entity, "devices", devices + insteon_entity, + "devices", + devices, ): yield diff --git a/tests/components/iqvia/conftest.py b/tests/components/iqvia/conftest.py index 075d7249d36..b24d473c7df 100644 --- a/tests/components/iqvia/conftest.py +++ b/tests/components/iqvia/conftest.py @@ -94,13 +94,9 @@ async def setup_iqvia_fixture( "pyiqvia.allergens.Allergens.outlook", return_value=data_allergy_outlook ), patch( "pyiqvia.asthma.Asthma.extended", return_value=data_asthma_forecast - ), patch( - "pyiqvia.asthma.Asthma.current", return_value=data_asthma_index - ), patch( + ), patch("pyiqvia.asthma.Asthma.current", return_value=data_asthma_index), patch( "pyiqvia.disease.Disease.extended", return_value=data_disease_forecast - ), patch( - "pyiqvia.disease.Disease.current", return_value=data_disease_index - ), patch( + ), patch("pyiqvia.disease.Disease.current", return_value=data_disease_index), patch( "homeassistant.components.iqvia.PLATFORMS", [] ): assert await async_setup_component(hass, DOMAIN, config) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 5d42ed79542..0f2d8e56050 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -77,9 +77,9 @@ def patch_file_upload(return_value=FIXTURE_KEYRING, side_effect=None): return_value=return_value, side_effect=side_effect, ), patch( - "pathlib.Path.mkdir" + "pathlib.Path.mkdir", ) as mkdir_mock, patch( - "shutil.move" + "shutil.move", ) as shutil_move_mock: file_upload_mock.return_value.__enter__.return_value = Mock() yield return_value diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py index 88cfca71f98..64664745c54 100644 --- a/tests/components/linear_garage_door/test_config_flow.py +++ b/tests/components/linear_garage_door/test_config_flow.py @@ -30,7 +30,8 @@ async def test_form(hass: HomeAssistant) -> None: "homeassistant.components.linear_garage_door.config_flow.Linear.close", return_value=None, ), patch( - "uuid.uuid4", return_value="test-uuid" + "uuid.uuid4", + return_value="test-uuid", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -89,7 +90,8 @@ async def test_reauth(hass: HomeAssistant) -> None: "homeassistant.components.linear_garage_door.config_flow.Linear.close", return_value=None, ), patch( - "uuid.uuid4", return_value="test-uuid" + "uuid.uuid4", + return_value="test-uuid", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index eaa2a1e4192..d95b409a67b 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -493,9 +493,13 @@ async def test_logbook_describe_event( hass, "fake_integration.logbook", Mock( - async_describe_events=lambda hass, async_describe_event: async_describe_event( - "test_domain", "some_event", _describe - ) + async_describe_events=( + lambda hass, async_describe_event: async_describe_event( + "test_domain", + "some_event", + _describe, + ) + ), ), ) diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index da26a55a4ef..631cb0ff1e7 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -60,7 +60,8 @@ async def test_bridge_import_flow(hass: HomeAssistant) -> None: ) as mock_setup_entry, patch( "homeassistant.components.lutron_caseta.async_setup", return_value=True ), patch.object( - Smartbridge, "create_tls" + Smartbridge, + "create_tls", ) as create_tls: create_tls.return_value = MockBridge(can_connect=True) diff --git a/tests/components/mill/test_init.py b/tests/components/mill/test_init.py index 694e9537a8c..15175dedada 100644 --- a/tests/components/mill/test_init.py +++ b/tests/components/mill/test_init.py @@ -115,7 +115,8 @@ async def test_unload_entry(hass: HomeAssistant) -> None: ) as unload_entry, patch( "mill.Mill.fetch_heater_and_sensor_data", return_value={} ), patch( - "mill.Mill.connect", return_value=True + "mill.Mill.connect", + return_value=True, ): assert await async_setup_component(hass, "mill", {}) diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 883a94ea02e..64fbb61aac3 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -59,7 +59,8 @@ async def serial_transport_fixture( ) as transport_class, patch("mysensors.task.OTAFirmware", autospec=True), patch( "mysensors.task.load_fw", autospec=True ), patch( - "mysensors.task.Persistence", autospec=True + "mysensors.task.Persistence", + autospec=True, ) as persistence_class: persistence = persistence_class.return_value diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 0776b80a3cd..61a7bc2354d 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -97,6 +97,6 @@ def selected_platforms(platforms): ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ): yield diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index e9a66cfefc8..6dcc11d31ab 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -388,7 +388,7 @@ async def test_camera_reconnect_webhook(hass: HomeAssistant, config_entry) -> No ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ) as mock_webhook: mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() @@ -482,7 +482,7 @@ async def test_setup_component_no_devices(hass: HomeAssistant, config_entry) -> ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_no_data mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() @@ -522,7 +522,7 @@ async def test_camera_image_raises_exception( ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ): mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_get_image.side_effect = fake_post diff --git a/tests/components/netatmo/test_diagnostics.py b/tests/components/netatmo/test_diagnostics.py index 0ece935abcb..19f83830a4e 100644 --- a/tests/components/netatmo/test_diagnostics.py +++ b/tests/components/netatmo/test_diagnostics.py @@ -25,7 +25,7 @@ async def test_entry_diagnostics( ) as mock_auth, patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index e04295ae668..75b1e9e47e6 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -205,7 +205,7 @@ async def test_setup_with_cloud(hass: HomeAssistant, config_entry) -> None: ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request assert await async_setup_component( @@ -271,7 +271,7 @@ async def test_setup_with_cloudhook(hass: HomeAssistant) -> None: ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index 83218b6d6d1..b6df9191976 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -103,7 +103,7 @@ async def test_setup_component_no_devices(hass: HomeAssistant, config_entry) -> ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.netatmo.webhook_generate_url" + "homeassistant.components.netatmo.webhook_generate_url", ): mock_auth.return_value.async_post_api_request.side_effect = ( fake_post_request_no_data diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index c888381230c..47568a7d760 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -101,7 +101,8 @@ async def mock_supervisor_fixture(hass, aioclient_mock): "homeassistant.components.hassio.HassIO.get_ingress_panels", return_value={"panels": {}}, ), patch.dict( - os.environ, {"SUPERVISOR_TOKEN": "123456"} + os.environ, + {"SUPERVISOR_TOKEN": "123456"}, ): yield diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 0f2c15a5e4a..ef1ac166f1e 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -210,9 +210,11 @@ async def test_options_migration(hass: HomeAssistant) -> None: "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe", return_value=True, ), patch( - "homeassistant.components.opentherm_gw.async_setup", return_value=True + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, ), patch( - "pyotgw.status.StatusManager._process_updates", return_value=None + "pyotgw.status.StatusManager._process_updates", + return_value=None, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/otbr/test_util.py b/tests/components/otbr/test_util.py index 171a607d200..941c80a52da 100644 --- a/tests/components/otbr/test_util.py +++ b/tests/components/otbr/test_util.py @@ -73,7 +73,7 @@ async def test_factory_reset_error_1( ) as factory_reset_mock, patch( "python_otbr_api.OTBR.delete_active_dataset" ) as delete_active_dataset_mock, pytest.raises( - HomeAssistantError + HomeAssistantError, ): await data.factory_reset() @@ -94,7 +94,7 @@ async def test_factory_reset_error_2( "python_otbr_api.OTBR.delete_active_dataset", side_effect=python_otbr_api.OTBRError, ) as delete_active_dataset_mock, pytest.raises( - HomeAssistantError + HomeAssistantError, ): await data.factory_reset() diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index cba046a2a9d..8288e7e9f70 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -189,7 +189,7 @@ async def test_create_network_fails_3( ), patch( "python_otbr_api.OTBR.create_active_dataset", ), patch( - "python_otbr_api.OTBR.factory_reset" + "python_otbr_api.OTBR.factory_reset", ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -211,7 +211,7 @@ async def test_create_network_fails_4( "python_otbr_api.OTBR.get_active_dataset_tlvs", side_effect=python_otbr_api.OTBRError, ), patch( - "python_otbr_api.OTBR.factory_reset" + "python_otbr_api.OTBR.factory_reset", ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 235596715f4..47d70727890 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -851,7 +851,7 @@ async def test_client_header_issues( ), patch( "homeassistant.components.http.current_request.get", return_value=MockRequest() ), pytest.raises( - RuntimeError + RuntimeError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 9326869b272..4744c065ede 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -367,7 +367,7 @@ async def test_service_descriptions(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.python_script.os.path.exists", return_value=True ), patch_yaml_files( - services_yaml1 + services_yaml1, ): await async_setup_component(hass, DOMAIN, {}) @@ -416,7 +416,7 @@ async def test_service_descriptions(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.python_script.os.path.exists", return_value=True ), patch_yaml_files( - services_yaml2 + services_yaml2, ): await hass.services.async_call(DOMAIN, "reload", {}, blocking=True) descriptions = await async_get_all_descriptions(hass) diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 04e423a399c..922ec7b0a5a 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -232,7 +232,8 @@ async def test_calendar_not_supported_by_device( @pytest.mark.parametrize( - "mock_insert_schedule_response", [([None])] # Disable success responses + "mock_insert_schedule_response", + [([None])], # Disable success responses ) async def test_no_schedule( hass: HomeAssistant, diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index db9c4c8739e..7048e1d63f4 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus +from typing import Any import pytest @@ -10,7 +11,7 @@ from homeassistant.components.rainbird.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_MAC from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import ( CONFIG_ENTRY_DATA, @@ -35,7 +36,7 @@ async def test_init_success( ) -> None: """Test successful setup and unload.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) @@ -86,7 +87,7 @@ async def test_communication_failure( config_entry_state: list[ConfigEntryState], ) -> None: """Test unable to talk to device on startup, which fails setup.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == config_entry_state @@ -115,7 +116,7 @@ async def test_fix_unique_id( assert entries[0].unique_id is None assert entries[0].data.get(CONF_MAC) is None - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED # Verify config entry now has a unique id @@ -167,7 +168,7 @@ async def test_fix_unique_id_failure( responses.insert(0, initial_response) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) # Config entry is loaded, but not updated assert config_entry.state == ConfigEntryState.LOADED assert config_entry.unique_id is None @@ -202,14 +203,10 @@ async def test_fix_unique_id_duplicate( responses.append(mock_json_response(WIFI_PARAMS_RESPONSE)) responses.extend(responses_copy) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS_UNIQUE_ID - await other_entry.async_setup(hass) - # Config entry unique id could not be updated since it already exists - assert other_entry.state == ConfigEntryState.SETUP_ERROR - assert "Unable to fix missing unique id (already exists)" in caplog.text await hass.async_block_till_done() @@ -221,34 +218,51 @@ async def test_fix_unique_id_duplicate( "config_entry_unique_id", "serial_number", "entity_unique_id", + "device_identifier", "expected_unique_id", + "expected_device_identifier", ), [ - (SERIAL_NUMBER, SERIAL_NUMBER, SERIAL_NUMBER, MAC_ADDRESS_UNIQUE_ID), + ( + SERIAL_NUMBER, + SERIAL_NUMBER, + SERIAL_NUMBER, + str(SERIAL_NUMBER), + MAC_ADDRESS_UNIQUE_ID, + MAC_ADDRESS_UNIQUE_ID, + ), ( SERIAL_NUMBER, SERIAL_NUMBER, f"{SERIAL_NUMBER}-rain-delay", + f"{SERIAL_NUMBER}-1", f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + f"{MAC_ADDRESS_UNIQUE_ID}-1", ), - ("0", 0, "0", MAC_ADDRESS_UNIQUE_ID), + ("0", 0, "0", "0", MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID), ( "0", 0, "0-rain-delay", + "0-1", f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + f"{MAC_ADDRESS_UNIQUE_ID}-1", ), ( MAC_ADDRESS_UNIQUE_ID, SERIAL_NUMBER, MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID, + MAC_ADDRESS_UNIQUE_ID, + MAC_ADDRESS_UNIQUE_ID, ), ( MAC_ADDRESS_UNIQUE_ID, SERIAL_NUMBER, f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + f"{MAC_ADDRESS_UNIQUE_ID}-1", f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + f"{MAC_ADDRESS_UNIQUE_ID}-1", ), ], ids=( @@ -264,18 +278,150 @@ async def test_fix_entity_unique_ids( hass: HomeAssistant, config_entry: MockConfigEntry, entity_unique_id: str, + device_identifier: str, expected_unique_id: str, + expected_device_identifier: str, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test fixing entity unique ids from old unique id formats.""" - entity_registry = er.async_get(hass) entity_entry = entity_registry.async_get_or_create( DOMAIN, "number", unique_id=entity_unique_id, config_entry=config_entry ) + device_entry = device_registry.async_get_or_create( + identifiers={(DOMAIN, device_identifier)}, + config_entry_id=config_entry.entry_id, + serial_number=config_entry.data["serial_number"], + ) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED entity_entry = entity_registry.async_get(entity_entry.id) assert entity_entry assert entity_entry.unique_id == expected_unique_id + + device_entry = device_registry.async_get_device( + {(DOMAIN, expected_device_identifier)} + ) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, expected_device_identifier)} + + +@pytest.mark.parametrize( + ( + "entry1_updates", + "entry2_updates", + "expected_device_name", + "expected_disabled_by", + ), + [ + ({}, {}, None, None), + ( + { + "name_by_user": "Front Sprinkler", + }, + {}, + "Front Sprinkler", + None, + ), + ( + {}, + { + "name_by_user": "Front Sprinkler", + }, + "Front Sprinkler", + None, + ), + ( + { + "name_by_user": "Sprinkler 1", + }, + { + "name_by_user": "Sprinkler 2", + }, + "Sprinkler 2", + None, + ), + ( + { + "disabled_by": dr.DeviceEntryDisabler.USER, + }, + {}, + None, + None, + ), + ( + {}, + { + "disabled_by": dr.DeviceEntryDisabler.USER, + }, + None, + None, + ), + ( + { + "disabled_by": dr.DeviceEntryDisabler.USER, + }, + { + "disabled_by": dr.DeviceEntryDisabler.USER, + }, + None, + dr.DeviceEntryDisabler.USER, + ), + ], + ids=[ + "duplicates", + "prefer-old-name", + "prefer-new-name", + "both-names-prefers-new", + "old-disabled-prefer-new", + "new-disabled-prefer-old", + "both-disabled", + ], +) +async def test_fix_duplicate_device_ids( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + entry1_updates: dict[str, Any], + entry2_updates: dict[str, Any], + expected_device_name: str | None, + expected_disabled_by: dr.DeviceEntryDisabler | None, +) -> None: + """Test fixing duplicate device ids.""" + + entry1 = device_registry.async_get_or_create( + identifiers={(DOMAIN, str(SERIAL_NUMBER))}, + config_entry_id=config_entry.entry_id, + serial_number=config_entry.data["serial_number"], + ) + device_registry.async_update_device(entry1.id, **entry1_updates) + + entry2 = device_registry.async_get_or_create( + identifiers={(DOMAIN, MAC_ADDRESS_UNIQUE_ID)}, + config_entry_id=config_entry.entry_id, + serial_number=config_entry.data["serial_number"], + ) + device_registry.async_update_device(entry2.id, **entry2_updates) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + assert len(device_entries) == 2 + + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ConfigEntryState.LOADED + + # Only the device with the new format exists + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + assert len(device_entries) == 1 + + device_entry = device_registry.async_get_device({(DOMAIN, MAC_ADDRESS_UNIQUE_ID)}) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, MAC_ADDRESS_UNIQUE_ID)} + assert device_entry.name_by_user == expected_device_name + assert device_entry.disabled_by == expected_disabled_by diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 685f307d197..2697e908c94 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -134,7 +134,8 @@ async def setup_rainmachine_fixture(hass, client, config): ), patch( "homeassistant.components.rainmachine.config_flow.Client", return_value=client ), patch( - "homeassistant.components.rainmachine.PLATFORMS", [] + "homeassistant.components.rainmachine.PLATFORMS", + [], ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index a982eeb39be..d0ed6f15d43 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -412,17 +412,11 @@ def old_db_schema(schema_version_postfix: str) -> Iterator[None]: recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( core, "EventTypes", old_db_schema.EventTypes - ), patch.object( - core, "EventData", old_db_schema.EventData - ), patch.object( + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( core, "States", old_db_schema.States - ), patch.object( - core, "Events", old_db_schema.Events - ), patch.object( + ), patch.object(core, "Events", old_db_schema.Events), patch.object( core, "StateAttributes", old_db_schema.StateAttributes - ), patch.object( - core, "EntityIDMigrationTask", core.RecorderTask - ), patch( + ), patch.object(core, "EntityIDMigrationTask", core.RecorderTask), patch( CREATE_ENGINE_TARGET, new=partial( create_engine_test_for_schema_version_postfix, diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 852419559b2..b9d0801d788 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -85,17 +85,11 @@ def db_schema_32(): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( core, "EventTypes", old_db_schema.EventTypes - ), patch.object( - core, "EventData", old_db_schema.EventData - ), patch.object( + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( core, "States", old_db_schema.States - ), patch.object( - core, "Events", old_db_schema.Events - ), patch.object( + ), patch.object(core, "Events", old_db_schema.Events), patch.object( core, "StateAttributes", old_db_schema.StateAttributes - ), patch.object( - core, "EntityIDMigrationTask", core.RecorderTask - ), patch( + ), patch.object(core, "EntityIDMigrationTask", core.RecorderTask), patch( CREATE_ENGINE_TARGET, new=_create_engine_test ): yield diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 4faa8dc7e8a..1696c9018b4 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -244,9 +244,7 @@ async def test_purge_old_states_encounters_temporary_mysql_error( ) as sleep_mock, patch( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=[mysql_exception, None], - ), patch.object( - instance.engine.dialect, "name", "mysql" - ): + ), patch.object(instance.engine.dialect, "name", "mysql"): await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index f386fd19e36..e8f9130165f 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -212,9 +212,7 @@ async def test_purge_old_states_encounters_temporary_mysql_error( ) as sleep_mock, patch( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=[mysql_exception, None], - ), patch.object( - instance.engine.dialect, "name", "mysql" - ): + ), patch.object(instance.engine.dialect, "name", "mysql"): await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 98f401e45d8..b11cc67707f 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -98,13 +98,9 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( core, "EventTypes", old_db_schema.EventTypes - ), patch.object( - core, "EventData", old_db_schema.EventData - ), patch.object( + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( core, "States", old_db_schema.States - ), patch.object( - core, "Events", old_db_schema.Events - ), patch( + ), patch.object(core, "Events", old_db_schema.Events), patch( CREATE_ENGINE_TARGET, new=_create_engine_test ), patch( "homeassistant.components.recorder.Recorder._migrate_events_context_ids", @@ -269,13 +265,9 @@ async def test_migrate_can_resume_entity_id_post_migration( recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( core, "EventTypes", old_db_schema.EventTypes - ), patch.object( - core, "EventData", old_db_schema.EventData - ), patch.object( + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( core, "States", old_db_schema.States - ), patch.object( - core, "Events", old_db_schema.Events - ), patch( + ), patch.object(core, "Events", old_db_schema.Events), patch( CREATE_ENGINE_TARGET, new=_create_engine_test ), patch( "homeassistant.components.recorder.Recorder._migrate_events_context_ids", diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index b371d69fe5f..323b81211d7 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2227,9 +2227,7 @@ async def test_recorder_info_migration_queue_exhausted( ), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, - ), patch.object( - recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1 - ), patch.object( + ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), patch.object( recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0 ), patch( "homeassistant.components.recorder.migration._apply_update", diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index 325e787bb4f..a8a764cd502 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -140,7 +140,7 @@ async def setup_risco_cloud(hass, cloud_config_entry, events): "homeassistant.components.risco.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.RiscoCloud.close" + "homeassistant.components.risco.RiscoCloud.close", ), patch( "homeassistant.components.risco.RiscoCloud.get_events", return_value=events, @@ -191,7 +191,7 @@ async def setup_risco_local(hass, local_config_entry): "homeassistant.components.risco.RiscoLocal.id", new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( - "homeassistant.components.risco.RiscoLocal.disconnect" + "homeassistant.components.risco.RiscoLocal.disconnect", ): await hass.config_entries.async_setup(local_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index fdb51c65dda..8207ad819b7 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -162,7 +162,7 @@ async def test_form_reauth(hass: HomeAssistant, cloud_config_entry) -> None: "homeassistant.components.risco.config_flow.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.close" + "homeassistant.components.risco.config_flow.RiscoCloud.close", ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, @@ -198,7 +198,7 @@ async def test_form_reauth_with_new_username( "homeassistant.components.risco.config_flow.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.close" + "homeassistant.components.risco.config_flow.RiscoCloud.close", ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, @@ -307,7 +307,7 @@ async def test_form_local_already_exists(hass: HomeAssistant) -> None: "homeassistant.components.risco.config_flow.RiscoLocal.id", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" + "homeassistant.components.risco.config_flow.RiscoLocal.disconnect", ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_LOCAL_DATA diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index b0a01137ab9..711ae203e0f 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -55,11 +55,12 @@ def bypass_api_fixture() -> None: ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient._wait_response" ), patch( - "roborock.api.AttributeCache.async_value" + "roborock.api.AttributeCache.async_value", ), patch( - "roborock.api.AttributeCache.value" + "roborock.api.AttributeCache.value", ), patch( - "homeassistant.components.roborock.image.MAP_SLEEP", 0 + "homeassistant.components.roborock.image.MAP_SLEEP", + 0, ): yield diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 5e8ab9311aa..874697bf777 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -45,9 +45,9 @@ async def silent_ssdp_scanner(hass): ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "homeassistant.components.ssdp.Scanner.async_scan" ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers" + "homeassistant.components.ssdp.Server._async_start_upnp_servers", ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers" + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", ): yield diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py index da6a68af2d1..2277c84d187 100644 --- a/tests/components/sensibo/test_button.py +++ b/tests/components/sensibo/test_button.py @@ -100,7 +100,7 @@ async def test_button_failure( "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", return_value={"status": "failure"}, ), pytest.raises( - HomeAssistantError + HomeAssistantError, ): await hass.services.async_call( BUTTON_DOMAIN, diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 530034720f2..9cf0a8972a9 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -742,7 +742,7 @@ async def test_climate_set_timer( "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", return_value={"status": "failure"}, ), pytest.raises( - MultipleInvalid + MultipleInvalid, ): await hass.services.async_call( DOMAIN, @@ -761,7 +761,7 @@ async def test_climate_set_timer( "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", return_value={"status": "failure"}, ), pytest.raises( - HomeAssistantError + HomeAssistantError, ): await hass.services.async_call( DOMAIN, @@ -845,7 +845,7 @@ async def test_climate_pure_boost( ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", ), pytest.raises( - MultipleInvalid + MultipleInvalid, ): await hass.services.async_call( DOMAIN, @@ -947,7 +947,7 @@ async def test_climate_climate_react( ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", ), pytest.raises( - MultipleInvalid + MultipleInvalid, ): await hass.services.async_call( DOMAIN, @@ -1254,7 +1254,7 @@ async def test_climate_full_ac_state( ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", ), pytest.raises( - MultipleInvalid + MultipleInvalid, ): await hass.services.async_call( DOMAIN, diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py index 7d8e3731415..41a67dfbe79 100644 --- a/tests/components/sensibo/test_select.py +++ b/tests/components/sensibo/test_select.py @@ -90,7 +90,7 @@ async def test_select_set_option( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "failed"}}, ), pytest.raises( - HomeAssistantError + HomeAssistantError, ): await hass.services.async_call( SELECT_DOMAIN, @@ -132,7 +132,7 @@ async def test_select_set_option( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Failed", "failureReason": "No connection"}}, ), pytest.raises( - HomeAssistantError + HomeAssistantError, ): await hass.services.async_call( SELECT_DOMAIN, diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py index c6d47ceed66..e319be85c73 100644 --- a/tests/components/sensibo/test_switch.py +++ b/tests/components/sensibo/test_switch.py @@ -196,7 +196,7 @@ async def test_switch_command_failure( "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", return_value={"status": "failure"}, ), pytest.raises( - HomeAssistantError + HomeAssistantError, ): await hass.services.async_call( SWITCH_DOMAIN, @@ -214,7 +214,7 @@ async def test_switch_command_failure( "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", return_value={"status": "failure"}, ), pytest.raises( - HomeAssistantError + HomeAssistantError, ): await hass.services.async_call( SWITCH_DOMAIN, diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 4b8686d7a7f..1b9f9f02cee 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -106,7 +106,8 @@ async def setup_simplisafe_fixture(hass, api, config): ), patch( "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" ), patch( - "homeassistant.components.simplisafe.PLATFORMS", [] + "homeassistant.components.simplisafe.PLATFORMS", + [], ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/simplisafe/test_init.py b/tests/components/simplisafe/test_init.py index 617b77f7c98..cc7b2b8d2b6 100644 --- a/tests/components/simplisafe/test_init.py +++ b/tests/components/simplisafe/test_init.py @@ -34,7 +34,8 @@ async def test_base_station_migration( ), patch( "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" ), patch( - "homeassistant.components.simplisafe.PLATFORMS", [] + "homeassistant.components.simplisafe.PLATFORMS", + [], ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index f6f5ab66708..8d4d7b8c3b2 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -146,9 +146,7 @@ async def test_user_local_connection_error(hass: HomeAssistant) -> None: "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True ), patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=True), patch( "pysmappee.mqtt.SmappeeLocalMqtt.stop", return_value=True - ), patch( - "pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", return_value=None - ): + ), patch("pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", return_value=None): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -474,9 +472,7 @@ async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: ), patch( "pysmappee.api.SmappeeLocalApi.load_instantaneous", return_value=[{"key": "phase0ActivePower", "value": 0}], - ), patch( - "homeassistant.components.smappee.async_setup_entry", return_value=True - ): + ), patch("homeassistant.components.smappee.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -516,9 +512,7 @@ async def test_full_user_local_flow(hass: HomeAssistant) -> None: ), patch( "pysmappee.api.SmappeeLocalApi.load_instantaneous", return_value=[{"key": "phase0ActivePower", "value": 0}], - ), patch( - "homeassistant.components.smappee.async_setup_entry", return_value=True - ): + ), patch("homeassistant.components.smappee.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index cb912af1cf6..648ca12803c 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -230,9 +230,9 @@ async def silent_ssdp_scanner(hass): ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "homeassistant.components.ssdp.Scanner.async_scan" ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers" + "homeassistant.components.ssdp.Server._async_start_upnp_servers", ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers" + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", ): yield diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 678e8ba5034..8bed67cb15f 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -145,9 +145,7 @@ async def setup_subaru_config_entry( return_value=vehicle_status, ), patch( MOCK_API_UPDATE, - ), patch( - MOCK_API_FETCH, side_effect=fetch_effect - ): + ), patch(MOCK_API_FETCH, side_effect=fetch_effect): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py index 239777a4da3..98d413c3b96 100644 --- a/tests/components/switchbee/test_config_flow.py +++ b/tests/components/switchbee/test_config_flow.py @@ -39,9 +39,7 @@ async def test_form(hass: HomeAssistant, test_cucode_in_coordinator_data) -> Non return_value=True, ), patch( "switchbee.api.polling.CentralUnitPolling.fetch_states", return_value=None - ), patch( - "switchbee.api.polling.CentralUnitPolling._login", return_value=None - ): + ), patch("switchbee.api.polling.CentralUnitPolling._login", return_value=None): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 39ecc95d89e..ff517b8963d 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -152,7 +152,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: "systembridgeconnector.websocket_client.WebSocketClient.get_data", return_value=FIXTURE_DATA_RESPONSE, ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen" + "systembridgeconnector.websocket_client.WebSocketClient.listen", ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -450,7 +450,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "systembridgeconnector.websocket_client.WebSocketClient.get_data", return_value=FIXTURE_DATA_RESPONSE, ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen" + "systembridgeconnector.websocket_client.WebSocketClient.listen", ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -484,7 +484,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: "systembridgeconnector.websocket_client.WebSocketClient.get_data", return_value=FIXTURE_DATA_RESPONSE, ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen" + "systembridgeconnector.websocket_client.WebSocketClient.listen", ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 87176c57692..db166144925 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -143,9 +143,9 @@ async def silent_ssdp_scanner(hass): ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "homeassistant.components.ssdp.Scanner.async_scan" ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers" + "homeassistant.components.ssdp.Server._async_start_upnp_servers", ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers" + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", ): yield diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index e7c878b6f40..a1637f62b01 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -94,9 +94,7 @@ async def test_observer_discovery( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -145,9 +143,7 @@ async def test_removal_by_observer_before_started( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch( - "pyudev.MonitorObserver", new=_create_mock_monitor_observer - ), patch.object( + ), patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -184,9 +180,7 @@ async def test_discovered_by_websocket_scan( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -224,9 +218,7 @@ async def test_discovered_by_websocket_scan_limited_by_description_matcher( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -265,9 +257,7 @@ async def test_most_targeted_matcher_wins( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -305,9 +295,7 @@ async def test_discovered_by_websocket_scan_rejected_by_description_matcher( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -349,9 +337,7 @@ async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -389,9 +375,7 @@ async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -433,9 +417,7 @@ async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -478,9 +460,7 @@ async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -517,9 +497,7 @@ async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -554,9 +532,7 @@ async def test_discovered_by_websocket_scan_match_vid_only( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -592,9 +568,7 @@ async def test_discovered_by_websocket_scan_match_vid_wrong_pid( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -629,9 +603,7 @@ async def test_discovered_by_websocket_no_vid_pid( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -667,9 +639,7 @@ async def test_non_matching_discovered_by_scanner_after_started( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -708,9 +678,7 @@ async def test_observer_on_wsl_fallback_without_throwing_exception( "pyudev.Monitor.filter_by", side_effect=ValueError ), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -760,9 +728,7 @@ async def test_not_discovered_by_observer_before_started_on_docker( "homeassistant.components.usb.async_get_usb", return_value=new_usb ), patch( "homeassistant.components.usb.comports", return_value=mock_comports - ), patch( - "pyudev.MonitorObserver", new=_create_mock_monitor_observer - ): + ), patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() @@ -1047,9 +1013,7 @@ async def test_resolve_serial_by_id( ), patch( "homeassistant.components.usb.get_serial_by_id", return_value="/dev/serial/by-id/bla", - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 0aa59c9271f..b893d2df550 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -24,9 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: "vilfo.Client.get_board_information", return_value=None ), patch( "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch( - "vilfo.Client.resolve_mac_address", return_value=mock_mac - ), patch( + ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), patch( "homeassistant.components.vilfo.async_setup_entry" ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( @@ -117,9 +115,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: return_value=None, ), patch( "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch( - "vilfo.Client.resolve_mac_address", return_value=None - ): + ), patch("vilfo.Client.resolve_mac_address", return_value=None): first_flow_result2 = await hass.config_entries.flow.async_configure( first_flow_result1["flow_id"], {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, @@ -134,9 +130,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: return_value=None, ), patch( "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch( - "vilfo.Client.resolve_mac_address", return_value=None - ): + ), patch("vilfo.Client.resolve_mac_address", return_value=None): second_flow_result2 = await hass.config_entries.flow.async_configure( second_flow_result1["flow_id"], {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, @@ -177,9 +171,7 @@ async def test_validate_input_returns_data(hass: HomeAssistant) -> None: "vilfo.Client.get_board_information", return_value=None ), patch( "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch( - "vilfo.Client.resolve_mac_address", return_value=None - ): + ), patch("vilfo.Client.resolve_mac_address", return_value=None): result = await hass.components.vilfo.config_flow.validate_input( hass, data=mock_data ) @@ -193,9 +185,7 @@ async def test_validate_input_returns_data(hass: HomeAssistant) -> None: "vilfo.Client.get_board_information", return_value=None ), patch( "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch( - "vilfo.Client.resolve_mac_address", return_value=mock_mac - ): + ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac): result2 = await hass.components.vilfo.config_flow.validate_input( hass, data=mock_data ) diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index 91ea5b3e439..a94f290f7e6 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -124,7 +124,7 @@ async def test_errors( "homeassistant.components.vlc_telnet.config_flow.Client.login", side_effect=login_side_effect, ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -219,7 +219,7 @@ async def test_reauth_errors( "homeassistant.components.vlc_telnet.config_flow.Client.login", side_effect=login_side_effect, ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -316,7 +316,7 @@ async def test_hassio_errors( "homeassistant.components.vlc_telnet.config_flow.Client.login", side_effect=login_side_effect, ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py index 982a14a80f4..00b1ae6e72a 100644 --- a/tests/components/vodafone_station/test_config_flow.py +++ b/tests/components/vodafone_station/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.vodafone_station.async_setup_entry" ) as mock_setup_entry, patch( - "requests.get" + "requests.get", ) as mock_request_get: mock_request_get.return_value.status_code = 200 @@ -90,7 +90,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> ), patch( "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", ), patch( - "homeassistant.components.vodafone_station.async_setup_entry" + "homeassistant.components.vodafone_station.async_setup_entry", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -122,9 +122,9 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", ), patch( - "homeassistant.components.vodafone_station.async_setup_entry" + "homeassistant.components.vodafone_station.async_setup_entry", ), patch( - "requests.get" + "requests.get", ) as mock_request_get: mock_request_get.return_value.status_code = 200 @@ -170,7 +170,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> ), patch( "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", ), patch( - "homeassistant.components.vodafone_station.async_setup_entry" + "homeassistant.components.vodafone_station.async_setup_entry", ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -204,7 +204,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> ), patch( "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", ), patch( - "homeassistant.components.vodafone_station.async_setup_entry" + "homeassistant.components.vodafone_station.async_setup_entry", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/waqi/test_config_flow.py b/tests/components/waqi/test_config_flow.py index 7a95e000d82..ecc7e07158d 100644 --- a/tests/components/waqi/test_config_flow.py +++ b/tests/components/waqi/test_config_flow.py @@ -235,9 +235,9 @@ async def test_error_in_second_step( with patch( "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_coordinates", side_effect=exception - ), patch("aiowaqi.WAQIClient.get_by_station_number", side_effect=exception): + ), patch("aiowaqi.WAQIClient.get_by_coordinates", side_effect=exception), patch( + "aiowaqi.WAQIClient.get_by_station_number", side_effect=exception + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], payload, diff --git a/tests/components/watttime/conftest.py b/tests/components/watttime/conftest.py index f3c1986fcb0..f636ffefcfb 100644 --- a/tests/components/watttime/conftest.py +++ b/tests/components/watttime/conftest.py @@ -106,9 +106,7 @@ async def setup_watttime_fixture(hass, client, config_auth, config_coordinates): ), patch( "homeassistant.components.watttime.config_flow.Client.async_login", return_value=client, - ), patch( - "homeassistant.components.watttime.PLATFORMS", [] - ): + ), patch("homeassistant.components.watttime.PLATFORMS", []): assert await async_setup_component( hass, DOMAIN, {**config_auth, **config_coordinates} ) diff --git a/tests/components/withings/test_diagnostics.py b/tests/components/withings/test_diagnostics.py index bb5c93e1f09..928eccdde0f 100644 --- a/tests/components/withings/test_diagnostics.py +++ b/tests/components/withings/test_diagnostics.py @@ -67,9 +67,9 @@ async def test_diagnostics_cloudhook_instance( ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( - "homeassistant.components.cloud.async_delete_cloudhook" + "homeassistant.components.cloud.async_delete_cloudhook", ), patch( - "homeassistant.components.withings.webhook_generate_url" + "homeassistant.components.withings.webhook_generate_url", ): await setup_integration(hass, webhook_config_entry) await prepare_webhook_setup(hass, freezer) diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 3f20791ac4d..390fbc3bbc3 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -352,7 +352,7 @@ async def test_removing_entry_with_cloud_unavailable( "homeassistant.components.cloud.async_delete_cloudhook", side_effect=CloudNotAvailable(), ), patch( - "homeassistant.components.withings.webhook_generate_url" + "homeassistant.components.withings.webhook_generate_url", ): await setup_integration(hass, cloudhook_config_entry) assert hass.components.cloud.async_active_subscription() is True @@ -469,9 +469,9 @@ async def test_cloud_disconnect( ), patch( "homeassistant.components.withings.async_get_config_entry_implementation", ), patch( - "homeassistant.components.cloud.async_delete_cloudhook" + "homeassistant.components.cloud.async_delete_cloudhook", ), patch( - "homeassistant.components.withings.webhook_generate_url" + "homeassistant.components.withings.webhook_generate_url", ): await setup_integration(hass, webhook_config_entry) await prepare_webhook_setup(hass, freezer) diff --git a/tests/components/wyoming/test_tts.py b/tests/components/wyoming/test_tts.py index 68b7b2b62bc..2f2a25558e4 100644 --- a/tests/components/wyoming/test_tts.py +++ b/tests/components/wyoming/test_tts.py @@ -180,7 +180,7 @@ async def test_get_tts_audio_audio_oserror( ), patch.object( mock_client, "read_event", side_effect=OSError("Boom!") ), pytest.raises( - HomeAssistantError + HomeAssistantError, ): await tts.async_get_media_source_audio( hass, diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index ccccd98b3b6..4ce95e418d0 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -22,9 +22,9 @@ async def silent_ssdp_scanner(hass): ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "homeassistant.components.ssdp.Scanner.async_scan" ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers" + "homeassistant.components.ssdp.Server._async_start_upnp_servers", ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers" + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", ): yield diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 0bd5b5f59d0..e1d33ee5f75 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -440,9 +440,11 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb ), patch( - f"{MODULE}.async_setup", return_value=True + f"{MODULE}.async_setup", + return_value=True, ), patch( - f"{MODULE}.async_setup_entry", return_value=True + f"{MODULE}.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index ba0bbbe087d..f9615c84e1d 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -158,15 +158,13 @@ async def test_if_notification_notification_fires( node.receive_event(event) await hass.async_block_till_done() assert len(calls) == 2 - assert calls[0].data[ - "some" - ] == "event.notification.notification - device - zwave_js_notification - {}".format( - CommandClass.NOTIFICATION + assert ( + calls[0].data["some"] + == f"event.notification.notification - device - zwave_js_notification - {CommandClass.NOTIFICATION}" ) - assert calls[1].data[ - "some" - ] == "event.notification.notification2 - device - zwave_js_notification - {}".format( - CommandClass.NOTIFICATION + assert ( + calls[1].data["some"] + == f"event.notification.notification2 - device - zwave_js_notification - {CommandClass.NOTIFICATION}" ) @@ -288,15 +286,13 @@ async def test_if_entry_control_notification_fires( node.receive_event(event) await hass.async_block_till_done() assert len(calls) == 2 - assert calls[0].data[ - "some" - ] == "event.notification.notification - device - zwave_js_notification - {}".format( - CommandClass.ENTRY_CONTROL + assert ( + calls[0].data["some"] + == f"event.notification.notification - device - zwave_js_notification - {CommandClass.ENTRY_CONTROL}" ) - assert calls[1].data[ - "some" - ] == "event.notification.notification2 - device - zwave_js_notification - {}".format( - CommandClass.ENTRY_CONTROL + assert ( + calls[1].data["some"] + == f"event.notification.notification2 - device - zwave_js_notification - {CommandClass.ENTRY_CONTROL}" ) @@ -705,15 +701,13 @@ async def test_if_basic_value_notification_fires( node.receive_event(event) await hass.async_block_till_done() assert len(calls) == 2 - assert calls[0].data[ - "some" - ] == "event.value_notification.basic - device - zwave_js_value_notification - {}".format( - CommandClass.BASIC + assert ( + calls[0].data["some"] + == f"event.value_notification.basic - device - zwave_js_value_notification - {CommandClass.BASIC}" ) - assert calls[1].data[ - "some" - ] == "event.value_notification.basic2 - device - zwave_js_value_notification - {}".format( - CommandClass.BASIC + assert ( + calls[1].data["some"] + == f"event.value_notification.basic2 - device - zwave_js_value_notification - {CommandClass.BASIC}" ) @@ -888,15 +882,13 @@ async def test_if_central_scene_value_notification_fires( node.receive_event(event) await hass.async_block_till_done() assert len(calls) == 2 - assert calls[0].data[ - "some" - ] == "event.value_notification.central_scene - device - zwave_js_value_notification - {}".format( - CommandClass.CENTRAL_SCENE + assert ( + calls[0].data["some"] + == f"event.value_notification.central_scene - device - zwave_js_value_notification - {CommandClass.CENTRAL_SCENE}" ) - assert calls[1].data[ - "some" - ] == "event.value_notification.central_scene2 - device - zwave_js_value_notification - {}".format( - CommandClass.CENTRAL_SCENE + assert ( + calls[1].data["some"] + == f"event.value_notification.central_scene2 - device - zwave_js_value_notification - {CommandClass.CENTRAL_SCENE}" ) @@ -1064,15 +1056,13 @@ async def test_if_scene_activation_value_notification_fires( node.receive_event(event) await hass.async_block_till_done() assert len(calls) == 2 - assert calls[0].data[ - "some" - ] == "event.value_notification.scene_activation - device - zwave_js_value_notification - {}".format( - CommandClass.SCENE_ACTIVATION + assert ( + calls[0].data["some"] + == f"event.value_notification.scene_activation - device - zwave_js_value_notification - {CommandClass.SCENE_ACTIVATION}" ) - assert calls[1].data[ - "some" - ] == "event.value_notification.scene_activation2 - device - zwave_js_value_notification - {}".format( - CommandClass.SCENE_ACTIVATION + assert ( + calls[1].data["some"] + == f"event.value_notification.scene_activation2 - device - zwave_js_value_notification - {CommandClass.SCENE_ACTIVATION}" ) diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 5e5343fd43e..b65f09aeaf9 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -416,9 +416,7 @@ automation: service_to_call: test.automation input_datetime: """, - hass.config.path( - "blueprints/automation/test_event_service.yaml" - ): """ + hass.config.path("blueprints/automation/test_event_service.yaml"): """ blueprint: name: "Call service based on event" domain: automation diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index ebb0cc35c20..5c3697ad936 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -38,13 +38,9 @@ async def test_get_system_info_supervisor_not_available( "homeassistant.helpers.system_info.is_docker_env", return_value=True ), patch( "homeassistant.helpers.system_info.is_official_image", return_value=True - ), patch( - "homeassistant.components.hassio.is_hassio", return_value=True - ), patch( + ), patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( "homeassistant.components.hassio.get_info", return_value=None - ), patch( - "homeassistant.helpers.system_info.cached_get_user", return_value="root" - ): + ), patch("homeassistant.helpers.system_info.cached_get_user", return_value="root"): info = await async_get_system_info(hass) assert isinstance(info, dict) assert info["version"] == current_version @@ -60,9 +56,7 @@ async def test_get_system_info_supervisor_not_loaded(hass: HomeAssistant) -> Non "homeassistant.helpers.system_info.is_docker_env", return_value=True ), patch( "homeassistant.helpers.system_info.is_official_image", return_value=True - ), patch( - "homeassistant.components.hassio.get_info", return_value=None - ), patch.dict( + ), patch("homeassistant.components.hassio.get_info", return_value=None), patch.dict( os.environ, {"SUPERVISOR": "127.0.0.1"} ): info = await async_get_system_info(hass) @@ -79,9 +73,7 @@ async def test_container_installationtype(hass: HomeAssistant) -> None: "homeassistant.helpers.system_info.is_docker_env", return_value=True ), patch( "homeassistant.helpers.system_info.is_official_image", return_value=True - ), patch( - "homeassistant.helpers.system_info.cached_get_user", return_value="root" - ): + ), patch("homeassistant.helpers.system_info.cached_get_user", return_value="root"): info = await async_get_system_info(hass) assert info["installation_type"] == "Home Assistant Container" @@ -89,9 +81,7 @@ async def test_container_installationtype(hass: HomeAssistant) -> None: "homeassistant.helpers.system_info.is_docker_env", return_value=True ), patch( "homeassistant.helpers.system_info.is_official_image", return_value=False - ), patch( - "homeassistant.helpers.system_info.cached_get_user", return_value="user" - ): + ), patch("homeassistant.helpers.system_info.cached_get_user", return_value="user"): info = await async_get_system_info(hass) assert info["installation_type"] == "Unsupported Third Party Container" diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 4fa10b92706..fd01beed9ab 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -31,9 +31,7 @@ async def test_requirement_installed_in_venv(hass: HomeAssistant) -> None: "homeassistant.util.package.is_virtual_env", return_value=True ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( "homeassistant.util.package.install_package", return_value=True - ) as mock_install, patch.dict( - os.environ, env_without_wheel_links(), clear=True - ): + ) as mock_install, patch.dict(os.environ, env_without_wheel_links(), clear=True): hass.config.skip_pip = False mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) assert await setup.async_setup_component(hass, "comp", {}) @@ -51,9 +49,7 @@ async def test_requirement_installed_in_deps(hass: HomeAssistant) -> None: "homeassistant.util.package.is_virtual_env", return_value=False ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( "homeassistant.util.package.install_package", return_value=True - ) as mock_install, patch.dict( - os.environ, env_without_wheel_links(), clear=True - ): + ) as mock_install, patch.dict(os.environ, env_without_wheel_links(), clear=True): hass.config.skip_pip = False mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) assert await setup.async_setup_component(hass, "comp", {}) @@ -369,7 +365,7 @@ async def test_install_with_wheels_index(hass: HomeAssistant) -> None: ), patch("homeassistant.util.package.install_package") as mock_inst, patch.dict( os.environ, {"WHEELS_LINKS": "https://wheels.hass.io/test"} ), patch( - "os.path.dirname" + "os.path.dirname", ) as mock_dir: mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) @@ -391,9 +387,7 @@ async def test_install_on_docker(hass: HomeAssistant) -> None: "homeassistant.util.package.is_docker_env", return_value=True ), patch("homeassistant.util.package.install_package") as mock_inst, patch( "os.path.dirname" - ) as mock_dir, patch.dict( - os.environ, env_without_wheel_links(), clear=True - ): + ) as mock_dir, patch.dict(os.environ, env_without_wheel_links(), clear=True): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components diff --git a/tests/test_runner.py b/tests/test_runner.py index 5fe5c2881ff..3b06e3b64dc 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -75,7 +75,7 @@ def test_run_executor_shutdown_throws( "homeassistant.runner.InterruptibleThreadPoolExecutor.shutdown", side_effect=RuntimeError, ) as mock_shutdown, patch( - "homeassistant.core.HomeAssistant.async_run" + "homeassistant.core.HomeAssistant.async_run", ) as mock_run: runner.run(default_config)